feat(dashboard): add pending/upcoming expenses table
- Backend: GET /expenses/pending-upcoming returns PENDING expenses with dueDate <= today+5 days, classified as overdue/upcoming - DashboardExpensesTable: groups by periodicExpenseId, shows summed totals with status badges, links to /expenses?periodicExpenseId=X - Expenses route accepts ?periodicExpenseId search param and pre-selects it in the filter - fix: BeloAnalysisPage DatePicker type mismatch
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
.vite
|
.vite
|
||||||
|
.tanstack
|
||||||
@@ -10,6 +10,7 @@ import { payExpenseHandler } from "./handlers/payExpense";
|
|||||||
import { getMonthlyPayedTotalHandler } from "./handlers/getMonthlyPayedTotal";
|
import { getMonthlyPayedTotalHandler } from "./handlers/getMonthlyPayedTotal";
|
||||||
import { getMonthlyTotalsHandler } from "./handlers/getMonthlyTotals";
|
import { getMonthlyTotalsHandler } from "./handlers/getMonthlyTotals";
|
||||||
import { getTotalPendingHandler } from "./handlers/getTotalPending";
|
import { getTotalPendingHandler } from "./handlers/getTotalPending";
|
||||||
|
import { getPendingUpcomingExpensesHandler } from "./handlers/getPendingUpcomingExpenses";
|
||||||
import { importPeriodicExpensesHandler } from "./handlers/importPeriodicExpenses";
|
import { importPeriodicExpensesHandler } from "./handlers/importPeriodicExpenses";
|
||||||
import { importExpensesHandler } from "./handlers/importExpenses";
|
import { importExpensesHandler } from "./handlers/importExpenses";
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ app.post("/periodic-expenses/:id/generate-month", generateMonthlyExpenseHandler)
|
|||||||
|
|
||||||
app.get("/expenses/monthly-total", getMonthlyPayedTotalHandler);
|
app.get("/expenses/monthly-total", getMonthlyPayedTotalHandler);
|
||||||
app.get("/expenses/totals", getMonthlyTotalsHandler);
|
app.get("/expenses/totals", getMonthlyTotalsHandler);
|
||||||
|
app.get("/expenses/pending-upcoming", getPendingUpcomingExpensesHandler);
|
||||||
app.get("/expenses/pending-total", getTotalPendingHandler);
|
app.get("/expenses/pending-total", getTotalPendingHandler);
|
||||||
app.get("/expenses", listExpensesHandler);
|
app.get("/expenses", listExpensesHandler);
|
||||||
app.post("/expenses", createNonPeriodicExpenseHandler);
|
app.post("/expenses", createNonPeriodicExpenseHandler);
|
||||||
|
|||||||
@@ -216,6 +216,27 @@ export async function getTotalPending() {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listPendingUpcomingExpenses() {
|
||||||
|
const now = new Date();
|
||||||
|
const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0));
|
||||||
|
const fiveDaysLater = new Date(today);
|
||||||
|
fiveDaysLater.setUTCDate(fiveDaysLater.getUTCDate() + 5);
|
||||||
|
|
||||||
|
const data = await prisma.expense.findMany({
|
||||||
|
where: {
|
||||||
|
status: PaymentStatus.PENDING,
|
||||||
|
dueDate: { lte: fiveDaysLater },
|
||||||
|
},
|
||||||
|
include: { periodicExpense: true },
|
||||||
|
orderBy: { dueDate: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.map((expense) => ({
|
||||||
|
...expense,
|
||||||
|
dueType: expense.dueDate < today ? "overdue" as const : "upcoming" as const,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateExpensesForCurrentMonth() {
|
export async function generateExpensesForCurrentMonth() {
|
||||||
const { year, month } = getCurrentYearMonthUTC();
|
const { year, month } = getCurrentYearMonthUTC();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Context } from "hono";
|
||||||
|
import { listPendingUpcomingExpenses } from "../expenses.service";
|
||||||
|
|
||||||
|
export async function getPendingUpcomingExpensesHandler(c: Context) {
|
||||||
|
const result = await listPendingUpcomingExpenses();
|
||||||
|
return c.json(result);
|
||||||
|
}
|
||||||
@@ -133,11 +133,11 @@ export function BeloAnalysisPage() {
|
|||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-xs text-muted-foreground">Desde</label>
|
<label className="text-xs text-muted-foreground">Desde</label>
|
||||||
<DatePicker value={startDate} onChange={setStartDate} placeholder="Fecha inicio" />
|
<DatePicker value={startDate} onChange={(d) => d && setStartDate(d)} placeholder="Fecha inicio" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-xs text-muted-foreground">Hasta</label>
|
<label className="text-xs text-muted-foreground">Hasta</label>
|
||||||
<DatePicker value={endDate} onChange={setEndDate} placeholder="Fecha fin" />
|
<DatePicker value={endDate} onChange={(d) => d && setEndDate(d)} placeholder="Fecha fin" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { DashboardExpensesTable } from "./components/DashboardExpensesTable";
|
||||||
|
|
||||||
const priceFormatter = new Intl.NumberFormat("es-AR", {
|
const priceFormatter = new Intl.NumberFormat("es-AR", {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
@@ -133,6 +134,7 @@ export function DashboardPage() {
|
|||||||
<QuoteCard quote={belo} title="BELO" to="/quotes/belo" variant="minimal" />
|
<QuoteCard quote={belo} title="BELO" to="/quotes/belo" variant="minimal" />
|
||||||
<QuoteCard quote={blue} title="BLUE" variant="minimal" />
|
<QuoteCard quote={blue} title="BLUE" variant="minimal" />
|
||||||
</div>
|
</div>
|
||||||
|
<DashboardExpensesTable />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { CalendarClock, ExternalLink } from "lucide-react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { usePendingUpcomingExpenses } from "@/lib/queries";
|
||||||
|
import type { PendingUpcomingExpense } from "@/lib/api";
|
||||||
|
|
||||||
|
type GroupedExpense = {
|
||||||
|
periodicExpenseId: number;
|
||||||
|
description: string;
|
||||||
|
totalAmount: number;
|
||||||
|
earliestDueDate: string;
|
||||||
|
dueType: "overdue" | "upcoming";
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function groupExpenses(expenses: PendingUpcomingExpense[]): GroupedExpense[] {
|
||||||
|
const map = new Map<number, GroupedExpense>();
|
||||||
|
|
||||||
|
for (const e of expenses) {
|
||||||
|
if (e.periodicExpenseId === null) continue;
|
||||||
|
|
||||||
|
const key = e.periodicExpenseId;
|
||||||
|
const existing = map.get(key);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.totalAmount += Number(e.amount);
|
||||||
|
existing.count += 1;
|
||||||
|
if (e.dueDate < existing.earliestDueDate) {
|
||||||
|
existing.earliestDueDate = e.dueDate;
|
||||||
|
}
|
||||||
|
if (e.dueType === "overdue") {
|
||||||
|
existing.dueType = "overdue";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
map.set(key, {
|
||||||
|
periodicExpenseId: key,
|
||||||
|
description: e.periodicExpense?.description ?? e.description,
|
||||||
|
totalAmount: Number(e.amount),
|
||||||
|
earliestDueDate: e.dueDate,
|
||||||
|
dueType: e.dueType,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values()).sort((a, b) => {
|
||||||
|
if (a.dueType !== b.dueType) return a.dueType === "overdue" ? -1 : 1;
|
||||||
|
return a.earliestDueDate.localeCompare(b.earliestDueDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value: string) {
|
||||||
|
return new Date(value).toLocaleDateString("es-AR", {
|
||||||
|
timeZone: "UTC",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(value: number) {
|
||||||
|
return `$${value.toLocaleString("es-AR", { minimumFractionDigits: 2 })}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DueTypeBadge({ dueType }: { dueType: "overdue" | "upcoming" }) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
dueType === "overdue"
|
||||||
|
? "inline-flex h-5 shrink-0 items-center rounded-md bg-red-100 px-1.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||||
|
: "inline-flex h-5 shrink-0 items-center rounded-md bg-amber-100 px-1.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{dueType === "overdue" ? "Vencido" : "Próximo"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardExpensesTable() {
|
||||||
|
const { data: expenses, isLoading } = usePendingUpcomingExpenses();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
if (!expenses) return [];
|
||||||
|
return groupExpenses(expenses);
|
||||||
|
}, [expenses]);
|
||||||
|
|
||||||
|
function goToExpenses(periodicExpenseId: number) {
|
||||||
|
navigate({ to: "/expenses", search: { periodicExpenseId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const overdueCount = groups.filter((g) => g.dueType === "overdue").length;
|
||||||
|
const upcomingCount = groups.filter((g) => g.dueType === "upcoming").length;
|
||||||
|
const grandTotal = groups.reduce((sum, g) => sum + g.totalAmount, 0);
|
||||||
|
const hasData = groups.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border">
|
||||||
|
<div className="flex items-center gap-2 border-b px-4 py-3">
|
||||||
|
<CalendarClock className="size-4 text-muted-foreground" />
|
||||||
|
<h2 className="text-sm font-medium">Gastos por vencer</h2>
|
||||||
|
{isLoading && (
|
||||||
|
<span className="ml-auto text-xs text-muted-foreground">Cargando...</span>
|
||||||
|
)}
|
||||||
|
{!isLoading && hasData && (
|
||||||
|
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
|
||||||
|
{overdueCount} vencido{overdueCount !== 1 ? "s" : ""}
|
||||||
|
{upcomingCount > 0 && ` · ${upcomingCount} próximo${upcomingCount !== 1 ? "s" : ""}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isLoading && !hasData && (
|
||||||
|
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
||||||
|
No hay gastos pendientes.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
||||||
|
Cargando...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-1 p-1 sm:hidden">
|
||||||
|
{groups.map((group) => (
|
||||||
|
<div
|
||||||
|
key={group.periodicExpenseId}
|
||||||
|
className="rounded-lg border p-3 text-sm"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => goToExpenses(group.periodicExpenseId)}
|
||||||
|
className="truncate font-medium text-left hover:underline cursor-pointer inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{group.description}
|
||||||
|
<ExternalLink className="size-3 shrink-0 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Vence {formatDate(group.earliestDueDate)}
|
||||||
|
{group.count > 1 && ` (${group.count} gastos)`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DueTypeBadge dueType={group.dueType} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-3 text-lg font-semibold tabular-nums">
|
||||||
|
{formatAmount(group.totalAmount)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{hasData && (
|
||||||
|
<div className="flex items-center justify-between rounded-lg border bg-muted/50 px-3 py-2.5 text-sm font-medium">
|
||||||
|
<span>Total</span>
|
||||||
|
<span className="tabular-nums">{formatAmount(grandTotal)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden overflow-x-auto sm:block">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b bg-muted/50">
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground">Descripción</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground">Monto</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground">Vencimiento</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground">Cantidad</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground">Estado</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<tr key={group.periodicExpenseId} className="border-b last:border-0 hover:bg-muted/30">
|
||||||
|
<td className="px-3 py-2.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => goToExpenses(group.periodicExpenseId)}
|
||||||
|
className="font-medium text-left hover:underline cursor-pointer inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{group.description}
|
||||||
|
<ExternalLink className="size-3 shrink-0 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 tabular-nums">{formatAmount(group.totalAmount)}</td>
|
||||||
|
<td className="px-3 py-2.5 text-muted-foreground">{formatDate(group.earliestDueDate)}</td>
|
||||||
|
<td className="px-3 py-2.5 text-muted-foreground tabular-nums">
|
||||||
|
{group.count > 1 ? `${group.count} gastos` : "1 gasto"}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5">
|
||||||
|
<DueTypeBadge dueType={group.dueType} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{hasData && (
|
||||||
|
<tr className="border-t bg-muted/50 font-medium">
|
||||||
|
<td className="px-3 py-2.5">Total</td>
|
||||||
|
<td className="px-3 py-2.5 tabular-nums">{formatAmount(grandTotal)}</td>
|
||||||
|
<td colSpan={3} />
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Route } from "@/routes/_authenticated/expenses";
|
||||||
import { ExpensesProvider } from "./ExpensesProvider";
|
import { ExpensesProvider } from "./ExpensesProvider";
|
||||||
import { ExpensesTabContent } from "./components/ExpensesTabContent";
|
import { ExpensesTabContent } from "./components/ExpensesTabContent";
|
||||||
import { PeriodicExpensesTabContent } from "./components/PeriodicExpensesTabContent";
|
import { PeriodicExpensesTabContent } from "./components/PeriodicExpensesTabContent";
|
||||||
@@ -41,8 +42,9 @@ function ExpensesPageInner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ExpensesPage() {
|
export function ExpensesPage() {
|
||||||
|
const { periodicExpenseId } = Route.useSearch();
|
||||||
return (
|
return (
|
||||||
<ExpensesProvider>
|
<ExpensesProvider initialPeriodicExpenseId={periodicExpenseId}>
|
||||||
<ExpensesPageInner />
|
<ExpensesPageInner />
|
||||||
</ExpensesProvider>
|
</ExpensesProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ interface ExpensesContextValue {
|
|||||||
|
|
||||||
const ExpensesContext = createContext<ExpensesContextValue | null>(null);
|
const ExpensesContext = createContext<ExpensesContextValue | null>(null);
|
||||||
|
|
||||||
export function ExpensesProvider({ children }: { children: ReactNode }) {
|
export function ExpensesProvider({ children, initialPeriodicExpenseId }: { children: ReactNode; initialPeriodicExpenseId?: number }) {
|
||||||
const [statusFilter, setStatusFilter] = useState("PENDING");
|
const [statusFilter, setStatusFilter] = useState("PENDING");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
const [periodicExpenseFilter, setPeriodicExpenseFilter] = useState<number | undefined>(undefined);
|
const [periodicExpenseFilter, setPeriodicExpenseFilter] = useState<number | undefined>(initialPeriodicExpenseId);
|
||||||
const [searchInput, setSearchInput] = useState("");
|
const [searchInput, setSearchInput] = useState("");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,14 @@ export function getExpenses(
|
|||||||
return fetcher<PaginatedResponse<Expense>>(`/api/expenses${qs ? `?${qs}` : ""}`);
|
return fetcher<PaginatedResponse<Expense>>(`/api/expenses${qs ? `?${qs}` : ""}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PendingUpcomingExpense = Expense & {
|
||||||
|
dueType: "overdue" | "upcoming";
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPendingUpcomingExpenses(): Promise<PendingUpcomingExpense[]> {
|
||||||
|
return fetcher<PendingUpcomingExpense[]>("/api/expenses/pending-upcoming");
|
||||||
|
}
|
||||||
|
|
||||||
export function getTotalPending(): Promise<MonthlyExpensesTotal> {
|
export function getTotalPending(): Promise<MonthlyExpensesTotal> {
|
||||||
return fetcher<MonthlyExpensesTotal>("/api/expenses/pending-total");
|
return fetcher<MonthlyExpensesTotal>("/api/expenses/pending-total");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
getTotalPending,
|
getTotalPending,
|
||||||
importPeriodicExpenses,
|
importPeriodicExpenses,
|
||||||
importExpenses,
|
importExpenses,
|
||||||
|
getPendingUpcomingExpenses,
|
||||||
type CreatePeriodicExpenseInput, type CreateNonPeriodicExpenseInput, type PayExpenseInput,
|
type CreatePeriodicExpenseInput, type CreateNonPeriodicExpenseInput, type PayExpenseInput,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ export const expenseKeys = {
|
|||||||
monthlyTotal: (year: number, month: number) => ["expenses", "monthly-total", year, month] as const,
|
monthlyTotal: (year: number, month: number) => ["expenses", "monthly-total", year, month] as const,
|
||||||
monthlyTotals: (year: number, month: number) => ["expenses", "totals", year, month] as const,
|
monthlyTotals: (year: number, month: number) => ["expenses", "totals", year, month] as const,
|
||||||
totalPending: ["expenses", "pending-total"] as const,
|
totalPending: ["expenses", "pending-total"] as const,
|
||||||
|
pendingUpcoming: ["expenses", "pending-upcoming"] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function usePeriodicExpenses() {
|
export function usePeriodicExpenses() {
|
||||||
@@ -163,6 +165,13 @@ export function useMonthlyTotals(year: number, month: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePendingUpcomingExpenses() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: expenseKeys.pendingUpcoming,
|
||||||
|
queryFn: getPendingUpcomingExpenses,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useTotalPending() {
|
export function useTotalPending() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: expenseKeys.totalPending,
|
queryKey: expenseKeys.totalPending,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
import { ExpensesPage } from "@/features/expenses/ExpensesPage";
|
import { ExpensesPage } from "@/features/expenses/ExpensesPage";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticated/expenses")({
|
export const Route = createFileRoute("/_authenticated/expenses")({
|
||||||
|
validateSearch: z.object({
|
||||||
|
periodicExpenseId: z.coerce.number().optional(),
|
||||||
|
}),
|
||||||
component: ExpensesPage,
|
component: ExpensesPage,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user