Add DEPOSITO transaction type and Salarios page
All checks were successful
Deploy to VPS / deploy (push) Successful in 21s

New TransactionType.DEPOSITO for salary deposits from n8n/Gmail flow.
New /salarios endpoint with summary. New top-level Salarios page with
DataTable and summary cards. Push notifications link to /salarios for
deposits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-03-26 22:57:41 -06:00
parent 8d76059ae8
commit 9cfa1c4eb1
9 changed files with 247 additions and 7 deletions

View File

@@ -10,6 +10,7 @@ import {
Check,
X,
BellRing,
Landmark,
} from 'lucide-react';
import api, { type Account, type Transaction } from '../api';
@@ -293,9 +294,9 @@ export default function Dashboard() {
<div className="flex items-center gap-3 min-w-0">
<div className={cn(
'w-8 h-8 rounded-lg flex items-center justify-center shrink-0',
tx.transaction_type === 'DEVOLUCION' ? 'bg-primary/10 text-primary' : 'bg-destructive/10 text-destructive'
tx.transaction_type === 'COMPRA' ? 'bg-destructive/10 text-destructive' : 'bg-primary/10 text-primary'
)}>
{tx.transaction_type === 'DEVOLUCION' ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
{tx.transaction_type === 'DEPOSITO' ? <Landmark className="w-4 h-4" /> : tx.transaction_type === 'DEVOLUCION' ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
</div>
<div className="min-w-0">
<p className="text-sm font-medium truncate">{tx.merchant}</p>
@@ -307,9 +308,9 @@ export default function Dashboard() {
</div>
<span className={cn(
'font-mono text-sm font-medium shrink-0 ml-4',
tx.transaction_type === 'DEVOLUCION' && 'text-primary'
tx.transaction_type !== 'COMPRA' && 'text-primary'
)}>
{tx.transaction_type === 'DEVOLUCION' ? '+' : '-'}{formatAmount(tx.amount, tx.currency)}
{tx.transaction_type === 'COMPRA' ? '-' : '+'}{formatAmount(tx.amount, tx.currency)}
</span>
</div>
))}

View File

@@ -0,0 +1,163 @@
import { useEffect, useMemo, useState } from 'react';
import { type ColumnDef } from '@tanstack/react-table';
import { Landmark, RefreshCw, Hash, CalendarDays, Banknote } from 'lucide-react';
import { type Transaction, type SalariosSummary, getSalarios, getSalariosSummary } from '../api';
import { formatAmount, formatDate } from '@/lib/format';
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 [deposits, setDeposits] = useState<Transaction[]>([]);
const [summary, setSummary] = useState<SalariosSummary | null>(null);
const [loading, setLoading] = useState(true);
const fetchData = async () => {
setLoading(true);
try {
const [depRes, sumRes] = await Promise.all([
getSalarios({ limit: 500 }),
getSalariosSummary(),
]);
setDeposits(depRes.data);
setSummary(sumRes.data);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchData(); }, []);
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 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="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 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 */}
<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>
);
}