mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 11:28:47 +02:00
GET /api/v1/transactions/export streams a CSV (optional source/type/ date filters), cookie-authenticated so window.open downloads work. Buttons on the Budget transactions tab (all) and Salarios (SALARY only). Quoting and category resolution covered by tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
175 lines
6.3 KiB
TypeScript
175 lines
6.3 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { type ColumnDef } from '@tanstack/react-table';
|
|
import { Landmark, RefreshCw, Hash, CalendarDays, Banknote, Download } from 'lucide-react';
|
|
|
|
import { type Transaction, type SalariosSummary, getSalarios, getSalariosSummary } from '@/lib/api';
|
|
import { formatAmount, formatDate } from '@/lib/format';
|
|
import ErrorState from '@/components/ErrorState';
|
|
import { DataTable } from '@/components/ui/data-table';
|
|
import { DataTableColumnHeader } from '@/components/ui/data-table-column-header';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
export default function Salarios() {
|
|
const query = useQuery({
|
|
queryKey: ['salarios'],
|
|
queryFn: async () => {
|
|
const [depRes, sumRes] = await Promise.all([
|
|
getSalarios({ limit: 500 }),
|
|
getSalariosSummary(),
|
|
]);
|
|
return { deposits: depRes.data, summary: sumRes.data };
|
|
},
|
|
});
|
|
const deposits = query.data?.deposits ?? [];
|
|
const summary = query.data?.summary ?? null;
|
|
const loading = query.isFetching;
|
|
const fetchData = () => query.refetch();
|
|
|
|
const columns = useMemo<ColumnDef<Transaction, unknown>[]>(
|
|
() => [
|
|
{
|
|
accessorKey: 'date',
|
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Fecha" />,
|
|
cell: ({ row }) => {
|
|
const d = new Date(row.original.date);
|
|
return (
|
|
<div>
|
|
<span className="font-medium">{formatDate(row.original.date)}</span>
|
|
<span className="text-muted-foreground ml-1 text-xs">{d.getFullYear()}</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'merchant',
|
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Detalle" />,
|
|
cell: ({ row }) => (
|
|
<div>
|
|
<span className="font-medium">{row.original.merchant}</span>
|
|
{row.original.notes && (
|
|
<p className="text-xs text-muted-foreground truncate max-w-xs">{row.original.notes}</p>
|
|
)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'amount',
|
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Monto" />,
|
|
cell: ({ row }) => (
|
|
<span data-sensitive className="font-mono font-bold text-primary">
|
|
+{formatAmount(row.original.amount, row.original.currency)}
|
|
</span>
|
|
),
|
|
meta: { className: 'text-right' },
|
|
},
|
|
{
|
|
accessorKey: 'bank',
|
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Banco" />,
|
|
cell: ({ row }) => (
|
|
<Badge variant="outline">{row.original.bank}</Badge>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'reference',
|
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Comprobante" />,
|
|
cell: ({ row }) => (
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
{row.original.reference || '—'}
|
|
</span>
|
|
),
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
<Landmark className="w-5 h-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl font-bold font-heading">Salarios</h1>
|
|
<p className="text-sm text-muted-foreground">Historial de depósitos salariales</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => window.open('/api/v1/transactions/export?transaction_type=SALARY', '_blank')}
|
|
title="Descargar salarios como CSV"
|
|
>
|
|
<Download className="w-4 h-4 mr-2" aria-hidden="true" />
|
|
CSV
|
|
</Button>
|
|
<Button variant="ghost" size="icon" onClick={fetchData} title="Refresh" aria-label="Refresh">
|
|
<RefreshCw className={loading ? 'w-4 h-4 animate-spin' : 'w-4 h-4'} />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Summary cards */}
|
|
{summary && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
|
<Hash className="w-4 h-4" />
|
|
<span className="text-xs font-medium uppercase tracking-wider">Depósitos</span>
|
|
</div>
|
|
<span className="text-2xl font-bold font-mono">{summary.count}</span>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
|
<Banknote className="w-4 h-4" />
|
|
<span className="text-xs font-medium uppercase tracking-wider">Total acumulado</span>
|
|
</div>
|
|
<span data-sensitive className="text-2xl font-bold font-mono text-primary">
|
|
{formatAmount(summary.total_amount, 'CRC')}
|
|
</span>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
|
<CalendarDays className="w-4 h-4" />
|
|
<span className="text-xs font-medium uppercase tracking-wider">Último depósito</span>
|
|
</div>
|
|
<span className="text-2xl font-bold font-mono">
|
|
{summary.latest_date ? formatDate(summary.latest_date) : '—'}
|
|
</span>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Data table */}
|
|
{query.isError ? (
|
|
<ErrorState
|
|
message="No se pudieron cargar los salarios"
|
|
onRetry={fetchData}
|
|
/>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<DataTable
|
|
columns={columns}
|
|
data={deposits}
|
|
pagination
|
|
pageSize={25}
|
|
initialSorting={[{ id: 'date', desc: true }]}
|
|
emptyMessage="No hay depósitos registrados aún."
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|