/* Trascend CRM · Primitivas UI compartidas (iconos, modal, etc.) */
const { useState, useEffect, useMemo, useRef, useCallback } = React;
const U = window.TrascendUtil;
const API = window.TrascendAPI;

/* ------------------------------------------------ iconos (stroke simple) */
const IconBase = ({ children, ...p }) => (
  <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...p}>{children}</svg>
);
const Icons = {
  dashboard: (p) => <IconBase {...p}><rect x="3" y="3" width="6" height="6" rx="1.5"></rect><rect x="11" y="3" width="6" height="6" rx="1.5"></rect><rect x="3" y="11" width="6" height="6" rx="1.5"></rect><rect x="11" y="11" width="6" height="6" rx="1.5"></rect></IconBase>,
  clients: (p) => <IconBase {...p}><circle cx="7" cy="7" r="3"></circle><path d="M2.5 16.5c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4"></path><circle cx="14" cy="8" r="2.2"></circle><path d="M13.5 12.6c2.2 0 4 1.3 4 3.4"></path></IconBase>,
  pipeline: (p) => <IconBase {...p}><path d="M3 5h14"></path><path d="M5.5 10h9"></path><path d="M8 15h4"></path></IconBase>,
  social: (p) => <IconBase {...p}><circle cx="10" cy="10" r="7"></circle><path d="M3 10h14M10 3c2.2 2 2.2 12 0 14M10 3c-2.2 2-2.2 12 0 14"></path></IconBase>,
  calendar: (p) => <IconBase {...p}><rect x="3" y="4.5" width="14" height="12.5" rx="2"></rect><path d="M3 8.5h14M7 2.5v3M13 2.5v3"></path></IconBase>,
  inbox: (p) => <IconBase {...p}><path d="M3 11l2.5-6.5h9L17 11"></path><path d="M3 11v4a1.5 1.5 0 0 0 1.5 1.5h11A1.5 1.5 0 0 0 17 15v-4"></path><path d="M3 11h4l1 2h4l1-2h4"></path></IconBase>,
  finance: (p) => <IconBase {...p}><circle cx="10" cy="10" r="7"></circle><path d="M10 6v8M12.5 7.5c-.5-.8-1.4-1.2-2.5-1.2-1.4 0-2.4.8-2.4 1.9 0 2.6 5 1.3 5 3.8 0 1.1-1.1 1.9-2.6 1.9-1.2 0-2.2-.5-2.7-1.3"></path></IconBase>,
  settings: (p) => <IconBase {...p}><circle cx="10" cy="10" r="2.6"></circle><path d="M10 2.8v2.4M10 14.8v2.4M2.8 10h2.4M14.8 10h2.4M4.9 4.9l1.7 1.7M13.4 13.4l1.7 1.7M15.1 4.9l-1.7 1.7M6.6 13.4l-1.7 1.7"></path></IconBase>,
  plus: (p) => <IconBase {...p}><path d="M10 4.5v11M4.5 10h11"></path></IconBase>,
  search: (p) => <IconBase {...p}><circle cx="9" cy="9" r="5.5"></circle><path d="M13.5 13.5 17 17"></path></IconBase>,
  arrow: (p) => <IconBase {...p}><path d="M4 10h11.5M11 5.5 15.5 10 11 14.5"></path></IconBase>,
  whatsapp: (p) => <IconBase {...p}><path d="M10 3a7 7 0 0 0-6 10.5L3 17l3.6-.9A7 7 0 1 0 10 3Z"></path><path d="M7.5 8.2c.3 2 2.3 4 4.3 4.3l1-1.1-1.8-1-.7.5c-.6-.4-1.1-.9-1.4-1.5l.5-.7-1-1.7-.9 1.2Z"></path></IconBase>,
  mail: (p) => <IconBase {...p}><rect x="2.5" y="4.5" width="15" height="11" rx="2"></rect><path d="m3.5 6 6.5 5 6.5-5"></path></IconBase>,
  edit: (p) => <IconBase {...p}><path d="M12.8 3.7a1.8 1.8 0 0 1 2.5 2.5L7 14.5 3.5 16.5l1-3.5 8.3-9.3Z"></path></IconBase>,
  trash: (p) => <IconBase {...p}><path d="M4 6h12M8 6V4.5A1.5 1.5 0 0 1 9.5 3h1A1.5 1.5 0 0 1 12 4.5V6M5.5 6l.7 9.5A1.5 1.5 0 0 0 7.7 17h4.6a1.5 1.5 0 0 0 1.5-1.5L14.5 6"></path></IconBase>,
  external: (p) => <IconBase {...p}><path d="M8.5 5H5a1.5 1.5 0 0 0-1.5 1.5V15A1.5 1.5 0 0 0 5 16.5h8.5A1.5 1.5 0 0 0 15 15v-3.5M11.5 3.5H16.5V8.5M16 4 9.5 10.5"></path></IconBase>,
  chevL: (p) => <IconBase {...p}><path d="M12.5 4.5 7 10l5.5 5.5"></path></IconBase>,
  chevR: (p) => <IconBase {...p}><path d="M7.5 4.5 13 10l-5.5 5.5"></path></IconBase>,
  logout: (p) => <IconBase {...p}><path d="M8 17H4.5A1.5 1.5 0 0 1 3 15.5v-11A1.5 1.5 0 0 1 4.5 3H8M13 13.5 16.5 10 13 6.5M16.5 10H7.5"></path></IconBase>,
  send: (p) => <IconBase {...p}><path d="M17 3 9 11M17 3 3 8.5l6 2.5 2.5 6L17 3Z"></path></IconBase>,
  check: (p) => <IconBase {...p}><path d="m4 10.5 4 4 8-9"></path></IconBase>,
  funnel: (p) => <IconBase {...p}><path d="M3 4.5h14l-5.2 6.2V16L8.2 17.5v-6.8L3 4.5Z"></path></IconBase>,
  activity: (p) => <IconBase {...p}><path d="M2 10h3.5l2-5 3 10 2.5-5H17"></path></IconBase>,
  zap: (p) => <IconBase {...p}><path d="M11 2.5 4.5 11H9l-1 6.5L15.5 9H11l1-6.5Z"></path></IconBase>,
  plug: (p) => <IconBase {...p}><path d="M7 2.5v4M13 2.5v4M5 6.5h10v2.2a5 5 0 0 1-10 0V6.5ZM10 13.7v3.8"></path></IconBase>,
  report: (p) => <IconBase {...p}><path d="M3 16.5h14"></path><path d="M5.5 13.5v-3M9 13.5v-7M12.5 13.5v-4M16 13.5v-6"></path></IconBase>,
  bell: (p) => <IconBase {...p}><path d="M10 3a4 4 0 0 0-4 4v3.2L4.5 13h11L14 10.2V7a4 4 0 0 0-4-4ZM8.2 15.2a1.9 1.9 0 0 0 3.6 0"></path></IconBase>,
  clock: (p) => <IconBase {...p}><circle cx="10" cy="10" r="6.7"></circle><path d="M10 6.2V10l2.8 1.8"></path></IconBase>,
  target: (p) => <IconBase {...p}><circle cx="10" cy="10" r="6.6"></circle><circle cx="10" cy="10" r="2.7"></circle></IconBase>,
  eye: (p) => <IconBase {...p}><path d="M2.4 10S5 5.2 10 5.2 17.6 10 17.6 10 15 14.8 10 14.8 2.4 10 2.4 10Z"></path><circle cx="10" cy="10" r="2.1"></circle></IconBase>,
  tag: (p) => <IconBase {...p}><path d="M10.6 3H16v5.4L9 15.4 3.6 10 10.6 3Z"></path><circle cx="12.6" cy="6.4" r="0.9"></circle></IconBase>,
  form: (p) => <IconBase {...p}><rect x="4" y="3" width="12" height="14" rx="2"></rect><path d="M7 7h6M7 10h6M7 13h3.5"></path></IconBase>,
  download: (p) => <IconBase {...p}><path d="M10 3v9M6.4 8.6 10 12.2l3.6-3.6M5 16h10"></path></IconBase>,
  upload:   (p) => <IconBase {...p}><path d="M10 16V7M6.4 10.4 10 6.8l3.6 3.6M5 4h10"></path></IconBase>,
  cursor: (p) => <IconBase {...p}><path d="M5 3.8 16 9l-4.8 1.4L9.5 16 5 3.8Z"></path></IconBase>,
  shield: (p) => <IconBase {...p}><path d="M10 2.5 4 5v4.4c0 3.6 2.5 6.9 6 8.1 3.5-1.2 6-4.5 6-8.1V5l-6-2.5Z"></path></IconBase>,
  shieldCheck: (p) => <IconBase {...p}><path d="M10 2.5 4 5v4.4c0 3.6 2.5 6.9 6 8.1 3.5-1.2 6-4.5 6-8.1V5l-6-2.5Z"></path><path d="M7.5 9.8 9.3 11.6 12.8 8"></path></IconBase>,
  lock: (p) => <IconBase {...p}><rect x="4.5" y="9" width="11" height="7.5" rx="1.6"></rect><path d="M6.8 9V6.8a3.2 3.2 0 0 1 6.4 0V9"></path></IconBase>,
  key: (p) => <IconBase {...p}><circle cx="7" cy="7" r="3.2"></circle><path d="M9.3 9.3 16 16M13 13l1.6-1.6M15 15l1.4-1.4"></path></IconBase>,
  users: (p) => <IconBase {...p}><circle cx="7" cy="7.2" r="2.6"></circle><path d="M2.6 16c0-2.4 2-3.9 4.4-3.9s4.4 1.5 4.4 3.9"></path><path d="M13.4 5.5a2.4 2.4 0 0 1 0 4.7M14.2 16c0-1.9-.6-3.1-1.7-3.9"></path></IconBase>,
  link: (p) => <IconBase {...p}><path d="M8.4 11.6 11.6 8.4M7.6 10.4 5.9 12.1a2.4 2.4 0 0 1-3.4-3.4l1.9-1.9a2.4 2.4 0 0 1 3.4 0M12.4 9.6l1.7-1.7a2.4 2.4 0 0 0-3.4-3.4L9.2 5.9"></path></IconBase>,
  refresh: (p) => <IconBase {...p}><path d="M16 10a6 6 0 1 1-1.8-4.3M16 4v3h-3"></path></IconBase>,
  alert: (p) => <IconBase {...p}><path d="M10 3.2 17.6 16.5H2.4L10 3.2Z"></path><path d="M10 8.2v3.4M10 14.1v.1"></path></IconBase>,
  copy: (p) => <IconBase {...p}><rect x="6.5" y="6.5" width="9" height="9" rx="2"></rect><path d="M4.6 11.4A1.5 1.5 0 0 1 3 10V4.6A1.5 1.5 0 0 1 4.6 3H10a1.5 1.5 0 0 1 1.4 1.6"></path></IconBase>,
  history: (p) => <IconBase {...p}><path d="M3.6 10a6.4 6.4 0 1 1 1.9 4.5M3.6 14v-3.6H7.2"></path><path d="M10 6.6V10l2.4 1.6"></path></IconBase>,
  ban: (p) => <IconBase {...p}><circle cx="10" cy="10" r="6.7"></circle><path d="M5.3 5.3l9.4 9.4"></path></IconBase>,
  dots: (p) => <IconBase {...p}><circle cx="5" cy="10" r="1.1"></circle><circle cx="10" cy="10" r="1.1"></circle><circle cx="15" cy="10" r="1.1"></circle></IconBase>,
  qr: (p) => <IconBase {...p}><rect x="3" y="3" width="5" height="5" rx="1"></rect><rect x="12" y="3" width="5" height="5" rx="1"></rect><rect x="3" y="12" width="5" height="5" rx="1"></rect><path d="M12 12h2.2v2.2M16.5 12v.1M14.2 16.4v.1M16.5 16.4h.1"></path></IconBase>,
  building: (p) => <IconBase {...p}><rect x="4.5" y="3" width="11" height="14" rx="1.6"></rect><path d="M8 6.4h1.4M10.6 6.4H12M8 9.4h1.4M10.6 9.4H12M8.6 17v-3h2.8v3"></path></IconBase>,
  filter: (p) => <IconBase {...p}><path d="M3 5h14l-5.2 6.2V16L8.2 17.5v-6.3L3 5Z"></path></IconBase>,
  sliders: (p) => <IconBase {...p}><path d="M4 6.5h7M14.5 6.5H16M4 13.5h2M9.5 13.5H16"></path><circle cx="12.5" cy="6.5" r="1.8"></circle><circle cx="7.5" cy="13.5" r="1.8"></circle></IconBase>,
  menu: (p) => <IconBase {...p}><path d="M3 5.5h14M3 10h14M3 14.5h14"></path></IconBase>,
  info: (p) => <IconBase {...p}><circle cx="10" cy="10" r="7"></circle><path d="M10 9v4.2M10 6.6v.1"></path></IconBase>,
};

