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) <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-07-04 10:43:26 -06:00
parent d2610b2eef
commit d117fb79ea
3 changed files with 101 additions and 0 deletions

View File

@@ -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<Transaction[]>("/transactions/recent", { params: { limit } });
// --- Installment Plans (Tasa Cero) ---
export type InstallmentPlan = Schema<'InstallmentPlanRead'>;

View File

@@ -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();

View File

@@ -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";
}