Skip to content

Plataforma técnica · Sevastopol

Prestamos Island

Islands Sevastopol Finanzas

PrestamosIsland opera el portafolio de préstamos del tenant: alta/edición/eliminación, generación de la tabla de amortización, registro de pagos por cuota con enlace opcional a un movimiento bancario. La island no calcula la amortización en cliente — sólo dispara la generación en Orchestrator y lista las cuotas resultantes.

  • Directorysevastopol/src/components/islands/financieros/
    • PrestamosIsland.tsx — componente único, internamente PrestamosPanel
    • FinancierosWorkspace.tsx — tokens compartidos
  • Directorysevastopol/src/lib/hooks/
    • useActiveTenant.ts

useActivePeriod no se consume — los préstamos y cuotas viven a través de varios períodos contables; el filtro por fecha está fuera del scope de la island.

Dos pestañas controladas por activeTab:

PestañaFunciónAcción primaria
prestamosPortafolio con búsqueda + filtro de estado.Nuevo préstamo / Editar / Eliminar / Generar amortización / Ver.
amortizacionCronograma del préstamo seleccionado.Registrar pago de cuota pendiente.

La pestaña amortizacion requiere selectedPrestamo; si está vacío pinta un estado vacío con instrucción de seleccionar desde el portafolio.

const { tenantId } = useActiveTenant(() => {
void Promise.all([loadBancos(), loadPrestamos()]);
setCuotas([]);
setSelectedPrestamo(null);
});

Cambiar tenant dispara la recarga de bancos+préstamos en paralelo y limpia el préstamo seleccionado.

Signals locales:

SignalTipoUso
activeTab"prestamos" | "amortizacion"Pestaña visible.
bancosBanco[]Catálogo del tenant para el <select> del modal.
prestamosPrestamo[]Portafolio completo.
cuotasAmortizacionCuota[]Cronograma del préstamo seleccionado.
selectedPrestamoPrestamo | nullPréstamo cuya amortización está cargada.
editingPrestamoPrestamo | nullDistingue modal de alta vs edición.
formobjeto planoEstado del modal de alta/edición.
pagoForm{ cuotaId, fecha_pago, monto_pagado, movimiento_id }Estado del modal de pago.
filtroTextoPrest / filtroEstadoPreststringFiltros locales de la pestaña portafolio.

Los dos filtros se aplican en cliente sobre prestamos():

const filtradosPrestamos = createMemo(() => {
let list = prestamos();
const query = filtroTextoPrest().toLowerCase();
if (query) {
list = list.filter((item) =>
item.nombre_prestamo.toLowerCase().includes(query) ||
(item.nombre_banco ?? "").toLowerCase().includes(query),
);
}
if (filtroEstadoPrest()) list = list.filter((item) => item.estado === filtroEstadoPrest());
return list;
});
FiltroTipoOpciones
Texto<input>Sub-string sobre nombre_prestamo y nombre_banco.
EstadoWorkspaceSegmentedControlTodos, VIGENTE, CANCELADO, VENCIDO.

15 campos. Los marcados con * son obligatorios; el resto son opcionales y se omiten del payload cuando vienen vacíos:

CampoTipoObligatorioNotas
banco_id<select>Catálogo bancos del tenant.
nombre_prestamo<input>
numero_operacion<input>NoSólo en alta — no se carga al editar.
tipo_prestamo<select>HIPOTECARIO, CONSUMO, COMERCIAL, LEASING, OTRO. Default COMERCIAL.
monto_original<input type=number>Parseado con parseFloat.
monedaconstanteFijo CLP en el formulario actual.
tasa_interes<input type=number step=0.01>% anual; parseFloat.
tasa_tipo<select>FIJA o VARIABLE. Default FIJA.
fecha_inicio<input type=date>
fecha_vencimiento<input type=date>
num_cuotas<input type=number>parseInt.
periodicidad<select>MENSUAL, TRIMESTRAL, SEMESTRAL, ANUAL.
cuenta_pasivo_codigo<input>NoCódigo contable del pasivo.
cuenta_gasto_interes<input>NoCódigo contable del gasto financiero.
observaciones<textarea>No
const payload: Record<string, unknown> = { /* campos obligatorios */ };
if (current.numero_operacion) payload.numero_operacion = current.numero_operacion;
if (current.cuenta_pasivo_codigo) payload.cuenta_pasivo_codigo = current.cuenta_pasivo_codigo;
if (current.cuenta_gasto_interes) payload.cuenta_gasto_interes = current.cuenta_gasto_interes;
if (current.observaciones) payload.observaciones = current.observaciones;