/* ------------------------------------------------ toast */
function useToast() {
  const [toast, setToast] = useState(null);
  const timer = useRef(null);
  const show = useCallback((msg) => {
    setToast(msg);
    clearTimeout(timer.current);
    timer.current = setTimeout(() => setToast(null), 2600);
  }, []);
  const node = toast ? <div className="toast">{toast}</div> : null;
  return [show, node];
}

/* ------------------------------------------------ modal */
function Modal({ title, onClose, children, foot, width }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);
  return (
    <div className="modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" style={width ? { maxWidth: width } : undefined}>
        <div className="modal-head">
          <h3>{title}</h3>
          <button className="modal-close" onClick={onClose} aria-label="Cerrar">×</button>
        </div>
        <div className="modal-body">{children}</div>
        {foot ? <div className="modal-foot">{foot}</div> : null}
      </div>
    </div>
  );
}

/* ------------------------------------------------ form helpers */
function Field({ label, children }) {
  return <div className="field"><label>{label}</label>{children}</div>;
}

function Avatar({ name, small }) {
  return <span className={"avatar" + (small ? " avatar-sm" : "")}>{U.initials(name)}</span>;
}

function NetChip({ network }) {
  const n = U.NETWORKS[network] || { short: network, color: '#999' };
  return <span className="net-chip"><i style={{ background: n.color }}></i>{n.short}</span>;
}

