// AGENTFORGE-IA — barra lateral + barra superior
const { useState, useEffect, useMemo, useRef } = React;
function Topbar({ theme, onTheme, onCmdK, currentUser, onLoginClick, onLogout, goBack, canGoBack, sessionExpired }) {
const { t, lang, setLang } = useI18n();
const [open, setOpen] = useState(false);
const [userOpen, setUserOpen] = useState(false);
const langTimer = useRef(null);
const userTimer = useRef(null);
const initials = currentUser
? (currentUser.full_name || currentUser.email || '?').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
: null;
const roleLabels = {
propietario: 'Propietario',
administrador: 'Administrador',
constructor: 'Builder',
revisor: 'Revisor',
lector: 'Lector',
operador_sensible: 'Operador sensible',
};
const sectionLabels = {
playground: 'Pruebas',
integrations: 'Integraciones',
deployment: 'Despliegue',
library: 'Biblioteca',
crm: 'CRM',
scheduling: 'Agenda',
billing: 'Facturación',
settings: 'Ajustes',
runs: 'Ejecuciones',
analytics: 'Analítica',
admin: 'Admin usuarios',
ops_finance: 'Ops Finanzas',
ops_sales: 'Ops Ventas',
ops_marketing: 'Ops Marketing',
ops_operations: 'Ops Operaciones',
ops_hr: 'Ops RRHH',
ops_support: 'Ops Soporte',
};
const roles = Array.isArray(currentUser?.roles) ? currentUser.roles : [];
const sections = currentUser?.sections && typeof currentUser.sections === 'object' ? currentUser.sections : {};
const sectionEntries = Object.entries(sections).filter(([, level]) => level === 'view' || level === 'edit');
return (
{canGoBack && (
)}
v2.4
{t('tb.search')}
⌘K
clearTimeout(langTimer.current)}
onMouseLeave={() => { langTimer.current = setTimeout(() => setOpen(false), 250); }}
>
setOpen(o => !o)} data-tip={t('lang.label')}>
{lang.toUpperCase()}
{open && (
{t('lang.label')}
{[['es','Español','🇪🇸'],['en','English','🇬🇧']].map(([code, name, flag]) => (
{ setLang(code); setOpen(false); }}>
{flag}
{name}
{lang === code && }
))}
)}
window.afNotify && window.afNotify('No hay notificaciones nuevas')}>
window.afNotify && window.afNotify('Centro de ayuda próximamente')}>
{currentUser ? (
clearTimeout(userTimer.current)}
onMouseLeave={() => { userTimer.current = setTimeout(() => setUserOpen(false), 250); }}
>
setUserOpen(o => !o)}
>
{initials}
{currentUser.full_name || currentUser.email}
{(() => {
const ROLE_LABELS = { propietario: 'Propietario', administrador: 'Admin', constructor: 'Builder', revisor: 'Revisor', lector: 'Lector', operador_sensible: 'Op. sensible' };
const ROLE_COLORS = { propietario: '#7c3aed', administrador: '#f472b6', constructor: '#34d399', revisor: '#60a5fa', lector: '#fbbf24', operador_sensible: '#ef4444' };
const topRole = currentUser.is_admin ? 'propietario' : (currentUser.roles && currentUser.roles[0]);
if (!topRole) return null;
return {ROLE_LABELS[topRole] || topRole} ;
})()}
{userOpen && (
{currentUser.full_name || '—'}
{currentUser.email}
Permisos efectivos
Roles: {' '}
{currentUser.is_admin
? 'Admin global'
: (roles.length > 0 ? roles.map((r) => roleLabels[r] || r).join(', ') : 'Sin roles')}
{sectionEntries.length > 0 ? (
sectionEntries.map(([section, level]) => (
{(sectionLabels[section] || section)} · {level}
))
) : (
Sin secciones RBAC asignadas
)}
{ setUserOpen(false); onLogout && onLogout(); }}
style={{
display: 'flex', alignItems: 'center', gap: 8,
width: '100%', padding: '8px 14px', background: 'none', border: 'none',
color: '#ff6b6b', cursor: 'pointer', fontSize: 13, fontFamily: 'inherit',
textAlign: 'left',
}}
>
Cerrar sesión
)}
) : (
Iniciar sesión
)}
);
}
// Rutas que requieren solo login (cualquier usuario registrado)
const SIDEBAR_USER = new Set(['library', 'builder', 'playground', 'integrations', 'deployment']);
// Rutas que requieren perfil admin/trabajador
const SIDEBAR_ADMIN = new Set([
'crm', 'scheduling',
'plans', 'billing', 'settings',
'runs', 'analytics', 'admin',
]);
const SIDEBAR_SENSITIVE = new Set([
'ops_finance', 'ops_sales', 'ops_marketing', 'ops_operations', 'ops_hr', 'ops_support',
]);
const STRICT_ADMIN_ONLY = new Set(['crm', 'scheduling']);
const USER_RBAC_ROUTES = new Set(['playground', 'integrations']);
function Sidebar({ route, setRoute, currentUser }) {
const { t } = useI18n();
const canAccessUserRbacRoute = (section) => {
if (!USER_RBAC_ROUTES.has(section)) return true;
if (!currentUser) return false;
if (currentUser.is_admin) return true;
return !!(currentUser.sections && currentUser.sections[section]);
};
const openContactForm = () => {
window.dispatchEvent(new CustomEvent('af:contact:open', { detail: { public: true } }));
};
const adminChildren = [
{ id: 'plans', label: t('sb.plans'), icon: 'layers' },
{ id: 'billing', label: t('sb.billing'), icon: 'wallet' },
];
if (currentUser && (currentUser.is_admin || (currentUser.sections && Object.keys(currentUser.sections).length > 0))) {
adminChildren.push({ id: 'admin', label: 'Admin usuarios', icon: 'shield' });
}
const sensitiveChildren = [
{ id: 'ops_finance', label: t('sb.ops_finance'), icon: 'wallet' },
{ id: 'ops_sales', label: t('sb.ops_sales'), icon: 'megaphone' },
{ id: 'ops_marketing', label: t('sb.ops_marketing'), icon: 'sparkles' },
{ id: 'ops_operations', label: t('sb.ops_operations'), icon: 'grid' },
{ id: 'ops_hr', label: t('sb.ops_hr'), icon: 'users' },
{ id: 'ops_support', label: t('sb.ops_support'), icon: 'headset' },
];
const items = [
{ group: t('sb.group.space'), children: [
{ id: 'home', label: t('sb.home'), icon: 'home' },
{ id: 'marketplace', label: t('sb.marketplace'), icon: 'store', count: '1.085' },
{ id: 'library', label: t('sb.library'), icon: 'grid', count: 6 },
{ id: 'builder', label: t('sb.builder'), icon: 'flow' },
{ id: 'playground', label: t('sb.playground'), icon: 'chat' },
{ id: 'integrations', label: t('sb.integrations'), icon: 'plug', count: 18 },
{ id: 'deployment', label: t('sb.deployment'), icon: 'cloud' },
]},
{ group: t('sb.group.observe'), children: [
{ id: 'crm', label: t('sb.crm'), icon: 'users', count: 60 },
{ id: 'scheduling', label: t('sb.scheduling'), icon: 'calendar' },
{ id: 'runs', label: t('sb.runs'), icon: 'logs', count: 412 },
{ id: 'analytics', label: t('sb.analytics'), icon: 'chart' },
]},
{ group: t('sb.group.admin'), children: adminChildren },
{ group: t('sb.group.ops'), children: sensitiveChildren },
];
return (
);
}
// ── Booking Modal global ───────────────────────────────────────────────────────
function BookingModal({ onClose, prefill = {}, currentUser }) {
const { lang } = useI18n();
const token = sessionStorage.getItem('af_token') || '';
const [googleOk, setGoogleOk] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const [done, setDone] = React.useState(null);
const [error, setError] = React.useState('');
const today = new Date().toISOString().slice(0, 10);
const durByType = { demo: 30, discovery: 20, support: 15, consulting: 45, qbr: 60 };
const localDateTime = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}T${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:00`;
const [form, setForm] = React.useState({
title: prefill.title || '',
email: prefill.email || '',
type: prefill.type || 'demo',
date: prefill.date || today,
start: prefill.start || '10:00',
duration: prefill.duration || durByType[prefill.type] || 30,
});
React.useEffect(() => {
if (!token) { setGoogleOk(false); return; }
fetch('/api/v1/scheduling/google/status', { headers: { Authorization: 'Bearer ' + token } })
.then(r => r.json()).then(d => setGoogleOk(!!d?.connected)).catch(() => setGoogleOk(false));
}, [token]);
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
const types = [
{ id: 'demo', label: lang === 'en' ? 'Sales demo (30 min)' : 'Demo comercial (30 min)', dur: 30 },
{ id: 'discovery', label: lang === 'en' ? 'Discovery call (20 min)' : 'Llamada de descubrimiento (20 min)', dur: 20 },
{ id: 'support', label: lang === 'en' ? 'Support session (15 min)' : 'Sesión de soporte (15 min)', dur: 15 },
{ id: 'consulting', label: lang === 'en' ? 'Consulting (45 min)' : 'Consultoría (45 min)', dur: 45 },
{ id: 'qbr', label: lang === 'en' ? 'Quarterly review (60 min)' : 'Revisión trimestral (60 min)', dur: 60 },
];
const submit = async () => {
if (!form.title.trim()) { setError(lang === 'en' ? 'Name required' : 'Nombre requerido'); return; }
setLoading(true); setError('');
try {
const endDate = new Date(`${form.date}T${form.start}:00`);
endDate.setMinutes(endDate.getMinutes() + Number(form.duration));
const res = await fetch('/api/v1/scheduling/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
body: JSON.stringify({
title: form.title,
start: `${form.date}T${form.start}:00`,
end: localDateTime(endDate),
description: `Tipo: ${form.type}${form.email ? '\nContacto: ' + form.email : ''}`,
attendee_email: form.email || null,
event_type: form.type,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Error');
setDone(data.html_link || true);
} catch (e) { setError(e.message); }
finally { setLoading(false); }
};
return (
e.target === e.currentTarget && onClose()}>
{lang === 'en' ? 'Book appointment' : 'Agendar cita'}
{done ? (
) : (
)}
);
}
window.BookingModal = BookingModal;
window.Topbar = Topbar;
window.Sidebar = Sidebar;