From d117fb79ea40f66076a2b25f2bacae20bc88ecc5 Mon Sep 17 00:00:00 2001 From: Carlos Escalante Date: Sat, 4 Jul 2026 10:43:26 -0600 Subject: [PATCH] Add nav config module, budget-cycle helper, me/recent API helpers navigation.ts becomes the single source of truth for sidebar and breadcrumbs (Inicio entry added; '/' is exact-match). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/lib/api.ts | 7 ++++ frontend/src/lib/dates.ts | 23 +++++++++++ frontend/src/lib/navigation.ts | 71 ++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 frontend/src/lib/navigation.ts diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 3a65a23..36216ef 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -130,6 +130,8 @@ export async function logout() { await fetch("/api/auth/logout", { method: "POST", credentials: "same-origin" }); } +export const getMe = () => api.get<{ username: string }>("/auth/me"); + // ─── Types ────────────────────────────────────────────────────────────────── export type Account = Schema<'AccountRead'>; @@ -240,6 +242,11 @@ export const upsertBalanceOverride = ( export const deleteBalanceOverride = (year: number, month: number) => api.delete(`/budget/balance-override/${year}/${month}`); +// --- Transactions --- + +export const getRecentTransactions = (limit = 5) => + api.get("/transactions/recent", { params: { limit } }); + // --- Installment Plans (Tasa Cero) --- export type InstallmentPlan = Schema<'InstallmentPlanRead'>; diff --git a/frontend/src/lib/dates.ts b/frontend/src/lib/dates.ts index 75f99bd..e132760 100644 --- a/frontend/src/lib/dates.ts +++ b/frontend/src/lib/dates.ts @@ -18,6 +18,29 @@ export function formatShortDate(dateStr: string): string { }); } +/** Budget month M = the credit-card cycle ENDING on the 18th of M + * (matches get_cycle_range in the backend). After the 18th we are already + * in next month's cycle — including the Dec 19 → January-next-year wrap. */ +export function currentBudgetCycle(today = new Date()): { + year: number; + month: number; + start: Date; + end: Date; +} { + let year = today.getFullYear(); + let month = today.getMonth() + 1; + if (today.getDate() > 18) { + month += 1; + if (month === 13) { + month = 1; + year += 1; + } + } + const end = new Date(year, month - 1, 18); + const start = new Date(year, month - 2, 18); + return { year, month, start, end }; +} + /** "hace 3 días" / "hace 2 horas" / "hace un momento" */ export function formatRelativeAge(iso: string): string { const ms = Date.now() - new Date(iso).getTime(); diff --git a/frontend/src/lib/navigation.ts b/frontend/src/lib/navigation.ts new file mode 100644 index 0000000..fb0d25c --- /dev/null +++ b/frontend/src/lib/navigation.ts @@ -0,0 +1,71 @@ +import { + BarChart3, + Calculator, + Droplets, + Home, + Landmark, + Layers, + PiggyBank, + RefreshCw, + Sparkles, + Telescope, + TrendingUp, + type LucideIcon, +} from "lucide-react"; + +/** Single source of truth for the sidebar nav and header breadcrumbs. */ + +export interface NavItem { + to: string; + icon: LucideIcon; + label: string; +} + +export interface NavSection { + label: string; + items: NavItem[]; +} + +export const navSections: NavSection[] = [ + { + label: "General", + items: [ + { to: "/", icon: Home, label: "Inicio" }, + { to: "/asistente", icon: Sparkles, label: "Asistente" }, + { to: "/sync", icon: RefreshCw, label: "Sincronización" }, + ], + }, + { + label: "Finanzas", + items: [ + { to: "/budget", icon: Calculator, label: "Presupuesto" }, + { to: "/salarios", icon: Landmark, label: "Salarios" }, + { to: "/financiamientos", icon: Layers, label: "Financiamientos" }, + { to: "/pensions", icon: PiggyBank, label: "Pensiones" }, + { to: "/proyecciones", icon: TrendingUp, label: "Proyecciones" }, + { to: "/planificador", icon: Telescope, label: "Planificador" }, + { to: "/analytics", icon: BarChart3, label: "Analytics" }, + ], + }, + { + label: "Servicios", + items: [ + { to: "/servicios-municipales", icon: Droplets, label: "Municipalidad" }, + ], + }, +]; + +/** "/" must match exactly — prefix-matching it would mark Inicio active everywhere. */ +export function isNavActive(to: string, pathname: string): boolean { + if (to === "/") return pathname === "/"; + return pathname === to || pathname.startsWith(`${to}/`); +} + +export function titleFor(pathname: string): string { + for (const section of navSections) { + for (const item of section.items) { + if (isNavActive(item.to, pathname)) return item.label; + } + } + return "WealthySmart"; +}