/* ============================================================ 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 (

{title}

{sub &&
{sub}
}
); } 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 (
Administración
{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 (
Nueva invitación
setForm(p => ({ ...p, max_uses: e.target.value }))} placeholder="Sin límite" style={{ ...inputSt, width: '100%' }} />
setForm(p => ({ ...p, expires_in_days: e.target.value }))} placeholder="Sin expiración" style={{ ...inputSt, width: '100%' }} />
{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 => ( ))} {assessments.length === 0 && ( )} {assessments.map(a => { const arch = IHD?.byId?.[a.dominant]; const ei = parseInt(a.evolution_index) || 0; return ( ); })}
{h}
Sin evaluaciones completadas.
{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 });