- Added ResponsiveDialog component for handling responsive dialogs in the UI. - Created ExpensesProvider to manage state and API interactions for expenses. - Developed ExpensesTabContent to display expenses with filtering options and dialogs for new and pay expense actions. - Implemented ExpensesTable for rendering expense data in a tabular format with actions. - Added dialogs for creating new expenses and paying existing ones with form validation. - Introduced PeriodicExpenseForm for managing periodic expenses with month selection. - Created PeriodicExpensesTabContent to manage and display periodic expenses with create/edit functionality. - Added GenerateCurrentMonthDialog for confirming monthly expense generation. - Implemented PeriodicExpensesTable for displaying periodic expenses with edit and delete actions.
155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
import { useMemo } from "react";
|
|
import {
|
|
flexRender,
|
|
getCoreRowModel,
|
|
useReactTable,
|
|
type ColumnDef,
|
|
} from "@tanstack/react-table";
|
|
import type { PeriodicExpense } from "@/lib/api";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Pencil, Trash2 } from "lucide-react";
|
|
|
|
const monthLabels = [
|
|
"", "Ene", "Feb", "Mar", "Abr", "May", "Jun",
|
|
"Jul", "Ago", "Sep", "Oct", "Nov", "Dic",
|
|
];
|
|
|
|
function formatPeriods(periods: number[]): string {
|
|
if (periods.length === 0) return "";
|
|
if (periods.length === 12) return "Ene-Dic";
|
|
|
|
const sorted = [...periods].sort((a, b) => a - b);
|
|
const ranges: string[] = [];
|
|
let start = sorted[0];
|
|
let prev = sorted[0];
|
|
|
|
for (let i = 1; i <= sorted.length; i++) {
|
|
const curr = sorted[i];
|
|
if (curr === prev + 1) {
|
|
prev = curr;
|
|
} else {
|
|
ranges.push(start === prev ? monthLabels[start] : `${monthLabels[start]}-${monthLabels[prev]}`);
|
|
start = curr;
|
|
prev = curr;
|
|
}
|
|
}
|
|
|
|
return ranges.join(", ");
|
|
}
|
|
|
|
interface PeriodicExpensesTableProps {
|
|
data: PeriodicExpense[];
|
|
onEdit: (item: PeriodicExpense) => void;
|
|
onDelete: (id: number) => void;
|
|
}
|
|
|
|
export function PeriodicExpensesTable({ data, onEdit, onDelete }: PeriodicExpensesTableProps) {
|
|
const columns = useMemo<ColumnDef<PeriodicExpense>[]>(
|
|
() => [
|
|
{
|
|
header: "Descripción",
|
|
accessorKey: "description",
|
|
cell: ({ row }) => (
|
|
<span className="font-medium">{row.getValue("description")}</span>
|
|
),
|
|
},
|
|
{
|
|
header: "Monto",
|
|
accessorKey: "defaultAmount",
|
|
cell: ({ row }) => {
|
|
const amount = Number(row.getValue("defaultAmount"));
|
|
return (
|
|
<span className="tabular-nums">
|
|
${amount.toLocaleString("es-AR", { minimumFractionDigits: 2 })}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: "Vence",
|
|
accessorKey: "defaultDueDay",
|
|
cell: ({ row }) => <span>Día {row.getValue("defaultDueDay")}</span>,
|
|
},
|
|
{
|
|
header: "Meses",
|
|
accessorKey: "periods",
|
|
cell: ({ row }) => {
|
|
const periods: number[] = row.getValue("periods");
|
|
return (
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatPeriods(periods)}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => (
|
|
<div className="flex justify-end gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-xs"
|
|
onClick={() => onEdit(row.original)}
|
|
>
|
|
<Pencil className="size-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-xs"
|
|
onClick={() => onDelete(row.original.id)}
|
|
>
|
|
<Trash2 className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
),
|
|
},
|
|
],
|
|
[onEdit, onDelete],
|
|
);
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
});
|
|
|
|
return (
|
|
<div className="overflow-x-auto rounded-lg border">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<tr key={headerGroup.id} className="border-b bg-muted/50">
|
|
{headerGroup.headers.map((header) => (
|
|
<th
|
|
key={header.id}
|
|
className="px-3 py-2 text-left text-xs font-medium text-muted-foreground"
|
|
>
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
<tbody>
|
|
{table.getRowModel().rows.map((row) => (
|
|
<tr key={row.id} className="border-b last:border-0 hover:bg-muted/30">
|
|
{row.getVisibleCells().map((cell) => (
|
|
<td key={cell.id} className="px-3 py-2.5">
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
{table.getRowModel().rows.length === 0 && (
|
|
<tr>
|
|
<td colSpan={columns.length} className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
No hay gastos periódicos todavía.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|