- Add Sales/Commissions routes to router (11 new routes) - Complete authStore (refreshTokens, updateProfile methods) - Add JSDoc documentation to 40+ components across all categories - Create FRONTEND-ROUTING.md with complete route mapping - Create FRONTEND-PAGES-SPEC.md with 38 page specifications - Update FRONTEND_INVENTORY.yml to v4.1.0 with resolved gaps Components documented: ai/, audit/, commissions/, feature-flags/, notifications/, sales/, storage/, webhooks/, whatsapp/ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
195 lines
7.3 KiB
TypeScript
195 lines
7.3 KiB
TypeScript
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import { CommissionEntry } from '../../services/commissions/entries.api';
|
|
import { EntryStatusBadge } from './EntryStatusBadge';
|
|
|
|
/**
|
|
* Props for the EntriesList component.
|
|
*/
|
|
interface EntriesListProps {
|
|
/** Array of commission entries to display in the table */
|
|
entries: CommissionEntry[];
|
|
/** Total number of entries across all pages */
|
|
total: number;
|
|
/** Current page number (1-indexed) */
|
|
page: number;
|
|
/** Total number of pages available */
|
|
totalPages: number;
|
|
/** Array of selected entry IDs for bulk operations */
|
|
selectedEntries: string[];
|
|
/** Callback when selection changes (for bulk approve/reject) */
|
|
onSelectionChange: (ids: string[]) => void;
|
|
/** Callback when page changes for pagination */
|
|
onPageChange: (page: number) => void;
|
|
/** Callback to refresh the entries list */
|
|
onRefresh: () => void;
|
|
}
|
|
|
|
/**
|
|
* Displays a paginated table of commission entries with selection support.
|
|
* Allows selecting pending entries for bulk approval/rejection operations.
|
|
* Shows entry details including date, user, reference, amounts, and status.
|
|
*
|
|
* @param props - The component props
|
|
* @param props.entries - Commission entries to display
|
|
* @param props.total - Total entry count for pagination info
|
|
* @param props.page - Current page number
|
|
* @param props.totalPages - Total pages for pagination controls
|
|
* @param props.selectedEntries - Currently selected entry IDs
|
|
* @param props.onSelectionChange - Handler for selection updates
|
|
* @param props.onPageChange - Handler for page navigation
|
|
* @param props.onRefresh - Handler to refresh data after operations
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const [selected, setSelected] = useState<string[]>([]);
|
|
* const [page, setPage] = useState(1);
|
|
*
|
|
* <EntriesList
|
|
* entries={commissionEntries}
|
|
* total={100}
|
|
* page={page}
|
|
* totalPages={10}
|
|
* selectedEntries={selected}
|
|
* onSelectionChange={setSelected}
|
|
* onPageChange={setPage}
|
|
* onRefresh={() => refetch()}
|
|
* />
|
|
* ```
|
|
*/
|
|
export function EntriesList({
|
|
entries,
|
|
total,
|
|
page,
|
|
totalPages,
|
|
selectedEntries,
|
|
onSelectionChange,
|
|
onPageChange,
|
|
}: EntriesListProps) {
|
|
const formatCurrency = (amount: number) =>
|
|
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
|
|
|
|
const handleSelectAll = () => {
|
|
const pendingIds = entries.filter((e) => e.status === 'pending').map((e) => e.id);
|
|
if (pendingIds.every((id) => selectedEntries.includes(id))) {
|
|
onSelectionChange(selectedEntries.filter((id) => !pendingIds.includes(id)));
|
|
} else {
|
|
onSelectionChange([...new Set([...selectedEntries, ...pendingIds])]);
|
|
}
|
|
};
|
|
|
|
const handleSelectOne = (id: string) => {
|
|
if (selectedEntries.includes(id)) {
|
|
onSelectionChange(selectedEntries.filter((i) => i !== id));
|
|
} else {
|
|
onSelectionChange([...selectedEntries, id]);
|
|
}
|
|
};
|
|
|
|
const pendingEntries = entries.filter((e) => e.status === 'pending');
|
|
const allPendingSelected = pendingEntries.length > 0 && pendingEntries.every((e) => selectedEntries.includes(e.id));
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b bg-gray-50">
|
|
<th className="py-3 px-4 w-10">
|
|
<input
|
|
type="checkbox"
|
|
checked={allPendingSelected}
|
|
onChange={handleSelectAll}
|
|
className="h-4 w-4 text-blue-600 rounded"
|
|
title="Select all pending"
|
|
/>
|
|
</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-gray-500">Date</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-gray-500">User</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-gray-500">Reference</th>
|
|
<th className="text-right py-3 px-4 text-sm font-medium text-gray-500">Base Amount</th>
|
|
<th className="text-right py-3 px-4 text-sm font-medium text-gray-500">Rate</th>
|
|
<th className="text-right py-3 px-4 text-sm font-medium text-gray-500">Commission</th>
|
|
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{entries.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={8} className="text-center py-8 text-gray-500">
|
|
No commission entries found
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
entries.map((entry) => (
|
|
<tr key={entry.id} className="border-b last:border-0 hover:bg-gray-50">
|
|
<td className="py-3 px-4">
|
|
{entry.status === 'pending' && (
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedEntries.includes(entry.id)}
|
|
onChange={() => handleSelectOne(entry.id)}
|
|
className="h-4 w-4 text-blue-600 rounded"
|
|
/>
|
|
)}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm">
|
|
{new Date(entry.created_at).toLocaleDateString()}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm">
|
|
{entry.user
|
|
? `${entry.user.first_name} ${entry.user.last_name}`
|
|
: entry.user_id.slice(0, 8)}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm capitalize">
|
|
{entry.reference_type}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-right">
|
|
{formatCurrency(entry.base_amount)}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-right">
|
|
{entry.rate_applied}%
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-right font-medium text-green-600">
|
|
{formatCurrency(entry.commission_amount)}
|
|
</td>
|
|
<td className="py-3 px-4 text-center">
|
|
<EntryStatusBadge status={entry.status} />
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center justify-between px-4 py-3 border-t">
|
|
<p className="text-sm text-gray-500">
|
|
Showing {entries.length} of {total} entries
|
|
</p>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => onPageChange(page - 1)}
|
|
disabled={page === 1}
|
|
className="p-1 rounded hover:bg-gray-100 disabled:opacity-50"
|
|
>
|
|
<ChevronLeft className="h-5 w-5" />
|
|
</button>
|
|
<span className="text-sm">
|
|
Page {page} of {totalPages}
|
|
</span>
|
|
<button
|
|
onClick={() => onPageChange(page + 1)}
|
|
disabled={page === totalPages}
|
|
className="p-1 rounded hover:bg-gray-100 disabled:opacity-50"
|
|
>
|
|
<ChevronRight className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|