No hay validación de cliente — todos los campos se aceptan y el backend rechaza si faltan obligatorios.

Acción inline desde la fila del portafolio (icono Database). Es destructiva sobre cuotas pendientes:

if (!confirm("¿Regenerar tabla de amortización? Se eliminarán las cuotas existentes no pagadas.")) return;
const res = await authenticatedFetch(`${API}/prestamos/${prestamoId}/amortizacion${tq()}`, {
method: "POST",
body: JSON.stringify({}),
});

Si el préstamo seleccionado coincide con el regenerado, recarga las cuotas inmediatamente para reflejar el cambio:

if (selectedPrestamo()?.id === prestamoId) void loadCuotas(prestamoId);

Las cuotas PAGADAS sobreviven a la regeneración (eso lo gestiona el backend); las PENDIENTES se reemplazan.

10 columnas. Sólo las cuotas PENDIENTE exponen la acción de registrar pago:

ColumnaRender
numero_cuota
VencimientofmtDate(fecha_vencimiento)
CapitalfmtMonto(capital)
InterésfmtMonto(interes)
TotalfmtMonto(total_cuota)
Cap. pendientefmtMonto(capital_pendiente) o
EstadoPENDIENTE | PAGADA (y eventualmente otros)
F. pagofmtDate(fecha_pago) o
Monto pagadofmtMonto(monto_pagado) o
(acción)<TableAction icon=Status> sólo si estado === "PENDIENTE".

3 campos. El monto se pre-rellena con total_cuota para registrar el caso normal sin escribir:

setPagoForm({
cuotaId: item.id,
fecha_pago: new Date().toISOString().slice(0, 10), // hoy
monto_pagado: String(item.total_cuota), // pre-rellenado
movimiento_id: "",
});
CampoObligatorioUso
fecha_pagoDefault hoy.
monto_pagadoDefault = total_cuota. Permite pago parcial o mayor (intereses moratorios).
movimiento_idNoUUID de un movimiento bancario ya cargado por la cartola para enlazar el pago a la contraparte bancaria.

Tras éxito, recarga las cuotas del préstamo seleccionado.

Cuatro contadores en el header se actualizan en vivo:

const prestamosVigentes = createMemo(() => prestamos().filter(i => i.estado === "VIGENTE").length);
const montoVisible = createMemo(() => filtradosPrestamos().reduce((s, i) => s + Number(i.monto_original), 0));
const cuotasPendientes = createMemo(() => cuotas().filter(i => i.estado === "PENDIENTE").length);
const cuotasPagadas = createMemo(() => cuotas().filter(i => i.estado === "PAGADA").length);

Monto visible suma sólo lo filtrado — el operador ve el impacto de cada filtro sin abrir un detalle.

MétodoRutaOperación
GET/api/financieros/bancos?tenant_id=...Catálogo de bancos.
GET/api/financieros/prestamos?tenant_id=...Lista del portafolio.
POST/api/financieros/prestamos?tenant_id=...Alta.
PUT/api/financieros/prestamos/:id?tenant_id=...Edición.
DELETE/api/financieros/prestamos/:id?tenant_id=...Eliminación.
GET/api/financieros/prestamos/:id/amortizacion?tenant_id=...Cuotas del préstamo.
POST/api/financieros/prestamos/:id/amortizacion?tenant_id=... body {}Genera/regenera la tabla.
PUT/api/financieros/prestamos/:id/amortizacion/:cuotaId/pagar?tenant_id=...Registra pago.

tenant_id viaja como query param en todas las llamadas.

ReglaMotivo
useActivePeriod no se consumeLos préstamos cruzan varios períodos; filtrar por mes/año oscurecería el portafolio.
Filtros de texto+estado en clienteEl portafolio es bajo volumen por tenant.
Modal con campos opcionales omitidos del payloadEl backend asigna defaults; mandar strings vacíos confundiría la validación.
Confirmar antes de regenerar amortizaciónLas cuotas PENDIENTE se borran y se reconstruyen.
Cambio de tenant limpia selectedPrestamo y cuotasEl cronograma del tenant anterior no aplica.
Pago pre-rellenado con total_cuota y fecha de hoyAcelera el caso normal; el operador sólo edita en pagos atípicos.
movimiento_id opcionalPermite registrar pagos sin cartola aún cargada; la conciliación queda como tarea posterior.
Sólo cuotas PENDIENTE exponen acción de pagoEvita re-pagos accidentales sobre cuotas ya cerradas.
Botón “Generar amortización” en cada fila, no globalEl operador debe decidir préstamo por préstamo; no hay “regenerar todas”.