Plataforma técnica · Sevastopol
DJ 1879 View Island
Islands Sevastopol Declaraciones
Propósito
Section titled “Propósito”DJ1879ViewIsland es la island anual que opera la Declaración Jurada 1879 sobre honorarios, directores y retenciones relacionadas. Lee del backend la cabecera + detalle por receptor del año activo, agrega los movimientos mensuales con factores de actualización y permite generar la DJ desde la base operacional y descargar el CSV con el formato del SII.
Ubicación
Section titled “Ubicación”Directorysevastopol/src/components/islands/declaraciones/
- DJ1879ViewIsland.tsx — componente único, internamente
DJ1879Panel - DeclaracionesWorkspace.tsx — tokens visuales compartidos
- DJ1879ViewIsland.tsx — componente único, internamente
Directorysevastopol/src/pages/api/declaraciones-juradas/1879/
- index.ts — proxy raíz
createProxy("/api/declaraciones-juradas/1879") - […all].ts — proxy catch-all
- index.ts — proxy raíz
Directorysevastopol/src/lib/hooks/
- useActiveTenant.ts
- useActivePeriod.ts
Hooks y estado global
Section titled “Hooks y estado global”const { tenantId } = useActiveTenant((id) => void loadData(id));const { period } = useActivePeriod(() => { if (tenantId()) loadData(tenantId());});const activeYear = () => period().year ?? new Date().getFullYear();| Hook | Aporta | Reactividad |
|---|---|---|
useActiveTenant | UUID del tenant. | Re-fetcha al cambiar tenant. |
useActivePeriod | { year } global. | Re-fetcha al cambiar año (el month no aplica aquí — la DJ es anual). |
Signals locales:
| Signal | Tipo | Uso |
|---|---|---|
header | DJ1879Header | null | Cabecera de la DJ del año (null si no existe). |
detalle | DJ1879Detalle[] | Líneas por receptor, con detalle_mensual anidado. |
loading | boolean | Bloqueo de UI durante el fetch. |
processing | boolean | Bloqueo durante el POST /generate. |
genModalOpen | boolean | Modal de confirmación de generación. |
searchTerm | string | Filtro libre por RUT o nombre del receptor. |
Modos de vista
Section titled “Modos de vista”La island bifurca según header():
| Header | Vista renderizada |
|---|---|
null (HTTP 404 del backend o sin generar) | Estado vacío con CTA Generar DJ 1879. |
| Existe | Dos WorkspaceSectionCard apiladas: Receptores informados + Resumen consolidado por mes. |
El 404 no es un error — el handler lo trata como “DJ no existe aún”:
if (resp.status === 404) { setHeader(null); setDetalle([]); return;}if (!resp.ok) throw new Error(`Error ${resp.status}`);Tabla de receptores
Section titled “Tabla de receptores”Filtro libre por RUT o nombre. Las columnas exponen los campos especiales del archivo SII en columnas independientes — no se suman entre sí:
| Columna | Origen | Notas |
|---|---|---|
numero_correlativo | Asignado por el generador. | — |
rut_receptor | RUT validado del receptor. | — |
nombre_receptor | Nombre del prestador o director. | Fallback "Desconocido" cuando viene null. |
retencion_honorarios | Artículo 42 N° 2 LIR. | Actualizada al cierre. |
retencion_directores | Artículo 48 LIR (S.A.). | Actualizada al cierre. |
monto_bruto_total | Bruto anual del receptor. | — |
retencion_prestamos | Retención adicional 3% por préstamos solidarios. | Columna independiente del SII. |
meses_count | meses_con_renta.length. | Derivado en cliente. |
.map((d) => ({ ...d, meses_count: d.meses_con_renta?.length || 0, nombre_receptor: d.nombre_receptor || "Desconocido", total_retencion: d.retencion_honorarios + d.retencion_directores,}));Detalle mensual consolidado
Section titled “Detalle mensual consolidado”La segunda tarjeta agrega los detalle_mensual de todos los receptores filtrados mes a mes. Es una vista comparativa histórico vs actualizado por mes:
const aggregatedMonthly = createMemo(() => { const months: Record<number, any> = {}; displayRows().forEach((receptor) => { receptor.detalle_mensual?.forEach((m) => { if (!months[m.mes]) { months[m.mes] = { ...m }; } else { months[m.mes].monto_bruto += m.monto_bruto; months[m.mes].retencion_sin_actualizar += m.retencion_sin_actualizar; months[m.mes].retencion_actualizada += m.retencion_actualizada; } }); }); return Object.values(months).sort((a, b) => a.mes - b.mes);});Columnas del detalle mensual:
| Columna | Significado |
|---|---|
| Mes | Nombre corto (Ene, Feb, …). |
| Bruto | Bruto del mes (suma de receptores filtrados). |
| Ret. Hist. | Retención original del mes en pesos nominales. |
| Factor | Factor de actualización IPC/UF del SII para ese mes (3 decimales). |
| Ret. Act. | Retención re-expresada al cierre del año tributario. |
Acciones
Section titled “Acciones”Generar DJ 1879
Section titled “Generar DJ 1879”Modal de confirmación → POST /api/declaraciones-juradas/1879/generate con { año: activeYear() }. La respuesta trae totalReceptores y se muestra en el toast:
toast.push(`Generada exitosamente (${json.totalReceptores} registros)`, "ok");Tras éxito el modal se cierra y loadData(tid) refresca la vista. Si ya existe DJ para el año, el backend la sobreescribe — la UI no advierte explícitamente.
Descargar CSV
Section titled “Descargar CSV”Disponible sólo cuando existe header(). Genera un archivo con nombre DJ1879_{año_tributario}.csv desde un Blob:
const res = await authenticatedFetch(`${API_BASE}/${h.id}/csv?tenant_id=${tid}`);const blob = await res.blob();const url = URL.createObjectURL(blob);const a = document.createElement("a");a.href = url;a.download = `DJ1879_${h.año_tributario}.csv`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);El CSV viaja como binario por el proxy (createProxy reenvía Content-Type y el cuerpo crudo).
Flujo de carga
Section titled “Flujo de carga”flowchart TB
MOUNT["onMount<br/>tenantId ready"]
CHGT["useActiveTenant onChange"]
CHGP["useActivePeriod onChange"]
LOAD["loadData(tid)<br/>GET /1879/anio/{year}"]
S404{"status === 404?"}
EMPTY["header=null<br/>detalle=[]"]
OK["header=DJ1879Header<br/>detalle=DJ1879Detalle[]"]
ROWS["displayRows<br/>(filter por searchTerm)"]
AGG["aggregatedMonthly<br/>(suma por mes)"]
T1["DataTable receptores"]
T2["DataTable detalle mensual"]
CSV["downloadCSV<br/>GET /1879/{id}/csv"]
GEN["generate<br/>POST /1879/generate"]
MOUNT --> LOAD
CHGT --> LOAD
CHGP --> LOAD
LOAD --> S404
S404 -- sí --> EMPTY
S404 -- no --> OK
OK --> ROWS --> T1
ROWS --> AGG --> T2
T1 --> CSV
T2 --> CSV
GEN --> LOAD Endpoints consumidos
Section titled “Endpoints consumidos”| Método | Ruta | Operación |
|---|---|---|
GET | /api/declaraciones-juradas/1879/anio/:año?tenant_id=... | Cabecera + detalle del año. 404 se trata como vacío. |
POST | /api/declaraciones-juradas/1879/generate?tenant_id=... | Genera/sobreescribe la DJ del año desde la base operacional. |
GET | /api/declaraciones-juradas/1879/:id/csv?tenant_id=... | Descarga el archivo en formato SII. |
Las llamadas usan authenticatedFetch directo, no un cliente Service envuelto.
Proxy local
Section titled “Proxy local”import { createProxy } from "@/lib/proxyUtils";export const { GET, POST, PUT, DELETE } = createProxy( "/api/declaraciones-juradas/1879",);Se exportan los cuatro métodos clásicos; el detalle del BFF está en BFF Proxy a Orchestrator.
Reglas de UI
Section titled “Reglas de UI”| Regla | Motivo |
|---|---|
404 como estado vacío | El backend no distingue “no existe aún” de error — la UI debe ofrecer la acción de generar. |
| Re-fetch al cambiar año, no al cambiar mes | La DJ es anual; el mes del período activo no influye. |
| Agregación mensual sobre filas filtradas | El “detalle mensual” refleja lo que el usuario está viendo; cambiar el search lo recalcula. |
| Cuadre con F29 usando históricos | Comparar actualizado contra F29 nominal produce diferencias artificiales por corrección monetaria. |
| Columnas independientes para campos especiales | retencion_directores, monto_isla_pascua y retencion_prestamos no se suman a la retención estándar — viajan por separado al SII. |
| Sobrescritura silenciosa en generate | El backend reemplaza la DJ del año sin advertir; la UI debería confirmar antes (TODO). |
Descarga deshabilitada sin header() | Sin DJ generada no hay CSV que descargar. |