semillero-special-hotel/src/app/plans/[id]/rules/page.tsx

326 lines
11 KiB
TypeScript

'use strict';
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter, useParams } from 'next/navigation';
import styles from './page.module.css';
import Header from '@/components/Header';
import { useLocale } from '@/lib/i18n/LocaleContext';
interface Rule {
id?: number;
tempId?: string;
type: 'TIER' | 'BONUS';
minAchievement: number;
maxAchievement: number;
rate: number;
payoutAmount: number;
}
interface Plan {
id: number;
name: string;
code: string;
type: string;
status: string;
version: number;
rules: Rule[];
}
export default function RulesPage() {
const { t } = useLocale();
const router = useRouter();
const params = useParams();
const planId = params?.id ? parseInt(params.id as string) : null;
const [plan, setPlan] = useState<Plan | null>(null);
const [rules, setRules] = useState<Rule[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [role, setRole] = useState<string | null>(null);
const fetchPlan = async () => {
if (!planId) return;
try {
const res = await fetch(`/api/plans/${planId}`);
if (res.ok) {
const data = await res.json();
setPlan(data.plan);
if (data.plan.rules && data.plan.rules.length > 0) {
// Format rules from server (Decimal fields come as strings/numbers)
const formatted = data.plan.rules.map((r: any) => ({
id: r.id,
tempId: Math.random().toString(36).substr(2, 9),
type: r.type as 'TIER' | 'BONUS',
minAchievement: parseFloat(r.minAchievement),
maxAchievement: parseFloat(r.maxAchievement),
rate: parseFloat(r.rate),
payoutAmount: parseFloat(r.payoutAmount)
}));
setRules(formatted);
} else {
// Default initial empty row
setRules([
{
tempId: Math.random().toString(36).substr(2, 9),
type: 'TIER',
minAchievement: 0.0,
maxAchievement: 1.0,
rate: 0.0,
payoutAmount: 0.0
}
]);
}
} else {
setError(t('plans.errorFind'));
}
} catch (err) {
setError(t('plans.errorLoad'));
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetch('/api/auth/me')
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data?.user) setRole(data.user.role);
})
.catch(err => console.error('Failed to fetch user role', err));
fetchPlan();
}, [planId]);
const handleAddRow = () => {
// Generate a default values row, potentially continuing from previous max
const lastRule = rules[rules.length - 1];
const newMin = lastRule ? lastRule.maxAchievement : 0.0;
setRules([
...rules,
{
tempId: Math.random().toString(36).substr(2, 9),
type: 'TIER',
minAchievement: newMin,
maxAchievement: newMin + 0.1 > 1.0 ? 1.0 : newMin + 0.1,
rate: 0.0,
payoutAmount: 0.0
}
]);
};
const handleDeleteRow = (tempId: string) => {
if (rules.length === 1) {
setError(t('rules.errorAtLeastOne'));
return;
}
setRules(rules.filter(r => r.tempId !== tempId));
};
const handleUpdateRow = (tempId: string, field: keyof Rule, value: any) => {
setError(null);
setSuccess(null);
setRules(
rules.map(r => {
if (r.tempId === tempId) {
let parsedValue = value;
if (field === 'minAchievement' || field === 'maxAchievement' || field === 'rate') {
parsedValue = parseFloat(value) || 0.0;
} else if (field === 'payoutAmount') {
parsedValue = parseFloat(value) || 0.0;
}
return { ...r, [field]: parsedValue };
}
return r;
})
);
};
const handleSaveRules = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSuccess(null);
// Validate boundaries
for (let i = 0; i < rules.length; i++) {
const r = rules[i];
if (r.minAchievement >= r.maxAchievement) {
setError(t('rules.errorBoundary').replace('{row}', String(i + 1)).replace('{min}', String(r.minAchievement)).replace('{max}', String(r.maxAchievement)));
return;
}
if (r.minAchievement < 0 || r.maxAchievement < 0 || r.rate < 0 || r.payoutAmount < 0) {
setError(t('rules.errorNegative').replace('{row}', String(i + 1)));
return;
}
}
try {
const res = await fetch(`/api/plans/${planId}/rules`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rules: rules.map(r => ({
type: r.type,
minAchievement: r.minAchievement,
maxAchievement: r.maxAchievement,
rate: r.rate,
payoutAmount: r.payoutAmount
}))
})
});
const data = await res.json();
if (!res.ok) {
setError(data.error || t('rules.errorSave'));
return;
}
setSuccess(t('rules.successSave'));
// Refresh current plan
fetchPlan();
} catch (err) {
setError(t('rules.errorNetwork'));
}
};
if (isLoading) {
return (
<div className={styles.container}>
<div style={{ textAlign: 'center', padding: '100px' }}>{t('rules.loading')}</div>
</div>
);
}
return (
<div className={styles.container}>
{/* Shared Dashboard Header */}
<Header activeTab="plans" />
<main className={styles.main}>
<div className={styles.titleArea}>
<Link href="/plans" className={styles.backLink}>
&larr; {t('rules.back')}
</Link>
<h1 className={styles.title}>{t('rules.title')}</h1>
<p className={styles.subtitle}>
{t('rules.subtitle').replace('{name}', plan?.name || '').replace('{code}', plan?.code || '').replace('{version}', String(plan?.version || '')).replace('{status}', t('status.' + plan?.status) || '')}
</p>
</div>
{error && <div className={styles.errorMsg} id="rules-error-msg">{error}</div>}
{success && <div className={styles.successMsg} id="rules-success-msg">{success}</div>}
<div className={styles.section}>
<form onSubmit={handleSaveRules}>
<div className={styles.tableHeader}>
<span>{t('rules.thType')}</span>
<span>{t('rules.thMin')}</span>
<span>{t('rules.thMax')}</span>
<span>{t('rules.thRate')}</span>
<span>{t('rules.thPayout')}</span>
<span></span>
</div>
<div className={styles.rows}>
{rules.map((rule, idx) => (
<div key={rule.tempId || idx} className={styles.row} data-rule-row={idx}>
<select
className={styles.input}
value={rule.type}
onChange={(e) => handleUpdateRow(rule.tempId!, 'type', e.target.value)}
id={`rule-type-${idx}`}
disabled={!(role === 'admin' || role === 'director')}
>
<option value="TIER">{t('rules.types.TIER')}</option>
<option value="BONUS">{t('rules.types.BONUS')}</option>
</select>
<input
type="number"
step="0.0001"
className={styles.input}
value={rule.minAchievement}
onChange={(e) => handleUpdateRow(rule.tempId!, 'minAchievement', e.target.value)}
id={`rule-min-${idx}`}
required
disabled={!(role === 'admin' || role === 'director')}
/>
<input
type="number"
step="0.0001"
className={styles.input}
value={rule.maxAchievement}
onChange={(e) => handleUpdateRow(rule.tempId!, 'maxAchievement', e.target.value)}
id={`rule-max-${idx}`}
required
disabled={!(role === 'admin' || role === 'director')}
/>
<input
type="number"
step="0.0001"
className={styles.input}
value={rule.rate}
onChange={(e) => handleUpdateRow(rule.tempId!, 'rate', e.target.value)}
id={`rule-rate-${idx}`}
required
disabled={!(role === 'admin' || role === 'director')}
/>
<input
type="number"
step="0.01"
className={styles.input}
value={rule.payoutAmount}
onChange={(e) => handleUpdateRow(rule.tempId!, 'payoutAmount', e.target.value)}
id={`rule-payout-${idx}`}
required
disabled={!(role === 'admin' || role === 'director')}
/>
{(role === 'admin' || role === 'director') && (
<button
type="button"
onClick={() => handleDeleteRow(rule.tempId!)}
className={styles.btnDelete}
id={`btn-delete-rule-${idx}`}
title={t('rules.delete')}
>
&times;
</button>
)}
</div>
))}
</div>
{(role === 'admin' || role === 'director') && (
<button
type="button"
onClick={handleAddRow}
className={styles.btnSecondary}
id="btn-add-rule"
>
{t('rules.addRule')}
</button>
)}
<div className={styles.footerActions}>
<Link href="/plans" className={styles.btnSecondary} style={{ marginRight: 'auto' }}>
{ (role === 'admin' || role === 'director') ? t('rules.cancel') : t('rules.backBtn') }
</Link>
{(role === 'admin' || role === 'director') && (
<button type="submit" className={styles.btnPrimary} id="btn-save-rules">
{t('rules.save')}
</button>
)}
</div>
</form>
</div>
</main>
</div>
);
}