Skip to content

Plataforma técnica · Sevastopol

Cartolas Island

Islands Sevastopol Finanzas

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.

  • Directorysevastopol/src/components/islands/financieros/
    • CartolasIsland.tsx — componente único, internamente CartolasPanel
    • FinancierosWorkspace.tsx — tokens visuales compartidos por las 5 islands del dominio
  • Directorysevastopol/src/lib/hooks/
    • useActiveTenant.ts
    • useActivePeriod.ts

Dos pestañas controladas por una signal local activeTab:

PestañaFunciónAcción primaria
movimientosFiltros + tabla de movimientos del período.Buscar / Limpiar / Eliminar fila pendiente.
etlSelecció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.

const { tenantId } = useActiveTenant(() => {
void loadBancos();
setMovimientos([]);
});
const { period } = useActivePeriod();
HookAportaReactividad
useActiveTenantUUID 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:

SignalTipoUso
activeTab"movimientos" | "etl"Pestaña visible.
bancosBanco[]Catálogo de bancos del tenant para los <select>.
movimientosMovimientoBancario[]Resultado del último loadMovimientos().
filtBanco / filtTipo / filtEstado / filtTexto / filtMontoMin / filtMontoMaxstringEstado de los 6 filtros independientes.
etlBanco / etlPeriodostringSelección del formulario ETL (YYYY-MM).
etlRunningbooleanBloquea el botón mientras se dispara.

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:

FiltroTipoOpciones / 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íodouseActivePeriod()anio + mes del workspace, no un filtro local.

Limpiar resetea los 6 filtros locales pero no toca el período activo.

8 columnas + acción condicional. CARGO y ABONO ocupan columnas separadas para lectura tipo libro mayor:

ColumnaRender
FechafmtDate(fecha_movimiento)
Banconombre_banco o
Descripcióndescripcion o
CargofmtMonto(monto) sólo si tipo_movimiento === "CARGO", sino
AbonofmtMonto(monto) sólo si tipo_movimiento === "ABONO", sino
SaldofmtMonto(saldo_banco) o
EstadoPENDIENTE | CONCILIADO | ANULADO
OrigenMarca 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.

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.

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.

MétodoRutaOperació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_idLista 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).

ReglaMotivo
Cambio de tenant vacía la tablaLos movimientos del tenant anterior no aplican; mostrarlos sería confuso.
Cambio de período no re-fetchaLos filtros locales suelen cambiar junto con el período; un fetch automático interrumpiría la edición.
Eliminar sólo en PENDIENTEConciliar destruye trazabilidad; anulado tiene su propia ruta.
Toast informativo cuando la lista viene vacíaEl operador debe saber qué filtró todo (banco, año, mes) sin inspeccionar la query.
Tabla con Cargo/Abono en columnas separadasLectura tipo libro mayor; el signo no se infiere del monto.
ETL retorna PID, no esperaLa carga es asíncrona; bloquear la UI sería inviable para archivos grandes.
Botón ETL en tono warningLa acción afecta el lado fuente (cartola) y debe destacar del CRUD normal.