/* ============================================================
PANEL DE ADMINISTRACIÓN
Áreas · Sucursales · Invitaciones · Colaboradores · Empresas
============================================================ */
const { useState: useSA, useEffect: useEA } = React;
const IHD = window.IH;
/* ── Shared hooks ─────────────────────────────────────────── */
function useList(fetchFn) {
const [items, setItems] = useSA([]);
const [busy, setBusy] = useSA(false);
const reload = () => {
setBusy(true);
fetchFn().then(d => { setItems(d); setBusy(false); }).catch(() => setBusy(false));
};
useEA(() => { reload(); }, []);
return { items, setItems, busy, reload };
}
/* ── Shared style constants ───────────────────────────────── */
const inputSt = {
fontFamily: 'var(--sans)', fontSize: 13, color: 'var(--ink)',
background: 'var(--paper)', border: '1px solid var(--hair-2)',
borderRadius: 7, padding: '9px 12px', outline: 'none', boxSizing: 'border-box',
};
const labelSt = {
display: 'block', marginBottom: 6, fontSize: 10.5,
fontFamily: 'var(--mono)', textTransform: 'uppercase',
letterSpacing: '0.06em', color: 'var(--ink-3)', fontWeight: 500,
};
const btnSm = (variant) => ({
border: 'none', cursor: 'pointer', borderRadius: 6, padding: '6px 12px',
fontSize: 12, fontFamily: 'var(--sans)', fontWeight: 500,
background: variant === 'danger' ? 'oklch(0.56 0.13 28 / 0.10)'
: variant === 'primary' ? 'var(--ink)' : 'var(--panel)',
color: variant === 'danger' ? 'var(--risk)' : variant === 'primary' ? '#fff' : 'var(--ink-2)',
border: '1px solid ' + (variant === 'danger' ? 'oklch(0.56 0.13 28 / 0.25)' : 'var(--hair-2)'),
});
/* ── PageTitle ────────────────────────────────────────────── */
function AdminTitle({ title, sub }) {
return (
);
}
function AdminLoading() {
return Cargando…
;
}
function AdminEmpty({ text }) {
return (
{text}
);
}
/* ── ScreenAdmin — contenedor principal ──────────────────── */
function ScreenAdmin({ adminUser, onBack, onLogout }) {
const [tab, setTab] = useSA('areas');
const [viewResult, setViewResult] = useSA(null);
const [selectedCompanyId, setSelectedCompanyId] = useSA(null);
const [mgmtCompanyId, setMgmtCompanyId] = useSA('');
const [allCompanies, setAllCompanies] = useSA([]);
const isSuperAdmin = adminUser?.role === 'superadmin';
useEA(() => {
if (isSuperAdmin) IHA.getCompanies().then(setAllCompanies).catch(() => {});
}, []);
const goToColabs = (companyId) => {
setSelectedCompanyId(companyId);
setTab('colaboradores');
};
if (viewResult) return setViewResult(null)} />;
const tabs = [
{ id: 'areas', label: 'Áreas' },
{ id: 'sucursales', label: 'Sucursales' },
{ id: 'invitaciones', label: 'Invitaciones' },
{ id: 'colaboradores', label: 'Colaboradores' },
...(isSuperAdmin ? [{ id: 'empresas', label: 'Empresas' }] : []),
];
return (
{isSuperAdmin && (tab === 'areas' || tab === 'sucursales') && (
Empresa
)}
{tab === 'areas' && (
IHA.getDepartments(mgmtCompanyId || undefined)}
createFn={name => IHA.createDepartment(name, mgmtCompanyId || undefined)}
updateFn={(id, name) => IHA.updateDepartment(id, name)}
deleteFn={id => IHA.deleteDepartment(id)} />
)}
{tab === 'sucursales' && (
IHA.getSucursales(mgmtCompanyId || undefined)}
createFn={name => IHA.createSucursal(name, mgmtCompanyId || undefined)}
updateFn={(id, name) => IHA.updateSucursal(id, name)}
deleteFn={id => IHA.deleteSucursal(id)} />
)}
{tab === 'invitaciones' && }
{tab === 'colaboradores' && }
{tab === 'empresas' && isSuperAdmin && }
);
}
/* ── TabCrud — reutilizable para áreas y sucursales ──────── */
function TabCrud({ title, subtitle, noun, fetchFn, createFn, updateFn, deleteFn }) {
const { items, setItems, busy } = useList(fetchFn);
const [newName, setNewName] = useSA('');
const [editing, setEditing] = useSA(null);
const [saving, setSaving] = useSA(false);
const handleCreate = async () => {
if (!newName.trim()) return;
setSaving(true);
try {
const item = await createFn(newName.trim());
setItems(prev => [...prev, item]);
setNewName('');
} finally { setSaving(false); }
};
const handleUpdate = async () => {
if (!editing?.name.trim()) return;
setSaving(true);
try {
await updateFn(editing.id, editing.name.trim());
setItems(prev => prev.map(i => i.id === editing.id ? { ...i, name: editing.name.trim() } : i));
setEditing(null);
} finally { setSaving(false); }
};
const handleDelete = async (id) => {
if (!confirm(`¿Eliminar esta ${noun}?`)) return;
await deleteFn(id);
setItems(prev => prev.filter(i => i.id !== id));
};
return (
setNewName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCreate()}
placeholder={`Nombre de la ${noun}…`}
style={{ flex: 1, ...inputSt }} />
{busy ?
: (
{items.length === 0 &&
}
{items.map(item => (
{editing?.id === item.id ? (
<>
setEditing(p => ({ ...p, name: e.target.value }))}
onKeyDown={e => { if (e.key === 'Enter') handleUpdate(); if (e.key === 'Escape') setEditing(null); }}
style={{ flex: 1, ...inputSt, fontSize: 14 }} />
>
) : (
<>
{item.name}
>
)}
))}
)}
);
}
/* ── TabInvitaciones ──────────────────────────────────────── */
function TabInvitaciones() {
const { items: invs, setItems: setInvs, busy, reload } = useList(() => IHA.getInvitations());
const { items: depts } = useList(() => IHA.getDepartments());
const [form, setForm] = useSA({ department_id: '', max_uses: '', expires_in_days: '' });
const [saving, setSaving] = useSA(false);
const [copied, setCopied] = useSA(null);
const handleCreate = async () => {
setSaving(true);
try {
const data = {};
if (form.department_id) data.department_id = parseInt(form.department_id);
if (form.max_uses) data.max_uses = parseInt(form.max_uses);
if (form.expires_in_days) data.expires_in_days = parseInt(form.expires_in_days);
await IHA.createInvitation(data);
setForm({ department_id: '', max_uses: '', expires_in_days: '' });
reload();
} finally { setSaving(false); }
};
const handleDelete = async (id) => {
if (!confirm('¿Eliminar esta invitación?')) return;
await IHA.deleteInvitation(id);
setInvs(prev => prev.filter(i => i.id !== id));
};
const copyUrl = (url, id) => {
navigator.clipboard.writeText(url).then(() => {
setCopied(id);
setTimeout(() => setCopied(null), 2000);
});
};
return (
{busy ?
: (
{invs.length === 0 &&
}
{invs.map(inv => (
{inv.url}
{inv.department_name || 'Todas las áreas'}
{' · '}{inv.uses}{inv.max_uses ? `/${inv.max_uses}` : ''} usos
{inv.expires_at && ` · Expira ${new Date(inv.expires_at).toLocaleDateString('es-MX')}`}
))}
)}
);
}
/* ── TabColaboradores ─────────────────────────────────────── */
function TabColaboradores({ onViewResult, adminUser, initCompanyId }) {
const isSuperAdmin = adminUser?.role === 'superadmin';
const [assessments, setAssessments] = useSA([]);
const [page, setPage] = useSA(1);
const [pages, setPages] = useSA(1);
const [total, setTotal] = useSA(0);
const [area, setArea] = useSA('');
const [companyId, setCompanyId] = useSA(initCompanyId || '');
const { items: depts } = useList(() => IHA.getDepartments());
const [companies, setCompanies] = useSA([]);
const [busy, setBusy] = useSA(true);
const [loadingId, setLoadingId] = useSA(null);
useEA(() => {
if (isSuperAdmin) IHA.getCompanies().then(setCompanies).catch(() => {});
}, []);
useEA(() => {
setBusy(true);
IHA.getAssessments({ page, area: area || undefined, company_id: companyId || undefined })
.then(d => { setAssessments(d.data); setPages(d.pages); setTotal(d.total); setBusy(false); })
.catch(() => setBusy(false));
}, [page, area, companyId]);
const viewResult = async (id) => {
setLoadingId(id);
try {
const result = await IHA.getResult(id);
onViewResult(result);
} finally { setLoadingId(null); }
};
const thSt = {
textAlign: 'left', padding: '8px 10px', fontFamily: 'var(--mono)',
fontSize: 10.5, fontWeight: 500, color: 'var(--ink-3)',
letterSpacing: '0.06em', textTransform: 'uppercase',
};
return (
{isSuperAdmin && (
)}
{busy ?
: (
<>
{['Nombre', 'Área', 'Puesto', 'Arquetipo', 'Evolución', 'Fecha', ''].map(h => (
| {h} |
))}
{assessments.length === 0 && (
|
Sin evaluaciones completadas.
|
)}
{assessments.map(a => {
const arch = IHD?.byId?.[a.dominant];
const ei = parseInt(a.evolution_index) || 0;
return (
| {a.respondent_name || '—'} |
{a.department || '—'} |
{a.puesto || '—'} |
{arch ? (
{arch.name}
) : '—'}
|
= 60 ? 'var(--good)' : ei >= 40 ? 'var(--warn)' : 'var(--risk)' }}>
{ei}
|
{a.updated_at ? new Date(a.updated_at).toLocaleDateString('es-MX') : '—'}
|
|
);
})}
{pages > 1 && (
{page} / {pages}
)}
>
)}
);
}
/* ── TabEmpresas (superadmin) ─────────────────────────────── */
const AREA_SUGGESTIONS = ['Ventas', 'Operaciones', 'Administración', 'Dirección', 'Tecnología', 'Recursos Humanos', 'Marketing', 'Finanzas', 'Logística', 'Servicio al cliente', 'Calidad', 'Producción'];
function TabEmpresas({ onViewColabs }) {
const { items: companies, reload } = useList(() => IHA.getCompanies());
const [form, setForm] = useSA({ name: '', admin_email: '', admin_password: '' });
const [areas, setAreas] = useSA([]);
const [sucursales, setSucursales] = useSA([]);
const [newArea, setNewArea] = useSA('');
const [newSuc, setNewSuc] = useSA('');
const [saving, setSaving] = useSA(false);
const [created, setCreated] = useSA(null);
const [err, setErr] = useSA(null);
const toggleArea = (name) => setAreas(prev =>
prev.includes(name) ? prev.filter(a => a !== name) : [...prev, name]
);
const addCustomArea = () => {
const v = newArea.trim();
if (v && !areas.includes(v)) { setAreas(p => [...p, v]); setNewArea(''); }
};
const addSuc = () => {
const v = newSuc.trim();
if (v && !sucursales.includes(v)) { setSucursales(p => [...p, v]); setNewSuc(''); }
};
const handleCreate = async () => {
if (!form.name || !form.admin_email || !form.admin_password) return;
setSaving(true);
setErr(null);
try {
const c = await IHA.createCompany({ ...form, departments: areas, sucursales });
setCreated(c);
setForm({ name: '', admin_email: '', admin_password: '' });
setAreas([]); setSucursales([]);
reload();
} catch (e) {
setErr(e.message);
} finally { setSaving(false); }
};
const tagSt = (active) => ({
display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12.5,
padding: '5px 11px', borderRadius: 20, cursor: 'pointer', border: '1px solid',
fontFamily: 'var(--sans)',
background: active ? 'var(--ink)' : 'var(--panel)',
color: active ? '#fff' : 'var(--ink-2)',
borderColor: active ? 'var(--ink)' : 'var(--hair-2)',
});
return (
{created && (
✓ Empresa creada: {created.name} — Admin: {created.admin_email}
)}
{err && (
{err}
)}
Nueva empresa
{/* Datos básicos */}
{[
['name', 'Nombre de empresa', 'text', 'Grupo Ejemplo'],
['admin_email', 'Email del admin', 'email', 'admin@empresa.com'],
['admin_password', 'Contraseña admin', 'password', 'Mínimo 8 caracteres'],
].map(([k, l, t, ph]) => (
setForm(p => ({ ...p, [k]: e.target.value }))}
style={{ ...inputSt, width: '100%' }} />
))}
{/* Áreas */}
{AREA_SUGGESTIONS.map(s => (
))}
setNewArea(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addCustomArea()}
placeholder="Área personalizada…" style={{ flex: 1, ...inputSt }} />
{areas.filter(a => !AREA_SUGGESTIONS.includes(a)).length > 0 && (
{areas.filter(a => !AREA_SUGGESTIONS.includes(a)).map(a => (
{a} setAreas(p => p.filter(x => x !== a))}>×
))}
)}
{/* Sucursales */}
setNewSuc(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addSuc()}
placeholder="Ej. Ciudad de México…" style={{ flex: 1, ...inputSt }} />
{sucursales.length > 0 && (
{sucursales.map(s => (
{s} setSucursales(p => p.filter(x => x !== s))}>×
))}
)}
{companies.length === 0 &&
}
{companies.map(c => (
{c.name}
{c.admin_count} admin · {c.assessment_count} evaluaciones
{c.created_at && ` · ${new Date(c.created_at).toLocaleDateString('es-MX')}`}
{c.assessment_count > 0 && (
)}
))}
);
}
/* ── ResultModal — overlay con resultado individual ───────── */
function ResultModal({ result, onClose }) {
const arch = IHD?.byId?.[result.dominant];
const scores = result.scores || {};
const respondent = result.respondent || {};
const ei = parseInt(result.evolution_index) || 0;
const diagLine = result.diagnostic_line;
return (
{ if (e.target === e.currentTarget) onClose(); }}>
Resultado individual
{respondent.name || result.respondent_name || 'Colaborador'}
{[respondent.area || result.department, respondent.puesto || result.puesto, respondent.antiguedad].filter(Boolean).map((t, i) => (
{t}
))}
{result.period && (
{result.period}
)}
{arch && (
<>
Opera principalmente desde
{arch.name}
{arch.glyph &&
{arch.glyph}
}
>
)}
Índice de evolución
{ei}
/100
Scores por arquetipo
{IHD && IHD.order.map(id =>
)}
{arch && }
{diagLine && (
{diagLine.lead}
{diagLine.turn && (
{diagLine.turn}
)}
)}
);
}
Object.assign(window, { ScreenAdmin });