/* global React */
const { useState: useStateD } = React;
const TITULO_DETALLE_DEFAULT = 'Fortalecimiento del régimen de declaración jurada de funcionarios públicos';
const ARCHIVOS_DEFAULT = [
{ name: 'reporte_MESICIC_RondaV_PRY.pdf', size: '2.4 MB' },
{ name: 'matriz_cobertura_DJBe_2025.xlsx', size: '318 KB' },
];
const ACCIONES_DEFAULT = [
{
descripcion: 'Ampliar la cobertura del sistema de declaración jurada electrónica (DJB-e) al 100% de los funcionarios obligados.',
estado: 'partial',
instituciones: [
{ inst: 'Secretaría Nacional Anticorrupción', rol: 'Líder', plazo: '2026-06-12', responsable: 'u-005' },
{ inst: 'Ministerio de Hacienda', rol: 'Apoyo', plazo: '2026-06-30', responsable: null },
],
avances: [
{ fecha: '2026-05-10', autor: 'R. Mendoza', inst: 'SENAC', texto: 'Cobertura DJB-e alcanzó el 76% de los obligados; se sumó el módulo de carga masiva.', archivos: [{ name: 'cobertura_djbe_may2026.xlsx', size: '214 KB' }] },
],
},
{
descripcion: 'Implementar el módulo de verificación cruzada con la base patrimonial de la SET.',
estado: 'pending',
instituciones: [
{ inst: 'Ministerio de Hacienda', rol: 'Líder', plazo: '2026-09-30', responsable: null },
],
avances: [],
},
{
descripcion: 'Habilitar el mecanismo de consulta pública con anonimización parcial y protección de datos.',
estado: 'pending',
instituciones: [
{ inst: 'Contraloría General de la República', rol: 'Coordinación', plazo: '2026-08-15', responsable: null },
],
avances: [],
},
];
const RESULTADOS_DEFAULT = '217.000 declaraciones presentadas electrónicamente · 32 verificaciones sustantivas iniciadas · 4 procesos administrativos derivados a Fiscalía.';
const JUSTIF_DEFAULT = 'La Contraloría General de la República implementó en 2025 la plataforma DJB-e con cobertura del 76% de los obligados. Quedan pendientes los módulos de verificación cruzada con la base patrimonial de la SET y el sistema de consulta pública con anonimización parcial — ambos en fase de desarrollo.';
const ALERTAS_DEF = [
{ code: 'D-7', label: 'Aviso de proximidad' },
{ code: 'D-3', label: 'Recordatorio' },
{ code: 'D-1', label: 'Urgencia' },
{ code: 'D', label: 'Vencimiento' },
{ code: 'D+1', label: 'Sin avance (escalamiento)' },
];
const MECANISMOS = ['MESICIC', 'UNCAC', 'OCDE'];
const TIPOS_INFORME = ['Informe', 'Informe tratado', 'Anexo', 'Evidencia', 'Resolución', 'Acta'];
const INFORMES_DEFAULT = [
{ name: 'informe_de_avance_DJBe_2026Q1.pdf', tipo: 'Informe tratado', size: '2.1 MB', fecha: '2026-05-12' },
{ name: 'matriz_cobertura_institucional.xlsx', tipo: 'Anexo', size: '318 KB', fecha: '2026-05-10' },
{ name: 'resolucion_CGR_114-2026.pdf', tipo: 'Resolución', size: '540 KB', fecha: '2026-04-28' },
];
const ESTADO_OPTS = [
{ v: 'pending', l: 'Pendiente' },
{ v: 'partial', l: 'Parcial' },
{ v: 'ok', l: 'Cumplido' },
{ v: 'bad', l: 'No cumplido' },
{ v: 'overdue', l: 'Vencido s/avance' },
];
function DetalleScreen({ onBack, onOpenTablero }) {
const I = window.Icons;
const d = window.COMP_DETAIL;
const [tab, setTab] = useStateD('info');
const buildData = () => ({
id: d.id,
idInternacional: d.idInternacional,
mecanismo: d.mecanismo,
ronda: d.ronda,
recibida: d.recibida,
vence: d.vence,
titulo: d.titulo || TITULO_DETALLE_DEFAULT,
estado: d.estado,
eje: d.eje, tipo: d.tipo, evaluacion: d.evaluacion,
recomendacion: d.recomendacion,
archivos: ARCHIVOS_DEFAULT.map(a => ({ ...a })),
acciones: ACCIONES_DEFAULT.map(a => ({ ...a })),
resultados: RESULTADOS_DEFAULT,
justificacion: JUSTIF_DEFAULT,
informes: INFORMES_DEFAULT.map(a => ({ ...a })),
alertas: { 'D-7': true, 'D-3': true, 'D-1': true, 'D': true, 'D+1': true },
});
const [data, setData] = window.usePersistentState('detalle_' + d.id, buildData);
const [heroEdit, setHeroEdit] = useStateD(false);
const [confirmDel, setConfirmDel] = useStateD(false);
const update = (k, v) => setData(s => ({ ...s, [k]: v }));
const removeArchivo = (i) => setData(s => ({ ...s, archivos: s.archivos.filter((_, idx) => idx !== i) }));
const setAccion = (i, k, v) => setData(s => ({ ...s, acciones: s.acciones.map((a, idx) => idx === i ? { ...a, [k]: v } : a) }));
const addAccion = () => setData(s => ({ ...s, acciones: [...s.acciones, { descripcion: '', estado: 'pending', instituciones: [{ inst: '', rol: 'Líder', plazo: '', responsable: null }], avances: [] }] }));
const removeAccion = (i) => setData(s => ({ ...s, acciones: s.acciones.filter((_, idx) => idx !== i) }));
const setAccionInst = (ai, ii, k, v) => setData(s => ({ ...s, acciones: s.acciones.map((a, idx) => idx === ai ? { ...a, instituciones: a.instituciones.map((r, j) => j === ii ? { ...r, [k]: v } : r) } : a) }));
const addAccionInst = (ai) => setData(s => ({ ...s, acciones: s.acciones.map((a, idx) => idx === ai ? { ...a, instituciones: [...a.instituciones, { inst: '', rol: 'Apoyo', plazo: '', responsable: null }] } : a) }));
const removeAccionInst = (ai, ii) => setData(s => ({ ...s, acciones: s.acciones.map((a, idx) => idx === ai ? { ...a, instituciones: a.instituciones.filter((_, j) => j !== ii) } : a) }));
const setAccionInstResponsable = (ai, ii, uid) => setData(s => ({ ...s, acciones: s.acciones.map((a, idx) => idx === ai ? { ...a, instituciones: a.instituciones.map((r, j) => j === ii ? { ...r, responsable: uid } : r) } : a) }));
const addAvanceAccion = (ai, payload) => setData(s => ({ ...s, acciones: s.acciones.map((a, idx) => idx === ai ? { ...a, avances: [{ fecha: new Date().toISOString().slice(0, 10), autor: payload.autor || 'C. Benítez', inst: payload.inst || '', texto: payload.texto, archivos: payload.archivos || [] }, ...a.avances] } : a) }));
const removeAvanceAccion = (ai, k) => setData(s => ({ ...s, acciones: s.acciones.map((a, idx) => idx === ai ? { ...a, avances: a.avances.filter((_, j) => j !== k) } : a) }));
const editAvanceAccion = (ai, k, texto) => setData(s => ({ ...s, acciones: s.acciones.map((a, idx) => idx === ai ? { ...a, avances: a.avances.map((av, j) => j === k ? { ...av, texto } : av) } : a) }));
const toggleAlerta = (code) => setData(s => ({ ...s, alertas: { ...s.alertas, [code]: !s.alertas[code] } }));
const removeInforme = (i) => setData(s => ({ ...s, informes: s.informes.filter((_, idx) => idx !== i) }));
const addInforme = (payload) => setData(s => ({ ...s, informes: [{ fecha: new Date().toISOString().slice(0, 10), tipo: payload.tipo || 'Informe', name: payload.name, size: payload.size || '—' }, ...s.informes] }));
const [heroErr, setHeroErr] = useStateD({});
const otrosComp = (window.COMPROMISOS || []).filter(c => c.id !== d.id);
const guardarHero = () => {
const e = {};
const id = (data.id || '').trim();
const idInt = (data.idInternacional || '').trim();
if (!id) e.id = 'Ingresa el ID interno.';
else if (otrosComp.some(c => (c.id || '').toLowerCase() === id.toLowerCase())) e.id = 'Ese ID interno ya está en uso por otro compromiso.';
if (!idInt) e.idInternacional = 'Ingresa el ID internacional.';
else if (otrosComp.some(c => (c.idInternacional || '').toLowerCase() === idInt.toLowerCase())) e.idInternacional = 'Ese ID internacional ya está en uso por otro compromiso.';
if (!(data.mecanismo || '').trim()) e.mecanismo = 'Indica el mecanismo.';
setHeroErr(e);
if (Object.keys(e).length === 0) setHeroEdit(false);
};
const abrirHeroEdit = () => { setHeroErr({}); setHeroEdit(true); };
const cancelarHeroEdit = () => { setHeroErr({}); setHeroEdit(false); };
return (
Volver al tablero
Compromisos
{data.mecanismo} · {data.ronda}
{data.id}
{/* Hero card */}
{heroEdit ? (
) : (
<>
ID interno {data.id}
ID internacional {data.idInternacional}
Mecanismo · Ronda
{data.mecanismo}
{data.ronda}
window.cgrStore.exportRows(
data.acciones.map((a, i) => ({
n: i + 1,
descripcion: a.descripcion,
estado: a.estado,
instituciones: (a.instituciones || []).map(x => x.inst).join(', '),
plazos: (a.instituciones || []).map(x => x.plazo).filter(Boolean).join(' · '),
avances: (a.avances || []).length,
})),
[
{ key: 'n', label: '#' },
{ key: 'descripcion', label: 'Acción' },
{ key: 'estado', label: 'Estado', format: v => ({ pending: 'Pendiente', partial: 'Parcial', ok: 'Cumplido', bad: 'No cumplido', overdue: 'Vencido s/avance' }[v] || v) },
{ key: 'instituciones', label: 'Instituciones' },
{ key: 'plazos', label: 'Plazos' },
{ key: 'avances', label: 'N° avances' },
],
'ficha_' + (data.id || 'recomendacion'),
'Plan de acción'
)}
>
setConfirmDel(true)}>
{data.titulo}
Recibida {data.recibida}
Vence {data.vence}
{data.acciones.length} acciones
>
)}
{confirmDel && (
¿Eliminar esta recomendación y todo su seguimiento? Esta acción no se puede deshacer.
setConfirmDel(false)}>Cancelar
{ setConfirmDel(false); onOpenTablero && onOpenTablero(); }}> Eliminar
)}
{/* Tabs */}
{tab === 'info' && }
{tab === 'acc' && }
{tab === 'av' && }
{tab === 'al' && }
{tab === 'msg' && }
{tab === 'res' && }
);
}
function Tab({ id, label, icon, active, onSel, badge }) {
const I = window.Icons;
const Ico = I[icon] || I.FileText;
return (
onSel(id)}>
{label}
{badge && {badge} }
);
}
// ---------- Encabezado de sección con acciones (editar / eliminar) ----------
function SectionCard({ label, sub, editing, onEdit, onDone, onDelete, children }) {
const I = window.Icons;
return (
{editing ? (
) : (
<>
{onEdit && }
{onDelete && }
>
)}
{children}
);
}
// ---------- Información (base) ----------
function TabInfo({ data, update, removeArchivo }) {
const I = window.Icons;
const [eRec, setERec] = useStateD(false);
const [eClas, setEClas] = useStateD(false);
const [eFiles, setEFiles] = useStateD(false);
return (
<>
setERec(true)} onDone={() => setERec(false)}>
{eRec
?
setEClas(true)} onDone={() => setEClas(false)}>
{eClas ? (
) : (
Eje temático {data.eje}
Tipo {data.tipo}
Evaluación de origen {data.evaluacion}
)}
setEFiles(true)} onDone={() => setEFiles(false)}>
{data.archivos.length === 0 && !eFiles ? (
Sin archivos adjuntos.
) : (
{data.archivos.map((f, i) => (
{f.name}
· {f.size}
{eFiles
? removeArchivo(i)}>
: }
))}
)}
{eFiles && (
Arrastra los archivos aquí o haz clic para seleccionar
PDF, DOCX, XLSX, PNG, JPG · máximo 25 MB cada uno
)}
>
);
}
// ---------- Acciones (plan de acción: institución + responsable opcional) ----------
const MESES_ABBR = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
const HOY_REF = new Date('2026-06-06T00:00:00');
const ROL_KEY = { 'Líder': 'lider', 'Coordinación': 'coord', 'Apoyo': 'apoyo' };
function instMetaOf(nombre) {
if (!nombre) return null;
return window.cgrData.instituciones().find(o => o.nombre === nombre || o.sigla === nombre) || null;
}
function siglaOf(nombre) {
const o = instMetaOf(nombre);
if (o) return o.sigla;
return nombre ? nombre.split(/\s+/).map(w => w[0]).join('').slice(0, 3).toUpperCase() : '—';
}
function userById(uid) {
return window.cgrData.usuarios().find(u => u.id === uid) || null;
}
function nombreFocal(nombre) {
const o = instMetaOf(nombre);
if (!o || !o.contacto) return null;
const n = o.contacto.split('·')[0].trim();
return n && n.toLowerCase().indexOf('sin contacto') === -1 ? n : null;
}
function iniciales(nombre) {
if (!nombre) return '—';
const limpio = nombre.replace(/\(.*?\)/g, '').trim();
const parts = limpio.split(/\s+/).filter(Boolean);
if (parts.length === 0) return '—';
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
function usersOf(instNombre) {
if (!instNombre) return [];
const cat = instMetaOf(instNombre);
if (!cat) return [];
const sig = cat.sigla.toLowerCase();
const nom = cat.nombre.toLowerCase();
const seen = {};
return window.cgrData.usuarios().filter(u => {
const seg = (u.institucion || '').toLowerCase().split('·')[0].trim();
const match = seg === sig || seg === nom || nom.startsWith(seg) || seg.startsWith(nom);
if (!match || seen[u.id]) return false; // sin duplicados
seen[u.id] = true;
return true;
});
}
function fmtFecha(iso) {
if (!iso) return 'sin plazo';
const p = iso.split('-');
if (p.length !== 3) return iso;
return `${+p[2]} ${MESES_ABBR[+p[1] - 1]} ${p[0]}`;
}
function plazoTone(iso) {
if (!iso) return '';
const dt = new Date(iso + 'T00:00:00');
const days = Math.round((dt - HOY_REF) / 86400000);
if (days < 0) return 'is-overdue';
if (days <= 7) return 'is-soon';
return '';
}
function TabAcciones({ data, setAccion, addAccion, removeAccion, setAccionInst, addAccionInst, removeAccionInst, setAccionInstResponsable, addAvanceAccion, removeAvanceAccion, editAvanceAccion }) {
const I = window.Icons;
return (
Plan de acción — {data.acciones.length} acciones
Cada acción se asigna a una o más instituciones (con rol y plazo). Todos los usuarios de cada institución ven la acción y pueden subir evidencia. El responsable queda en el punto focal de la institución, salvo que designes a otra persona.
{data.acciones.map((a, i) => (
))}
Agregar acción
);
}
function AccionCard({ index, a, setAccion, removeAccion, setAccionInst, addAccionInst, removeAccionInst, setAccionInstResponsable, addAvanceAccion, removeAvanceAccion, editAvanceAccion }) {
const I = window.Icons;
const instOpts = window.cgrData.instituciones();
const [editing, setEditing] = useStateD(!a.descripcion);
const [composing, setComposing] = useStateD(false);
const [avTexto, setAvTexto] = useStateD('');
const [avInst, setAvInst] = useStateD('');
const [avFile, setAvFile] = useStateD(null);
const [confirmDel, setConfirmDel] = useStateD(false);
const [avEdit, setAvEdit] = useStateD(-1);
const [avEditText, setAvEditText] = useStateD('');
const [avDel, setAvDel] = useStateD(-1);
const cardAlt = index % 2 === 0 ? ' pa-card--alt' : '';
const evidenciasDe = (sig) => (a.avances || []).filter(av => (av.inst || '') === sig).length;
const abrirComposer = () => {
const first = a.instituciones.find(r => r.inst);
setAvInst(first ? siglaOf(first.inst) : '');
setAvTexto(''); setAvFile(null); setComposing(true);
};
const guardarEvidencia = () => {
if (!avTexto.trim()) return;
const meta = instOpts.find(o => o.sigla === avInst);
const autor = nombreFocal(meta ? meta.nombre : '') || (avInst ? `Enlace ${avInst}` : 'CGR');
addAvanceAccion(index, {
texto: avTexto.trim(),
inst: avInst,
autor,
archivos: avFile ? [{ name: avFile, size: '—' }] : [],
});
setComposing(false); setAvTexto(''); setAvFile(null);
};
const delBanner = confirmDel ? (
¿Eliminar la Acción {index + 1} y toda su evidencia? Esta acción no se puede deshacer.
setConfirmDel(false)}>Cancelar
removeAccion(index)}> Eliminar acción
) : null;
// ---------- Modo edición ----------
if (editing) {
return (
Acción {index + 1}
Estado
setAccion(index, 'estado', e.target.value)}>
{ESTADO_OPTS.map(o => {o.l} )}
setConfirmDel(true)}>
{delBanner}
Descripción de la acción
Instituciones responsables {a.instituciones.length}
Se asigna a la institución: todos sus usuarios verán la acción y podrán subir evidencia. El responsable queda en el punto focal salvo que designes a otra persona.
{a.instituciones.map((r, ii) => {
const users = usersOf(r.inst);
const focal = nombreFocal(r.inst);
return (
Institución
setAccionInst(index, ii, 'inst', e.target.value)}>
Seleccionar…
{r.inst && !instOpts.some(o => o.nombre === r.inst || o.sigla === r.inst) && {r.inst} }
{instOpts.map(o => { const off = o.estado === 'desactivado'; return {o.nombre} ({o.sigla}){off ? ' — desactivada' : ''} ; })}
Rol
setAccionInst(index, ii, 'rol', e.target.value)}>
Líder Coordinación Apoyo
Plazo interno
setAccionInst(index, ii, 'plazo', e.target.value)} />
removeAccionInst(index, ii)} disabled={a.instituciones.length === 1}>
{r.inst && (
Responsable (opcional)
setAccionInstResponsable(index, ii, e.target.value || null)}>
{focal ? `Punto focal — ${focal} (por defecto)` : 'Punto focal de la institución (por defecto)'}
{users.filter(u => window.cgrData.usuarioSeleccionable(u)).map(u => {u.nombre} )}
Todos los usuarios de {siglaOf(r.inst)} verán la acción y recibirán aviso. El responsable es solo el contacto principal.
)}
);
})}
addAccionInst(index)}> Agregar institución
setEditing(false)}> Listo
);
}
// ---------- Modo lectura ----------
const instAsignadas = a.instituciones.filter(r => r.inst);
return (
{delBanner}
{a.descripcion || Sin descripción. }
Instituciones responsables {instAsignadas.length}
{instAsignadas.length === 0 &&
Sin instituciones asignadas todavía.
}
{instAsignadas.map((r, ii) => {
const sig = siglaOf(r.inst);
const meta = instMetaOf(r.inst);
const respUser = r.responsable ? userById(r.responsable) : null;
const respNombre = respUser ? respUser.nombre : (nombreFocal(r.inst) || '— sin punto focal —');
const respInit = respUser ? respUser.initials : iniciales(nombreFocal(r.inst));
const esFocal = !r.responsable;
const ev = evidenciasDe(sig);
const rolKey = ROL_KEY[r.rol] || 'apoyo';
return (
{sig}
{meta ? meta.nombre : r.inst}
{r.rol}
Plazo {fmtFecha(r.plazo)}
{respInit}
{respNombre}
{esFocal ? 'Punto focal' : 'Responsable'}
{ev} evidencia{ev === 1 ? '' : 's'}
);
})}
Evidencia y avances {a.avances.length}
{!composing && Cargar evidencia }
{composing && (
Subir como institución
setAvInst(e.target.value)}>
{instAsignadas.map((r, k) => {
const s = siglaOf(r.inst);
const m = instMetaOf(r.inst);
return {s} — {m ? m.nombre : r.inst} ;
})}
)}
{a.avances.length === 0 && !composing &&
Aún no se cargó evidencia para esta acción.
}
{a.avances.map((av, k) => (
{iniciales(av.autor)}
{av.autor}
{av.inst && {av.inst} }
{fmtFecha(av.fecha)}
{avEdit !== k && avDel !== k && (
{ setAvEdit(k); setAvEditText(av.texto); }}>
setAvDel(k)}>
)}
{avEdit === k ? (
) : (
{av.texto}
)}
{avDel === k && (
¿Eliminar esta evidencia? No se puede deshacer.
setAvDel(-1)}>Cancelar
{ removeAvanceAccion(index, k); setAvDel(-1); }}> Eliminar
)}
{avEdit !== k && (av.archivos || []).map((f, fi) => (
{f.name}{f.size && f.size !== '—' ? · {f.size} : null}
))}
))}
);
}
// ---------- Avances ----------
function TabAvances({ d, data }) {
const I = window.Icons;
const avances = [];
(data.acciones || []).forEach((a, ai) => (a.avances || []).forEach(av => avances.push({ ...av, accion: ai + 1 })));
avances.sort((x, y) => (y.fecha || '').localeCompare(x.fecha || ''));
const iconFor = t => {
if (t === 'submitted') return { icon: 'Upload', tone: 'default', label: 'Avance cargado' };
if (t === 'returned') return { icon: 'AlertCircle', tone: 'warn', label: 'Devolución para ajuste' };
if (t === 'state') return { icon: 'CheckCircle2', tone: 'ok', label: 'Cambio de estado' };
if (t === 'message') return { icon: 'MessageSquare', tone: 'default', label: 'Mensaje enviado' };
if (t === 'assigned') return { icon: 'Users', tone: 'default', label: 'Asignación' };
if (t === 'created') return { icon: 'PlusCircle', tone: 'default', label: 'Recomendación creada' };
return { icon: 'FileText', tone: 'default', label: 'Acción' };
};
return (
<>
Avances reportados · {avances.length}
Los avances se cargan sobre cada acción (en la pestaña Acciones). Aquí se ven todos juntos, indicando a qué acción pertenecen.
{avances.length === 0 ? (
Aún no se cargaron avances.
) : (
{avances.map((av, k) => (
Acción {av.accion}
{av.autor}
{av.fecha}
{av.texto}
))}
)}
Actividad del sistema — {d.historial.length}
{d.historial.map((h, i) => {
const m = iconFor(h.type);
return (
{React.createElement(I[m.icon], { size: 13 })}
{m.label}
{h.date}
{h.actor} {h.body}
{h.detail &&
{h.detail}
}
);
})}
>
);
}
// ---------- Alertas (vista admin: configurar, próximas y registro) ----------
const ALERTA_OFFSETS = [
{ code: 'D-7', dias: 7, sev: 'info', label: 'Aviso de proximidad', desc: '7 días antes del plazo' },
{ code: 'D-3', dias: 3, sev: 'warn', label: 'Recordatorio', desc: '3 días antes del plazo' },
{ code: 'D-1', dias: 1, sev: 'danger', label: 'Urgencia', desc: '24 h antes del plazo' },
{ code: 'D', dias: 0, sev: 'danger', label: 'Vencimiento', desc: 'El día del plazo interno' },
{ code: 'D+1', dias: -1, sev: 'dark', label: 'Sin avance', desc: 'Día siguiente sin carga · escala al punto focal' },
];
function buildAlertSchedule(acciones, enabled) {
const out = [];
(acciones || []).forEach((a, ai) => {
(a.instituciones || []).filter(r => r.inst && r.plazo).forEach(r => {
ALERTA_OFFSETS.forEach(o => {
if (!enabled[o.code]) return;
const base = new Date(r.plazo + 'T00:00:00');
const dt = new Date(base.getTime() - o.dias * 86400000);
out.push({
key: `${ai}-${siglaOf(r.inst)}-${o.code}`,
code: o.code, sev: o.sev, date: dt, iso: dt.toISOString().slice(0, 10),
accion: ai + 1, inst: r.inst, plazo: r.plazo, responsable: r.responsable,
});
});
});
});
return out;
}
function diasEntre(date) { return Math.round((date - HOY_REF) / 86400000); }
function diasTxt(n) {
if (n === 0) return 'hoy';
if (n === 1) return 'mañana';
if (n > 1) return `en ${n} días`;
if (n === -1) return 'ayer';
return `hace ${Math.abs(n)} días`;
}
function destinatarioDe(r) {
const u = r.responsable ? userById(r.responsable) : null;
return {
nombre: u ? u.nombre : (nombreFocal(r.inst) || 'Punto focal'),
init: u ? u.initials : iniciales(nombreFocal(r.inst)),
esFocal: !r.responsable,
};
}
function TabAlertas({ data, toggleAlerta }) {
const I = window.Icons;
const [sent, setSent] = useStateD({});
const enabled = data.alertas || {};
const schedule = buildAlertSchedule(data.acciones, enabled);
const proximas = schedule.filter(s => diasEntre(s.date) >= 0).sort((a, b) => a.date - b.date);
const enviadasAuto = schedule.filter(s => diasEntre(s.date) < 0).sort((a, b) => b.date - a.date);
const enviarAhora = (key) => setSent(s => ({ ...s, [key]: true }));
const sentKeys = Object.keys(sent);
return (
<>
Configuración de recordatorios automáticos
Se calculan sobre el plazo interno de cada acción y se envían a la institución responsable (todos sus usuarios) por los canales activos.
{ALERTA_OFFSETS.map(o => {
const on = !!enabled[o.code];
return (
{o.code}
{o.label}
{o.desc}
Correo
Plataforma
toggleAlerta(o.code)}>
);
})}
Próximas alertas — {proximas.length}
Calculadas a partir de los plazos de cada acción. Puedes adelantar el envío con “Enviar ahora”.
{proximas.length === 0 ? (
No hay alertas próximas con la configuración actual.
) : (
{proximas.slice(0, 8).map(s => {
const dest = destinatarioDe(s);
const n = diasEntre(s.date);
return (
{s.code}
{fmtFecha(s.iso)} {diasTxt(n)}
{siglaOf(s.inst)}
Acción {s.accion} · vence {fmtFecha(s.plazo)}
{dest.init} {dest.nombre}
{sent[s.key]
?
Enviada
:
enviarAhora(s.key)}> Enviar ahora }
);
})}
)}
Alertas enviadas — registro
Historial de avisos disparados por la plataforma para este compromiso.
{sentKeys.map(k => {
const s = schedule.find(x => x.key === k);
if (!s) return null;
const dest = destinatarioDe(s);
return (
{s.code} {siglaOf(s.inst)} · Acción {s.accion} recién · manual
Enviada a {dest.nombre} · correo + plataforma
);
})}
{enviadasAuto.slice(0, 8).map(s => {
const dest = destinatarioDe(s);
return (
{s.code} {siglaOf(s.inst)} · Acción {s.accion} {fmtFecha(s.iso)} · {diasTxt(diasEntre(s.date))}
Enviada a {dest.nombre} · correo + plataforma
);
})}
{enviadasAuto.length === 0 && sentKeys.length === 0 &&
Todavía no se enviaron alertas para este compromiso.
}
>
);
}
// ---------- Mensajería (vista admin: un hilo por institución asignada) ----------
function TabMensajeria({ d, data }) {
const I = window.Icons;
const asignadas = [];
(data.acciones || []).forEach(a => (a.instituciones || []).forEach(r => {
if (!r.inst) return;
const sig = siglaOf(r.inst);
if (sig === 'CGR') return;
if (!asignadas.some(x => x.sig === sig)) asignadas.push({ sig, inst: r.inst, responsable: r.responsable });
}));
const seed = () => {
const t = {};
asignadas.forEach(x => { t[x.sig] = []; });
if (t['SENAC']) t['SENAC'] = [
{ from: 'in', author: 'R. Mendoza (SENAC)', date: '14 may · 09:12', text: 'Subimos el primer ciclo de avance con las evidencias de implementación inicial del módulo DJB-e.', files: [] },
{ from: 'out', author: 'C. Benítez (CGR)', date: '18 may · 14:33', text: 'Gracias. Observamos que la conciliación del 2do trimestre 2025 no cuadra con el reporte semestral; necesitamos la matriz que cruza cobertura declarada con base SET.', files: [] },
{ from: 'in', author: 'R. Mendoza (SENAC)', date: 'Hoy · 10:48', text: 'Cargamos el tercer ciclo con la matriz de conciliación y la tabla de cobertura por dependencia. Quedamos atentos a observaciones.', files: [{ name: 'matriz_conciliacion_2026Q1.xlsx', size: '412 KB' }] },
];
if (t['MH']) t['MH'] = [
{ from: 'out', author: 'C. Benítez (CGR)', date: '12 may · 11:05', text: 'Recordatorio: el módulo de verificación cruzada con la base SET sigue pendiente. ¿Pueden confirmar fecha estimada?', files: [] },
{ from: 'in', author: 'L. Ramírez (MH)', date: '12 may · 16:40', text: 'Estamos coordinando con la SET el acceso a la base patrimonial. Estimamos cargar el avance antes del 30 de junio.', files: [] },
];
return t;
};
const [threads, setThreads] = useStateD(seed);
const [active, setActive] = useStateD(asignadas.length ? asignadas[0].sig : '');
const [unread, setUnread] = useStateD(() => {
const u = {}; asignadas.forEach(x => { u[x.sig] = x.sig === 'SENAC' ? 2 : 0; }); return u;
});
const [draft, setDraft] = useStateD('');
if (asignadas.length === 0) {
return Asigna instituciones en la pestaña Acciones para habilitar la mensajería.
;
}
const seleccionar = (sig) => { setActive(sig); setUnread(u => ({ ...u, [sig]: 0 })); };
const enviar = () => {
if (!draft.trim()) return;
setThreads(t => ({ ...t, [active]: [...(t[active] || []), { from: 'out', author: 'C. Benítez (CGR)', date: 'Ahora', text: draft.trim(), files: [] }] }));
setDraft('');
};
const activeInst = asignadas.find(x => x.sig === active) || asignadas[0];
const dest = destinatarioDe(activeInst);
const msgs = threads[active] || [];
return (
Mensajería con las instituciones asignadas
Un hilo por institución: escribes a la institución y lo ven todos sus usuarios; el punto focal o responsable queda notificado.
{asignadas.map(x => {
const arr = threads[x.sig] || [];
const last = arr[arr.length - 1];
const u = unread[x.sig] || 0;
return (
seleccionar(x.sig)}>
{x.sig}
{x.sig} {u > 0 && {u} }
{last ? (last.from === 'out' ? 'Tú: ' : '') + last.text : 'Sin mensajes'}
);
})}
{activeInst.sig}
{instMetaOf(activeInst.inst) ? instMetaOf(activeInst.inst).nombre : activeInst.inst}
{dest.esFocal ? 'Punto focal' : 'Responsable'}: {dest.nombre}
{d.id}
{msgs.length === 0 &&
No hay mensajes con {activeInst.sig} todavía.
}
{msgs.map((m, k) => (
{m.from === 'in' &&
{iniciales(m.author)} }
{m.author} {m.date}
{m.text}
{(m.files || []).map((f, fi) =>
{f.name} · {f.size} )}
{m.from === 'out' &&
CB }
))}
Para: {activeInst.sig} — llega a todos sus usuarios
);
}
// ---------- Resultados ----------
function TabResultados({ data, update, removeInforme, addInforme }) {
const I = window.Icons;
const [eRes, setERes] = useStateD(false);
const [eDocs, setEDocs] = useStateD(false);
const [eJus, setEJus] = useStateD(false);
const [docTipo, setDocTipo] = useStateD('Informe');
const [docFile, setDocFile] = useStateD(null);
const agregarDoc = () => { if (!docFile) return; addInforme({ name: docFile, tipo: docTipo, size: '—' }); setDocFile(null); setDocTipo('Informe'); };
return (
<>
setERes(true)} onDone={() => setERes(false)}>
{eRes
?
setEDocs(true)} onDone={() => setEDocs(false)}>
Informes, anexos y resoluciones que respaldan el resultado de la recomendación.
{eDocs && (
Tipo de documento
setDocTipo(e.target.value)}>
{TIPOS_INFORME.map(t => {t} )}
{docFile ? docFile : 'Seleccionar archivo (PDF, DOCX, XLSX, imagen)'}
setDocFile(e.target.files && e.target.files[0] ? e.target.files[0].name : null)} />
Agregar
)}
{data.informes.length === 0 ? (
Todavía no se cargaron informes.
) : (
{data.informes.map((f, i) => (
{f.name}
{f.tipo}
{fmtFecha(f.fecha)}{f.size && f.size !== '—' ? ` · ${f.size}` : ''}
{eDocs
? removeInforme(i)}>
: }
))}
)}
setEJus(true)} onDone={() => setEJus(false)}>
{eJus
?
>
);
}
Object.assign(window, { DetalleScreen });