"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; import { EXPIRY_WINDOW_DAYS, getBankStats, getBillingStats, getPolicyStats, getPropertyStats, getStats, listBankAccounts, } from "@/lib/api"; import { useAuth } from "@/lib/abilities"; import { balancePhrase, formatDate, formatMoney, formatNumber, policyStatusLabel, trustStatusLabel, } from "@/lib/labels"; import type { BankAccount, BankStats, BillingStats, CustomerStats, PolicyStats, PropertyStats, } from "@/lib/types"; export default function InicioPage() { return ( ); } interface DashboardData { customers: CustomerStats | null; policies: PolicyStats | null; properties: PropertyStats | null; billing: BillingStats | null; /** Figures for ONE chequera — see `bankAccount` for which. */ bank: BankStats | null; /** * The chequera the card above is reading. The office keeps more than one, in * different currencies, so this card shows the default account rather than a * cross-account total, which would be a figure that never existed. */ bankAccount: BankAccount | null; bankAccountCount: number; } /** Same default as /banco, so the two screens agree on which chequera opens. */ function defaultAccount(accounts: BankAccount[]): BankAccount | null { const remembered = typeof window !== "undefined" ? window.localStorage.getItem("banco.bankAccountId") : null; return ( accounts.find((a) => a.id === remembered) ?? accounts.find((a) => a.active) ?? accounts[0] ?? null ); } function HomeDashboard() { const user = useAuth(); const [data, setData] = useState({ customers: null, policies: null, properties: null, billing: null, bank: null, bankAccount: null, bankAccountCount: 0, }); const [loading, setLoading] = useState(true); useEffect(() => { let alive = true; // The chequera figures need an account id, so that read is a two-step: // list the accounts, then ask the default one for its stats. const bank = listBankAccounts().then(async (accounts) => { const account = defaultAccount(accounts); if (!account) return { account: null, stats: null, count: 0 }; return { account, stats: await getBankStats(account.id), count: accounts.length, }; }); Promise.allSettled([ getStats(), getPolicyStats(), getPropertyStats(), getBillingStats(), bank, ]).then((results) => { if (!alive) return; const bankResult = results[4].status === "fulfilled" ? results[4].value : null; setData({ customers: results[0].status === "fulfilled" ? results[0].value : null, policies: results[1].status === "fulfilled" ? results[1].value : null, properties: results[2].status === "fulfilled" ? results[2].value : null, billing: results[3].status === "fulfilled" ? results[3].value : null, bank: bankResult?.stats ?? null, bankAccount: bankResult?.account ?? null, bankAccountCount: bankResult?.count ?? 0, }); setLoading(false); }); return () => { alive = false; }; }, []); const greeting = greetingFor(user?.name); const lastBillingMovement = data.billing?.lastMovement ?? null; const lastBankMovement = data.bank?.lastMovement ?? null; return (

Resumen general

{greeting}

Vista rápida del estado de la cartera de clientes, las pólizas activas, los fideicomisos y los movimientos recientes.

Atención

Lo que conviene revisar antes de cerrar el día
) : ( "—" ) } sub={ data.billing ? `En ${formatNumber(data.billing.ledgerCustomers)} expedientes con cargo` : undefined } meta={ data.billing ? `${formatNumber(data.billing.crossLineCustomers)} clientes con cargo en ambos ramos` : undefined } /> {formatMoney(data.bank.net, data.bankAccount.currency)} ) : ( "—" ) } // Names the account, because this is one chequera's figure and the // office has more than one — they are never added together. sub={ data.bank && data.bankAccount ? `${data.bankAccount.label} · ${balancePhrase(data.bank.net)}` : undefined } meta={ data.bank ? `${formatNumber(data.bank.movements)} movimientos · ${formatNumber(data.bank.pending)} pendientes` + (data.bankAccountCount > 1 ? ` · ${formatNumber(data.bankAccountCount)} cuentas en total` : "") : undefined } />

Accesos rápidos

Ir directo a cada módulo
); } function greetingFor(name?: string): string { const hour = new Date().getHours(); const partOfDay = hour < 12 ? "Buenos días" : hour < 19 ? "Buenas tardes" : "Buenas noches"; const display = (name ?? "").trim().split(/\s+/)[0]; return display ? `${partOfDay}, ${display}` : partOfDay; } function LastSeenLine({ billing, bank, loading, }: { billing: string | null; bank: string | null; loading: boolean; }) { if (loading) { return ( ); } if (!billing && !bank) return null; return (

{billing && <>Último movimiento de cartera: {formatDate(billing)}} {billing && bank && } {bank && <>Último movimiento de chequera: {formatDate(bank)}}

); } function KpiCard({ href, label, primary, sub, loading, }: { href: string; label: string; primary: React.ReactNode; sub: string[]; loading: boolean; }) { return (
{label}
{loading ? ( ) : ( primary )}
); } function AttentionCard({ href, tone, title, primary, sub, meta, loading, }: { href: string; tone: "warn" | "info" | "muted"; title: string; primary: React.ReactNode; sub?: string; meta?: string; loading: boolean; }) { return (
{title}
{loading ? ( ) : ( primary )}
{sub && !loading &&
{sub}
} {meta && !loading &&
{meta}
} ); } function QuickLink({ href, label, sub, }: { href: string; label: string; sub: string; }) { return ( {label} {sub} ); } function CurrencyTotals({ totals, field, }: { totals: BillingStats["byCurrency"]; field: "owing" | "inCredit"; }) { if (!totals || totals.length === 0) return <>—; return ( {totals.map((c) => ( {c.currency} {formatNumber(c[field])} ))} ); }