function StatusBadge({ map, value }) {
  const m = map[value] || { label: value, cls: 'badge-dim' };
  return <span className={"badge " + (m.cls || '')}><span className="bdot"></span>{m.label}</span>;
}

const CLIENT_STATUS = {
  activo:     { label: 'Activo',     cls: 'badge-ok' },
  pausado:    { label: 'Pausado',    cls: 'badge-warn' },
  prospecto:  { label: 'Prospecto',  cls: 'badge-info' },
  finalizado: { label: 'Finalizado', cls: 'badge-dim' },
};
const INVOICE_STATUS = {
  pendiente: { label: 'Pendiente', cls: 'badge-warn' },
  pagada:    { label: 'Pagada',    cls: 'badge-ok' },
  vencida:   { label: 'Vencida',   cls: 'badge-danger' },
};
const QUOTE_STATUS = {
  borrador:  { label: 'Borrador',  cls: 'badge-dim' },
  enviado:   { label: 'Enviado',   cls: 'badge-info' },
  aceptado:  { label: 'Aceptado',  cls: 'badge-ok' },
  rechazado: { label: 'Rechazado', cls: 'badge-danger' },
};
const STAGE_BADGE = {
  prospecto:   { label: 'Prospecto identificado', cls: 'badge-dim' },
  contactado:  { label: 'Contactado',             cls: 'badge-info' },
  respondio:   { label: 'Respondió',              cls: 'badge-info' },
  reunion:     { label: 'Reunión agendada',       cls: '' },
  diagnostico: { label: 'Diagnóstico realizado',  cls: '' },
  propuesta:   { label: 'Propuesta enviada',      cls: 'badge-warn' },
  negociacion: { label: 'Negociación',            cls: 'badge-warn' },
  ganado:      { label: 'Ganado',                 cls: 'badge-ok' },
  perdido:     { label: 'Perdido',                cls: 'badge-danger' },
};
const POST_BADGE = {
  idea:       { label: 'Idea',       cls: 'badge-dim' },
  diseno:     { label: 'Diseño',     cls: 'badge-info' },
  aprobacion: { label: 'Aprobación', cls: 'badge-warn' },
  programado: { label: 'Programado', cls: '' },
  publicado:  { label: 'Publicado',  cls: 'badge-ok' },
};

