/* global React, Icon */
const { useState: uS, useEffect: uE, useMemo: uM, Fragment: Fr } = React;
// ============== SIDEBAR ==============
function Sidebar({ active, onNav, onLogout, user }) {
return (
);
}
function NavItem({ m, active, onClick }) {
return (
{m.name}
{m.n}
{m.badge && {m.badge}}
{m.badgeRed && {m.badgeRed}}
);
}
// ============== TOPBAR ==============
function Topbar({ moduleId, crumbs, right, onNav, onMenuClick }) {
const mod = MODULES.find(m => m.id === moduleId);
const [showNotif, setShowNotif] = uS(false);
return (
{onMenuClick && (
)}
{mod && M{mod.n}}
{mod?.name || ''}
{crumbs && crumbs.map((c, i) => (
/
{c}
))}
{right}
onNav?.('config')}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav?.('config')}
>PNCP · ONLINE
onNav?.('integracoes')}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav?.('integracoes')}
>Gmail · 24 não lidos
{showNotif && (
<>
setShowNotif(false)} style={{ position: 'fixed', inset: 0, zIndex: 90 }}>
{ setShowNotif(false); onNav?.('auditoria'); }} />
{ setShowNotif(false); onNav?.('auditoria'); }} />
{ setShowNotif(false); onNav?.('disputa'); }} />
setShowNotif(false)} />
>
)}
);
}
function NotifItem({ tone, title, sub, onClick }) {
const cols = { red: 'var(--red)', amber: 'var(--amber)', cyan: 'var(--cyan)', lime: 'var(--lime)' };
return (
(e.key === 'Enter' || e.key === ' ') && onClick()} style={{ padding: '10px 14px', borderBottom: '1px solid var(--line-1)', display: 'flex', gap: 10, alignItems: 'flex-start' }}>
);
}
// ============== TICKER ==============
let _tickerCache = null;
let _tickerCacheAt = 0;
const TICKER_TTL_MS = 60000;
function Ticker() {
const [stats, setStats] = uS(_tickerCache);
uE(() => {
let mounted = true;
const load = async () => {
const now = Date.now();
if (_tickerCache && (now - _tickerCacheAt) < TICKER_TTL_MS) {
setStats(_tickerCache);
return;
}
try {
const data = await window.dataApi.getTickerStats();
if (!mounted) return;
_tickerCache = data;
_tickerCacheAt = Date.now();
setStats(data);
} catch (_) { /* silencioso — ticker é decorativo */ }
};
load();
const intv = setInterval(load, TICKER_TTL_MS);
return () => { mounted = false; clearInterval(intv); };
}, []);
const items = stats ? [
{ l: 'Editais ativos', v: String(stats.editaisAtivos), cls: '' },
{ l: 'Empenhos ativos', v: String(stats.empenhosAtivos), cls: '' },
{ l: 'A receber', v: BRL(stats.aReceber), cls: 'up' },
...(stats.cndDias !== null ? [{ l: 'Próx. CND vence em', v: `${stats.cndDias}d`, cls: stats.cndDias <= 10 ? 'dn' : '' }] : []),
{ l: 'Lances hoje', v: String(stats.lancesHoje), cls: '' },
] : [];
return (
LIVE
{items.map((t, i) => (
{t.l}
{t.v}
))}
);
}
// ============== SHARED COMPONENTS ==============
function Sparkline({ data, color = 'lime', w = 90, h = 26, filled = true }) {
const max = Math.max(...data);
const min = Math.min(...data);
const r = max - min || 1;
const step = w / (data.length - 1);
const pts = data.map((v, i) => [i * step, h - 2 - ((v - min) / r) * (h - 4)]);
const path = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
const fill = `${path} L ${w},${h} L 0,${h} Z`;
const cls = color === 'red' ? 'red' : color === 'cyan' ? 'cyan' : '';
return (
);
}
function Avatar({ initials, color, size = 22 }) {
return (
{initials}
);
}
function StatusPill({ status }) {
const map = {
novo: { l: 'NOVO', c: 'var(--fg-2)' },
analise: { l: 'ANÁLISE', c: 'var(--cyan)' },
precificacao: { l: 'PRECIF.', c: 'var(--amber)' },
disputa: { l: 'DISPUTA', c: 'var(--red)' },
ganho: { l: 'GANHO', c: 'var(--lime)' },
descartado: { l: 'DESCART.', c: 'var(--fg-4)' },
};
const s = map[status] || map.novo;
return {s.l};
}
function ScoreDial({ score }) {
const color = score >= 85 ? 'var(--lime)' : score >= 70 ? 'var(--amber)' : 'var(--fg-3)';
const r = 10;
const c = 2 * Math.PI * r;
const off = c - (score / 100) * c;
return (
);
}
function Toggle({ on, onToggle }) {
return ;
}
function Section({ title, sub, right, children, padTop }) {
return (
);
}
// ============== DRAWER ==============
function Drawer({ open, onClose, title, subtitle, eyebrow, width, headRight, tabs, activeTab, onTabChange, footer, children }) {
uE(() => {
if (!open) return;
const onKey = e => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.removeEventListener('keydown', onKey);
document.body.style.overflow = prevOverflow;
};
}, [open, onClose]);
if (!open) return null;
return (
{eyebrow &&
{eyebrow}
}
{title}
{subtitle &&
{subtitle}
}
{headRight}
{tabs && tabs.length > 0 && (
{tabs.map(t => (
onTabChange(t.id)}>
{t.label}
{t.count !== undefined && {t.count}}
))}
)}
{children}
{footer &&
{footer}
}
);
}
Object.assign(window, { Sidebar, Topbar, Ticker, Sparkline, Avatar, StatusPill, ScoreDial, Toggle, Section, Drawer });