Improve expenses mobile layout and payment flow
This commit is contained in:
@@ -13,7 +13,7 @@ export function Layout() {
|
|||||||
<Sidebar open={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
<Sidebar open={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<Header onMenuClick={() => setSidebarOpen(true)} />
|
<Header onMenuClick={() => setSidebarOpen(true)} />
|
||||||
<main className="flex-1 p-6">
|
<main className="flex-1 px-3 py-4 sm:p-6">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ function ExpensesPageInner() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h1 className="text-2xl font-semibold">Gastos</h1>
|
<h1 className="text-2xl font-semibold text-balance">Gastos</h1>
|
||||||
|
|
||||||
<div className="flex gap-1 border-b">
|
<div className="flex gap-1 overflow-x-auto border-b">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@@ -25,8 +25,8 @@ function ExpensesPageInner() {
|
|||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={
|
className={
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? "-mb-px border-b-2 border-primary px-3 pb-2 text-sm font-medium text-foreground"
|
? "-mb-px shrink-0 border-b-2 border-primary px-3 pb-2 text-sm font-medium text-foreground"
|
||||||
: "px-3 pb-2 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
|
: "shrink-0 px-3 pb-2 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
|||||||
@@ -64,21 +64,22 @@ export function ExpensesTabContent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
<div className="space-y-3 sm:flex sm:flex-wrap sm:items-center sm:justify-between sm:gap-2 sm:space-y-0">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1 overflow-x-auto pb-1 sm:pb-0">
|
||||||
{filters.map((f) => (
|
{filters.map((f) => (
|
||||||
<Button
|
<Button
|
||||||
key={f.value}
|
key={f.value}
|
||||||
variant={statusFilter === f.value ? "default" : "outline"}
|
variant={statusFilter === f.value ? "default" : "outline"}
|
||||||
size="xs"
|
size="xs"
|
||||||
|
className="shrink-0"
|
||||||
onClick={() => { setStatusFilter(f.value); setPage(1); }}
|
onClick={() => { setStatusFilter(f.value); setPage(1); }}
|
||||||
>
|
>
|
||||||
{f.label}
|
{f.label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="grid gap-2 sm:flex sm:items-center">
|
||||||
<div className="relative w-48">
|
<div className="relative min-w-0 sm:w-48">
|
||||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Buscar por descripción..."
|
placeholder="Buscar por descripción..."
|
||||||
@@ -103,7 +104,7 @@ export function ExpensesTabContent() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-52">
|
<SelectTrigger className="w-full sm:w-52">
|
||||||
<SelectValue placeholder="Todos los gastos" />
|
<SelectValue placeholder="Todos los gastos" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -115,16 +116,18 @@ export function ExpensesTabContent() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button onClick={() => setImportDialogOpen(true)} variant="outline" size="sm">
|
<div className="grid grid-cols-2 gap-2 sm:flex">
|
||||||
|
<Button onClick={() => setImportDialogOpen(true)} variant="outline" size="sm" className="min-w-0">
|
||||||
<Upload className="size-4" />
|
<Upload className="size-4" />
|
||||||
Importar
|
<span className="truncate">Importar</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setNewExpenseOpen(true)} size="sm">
|
<Button onClick={() => setNewExpenseOpen(true)} size="sm" className="min-w-0">
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
Nuevo gasto
|
<span className="truncate">Nuevo gasto</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isLoadingExpenses ? (
|
{isLoadingExpenses ? (
|
||||||
<p className="text-sm text-muted-foreground">Cargando...</p>
|
<p className="text-sm text-muted-foreground">Cargando...</p>
|
||||||
|
|||||||
@@ -26,6 +26,38 @@ interface ExpensesTableProps {
|
|||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatExpenseDate(value: string) {
|
||||||
|
return new Date(value).toLocaleDateString("es-AR", {
|
||||||
|
timeZone: "UTC",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatExpenseAmount(expense: Expense) {
|
||||||
|
const isPayed = expense.status === "PAYED";
|
||||||
|
const value = isPayed && expense.amountPayed ? Number(expense.amountPayed) : Number(expense.amount);
|
||||||
|
|
||||||
|
return `$${value.toLocaleString("es-AR", { minimumFractionDigits: 2 })}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExpenseStatusBadge({ status }: { status: Expense["status"] }) {
|
||||||
|
const isPayed = status === "PAYED";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
isPayed
|
||||||
|
? "inline-flex h-5 shrink-0 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 shrink-0 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ExpensesTable({ data, onPay, page, total, pageSize, onPageChange }: ExpensesTableProps) {
|
export function ExpensesTable({ data, onPay, page, total, pageSize, onPageChange }: ExpensesTableProps) {
|
||||||
const columns = useMemo<ColumnDef<Expense>[]>(
|
const columns = useMemo<ColumnDef<Expense>[]>(
|
||||||
() => [
|
() => [
|
||||||
@@ -41,11 +73,9 @@ export function ExpensesTable({ data, onPay, page, total, pageSize, onPageChange
|
|||||||
accessorKey: "amount",
|
accessorKey: "amount",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const expense = row.original;
|
const expense = row.original;
|
||||||
const isPayed = expense.status === "PAYED";
|
|
||||||
const value = isPayed && expense.amountPayed ? Number(expense.amountPayed) : Number(expense.amount);
|
|
||||||
return (
|
return (
|
||||||
<span className="tabular-nums">
|
<span className="tabular-nums">
|
||||||
${value.toLocaleString("es-AR", { minimumFractionDigits: 2 })}
|
{formatExpenseAmount(expense)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -54,15 +84,9 @@ export function ExpensesTable({ data, onPay, page, total, pageSize, onPageChange
|
|||||||
header: "Vencimiento",
|
header: "Vencimiento",
|
||||||
accessorKey: "dueDate",
|
accessorKey: "dueDate",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const dueDate = new Date(row.getValue("dueDate"));
|
|
||||||
return (
|
return (
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{dueDate.toLocaleDateString("es-AR", {
|
{formatExpenseDate(row.getValue("dueDate") as string)}
|
||||||
timeZone: "UTC",
|
|
||||||
day: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -71,19 +95,7 @@ export function ExpensesTable({ data, onPay, page, total, pageSize, onPageChange
|
|||||||
header: "Estado",
|
header: "Estado",
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.getValue("status");
|
return <ExpenseStatusBadge status={row.getValue("status") as Expense["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>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -94,15 +106,9 @@ export function ExpensesTable({ data, onPay, page, total, pageSize, onPageChange
|
|||||||
if (!paymentDate) {
|
if (!paymentDate) {
|
||||||
return <span className="text-muted-foreground">--</span>;
|
return <span className="text-muted-foreground">--</span>;
|
||||||
}
|
}
|
||||||
const date = new Date(paymentDate as string);
|
|
||||||
return (
|
return (
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{date.toLocaleDateString("es-AR", {
|
{formatExpenseDate(paymentDate as string)}
|
||||||
timeZone: "UTC",
|
|
||||||
day: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -141,10 +147,81 @@ export function ExpensesTable({ data, onPay, page, total, pageSize, onPageChange
|
|||||||
const currentGroup = Math.floor((page - 1) / GROUP_SIZE);
|
const currentGroup = Math.floor((page - 1) / GROUP_SIZE);
|
||||||
const startPage = currentGroup * GROUP_SIZE + 1;
|
const startPage = currentGroup * GROUP_SIZE + 1;
|
||||||
const endPage = Math.min(startPage + GROUP_SIZE - 1, totalPages);
|
const endPage = Math.min(startPage + GROUP_SIZE - 1, totalPages);
|
||||||
|
const firstItem = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
||||||
|
const lastItem = Math.min(page * pageSize, total);
|
||||||
|
const mobilePagination = total > 0 ? (
|
||||||
|
<div className="rounded-lg border bg-card p-2 sm:hidden">
|
||||||
|
<div className="mb-2 text-center text-xs text-muted-foreground tabular-nums">
|
||||||
|
{firstItem}-{lastItem} de {total} · Página {page} de {totalPages}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={page <= 1}
|
||||||
|
onClick={() => onPageChange(page - 1)}
|
||||||
|
>
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
onClick={() => onPageChange(page + 1)}
|
||||||
|
>
|
||||||
|
Siguiente
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="overflow-x-auto rounded-lg border">
|
{mobilePagination}
|
||||||
|
|
||||||
|
<div className="space-y-2 sm:hidden">
|
||||||
|
{data.map((expense) => (
|
||||||
|
<div key={expense.id} className="rounded-lg border bg-card p-3 text-sm">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate font-medium">{expense.description}</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Vence {formatExpenseDate(expense.dueDate)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ExpenseStatusBadge status={expense.status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex items-end justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-lg font-semibold tabular-nums">{formatExpenseAmount(expense)}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{expense.paymentDate ? `Pagado ${formatExpenseDate(expense.paymentDate)}` : "Sin fecha de pago"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expense.status !== "PAYED" && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0"
|
||||||
|
onClick={() => onPay(expense)}
|
||||||
|
>
|
||||||
|
<CircleDollarSign className="size-3.5" />
|
||||||
|
Pagar
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{data.length === 0 && (
|
||||||
|
<div className="rounded-lg border px-3 py-8 text-center text-sm text-muted-foreground">
|
||||||
|
No hay gastos para mostrar.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden overflow-x-auto rounded-lg border sm:block">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -181,8 +258,10 @@ export function ExpensesTable({ data, onPay, page, total, pageSize, onPageChange
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{mobilePagination}
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Pagination>
|
<Pagination className="hidden sm:flex">
|
||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -29,6 +29,7 @@ interface PayExpenseDialogProps {
|
|||||||
|
|
||||||
export function PayExpenseDialog({ expense, open, onOpenChange }: PayExpenseDialogProps) {
|
export function PayExpenseDialog({ expense, open, onOpenChange }: PayExpenseDialogProps) {
|
||||||
const { payExpense } = useExpensesContext();
|
const { payExpense } = useExpensesContext();
|
||||||
|
const amountInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -43,6 +44,9 @@ export function PayExpenseDialog({ expense, open, onOpenChange }: PayExpenseDial
|
|||||||
paymentDate: undefined,
|
paymentDate: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const { ref: amountPayedRef, ...amountPayedField } = register("amountPayed", {
|
||||||
|
setValueAs: (value) => Number(value),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (expense) {
|
if (expense) {
|
||||||
@@ -53,6 +57,17 @@ export function PayExpenseDialog({ expense, open, onOpenChange }: PayExpenseDial
|
|||||||
}
|
}
|
||||||
}, [expense, reset]);
|
}, [expense, reset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !expense) return;
|
||||||
|
|
||||||
|
const frame = requestAnimationFrame(() => {
|
||||||
|
amountInputRef.current?.focus();
|
||||||
|
amountInputRef.current?.select();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => cancelAnimationFrame(frame);
|
||||||
|
}, [open, expense]);
|
||||||
|
|
||||||
function handleClose(open: boolean) {
|
function handleClose(open: boolean) {
|
||||||
onOpenChange(open);
|
onOpenChange(open);
|
||||||
if (!open) reset();
|
if (!open) reset();
|
||||||
@@ -90,10 +105,14 @@ export function PayExpenseDialog({ expense, open, onOpenChange }: PayExpenseDial
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="amountPayed"
|
id="amountPayed"
|
||||||
type="number"
|
type="text"
|
||||||
step="0.01"
|
inputMode="numeric"
|
||||||
min="0"
|
pattern="[0-9]*"
|
||||||
{...register("amountPayed", { valueAsNumber: true })}
|
{...amountPayedField}
|
||||||
|
ref={(node) => {
|
||||||
|
amountPayedRef(node);
|
||||||
|
amountInputRef.current = node;
|
||||||
|
}}
|
||||||
className="h-8 rounded-lg border border-input bg-background px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
className="h-8 rounded-lg border border-input bg-background px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||||
/>
|
/>
|
||||||
{errors.amountPayed && (
|
{errors.amountPayed && (
|
||||||
|
|||||||
@@ -92,18 +92,18 @@ export function PeriodicExpensesTabContent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-3 sm:flex sm:items-center sm:justify-between sm:space-y-0">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Administrá tus gastos recurrentes.
|
Administrá tus gastos recurrentes.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="grid grid-cols-2 gap-2 sm:flex">
|
||||||
<Button onClick={() => setImportDialogOpen(true)} variant="outline" size="sm">
|
<Button onClick={() => setImportDialogOpen(true)} variant="outline" size="sm" className="min-w-0">
|
||||||
<Upload className="size-4" />
|
<Upload className="size-4" />
|
||||||
Importar
|
<span className="truncate">Importar</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={openCreate} size="sm">
|
<Button onClick={openCreate} size="sm" className="min-w-0">
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
Nuevo
|
<span className="truncate">Nuevo</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -114,7 +114,57 @@ export function PeriodicExpensesTable({ data, onEdit, onDelete }: PeriodicExpens
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto rounded-lg border">
|
<>
|
||||||
|
<div className="space-y-2 sm:hidden">
|
||||||
|
{data.map((item) => (
|
||||||
|
<div key={item.id} className="rounded-lg border bg-card p-3 text-sm">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate font-medium">{item.description}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">
|
||||||
|
${Number(item.defaultAmount).toLocaleString("es-AR", { minimumFractionDigits: 2 })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label={`Editar ${item.description}`}
|
||||||
|
onClick={() => onEdit(item)}
|
||||||
|
>
|
||||||
|
<Pencil className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label={`Eliminar ${item.description}`}
|
||||||
|
onClick={() => onDelete(item.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||||
|
<div className="rounded-md bg-muted/40 px-2 py-1.5">
|
||||||
|
<span className="block">Vence</span>
|
||||||
|
<span className="font-medium text-foreground">Día {item.defaultDueDay}</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 rounded-md bg-muted/40 px-2 py-1.5">
|
||||||
|
<span className="block">Meses</span>
|
||||||
|
<span className="block truncate font-medium text-foreground">{formatPeriods(item.periods)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{data.length === 0 && (
|
||||||
|
<div className="rounded-lg border px-3 py-8 text-center text-sm text-muted-foreground">
|
||||||
|
No hay gastos periódicos todavía.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden overflow-x-auto rounded-lg border sm:block">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -150,5 +200,6 @@ export function PeriodicExpensesTable({ data, onEdit, onDelete }: PeriodicExpens
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { keepPreviousData, useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
getQuotes, fetchQuotes, getQuoteHistory, getDailyMinMax, getDailyQuotes,
|
getQuotes, fetchQuotes, getQuoteHistory, getDailyMinMax, getDailyQuotes,
|
||||||
getPeriodicExpenses, createPeriodicExpense as createPeriodicExpenseApi,
|
getPeriodicExpenses, createPeriodicExpense as createPeriodicExpenseApi,
|
||||||
@@ -135,6 +135,7 @@ export function useGenerateMonthlyExpense() {
|
|||||||
export function useExpenses(status: string, page: number = 1, pageSize: number = 10, periodicExpenseId?: number, search?: string) {
|
export function useExpenses(status: string, page: number = 1, pageSize: number = 10, periodicExpenseId?: number, search?: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: expenseKeys.byFilters(status, page, pageSize, periodicExpenseId, search),
|
queryKey: expenseKeys.byFilters(status, page, pageSize, periodicExpenseId, search),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
queryFn: () => getExpenses(
|
queryFn: () => getExpenses(
|
||||||
status === "all" ? undefined : status,
|
status === "all" ? undefined : status,
|
||||||
page,
|
page,
|
||||||
|
|||||||
Reference in New Issue
Block a user