277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import Header from '@/components/Header';
|
|
import { useLocale } from '@/lib/i18n/LocaleContext';
|
|
import { useUser } from '@/lib/auth/UserContext';
|
|
import styles from './page.module.css';
|
|
|
|
interface Settlement {
|
|
id: number;
|
|
period: string;
|
|
salesAmount: string | number;
|
|
goalAmount: string | number;
|
|
achievementPercentage: string | number;
|
|
calculatedCommission: string | number;
|
|
calculatedBonus: string | number;
|
|
adjustmentAmount: string | number;
|
|
totalPayout: string | number;
|
|
status: string;
|
|
aiAuditNotes: any;
|
|
colaborador: {
|
|
username: string;
|
|
email: string;
|
|
hotel: {
|
|
name: string;
|
|
};
|
|
};
|
|
plan: {
|
|
name: string;
|
|
code: string;
|
|
};
|
|
}
|
|
|
|
interface UserOption {
|
|
id: number;
|
|
username: string;
|
|
}
|
|
|
|
export default function HistoryPage() {
|
|
const { t, locale } = useLocale();
|
|
const { role, user } = useUser();
|
|
const [settlements, setSettlements] = useState<Settlement[]>([]);
|
|
const [users, setUsers] = useState<UserOption[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// Filters state
|
|
const [period, setPeriod] = useState('');
|
|
const [status, setStatus] = useState('');
|
|
const [selectedUserId, setSelectedUserId] = useState('');
|
|
const [expandedRowId, setExpandedRowId] = useState<number | null>(null);
|
|
|
|
const fetchSettlements = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const queryParams = new URLSearchParams();
|
|
if (period) queryParams.append('period', period);
|
|
if (status) queryParams.append('status', status);
|
|
if (selectedUserId) queryParams.append('userId', selectedUserId);
|
|
|
|
const res = await fetch(`/api/settlements?${queryParams.toString()}`);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setSettlements(data.settlements || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch settlements', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchUsers = async () => {
|
|
if (role !== 'admin' && role !== 'director') return;
|
|
try {
|
|
const res = await fetch('/api/users');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setUsers(data.users || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch users', err);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchSettlements();
|
|
}, [period, status, selectedUserId]);
|
|
|
|
useEffect(() => {
|
|
fetchUsers();
|
|
}, [role]);
|
|
|
|
const toggleRow = (id: number) => {
|
|
if (expandedRowId === id) {
|
|
setExpandedRowId(null);
|
|
} else {
|
|
setExpandedRowId(id);
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (val: string | number) => {
|
|
const num = typeof val === 'string' ? parseFloat(val) : val;
|
|
if (isNaN(num)) return '$0.00';
|
|
return new Intl.NumberFormat(locale === 'es' ? 'es-CO' : 'en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 2
|
|
}).format(num);
|
|
};
|
|
|
|
const formatPercent = (val: string | number) => {
|
|
const num = typeof val === 'string' ? parseFloat(val) : val;
|
|
if (isNaN(num)) return '0%';
|
|
return `${(num * 100).toFixed(1)}%`;
|
|
};
|
|
|
|
const getAuditNotes = (notes: any) => {
|
|
if (!notes) return null;
|
|
if (typeof notes === 'string') return notes;
|
|
if (notes[locale]) return notes[locale];
|
|
if (notes.es) return notes.es;
|
|
if (notes.en) return notes.en;
|
|
return JSON.stringify(notes);
|
|
};
|
|
|
|
const handlePrint = () => {
|
|
window.print();
|
|
};
|
|
|
|
const showUserFilter = role === 'admin' || role === 'director';
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<Header activeTab="none" />
|
|
<main className={styles.main}>
|
|
<div className={styles.titleSection}>
|
|
<h1 className={styles.title}>{t('history.title')}</h1>
|
|
<button onClick={handlePrint} className={styles.btnPrimary} id="btn-print-pdf">
|
|
{t('history.download_pdf')}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filter Card */}
|
|
<div className={styles.filterCard}>
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>{t('history.filters')}</label>
|
|
<input
|
|
type="text"
|
|
placeholder="YYYY-MM (e.g. 2026-06)"
|
|
className={styles.input}
|
|
value={period}
|
|
onChange={(e) => setPeriod(e.target.value)}
|
|
id="filter-period"
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>{t('history.status')}</label>
|
|
<select
|
|
className={styles.select}
|
|
value={status}
|
|
onChange={(e) => setStatus(e.target.value)}
|
|
id="filter-status"
|
|
>
|
|
<option value="">{t('history.all_statuses')}</option>
|
|
<option value="APPROVED">{t('status.APPROVED')}</option>
|
|
<option value="PENDING">{t('status.PENDING')}</option>
|
|
<option value="REJECTED">{t('status.REJECTED')}</option>
|
|
<option value="SIMULATED">{t('status.SIMULATED')}</option>
|
|
</select>
|
|
</div>
|
|
|
|
{showUserFilter && (
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>{t('simulation.thCollaborator')}</label>
|
|
<select
|
|
className={styles.select}
|
|
value={selectedUserId}
|
|
onChange={(e) => setSelectedUserId(e.target.value)}
|
|
id="filter-collaborator"
|
|
>
|
|
<option value="">{t('history.all_statuses')}</option>
|
|
{users.map((u) => (
|
|
<option key={u.id} value={u.id}>
|
|
{u.username}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* History Table */}
|
|
{isLoading ? (
|
|
<div className={styles.loading}>{t('history.loading')}</div>
|
|
) : settlements.length === 0 ? (
|
|
<div className={styles.emptyState}>{t('history.empty')}</div>
|
|
) : (
|
|
<div className={styles.tableContainer}>
|
|
<table className={styles.table}>
|
|
<thead>
|
|
<tr>
|
|
<th className={styles.th}>{t('history.period')}</th>
|
|
{showUserFilter && <th className={styles.th}>{t('simulation.thCollaborator')}</th>}
|
|
<th className={styles.th}>{t('simulation.thPlan')}</th>
|
|
<th className={styles.th + ' ' + styles.thAmount}>{t('history.goal')}</th>
|
|
<th className={styles.th + ' ' + styles.thAmount}>{t('history.sales')}</th>
|
|
<th className={styles.th + ' ' + styles.thAmount}>%</th>
|
|
<th className={styles.th + ' ' + styles.thAmount}>{t('history.commission')}</th>
|
|
<th className={styles.th + ' ' + styles.thAmount}>{t('history.adjustment')}</th>
|
|
<th className={styles.th + ' ' + styles.thAmount}>{t('history.payout')}</th>
|
|
<th className={styles.th}>{t('history.status')}</th>
|
|
<th className={styles.th}>{t('simulation.thAiAudit')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{settlements.map((item) => (
|
|
<React.Fragment key={item.id}>
|
|
<tr className={styles.tr} data-settlement-id={item.id}>
|
|
<td className={styles.td}>{item.period}</td>
|
|
{showUserFilter && (
|
|
<td className={styles.td}>
|
|
<div><strong>{item.colaborador.username}</strong></div>
|
|
<div style={{ fontSize: '0.75rem', opacity: 0.7 }}>
|
|
{item.colaborador.hotel.name}
|
|
</div>
|
|
</td>
|
|
)}
|
|
<td className={styles.td}>
|
|
<div>{item.plan.name}</div>
|
|
<div style={{ fontSize: '0.75rem', opacity: 0.7 }}>{item.plan.code}</div>
|
|
</td>
|
|
<td className={styles.td + ' ' + styles.amount}>{formatCurrency(item.goalAmount)}</td>
|
|
<td className={styles.td + ' ' + styles.amount}>{formatCurrency(item.salesAmount)}</td>
|
|
<td className={styles.td + ' ' + styles.amount}>{formatPercent(item.achievementPercentage)}</td>
|
|
<td className={styles.td + ' ' + styles.amount}>{formatCurrency(item.calculatedCommission)}</td>
|
|
<td className={styles.td + ' ' + styles.amount}>{formatCurrency(item.adjustmentAmount)}</td>
|
|
<td className={styles.td + ' ' + styles.amount}><strong>{formatCurrency(item.totalPayout)}</strong></td>
|
|
<td className={styles.td}>
|
|
<span className={`${styles.badge} ${styles['badge' + item.status]}`}>
|
|
{t(`status.${item.status}`)}
|
|
</span>
|
|
</td>
|
|
<td className={styles.td}>
|
|
{item.aiAuditNotes ? (
|
|
<button
|
|
onClick={() => toggleRow(item.id)}
|
|
className={styles.expandBtn}
|
|
id={`btn-toggle-notes-${item.id}`}
|
|
>
|
|
{expandedRowId === item.id ? t('history.hideNotes') : t('history.showNotes')}
|
|
</button>
|
|
) : (
|
|
<span style={{ fontSize: '0.75rem', opacity: 0.5 }}>-</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
{expandedRowId === item.id && item.aiAuditNotes && (
|
|
<tr className={styles.expandedRow}>
|
|
<td colSpan={showUserFilter ? 11 : 10}>
|
|
<div className={styles.auditNotesBox}>
|
|
<h4 className={styles.auditNotesTitle}>{t('history.audit_notes')}</h4>
|
|
<p className={styles.auditNotesText}>{getAuditNotes(item.aiAuditNotes)}</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|