Plataforma técnica · Sevastopol
Cartolas Island
Islands Sevastopol Finanzas
Propósito
Section titled “Propósito”CartolasIsland opera la cartola bancaria del tenant: lista movimientos filtrados por banco, período, tipo, estado, texto y rango de monto; permite eliminar movimientos pendientes y disparar una carga ETL desde XLSX para un banco/período. No toca el libro contable — eso es responsabilidad de LibroBancoIsland.
Ubicación
Section titled “Ubicación”Directorysevastopol/src/components/islands/financieros/
- CartolasIsland.tsx — componente único, internamente
CartolasPanel - FinancierosWorkspace.tsx — tokens visuales compartidos por las 5 islands del dominio
- CartolasIsland.tsx — componente único, internamente
Directorysevastopol/src/lib/hooks/
- useActiveTenant.ts
- useActivePeriod.ts
Composición
Section titled “Composición”Dos pestañas controladas por una signal local activeTab:
| Pestaña | Función | Acción primaria |
|---|---|---|
movimientos | Filtros + tabla de movimientos del período. | Buscar / Limpiar / Eliminar fila pendiente. |
etl | Selección de banco + período + dispara la carga. | Cargar cartola (POST ETL). |
El cambio de pestaña no recarga datos — los movimientos quedan en memoria al volver.
Hooks y estado global
Section titled “Hooks y estado global”const { tenantId } = useActiveTenant(() => { void loadBancos(); setMovimientos([]);});const { period } = useActivePeriod();| Hook | Aporta | Reactividad |
|---|---|---|
useActiveTenant | UUID del tenant. | Recarga bancos y vacía la tabla de movimientos al cambiar tenant. |
useActivePeriod | { year, month } global. | No re-fetcha automáticamente; los valores entran al URLSearchParams la próxima vez que se pulsa Buscar. |
Signals locales:
| Signal | Tipo | Uso |
|---|---|---|
activeTab | "movimientos" | "etl" | Pestaña visible. |
bancos | Banco[] | Catálogo de bancos del tenant para los <select>. |
movimientos | MovimientoBancario[] | Resultado del último loadMovimientos(). |
filtBanco / filtTipo / filtEstado / filtTexto / filtMontoMin / filtMontoMax | string | Estado de los 6 filtros independientes. |
etlBanco / etlPeriodo | string | Selección del formulario ETL (YYYY-MM). |
etlRunning | boolean | Bloquea el botón mientras se dispara. |
Filtros y consulta
Section titled “Filtros y consulta”El botón Buscar arma un URLSearchParams con sólo los campos no vacíos, anexa tenant_id y llama GET /api/financieros/movimientos:
async function loadMovimientos() { const params = new URLSearchParams(); if (filtBanco()) params.set("bancoId", filtBanco()); if (period().year) params.set("anio", String(period().year)); if (period().month) params.set("mes", String(period().month)); if (filtTipo()) params.set("tipo", filtTipo()); if (filtEstado()) params.set("estado", filtEstado()); if (filtTexto()) params.set("texto", filtTexto()); if (filtMontoMin()) params.set("montoMin", filtMontoMin()); if (filtMontoMax()) params.set("montoMax", filtMontoMax()); const res = await authenticatedFetch(`${API}/movimientos${buildQuery(params)}`); // …}Si la respuesta es un array vacío, el toast describe cuál combinación filtró todo (Sin movimientos para banco, año 2026, mes 5):
if (Array.isArray(data) && data.length === 0) { const parts: string[] = []; if (filtBanco()) parts.push("banco"); if (period().year) parts.push(`año ${period().year}`); if (period().month) parts.push(`mes ${period().month}`); const detail = parts.length > 0 ? ` para ${parts.join(", ")}` : ""; toast.info(`Sin movimientos${detail}`);}El catálogo de filtros:
| Filtro | Tipo | Opciones / Origen |
|---|---|---|
| Banco | <select> | bancos del tenant. |
| Tipo | <select> | ABONO, CARGO. |
| Estado | <select> | PENDIENTE, CONCILIADO, ANULADO. |
| Texto | <input> | Búsqueda en descripcion (backend). |
| Monto mínimo / máximo | <input type="number"> | Rango sobre monto. |
| Período | useActivePeriod() | anio + mes del workspace, no un filtro local. |
Limpiar resetea los 6 filtros locales pero no toca el período activo.
Tabla de movimientos
Section titled “Tabla de movimientos”8 columnas + acción condicional. CARGO y ABONO ocupan columnas separadas para lectura tipo libro mayor:
| Columna | Render |
|---|---|
| Fecha | fmtDate(fecha_movimiento) |
| Banco | nombre_banco o — |
| Descripción | descripcion o — |
| Cargo | fmtMonto(monto) sólo si tipo_movimiento === "CARGO", sino — |
| Abono | fmtMonto(monto) sólo si tipo_movimiento === "ABONO", sino — |
| Saldo | fmtMonto(saldo_banco) o — |
| Estado | PENDIENTE | CONCILIADO | ANULADO |
| Origen | Marca de procedencia del registro (ETL, manual). |
| (acción) | <TableAction icon=Delete> sólo si estado === "PENDIENTE". |
Totales en pills del header:
const totales = createMemo(() => { const abonos = rows.filter(r => r.tipo_movimiento === "ABONO").reduce((s, r) => s + Number(r.monto), 0); const cargos = rows.filter(r => r.tipo_movimiento === "CARGO").reduce((s, r) => s + Number(r.monto), 0); return { abonos, cargos, diferencia: abonos - cargos };});La diferencia se pinta en tono success si es positiva (neto a favor del tenant) o warning si es negativa.
Eliminación de movimientos
Section titled “Eliminación de movimientos”Sólo aplica a PENDIENTE. El botón pide confirm() antes de invocar DELETE /api/financieros/movimientos/:id. La UI acepta tanto 200 OK como 204 No Content:
if (res.ok || res.status === 204) { toast.success("Movimiento eliminado"); void loadMovimientos();}Movimientos CONCILIADO o ANULADO no muestran la acción — el backend igualmente la rechazaría, pero la UI evita el viaje.
Carga ETL
Section titled “Carga ETL”Pestaña independiente con dos campos (banco + periodo) y un botón con estilo warning. La acción dispara POST /api/financieros/movimientos/etl:
const res = await authenticatedFetch(`${API}/movimientos/etl${tq()}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ bancoId: etlBanco(), periodo: etlPeriodo() }),});if (res.ok) { const data = await res.json(); toast.success(`ETL iniciado (PID: ${data.pid ?? "N/A"})`);}La respuesta incluye pid del proceso lanzado en backend — el ETL corre asíncrono y no bloquea la UI. El operador debe volver a la pestaña Movimientos y refrescar manualmente para ver el resultado.
Pre-condición: existe archivo XLSX para ese banco y período en la carpeta configurada del backend. La UI no valida nada de eso; sólo dispara.
Endpoints consumidos
Section titled “Endpoints consumidos”| Método | Ruta | Operación |
|---|---|---|
GET | /api/financieros/bancos?tenant_id=... | Catálogo de bancos del tenant. |
GET | /api/financieros/movimientos?bancoId&anio&mes&tipo&estado&texto&montoMin&montoMax&tenant_id | Lista con filtros. |
DELETE | /api/financieros/movimientos/:id?tenant_id=... | Elimina movimiento pendiente. |
POST | /api/financieros/movimientos/etl?tenant_id=... body { bancoId, periodo } | Dispara la carga ETL. |
tenant_id se inyecta como query en todas las llamadas vía los helpers tq() y buildQuery(params).
Reglas de UI
Section titled “Reglas de UI”| Regla | Motivo |
|---|---|
| Cambio de tenant vacía la tabla | Los movimientos del tenant anterior no aplican; mostrarlos sería confuso. |
| Cambio de período no re-fetcha | Los filtros locales suelen cambiar junto con el período; un fetch automático interrumpiría la edición. |
Eliminar sólo en PENDIENTE | Conciliar destruye trazabilidad; anulado tiene su propia ruta. |
| Toast informativo cuando la lista viene vacía | El operador debe saber qué filtró todo (banco, año, mes) sin inspeccionar la query. |
Tabla con Cargo/Abono en columnas separadas | Lectura tipo libro mayor; el signo no se infiere del monto. |
| ETL retorna PID, no espera | La carga es asíncrona; bloquear la UI sería inviable para archivos grandes. |
Botón ETL en tono warning | La acción afecta el lado fuente (cartola) y debe destacar del CRUD normal. |