feat(expenses): implement responsive dialog for expense management and create expense provider
- 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.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user