/* ------------------------------------------------ mini charts (SVG) */
function BarChart({ series, labels, height = 160, colors }) {
  // series: [{name, values:[...]}], labels: ['Ene',...]
  const max = Math.max(1, ...series.flatMap((s) => s.values));
  const n = labels.length;
  const groupW = 100 / n;
  const barW = Math.min(14, (groupW * 0.7) / series.length);
  return (
    <div className="chart-wrap">
      <svg viewBox={`0 0 100 ${height / 3}`} width="100%" height={height} preserveAspectRatio="none" style={{ display: 'block' }}>
        {[0.25, 0.5, 0.75].map((f) => (
          <line key={f} x1="0" x2="100" y1={(height / 3) * (1 - f)} y2={(height / 3) * (1 - f)} stroke="rgba(26,14,62,0.07)" strokeWidth="0.3"></line>
        ))}
        {labels.map((_, i) =>
          series.map((s, j) => {
            const h = (s.values[i] / max) * (height / 3 - 6);
            const x = i * groupW + groupW / 2 - (series.length * barW) / 2 + j * barW;
            return <rect key={i + '-' + j} x={x + 0.5} y={height / 3 - h} width={barW - 1} height={h} rx="0.8" fill={colors[j]}></rect>;
          })
        )}
      </svg>
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${n}, 1fr)`, textAlign: 'center' }}>
        {labels.map((l) => <span key={l} className="mono dim" style={{ fontSize: '0.62rem' }}>{l}</span>)}
      </div>
    </div>
  );
}

function Donut({ parts, size = 120 }) {
  // parts: [{value, color, label}]
  const total = Math.max(1, parts.reduce((a, p) => a + p.value, 0));
  const r = 15.9155;
  let acc = 0;
  return (
    <svg viewBox="0 0 42 42" width={size} height={size}>
      <circle cx="21" cy="21" r={r} fill="none" stroke="rgba(26,14,62,0.08)" strokeWidth="6"></circle>
      {parts.map((p, i) => {
        const frac = p.value / total;
        const el = (
          <circle key={i} cx="21" cy="21" r={r} fill="none" stroke={p.color} strokeWidth="6"
            strokeDasharray={`${frac * 100} ${100 - frac * 100}`}
            strokeDashoffset={25 - acc * 100} strokeLinecap="butt"></circle>
        );
        acc += frac;
        return el;
      })}
    </svg>
  );
}

/* ------------------------------------------------ barra de progreso */
function ProgressBar({ value, max = 100, color }) {
  const pct = Math.max(0, Math.min(100, (Number(value) / (Number(max) || 1)) * 100));
  return <div className="pbar"><span style={{ width: pct + '%', background: color || 'var(--violet)' }}></span></div>;
}

/* ------------------------------------------------ hooks de datos */
function useCollection(name) {
  const [rows, setRows] = useState(null);
  const reload = useCallback(() => { API[name].list().then(setRows); }, [name]);
  useEffect(reload, [reload]);
  return [rows, reload];
}

/* ------------------------------------------------ confirmación global (¿estás seguro?) */
let _confirmResolve = null;
function ConfirmHost() {
  const [state, setState] = useState(null);
  const [typed, setTyped] = useState('');
  const finish = useCallback((val) => {
    setState(null); setTyped('');
    if (_confirmResolve) { _confirmResolve(val); _confirmResolve = null; }
  }, []);
  useEffect(() => {
    window.trascendConfirm = (opts) => new Promise((resolve) => { _confirmResolve = resolve; setTyped(''); setState(opts || {}); });
    const onKey = (e) => { if (e.key === 'Escape' && _confirmResolve) finish(false); };
    window.addEventListener('keydown', onKey);
    return () => { window.removeEventListener('keydown', onKey); delete window.trascendConfirm; };
  }, [finish]);
  if (!state) return null;
  const danger = state.danger !== false;
  const needType = !!state.requireText;
  const ready = !needType || typed.trim().toUpperCase() === String(state.requireText).toUpperCase();
  return (
    <div className="modal-backdrop confirm-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) finish(false); }}>
      <div className="confirm" role="alertdialog" aria-modal="true">
        <div className={'confirm-ico' + (danger ? ' is-danger' : ' is-info')}>{danger ? <Icons.alert /> : <Icons.bell />}</div>
        <h3>{state.title || '¿Confirmás la acción?'}</h3>
        {state.message ? <p className="confirm-msg">{state.message}</p> : null}
        {needType ? (
          <div className="confirm-type">
            <label>Escribí <b>{state.requireText}</b> para confirmar</label>
            <input className="input" value={typed} autoFocus onChange={(e) => setTyped(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter' && ready) finish(true); }} placeholder={state.requireText} />
          </div>
        ) : null}
        <div className="confirm-foot">
          <button className="btn btn-ghost" onClick={() => finish(false)}>{state.cancelLabel || 'Cancelar'}</button>
          <button className={'btn ' + (danger ? 'btn-danger-solid' : 'btn-primary')} disabled={!ready} onClick={() => finish(true)}>
            {state.confirmLabel || (danger ? 'Sí, borrar' : 'Confirmar')}
          </button>
        </div>
      </div>
    </div>
  );
}
/** Abre el diálogo y resuelve true/false. confirmDialog('texto') o confirmDialog({title,message,...}). */
async function confirmDialog(opts) {
  if (typeof opts === 'string') opts = { message: opts };
  if (!window.trascendConfirm) return true;
  return await window.trascendConfirm({ danger: false, ...opts });
}
/** Confirmación de borrado (rojo). Para datos sensibles pasar requireText:'BORRAR'. */
async function confirmDelete(opts) {
  if (typeof opts === 'string') opts = { message: opts };
  if (!window.trascendConfirm) return true;
  return await window.trascendConfirm({ danger: true, confirmLabel: 'Sí, borrar', ...opts });
}

/* ------------------------------------------------ switch on/off (React) */
function Toggle({ on, onChange, title, disabled }) {
  return <button type="button" className={'switch' + (on ? ' on' : '') + (disabled ? ' is-disabled' : '')}
    title={title} aria-pressed={!!on} disabled={disabled}
    onClick={() => { if (!disabled) onChange(!on); }}></button>;
}

/* ------------------------------------------------ medidor de fuerza de contraseña */
function PwdMeter({ value, settings }) {
  const s = U.pwdStrength(value, settings);
  if (!value) return null;
  const labels = ['Muy débil', 'Débil', 'Aceptable', 'Buena', 'Fuerte'];
  const colors = ['var(--danger)', 'var(--danger)', 'var(--warn)', 'var(--ok)', 'var(--ok)'];
  return (
    <div className="pwd-meter">
      <div className="pwd-bars">{[0, 1, 2, 3].map((i) => <span key={i} style={{ background: i < s.score ? colors[s.score] : 'rgba(26,14,62,0.12)' }}></span>)}</div>
      <span className="pwd-label" style={{ color: s.ok ? 'var(--ok)' : 'var(--text-dim)' }}>
        {labels[s.score]}{!s.ok ? ' · mín. ' + s.minLen + (s.needMix ? ', con mayús, minús y número' : ' caracteres') : ''}
      </span>
    </div>
  );
}

/* ------------------------------------------------ tick para relojes / códigos vivos */
function useTick(ms) {
  const [, setN] = useState(0);
  useEffect(() => { const id = setInterval(() => setN((n) => n + 1), ms || 1000); return () => clearInterval(id); }, [ms]);
}

/* ------------------------------------------------ botón (i) con explicación de sección */
function InfoButton({ title, children }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
  }, [open]);
  return (
    <span className="info-wrap" ref={ref}>
      <button type="button" className={'info-btn' + (open ? ' is-open' : '')} aria-label="Qué es esta sección" onClick={() => setOpen((o) => !o)}>i</button>
      {open ? (
        <div className="info-pop" role="dialog">
          {title ? <div className="info-pop-title">{title}</div> : null}
          <div className="info-pop-body">{children}</div>
        </div>
      ) : null}
    </span>
  );
}

/* ------------------------------------------------ alta de 2FA por el propio usuario */
function randomSecret() {
  const ch = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  let s = ''; for (let i = 0; i < 16; i++) s += ch[Math.floor(Math.random() * ch.length)];
  return s;
}
function TwoFASetup({ user, forced, onClose, onSaved }) {
  const enabled = !!user.twofa_enabled;
  const [secret] = useState(() => user.twofa_secret || randomSecret());
  const [code, setCode] = useState('');
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState(false);
  useTick(1000);
  const demo = window.TRASCEND_BACKEND !== 'mysql';
  const current = U.totpCode(secret);
  const secsLeft = 30 - Math.floor((Date.now() / 1000) % 30);
  const otpauth = 'otpauth://totp/Trascend:' + encodeURIComponent(user.email) + '?secret=' + secret + '&issuer=Trascend&period=30&digits=6';
  const qrUrl = useMemo(() => {
    try {
      if (typeof window.qrcode !== 'function') return null;
      const qr = window.qrcode(0, 'M'); qr.addData(otpauth); qr.make();
      return qr.createDataURL(5, 4);
    } catch (e) { return null; }
  }, [otpauth]);

  async function enable() {
    if (String(code).trim().length !== 6) { setErr('Ingresá los 6 dígitos.'); return; }
    if (demo && String(code).trim() !== current) { setErr('El código no coincide. Usá el que se muestra arriba.'); return; }
    setBusy(true);
    try { await TrascendAPI.users.update(user.id, { twofa_enabled: 1, twofa_secret: secret }); onSaved && onSaved(); onClose(); }
    catch (e) { setErr(e.message || 'No se pudo activar.'); setBusy(false); }
  }
  async function disable() {
    setBusy(true);
    try { await TrascendAPI.users.update(user.id, { twofa_enabled: 0, twofa_secret: '' }); onSaved && onSaved(); onClose(); }
    catch (e) { setErr(e.message || 'No se pudo desactivar.'); setBusy(false); }
  }

  return (
    <Modal width={460} title="Verificación en 2 pasos (2FA)" onClose={onClose}
      foot={enabled ? (
        <React.Fragment>
          <button className="btn btn-ghost" onClick={onClose}>Cerrar</button>
          {!forced ? <button className="btn btn-ghost" style={{ color: 'var(--danger)' }} onClick={disable} disabled={busy}><Icons.ban /> Desactivar</button> : null}
        </React.Fragment>
      ) : (
        <React.Fragment>
          {!forced ? <button className="btn btn-ghost" onClick={onClose}>Cancelar</button> : null}
          <button className="btn btn-primary" onClick={enable} disabled={busy}><Icons.shieldCheck /> Activar 2FA</button>
        </React.Fragment>
      )}>
      {enabled ? (
        <div className="stack" style={{ gap: 12 }}>
          <div className="conn-note" style={{ margin: 0 }}><Icons.shieldCheck /> La verificación en 2 pasos está <b>activa</b> en tu cuenta. Al iniciar sesión te pediremos un código de 6 dígitos.</div>
          {demo ? <div className="twofa-code"><span className="dim">Tu código ahora (demo)</span><b className="mono">{current}</b><span className="twofa-count">{secsLeft}s</span></div> : null}
        </div>
      ) : (
        <div className="stack" style={{ gap: 14 }}>
          <p className="dim" style={{ fontSize: '0.82rem', margin: 0 }}>
            {forced ? 'Tu organización exige 2FA. Configurala para entrar. ' : 'Sumá una capa extra de seguridad. '}
            Escaneá o ingresá esta clave en una app de autenticación (Google Authenticator, Authy, 1Password…).
          </p>
          <div className="twofa-qr">
            {qrUrl
              ? <div className="twofa-qr-box"><img src={qrUrl} alt="Código QR para vincular 2FA" /></div>
              : <div className="twofa-qr-box twofa-qr-fallback"><Icons.qr /></div>}
            <div className="col" style={{ gap: 4 }}>
              <b style={{ fontSize: '0.88rem' }}>Escaneá el código</b>
              <span className="dim" style={{ fontSize: '0.76rem', lineHeight: 1.5 }}>Abrí tu app de autenticación y escaneá este QR para vincular la cuenta.</span>
            </div>
          </div>
          <details className="twofa-manual">
            <summary>¿No podés escanear? Ingresá la clave manualmente</summary>
            <div className="twofa-key" style={{ marginTop: 8 }}>
              <span className="dim" style={{ fontSize: '0.7rem' }}>Clave para tu app</span>
              <div className="row" style={{ gap: 8, justifyContent: 'space-between' }}>
                <code className="mono" style={{ fontSize: '0.95rem', letterSpacing: '0.12em' }}>{secret}</code>
                <button className="btn btn-ghost btn-sm" onClick={() => { try { navigator.clipboard.writeText(secret); } catch (e) {} }}><Icons.copy /> Copiar</button>
              </div>
            </div>
          </details>
          {demo ? (
            <div className="twofa-code"><span className="dim">Código actual (modo demo)</span><b className="mono">{current}</b><span className="twofa-count">{secsLeft}s</span></div>
          ) : null}
          <Field label="Código de 6 dígitos para confirmar">
            <input className="input mono" inputMode="numeric" maxLength={6} value={code}
              onChange={(e) => { setCode(e.target.value.replace(/\D/g, '')); setErr(null); }}
              placeholder="000000" style={{ letterSpacing: '0.3em', textAlign: 'center', fontSize: '1.1rem' }} />
          </Field>
          {err ? <div className="login-error" style={{ margin: 0 }}>{err}</div> : null}
        </div>
      )}
    </Modal>
  );
}

/* ------------------------------------------------ banner global de error de API
   Escucha 'trascend:apierror' (lo dispara data/db.js cuando falla un guardado)
   y lo muestra en pantalla, así un error al guardar deja de fallar en silencio. */
function ApiErrorHost() {
  const [msg, setMsg] = useState(null);
  const timer = useRef(null);
  useEffect(() => {
    const onErr = (e) => {
      const m = (e && e.detail && e.detail.message) || 'No se pudo guardar. Reintentá.';
      setMsg(m);
      clearTimeout(timer.current);
      timer.current = setTimeout(() => setMsg(null), 8000);
    };
    window.addEventListener('trascend:apierror', onErr);
    return () => { window.removeEventListener('trascend:apierror', onErr); clearTimeout(timer.current); };
  }, []);
  if (!msg) return null;
  return (
    <div className="api-error-banner" role="alert">
      <Icons.alert />
      <span>{msg}</span>
      <button type="button" onClick={() => setMsg(null)} aria-label="Cerrar">×</button>
    </div>
  );
}

/* ------------------------------------------------ Importar / Exportar CSV
   Botón + modal reutilizable. `table` es la clave whitelisted en api.php.
   Al importar, muestra un resumen (creados / salteados). Al exportar,
   descarga el CSV en una pestaña nueva. Acepta también .xlsx: si el
   navegador selecciona uno, se carga SheetJS on-demand y se convierte a
   CSV antes de enviarlo. */
let __sheetJSPromise = null;
function loadSheetJS() {
  if (window.XLSX) return Promise.resolve(window.XLSX);
  if (__sheetJSPromise) return __sheetJSPromise;
  __sheetJSPromise = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = 'https://unpkg.com/xlsx@0.18.5/dist/xlsx.full.min.js';
    s.onload = () => resolve(window.XLSX);
    s.onerror = () => reject(new Error('No se pudo cargar el motor de Excel (revisá tu conexión).'));
    document.head.appendChild(s);
  });
  return __sheetJSPromise;
}
function ImportExportCSV({ table, label, onImported }) {
  const [open, setOpen] = React.useState(false);
  const [csv, setCsv] = React.useState('');
  const [file, setFile] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [result, setResult] = React.useState(null);
  const [parsing, setParsing] = React.useState(false);
  const [mode, setMode] = React.useState('skip');
  async function pick(e) {
    const f = e.target.files && e.target.files[0]; if (!f) return;
    setFile(f); setResult(null);
    const isExcel = /\.xlsx?$/i.test(f.name) || (f.type && f.type.indexOf('spreadsheet') !== -1);
    if (isExcel) {
      setParsing(true);
      try {
        const XLSX = await loadSheetJS();
        const buf = await f.arrayBuffer();
        const wb = XLSX.read(buf, { type: 'array' });
        const sheetName = wb.SheetNames[0];
        if (!sheetName) throw new Error('El Excel no tiene ninguna hoja.');
        const csvOut = XLSX.utils.sheet_to_csv(wb.Sheets[sheetName], { FS: ',' });
        setCsv(csvOut);
      } catch (err) { setResult({ error: 'No pudimos leer el Excel: ' + err.message }); setCsv(''); }
      setParsing(false);
    } else {
      const fr = new FileReader();
      fr.onload = () => setCsv(String(fr.result || ''));
      fr.readAsText(f, 'utf-8');
    }
  }
  async function doImport() {
    if (!csv.trim()) return;
    setBusy(true);
    try { const r = await TrascendAPI.csv.import(table, csv, mode); setResult(r); if (onImported) onImported(r); }
    catch (e) { setResult({ error: e.message }); }
    setBusy(false);
  }
  return (
    <React.Fragment>
      <div className="row" style={{ gap: 6 }}>
        <button className="btn btn-ghost btn-sm" onClick={() => TrascendAPI.csv.export(table)} title={'Exportar ' + (label || table) + ' a CSV'}>
          <Icons.download /> Exportar
        </button>
        <button className="btn btn-ghost btn-sm" onClick={() => { setOpen(true); setCsv(''); setFile(null); setResult(null); }} title={'Importar ' + (label || table) + ' desde CSV'}>
          <Icons.upload /> Importar
        </button>
      </div>
      {open ? (
        <Modal title={'Importar ' + (label || table) + ' desde CSV'} onClose={() => setOpen(false)}
          foot={<React.Fragment>
            <button className="btn btn-ghost" onClick={() => setOpen(false)}>Cerrar</button>
            <button className="btn btn-primary" disabled={busy || !csv.trim()} onClick={doImport}>{busy ? 'Importando…' : 'Importar'}</button>
          </React.Fragment>}>
          <div className="stack" style={{ gap: 10 }}>
            <p className="dim" style={{ margin: 0, fontSize: '0.85rem' }}>
              Podés subir <b>.csv</b> o <b>.xlsx</b> (Excel se convierte automáticamente). La cabecera debe usar los nombres de columna reales.
              La plantilla con las columnas correctas la bajás desde <b>Exportar</b> (sale aunque la tabla esté vacía).
            </p>
            <Field label="Archivo .csv o .xlsx (hasta 25 MB)">
              <input className="input" type="file" accept=".csv,text/csv,.xls,.xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel" onChange={pick} />
              {parsing ? <span className="dim" style={{ fontSize: '0.78rem' }}>Convirtiendo Excel a CSV…</span> : null}
              {file && !parsing ? <span className="dim" style={{ fontSize: '0.78rem' }}>{file.name} · {Math.ceil(file.size / 1024)} KB{csv ? ' · ' + (csv.split('\n').length - 1) + ' filas detectadas' : ''}</span> : null}
            </Field>
            <Field label="O pegá el CSV">
              <textarea className="input mono" rows="8" value={csv} onChange={(e) => setCsv(e.target.value)} placeholder="id,name,email,..." />
            </Field>
            <Field label="Qué hacer si el registro ya existe">
              <div className="col" style={{ gap: 6 }}>
                <label className="row" style={{ gap: 8 }}>
                  <input type="radio" name={"imp-mode-" + table} value="skip" checked={mode === 'skip'} onChange={() => setMode('skip')} />
                  <span><b>No pisar</b> · si coincide <code>custom_id</code>, <code>name</code>, <code>email</code> o <code>number</code> se saltea (recomendado).</span>
                </label>
                <label className="row" style={{ gap: 8 }}>
                  <input type="radio" name={"imp-mode-" + table} value="append" checked={mode === 'append'} onChange={() => setMode('append')} />
                  <span><b>Sumar siempre</b> · crea todas las filas del CSV. Puede generar duplicados.</span>
                </label>
              </div>
            </Field>
            {result ? (
              result.error ? (
                <div className="conn-note" style={{ borderColor: 'var(--danger)' }}>❌ {result.error}</div>
              ) : (
                <div className="conn-note">
                  ✅ Creados: <b>{result.created}</b>
                  {result.duplicates != null ? <span> · Duplicados (no pisados): <b>{result.duplicates}</b></span> : null}
                  {result.skipped ? <span> · Sin datos válidos: <b>{result.skipped}</b></span> : null}
                  <span> · Modo: <code>{result.mode || mode}</code></span>
                  {result.errors && result.errors.length ? <div style={{ marginTop: 6, fontSize: '0.75rem' }}>Errores: {result.errors.slice(0, 3).join(' · ')}</div> : null}
                </div>
              )
            ) : null}
          </div>
        </Modal>
      ) : null}
    </React.Fragment>
  );
}

/* ------------------------------------------------ Bell de notificaciones
   Se pone en el topbar del CRM. Hace polling cada 25 s del contador y baja
   la lista completa solo al abrir el dropdown (menos carga). Al clickear
   un item, marca leído y navega vía onNavigate({route, entity_id}). */
function NotifBell({ onNavigate }) {
  const [count, setCount] = React.useState(0);
  const [open, setOpen] = React.useState(false);
  const [list, setList] = React.useState(null);
  const ref = React.useRef(null);

  const refreshCount = React.useCallback(async () => {
    try { const r = await TrascendAPI.notifications.count(); setCount(Number(r.count || 0)); } catch (e) {}
  }, []);
  React.useEffect(() => {
    refreshCount();
    const iv = setInterval(refreshCount, 25000);
    return () => clearInterval(iv);
  }, [refreshCount]);

  /* Cerrar al hacer click afuera */
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);

  async function openDropdown() {
    setOpen(true);
    setList(null);
    try { setList(await TrascendAPI.notifications.list()); } catch (e) { setList([]); }
  }
  async function pick(n) {
    setOpen(false);
    try { await TrascendAPI.notifications.markRead(n.id); } catch (e) {}
    refreshCount();
    if (n.route && onNavigate) onNavigate(n.route);
  }
  async function markAll() {
    try { await TrascendAPI.notifications.markAllRead(); } catch (e) {}
    setCount(0);
    setList((prev) => (prev || []).map((n) => ({ ...n, seen: 1 })));
  }
  function timeAgo(iso) {
    if (!iso) return '';
    const d = new Date(String(iso).replace(' ', 'T'));
    const s = Math.max(0, (Date.now() - d.getTime()) / 1000);
    if (s < 60) return 'hace segundos';
    const m = Math.floor(s / 60); if (m < 60) return 'hace ' + m + ' min';
    const h = Math.floor(m / 60); if (h < 24) return 'hace ' + h + ' h';
    const dd = Math.floor(h / 24); if (dd < 30) return 'hace ' + dd + ' d';
    return d.toLocaleDateString('es-UY', { day: '2-digit', month: 'short' });
  }

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button className="btn btn-ghost btn-icon" title="Notificaciones" onClick={() => open ? setOpen(false) : openDropdown()}
        style={{ position: 'relative' }}>
        <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
          <path d="M4 14V9a6 6 0 1 1 12 0v5l1.5 2H2.5L4 14z" />
          <path d="M8 17.5a2 2 0 0 0 4 0" />
        </svg>
        {count > 0 ? (
          <span style={{ position: 'absolute', top: -2, right: -2, minWidth: 16, height: 16, borderRadius: 8, background: '#F04766', color: '#fff', fontSize: '0.62rem', fontWeight: 700, padding: '0 4px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
            {count > 99 ? '99+' : count}
          </span>
        ) : null}
      </button>
      {open ? (
        <div style={{
          position: 'absolute', right: 0, top: 'calc(100% + 6px)', width: 360, maxHeight: 480, overflow: 'auto',
          background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 12,
          boxShadow: '0 24px 60px -20px rgba(0,0,0,0.35)', zIndex: 9000, padding: 8,
        }}>
          <div className="row" style={{ padding: '6px 8px 10px', borderBottom: '1px solid var(--line-soft)', marginBottom: 6 }}>
            <b style={{ fontSize: '0.86rem' }}>Notificaciones</b>
            <div className="spacer"></div>
            <button className="btn btn-ghost btn-sm" onClick={markAll} disabled={count === 0}>Marcar todo leído</button>
          </div>
          {list === null ? <div className="dim" style={{ padding: 18, textAlign: 'center', fontSize: '0.82rem' }}>Cargando…</div> :
           list.length === 0 ? <div className="dim" style={{ padding: 22, textAlign: 'center', fontSize: '0.82rem' }}>Sin notificaciones.</div> :
           list.map((n) => (
             <button key={n.id} className="row" onClick={() => pick(n)} style={{
               width: '100%', textAlign: 'left', gap: 8, padding: '10px 8px', border: 0, background: n.seen ? 'transparent' : 'rgba(122,79,192,0.06)',
               borderRadius: 8, cursor: 'pointer', color: 'inherit',
             }}>
               <span style={{ width: 6, height: 6, borderRadius: 3, background: n.seen ? 'var(--text-dim)' : 'var(--violet)', flex: 'none', marginTop: 6 }}></span>
               <span className="col" style={{ gap: 2, flex: 1, minWidth: 0 }}>
                 <b style={{ fontSize: '0.82rem' }}>{n.title}</b>
                 {n.body ? <span className="dim" style={{ fontSize: '0.74rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{n.body}</span> : null}
                 <span className="dim" style={{ fontSize: '0.68rem' }}>{timeAgo(n.at)}{n.route ? ' · abrir' : ''}</span>
               </span>
             </button>
           ))}
        </div>
      ) : null}
    </div>
  );
}

Object.assign(window, {
  Icons, Modal, Field, Avatar, NetChip, StatusBadge, useToast, useCollection,
  InfoButton, TwoFASetup, useTick, ApiErrorHost, ImportExportCSV, NotifBell,
  BarChart, Donut, ProgressBar, ConfirmHost, confirmDialog, confirmDelete, Toggle, PwdMeter,
  CLIENT_STATUS, INVOICE_STATUS, QUOTE_STATUS, STAGE_BADGE, POST_BADGE,
});
