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:
142
apps/frontend/src/features/expenses/components/ExpensesTable.tsx
Normal file
142
apps/frontend/src/features/expenses/components/ExpensesTable.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import type { Expense } from "@/lib/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CircleDollarSign } from "lucide-react";
|
||||
|
||||
interface ExpensesTableProps {
|
||||
data: Expense[];
|
||||
onPay: (expense: Expense) => void;
|
||||
}
|
||||
|
||||
export function ExpensesTable({ data, onPay }: ExpensesTableProps) {
|
||||
const columns = useMemo<ColumnDef<Expense>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Descripción",
|
||||
accessorKey: "description",
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium">{row.getValue("description")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Monto",
|
||||
accessorKey: "amount",
|
||||
cell: ({ row }) => {
|
||||
const amount = Number(row.getValue("amount"));
|
||||
return (
|
||||
<span className="tabular-nums">
|
||||
${amount.toLocaleString("es-AR", { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Vencimiento",
|
||||
accessorKey: "dueDate",
|
||||
cell: ({ row }) => {
|
||||
const dueDate = new Date(row.getValue("dueDate"));
|
||||
return (
|
||||
<span className="text-muted-foreground">
|
||||
{dueDate.toLocaleDateString("es-AR", {
|
||||
timeZone: "UTC",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Estado",
|
||||
accessorKey: "status",
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue("status");
|
||||
const isPayed = status === "PAYED";
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
isPayed
|
||||
? "inline-flex h-5 items-center rounded-md bg-green-100 px-1.5 text-xs font-medium text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
||||
: "inline-flex h-5 items-center rounded-md bg-yellow-100 px-1.5 text-xs font-medium text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
}
|
||||
>
|
||||
{isPayed ? "Pagado" : "Pendiente"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const expense = row.original;
|
||||
if (expense.status === "PAYED") return null;
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => onPay(expense)}
|
||||
>
|
||||
<CircleDollarSign className="size-3.5" />
|
||||
Pagar
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[onPay],
|
||||
);
|
||||
|
||||
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 para mostrar.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user