"use client"; import Link from "next/link"; import { useEffect, useState } from "react"; import { AppShell } from "@/components/AppShell"; import { useCan } from "@/lib/abilities"; import { createBankAccount, createBankInstitution, listBankAccounts, listBankInstitutions, updateBankAccount, updateBankInstitution, } from "@/lib/api"; import { domainLabel } from "@/lib/labels"; import type { BankAccount, BankInstitution, Currency, TransactionDomain, } from "@/lib/types"; /** * Chequera accounts admin — docs/RECEIPT_CAPTURE_SPEC.md §3. * * Two levels: the bank (institution) and the accounts held at it. Opening an * account is rare and consequential — its currency is what every movement * booked into it is denominated in, and it can't be changed afterwards without * silently re-denominating history, so the edit form deliberately has no * currency field. * * Accounts are never deleted: `bank_transactions.bankAccountId` is a required * FK, so a used account can't be removed without destroying its register. * Closing one (`active: false`) hides it from new captures while leaving the * history readable, matching this app's never-hard-delete convention. */ const CURRENCIES: Currency[] = ["MXN", "USD"]; const BUSINESS_LINES: TransactionDomain[] = ["UTILITY", "INSURANCE", "TRUST"]; export default function CuentasChequeraPage() { return ( ); } function Cuentas() { const canEdit = useCan("bank:manage-accounts"); const [banks, setBanks] = useState(null); const [accounts, setAccounts] = useState(null); const [error, setError] = useState(null); function reload() { Promise.all([listBankInstitutions(), listBankAccounts()]) .then(([b, a]) => { setBanks(b); setAccounts(a); }) .catch((e) => setError(e?.message ?? "No se pudieron cargar las cuentas.")); } useEffect(reload, []); if (!canEdit) { return ( <>

Cuentas de chequera

No tiene permisos para administrar cuentas bancarias.
); } return ( <>

Chequera

Cuentas de chequera

Cada cuenta es una chequera física y se lleva por separado. La moneda se fija al darla de alta porque todos sus movimientos quedan registrados en ella; para cambiarla hay que abrir otra cuenta. Las cuentas no se eliminan: se cierran, y su historial sigue consultable.

{error &&
{error}
} {!banks || !accounts ? (
) : ( <> )} ); } /* ------------------------------------------------------------------ banks */ function BanksSection({ banks, onChanged, }: { banks: BankInstitution[]; onChanged: () => void; }) { const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [name, setName] = useState(""); const [country, setCountry] = useState(""); const [busy, setBusy] = useState(false); function startAdd() { setEditingId(null); setAdding(true); setName(""); setCountry(""); } function startEdit(b: BankInstitution) { setAdding(false); setEditingId(b.id); setName(b.name); setCountry(b.country ?? ""); } function cancel() { setAdding(false); setEditingId(null); } async function submit() { if (!name.trim()) { window.alert("El nombre del banco es obligatorio."); return; } setBusy(true); try { const payload = { name: name.trim(), country: country.trim() }; if (editingId) await updateBankInstitution(editingId, payload); else await createBankInstitution(payload); cancel(); onChanged(); } catch (e) { window.alert((e as Error)?.message ?? "No se pudo guardar el banco."); } finally { setBusy(false); } } const editor = (
); return (

Bancos {banks.length}

{!adding && editingId === null && ( )}
{banks.length === 0 && !adding ? (
Sin bancos registrados.
) : (
{adding && ( )} {banks.map((b) => editingId === b.id ? ( ) : ( ), )}
Banco País Acciones
{editor}
{editor}
{b.name} {b.country || "—"}
)}
); } /* --------------------------------------------------------------- accounts */ function AccountsSection({ banks, accounts, onChanged, }: { banks: BankInstitution[]; accounts: BankAccount[]; onChanged: () => void; }) { const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [bankId, setBankId] = useState(""); const [label, setLabel] = useState(""); const [currency, setCurrency] = useState("MXN"); const [businessLine, setBusinessLine] = useState(""); const [active, setActive] = useState(true); const [busy, setBusy] = useState(false); function startAdd() { setEditingId(null); setAdding(true); setBankId(banks[0]?.id ?? ""); setLabel(""); setCurrency("MXN"); setBusinessLine(""); setActive(true); } function startEdit(a: BankAccount) { setAdding(false); setEditingId(a.id); setBankId(a.bankId); setLabel(a.label); setCurrency(a.currency); setBusinessLine(a.businessLine ?? ""); setActive(a.active); } function cancel() { setAdding(false); setEditingId(null); } async function submit() { if (!bankId) { window.alert("Selecciona el banco de la cuenta."); return; } if (!label.trim()) { window.alert("El nombre de la cuenta es obligatorio."); return; } setBusy(true); try { const line = businessLine ? (businessLine as TransactionDomain) : undefined; if (editingId) { // No `currency`: see the file header. await updateBankAccount(editingId, { bankId, label: label.trim(), businessLine: line, active, }); } else { await createBankAccount({ bankId, label: label.trim(), currency, businessLine: line, active, }); } cancel(); onChanged(); } catch (e) { window.alert((e as Error)?.message ?? "No se pudo guardar la cuenta."); } finally { setBusy(false); } } const editor = (
); return (

Cuentas {accounts.length}

{!adding && editingId === null && banks.length > 0 && ( )}
{banks.length === 0 ? (
Registra primero el banco donde está la cuenta.
) : accounts.length === 0 && !adding ? (
Sin cuentas registradas.
) : (
{adding && ( )} {accounts.map((a) => editingId === a.id ? ( ) : ( ), )}
Cuenta Banco Moneda Línea Estatus Acciones
{editor}
{editor}
{a.label} {a.bankName} {a.currency} {a.businessLine ? domainLabel(a.businessLine) : "—"} {a.active ? "Abierta" : "Cerrada"}
Ver movimientos
)}
); }