Skip to content

Plataforma técnica · Sevastopol

Bootstrap Cierre Island

Islands Sevastopol Administración

BootstrapCierreIsland ejecuta la inicialización de saldos de apertura de un ejercicio contable para el tenant activo. Es una operación crítica: no tiene reversa desde la UI y solo puede dispararla SUPER_ADMIN. La UI verifica el rol vía /api/auth/validate antes de mostrar el formulario.

  • Directorysevastopol/src/components/islands/admin/
    • BootstrapCierreIsland.tsx — componente único, internamente BootstrapCierreView
const { tenantId } = useActiveTenant();
const [role, setRole] = createSignal<string | null>(null);
const isSuperAdmin = () => role() === "SUPER_ADMIN";

useActiveTenant aporta el tenant; el rol viene de un fetch independiente al iniciar:

async function fetchRole() {
const r = await authenticatedFetch("/api/auth/validate", { method: "POST" });
if (!r.ok) throw new Error("No se pudo validar la sesión");
const data = await r.json();
setRole(data?.user?.role ?? null);
}

Si role !== "SUPER_ADMIN", la island renderiza 403 en lugar del formulario.

Signals locales:

SignalTipoUso
rolestring | nullResultado de /api/auth/validate.
aniostringSelección del año (default año actual - 1).
messtringSelección del mes (default "12").
isLoadingbooleanValidando sesión inicial.
confirmOpenbooleanModal de confirmación abierto.
isSubmittingbooleanBloqueo del botón mientras se ejecuta.

El año cubre una ventana de 6 valores centrada en hoy menos 3:

const AÑO_ACTUAL = new Date().getFullYear();
const ANIOS = Array.from({ length: 6 }, (_, i) => {
const y = AÑO_ACTUAL - 3 + i;
return { id: String(y), nombre: String(y) };
});

Los meses son 1..12 con etiqueta "01 — Enero", etc.

Default sugerido: año anterior + diciembre. La caja de texto del header recomienda explícitamente:

“Selecciona el período de apertura. Se recomienda 12 del año anterior al ejercicio que vas a iniciar.”

async function ejecutarBootstrap() {
const tid = tenantId();
if (!tid) { toast.push("Selecciona un tenant", "error"); return; }
setSubmitting(true);
try {
const r = await authenticatedFetch(
`/api/reportes/bootstrap-cierre?tenant_id=${tid}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ anio: parseInt(anio()), mes: parseInt(mes()) }),
},
);
if (!r.ok) throw new Error((await r.json())?.error || "Falló el bootstrap");
toast.push(`Bootstrap ejecutado para ${anio()}-${mes().padStart(2, "0")}`, "ok");
setConfirmOpen(false);
} catch (e: any) {
toast.push(e.message, "error");
} finally {
setSubmitting(false);
}
}

El click en Ejecutar bootstrap abre el modal de confirmación; el botón de confirmar es workspaceDangerButtonClass para enfatizar la criticidad. Tras ejecutar, el toast muestra el período procesado y el modal se cierra.

Estado UICuándoRender
Validando sesiónisLoading === trueWorkspaceSectionCard con texto “Validando sesión…“.
403!isSuperAdminWorkspaceSectionCard con eyebrow "403", mensaje “No tienes permisos”.
ListoisSuperAdmin && tenantIdFormulario con dos <Select> + botón.
Sin tenantisSuperAdmin && !tenantIdBotón deshabilitado, pill “Sin seleccionar” en tono warning.
MétodoRutaOperación
POST/api/auth/validateDevuelve { user: { role } } — valida rol del usuario actual.
POST/api/reportes/bootstrap-cierre?tenant_id=... body { anio, mes }Inicializa saldos de apertura del ejercicio.
ReglaMotivo
Validar rol al montarMostrar el formulario sin rol válido confundiría; el backend igual rechazaría.
Doble confirm (botón → modal → botón danger)La operación es crítica y no reversible.
Botón en tono danger en el modalLenguaje visual claro de “no es CRUD normal”.
Default año anterior + diciembreEl bootstrap típico inicia el ejercicio siguiente con saldos del cierre anterior.
Sin useActivePeriodEl período del workspace es para reportes operativos; bootstrap usa selección explícita para evitar confusión.
Tenant obligatorio antes de enviarEl backend rechazaría sin tenant; el botón se deshabilita para feedback inmediato.
Aviso explícito de “no reversible” en el note del headerEl operador debe leer la consecuencia antes de hacer click.