// ui.jsx — shared primitives. Class-based styling (see styles.css);
// no global `styles` object. Exports to window for other files.
const { useState, useRef, useEffect, useCallback, useContext, createContext } = React;

/* ---------- hooks ---------- */
function useOutside(ref, onClose) {
  useEffect(() => {
    function h(e){ if (ref.current && !ref.current.contains(e.target)) onClose(); }
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [onClose]);
}
function usePersist(key, initial) {
  const k = 'swo-portal-' + key;
  const [v, setV] = useState(() => {
    try { const s = localStorage.getItem(k); return s !== null ? JSON.parse(s) : initial; } catch { return initial; }
  });
  useEffect(() => { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} }, [v]);
  return [v, setV];
}

/* ---------- toast ---------- */
const ToastContext = createContext(() => {});
function ToastProvider({ children }) {
  const [toasts, setToasts] = useState([]);
  const push = useCallback((msg, type = 'success') => {
    const id = Math.random().toString(36).slice(2);
    setToasts(t => [...t, { id, msg, type }]);
    setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 2800);
  }, []);
  const icon = { success:'bi-check-circle-fill', error:'bi-exclamation-circle-fill', info:'bi-info-circle-fill' };
  return (
    <ToastContext.Provider value={push}>
      {children}
      <div className="toast-wrap">
        {toasts.map(t => (
          <div key={t.id} className={'toast ' + t.type}>
            <i className={'bi ' + (icon[t.type] || icon.info)}></i><span>{t.msg}</span>
          </div>
        ))}
      </div>
    </ToastContext.Provider>
  );
}
function useToast(){ return useContext(ToastContext); }

/* ---------- confirm dialog (thay window.confirm bằng popup riêng) ---------- */
const ConfirmContext = createContext(() => Promise.resolve(false));
function ConfirmProvider({ children }) {
  const { lang } = useLang();
  const [state, setState] = useState(null);   // { message, title, confirmLabel, cancelLabel, danger, resolve }
  const confirm = useCallback((opts) => {
    const o = typeof opts === 'string' ? { message:opts } : (opts || {});
    return new Promise(resolve => setState({ ...o, resolve }));
  }, []);
  const close = (val) => setState(s => { if (s) s.resolve(val); return null; });
  const L = (vi, en) => (lang === 'vi' ? vi : en);
  return (
    <ConfirmContext.Provider value={confirm}>
      {children}
      {state && (
        <Modal title={state.title || L('Xác nhận', 'Confirm')} width={420} onClose={() => close(false)}
          footer={<><button className="btn" onClick={() => close(false)}>{state.cancelLabel || L('Hủy', 'Cancel')}</button>
            <button className={'btn ' + (state.danger === false ? 'btn-primary' : 'btn-danger')} autoFocus onClick={() => close(true)}>{state.confirmLabel || L('Xác nhận', 'Confirm')}</button></>}>
          <div style={{ fontSize:'var(--fs-md)', color:'var(--text)', lineHeight:1.5 }}>{state.message}</div>
        </Modal>
      )}
    </ConfirmContext.Provider>
  );
}
function useConfirm(){ return useContext(ConfirmContext); }

/* ---------- CatalogEditor: CRUD danh mục cấu hình (Settings module) ----------
   catalogKey: industries|vendorFields|contractCats|productCats|expenseCats|contactTypes
   stringMode: true → item là chuỗi (chỉ Thêm/Xóa); false → {id,vi,en} (Thêm/Sửa nhãn/Xóa). */
function catSlug(s){ return (s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd').replace(/Đ/g, 'D').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'item'; }
function CatalogEditor({ catalogKey, title, stringMode }){
  const p = usePortal();
  const { lang } = useLang();
  const toast = useToast();
  const confirm = useConfirm();
  const L = (vi, en) => (lang === 'vi' ? vi : en);
  const items = (p.catalogs && p.catalogs[catalogKey]) || [];
  const [draft, setDraft] = useState({ vi:'', en:'' });
  const [editId, setEditId] = useState(null);
  const [edraft, setEdraft] = useState({ vi:'', en:'' });

  const add = () => {
    const vi = draft.vi.trim(); if (!vi){ toast(L('Nhập tên mục', 'Enter a label'), 'error'); return; }
    if (stringMode){
      if (items.includes(vi)){ toast(L('Mục đã tồn tại', 'Already exists'), 'error'); return; }
      p.addCatalogItem(catalogKey, vi);
    } else {
      let id = catSlug(vi); if (items.some(x => x.id === id)) id = id + '-' + (items.length + 1);
      p.addCatalogItem(catalogKey, { id, vi, en:draft.en.trim() || vi });
    }
    toast(L('Đã thêm', 'Added'), 'success'); setDraft({ vi:'', en:'' });
  };
  const startEdit = (it) => { setEditId(it.id); setEdraft({ vi:it.vi, en:it.en || '' }); };
  const saveEdit = () => {
    const vi = edraft.vi.trim(); if (!vi){ toast(L('Nhập tên mục', 'Enter a label'), 'error'); return; }
    p.updateCatalogItem(catalogKey, editId, { vi, en:edraft.en.trim() || vi });
    toast(L('Đã lưu', 'Saved'), 'success'); setEditId(null);
  };
  const del = async (id, label) => {
    if (!(await confirm({ message:L('Xóa mục “', 'Delete “') + label + '”?', confirmLabel:L('Xóa', 'Delete') }))) return;
    p.deleteCatalogItem(catalogKey, id); toast(L('Đã xóa', 'Deleted'), 'success');
  };

  return (
    <div className="card" style={{ maxWidth:640 }}>
      <div className="card-head"><h3>{title}</h3><span className="subtle" style={{ fontSize:'var(--fs-sm)' }}>{items.length} {L('mục', 'items')}</span></div>
      <div className="card-body col g8">
        {items.map((it) => {
          const id = stringMode ? it : it.id;
          const label = stringMode ? it : it.vi;
          const editing = !stringMode && editId === id;
          return (
            <div key={id} className="list-row" style={{ cursor:'default' }}>
              {editing ? (
                <div className="flex g8 grow wrap">
                  <input className="input" style={{ flex:'1 1 160px' }} value={edraft.vi} onChange={e => setEdraft(s => ({ ...s, vi:e.target.value }))} placeholder="VI" autoFocus />
                  <input className="input" style={{ flex:'1 1 160px' }} value={edraft.en} onChange={e => setEdraft(s => ({ ...s, en:e.target.value }))} placeholder="EN" />
                  <button className="btn btn-sm btn-primary" onClick={saveEdit}><i className="bi bi-check-lg"></i></button>
                  <button className="btn btn-sm" onClick={() => setEditId(null)}><i className="bi bi-x-lg"></i></button>
                </div>
              ) : (<>
                <span className="grow" style={{ fontWeight:500 }}>{label}{!stringMode && it.en && it.en !== it.vi && <span className="subtle" style={{ marginLeft:8, fontWeight:400 }}>· {it.en}</span>}</span>
                {!stringMode && <button className="iconbtn" title={L('Sửa', 'Edit')} onClick={() => startEdit(it)}><i className="bi bi-pencil" style={{ fontSize:12 }}></i></button>}
                <button className="iconbtn" title={L('Xóa', 'Delete')} onClick={() => del(id, label)}><i className="bi bi-trash" style={{ fontSize:12 }}></i></button>
              </>)}
            </div>
          );
        })}
        {!items.length && <div className="subtle" style={{ fontSize:'var(--fs-base)' }}>{L('Chưa có mục nào', 'No items')}</div>}
        <div className="flex g8 wrap" style={{ marginTop:6, paddingTop:8, borderTop:'1px solid var(--border-light)' }}>
          <input className="input" style={{ flex:'1 1 160px' }} value={draft.vi} onChange={e => setDraft(s => ({ ...s, vi:e.target.value }))} placeholder={stringMode ? L('Thêm mục mới…', 'New item…') : L('Tên (VI)', 'Label (VI)')} onKeyDown={e => { if (e.key === 'Enter') add(); }} />
          {!stringMode && <input className="input" style={{ flex:'1 1 140px' }} value={draft.en} onChange={e => setDraft(s => ({ ...s, en:e.target.value }))} placeholder="EN" onKeyDown={e => { if (e.key === 'Enter') add(); }} />}
          <button className="btn btn-primary" onClick={add}><i className="bi bi-plus-lg"></i>{L('Thêm', 'Add')}</button>
        </div>
      </div>
    </div>
  );
}

/* ---------- avatar ---------- */
function Avatar({ name = '', size = 26, square, color }) {
  const initials = name.split(' ').filter(Boolean).slice(-2).map(w => w[0]).join('');
  return (
    <span className={'avatar' + (square ? ' sq' : '')}
      style={{ width:size, height:size, fontSize:Math.round(size*0.42), background:color || undefined }}>
      {initials}
    </span>
  );
}

/* ---------- placeholder image ---------- */
function PlaceholderImg({ label, className = '', style }) {
  return <div className={'ph-img ' + className} style={style}>{label && <span>{label}</span>}</div>;
}

/* ---------- popover ---------- */
function Popover({ button, children, align = 'left', width }) {
  const [open, setOpen] = useState(false);
  const ref = useRef();
  useOutside(ref, () => setOpen(false));
  const close = () => setOpen(false);
  return (
    <div ref={ref} style={{ position:'relative' }}>
      {button({ open, toggle:() => setOpen(o => !o), close })}
      {open && (
        <div className="pop" style={{ top:'calc(100% + 6px)', [align === 'right' ? 'right' : 'left']:0, width }}>
          {typeof children === 'function' ? children({ close }) : children}
        </div>
      )}
    </div>
  );
}

/* ---------- modal ---------- */
// staticBackdrop: form nhập liệu dài — bấm ra ngoài / Esc KHÔNG đóng (tránh mất dữ liệu đang gõ); chỉ đóng bằng nút X / Hủy.
function Modal({ title, onClose, children, footer, width = 560, staticBackdrop = false }) {
  useEffect(() => {
    if (staticBackdrop) return;
    const h = e => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [onClose, staticBackdrop]);
  return (
    <div className="modal-overlay" onMouseDown={e => { if (!staticBackdrop && e.target === e.currentTarget) onClose(); }}>
      <div className="modal" style={{ maxWidth:width }}>
        <div className="modal-head">
          <h3>{title}</h3>
          <button className="iconbtn" onClick={onClose} aria-label="Close"><i className="bi bi-x-lg"></i></button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </div>
  );
}

/* ---------- field / inputs ---------- */
function Field({ label, children, hint, hintError }) {
  return (
    <div className="field">
      {label && <label>{label}</label>}
      {children}
      {hintError
        ? <div style={{ fontSize:'var(--fs-sm)', color:'var(--error)' }}><i className="bi bi-exclamation-circle" style={{ marginRight:4 }}></i>{hintError}</div>
        : hint && <div className="subtle" style={{ fontSize:'var(--fs-sm)' }}>{hint}</div>}
    </div>
  );
}

function ChipInput({ value, onChange, suggestions = [], placeholder, listId }) {
  const [draft, setDraft] = useState('');
  const valid = s => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
  const add = (raw) => {
    const s = raw.trim().replace(/,$/, '');
    if (!s) return;
    if (!value.includes(s)) onChange([...value, s]);
    setDraft('');
  };
  return (
    <div className="chip-input">
      {value.map(v => (
        <span key={v} className={'chip' + (valid(v) ? '' : ' is-invalid')}>
          {v}<button onClick={() => onChange(value.filter(x => x !== v))}><i className="bi bi-x"></i></button>
        </span>
      ))}
      <input value={draft} placeholder={value.length ? '' : placeholder} list={listId}
        onChange={e => { const val = e.target.value; if (val.endsWith(',')) add(val); else setDraft(val); }}
        onKeyDown={e => { if (e.key === 'Enter' || e.key === ';') { e.preventDefault(); add(draft); }
          else if (e.key === 'Backspace' && !draft && value.length) onChange(value.slice(0, -1)); }}
        onBlur={() => add(draft)} />
      {listId && <datalist id={listId}>{suggestions.map(s => <option key={s} value={s} />)}</datalist>}
    </div>
  );
}

/* ---------- money input (hiển thị phân cách hàng nghìn, lưu số) ---------- */
function MoneyInput({ value, onChange, className = 'input mono', style, placeholder = '0', disabled }) {
  const n = +value || 0;
  const display = n ? n.toLocaleString('vi-VN') : '';
  const onIn = (e) => { const d = (e.target.value || '').replace(/[^\d]/g, ''); onChange(d ? parseInt(d, 10) : 0); };
  return <input type="text" inputMode="numeric" disabled={disabled} className={className}
    style={{ textAlign:'right', ...style }} value={display} placeholder={placeholder} onChange={onIn} />;
}

/* ---------- date input (hiển thị theo định dạng đã cấu hình, KHÔNG phụ thuộc locale trình duyệt) ----------
   Native <input type=date> in theo locale OS/trình duyệt (vd en-US → MM/DD/YYYY), KHÔNG đổi được bằng lang/CSS.
   → Dùng ô text hiển thị/nhập theo __DATEFMT (mặc định DD/MM/YYYY) + nút lịch mở native picker ẩn. value/onChange = ISO 'yyyy-mm-dd'. */
function parseDateInput(text, fmt){
  const t = (text || '').trim();
  if (!t) return '';                                  // rỗng = hợp lệ (xoá ngày)
  fmt = fmt || 'dmy';
  let dd, mm, yy;
  if (fmt === 'iso'){ const m = t.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$/); if (!m) return null; yy = m[1]; mm = m[2]; dd = m[3]; }
  else { const m = t.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/); if (!m) return null; if (fmt === 'mdy'){ mm = m[1]; dd = m[2]; } else { dd = m[1]; mm = m[2]; } yy = m[3]; }
  mm = String(mm).padStart(2, '0'); dd = String(dd).padStart(2, '0');
  if (+mm < 1 || +mm > 12 || +dd < 1 || +dd > 31) return null;
  const iso = yy + '-' + mm + '-' + dd;
  const d = new Date(iso + 'T00:00:00');
  if (isNaN(d.getTime()) || d.getFullYear() !== +yy || (d.getMonth() + 1) !== +mm || d.getDate() !== +dd) return null;  // chặn 31/02…
  return iso;
}
function DateInput({ value, onChange, className = 'input', style, disabled }){
  const fmt = (typeof window !== 'undefined' && window.__DATEFMT) || 'dmy';
  const ph = fmt === 'iso' ? 'YYYY-MM-DD' : (fmt === 'mdy' ? 'MM/DD/YYYY' : 'DD/MM/YYYY');
  const disp = (iso) => (iso && window.fmtDate) ? fmtDate(iso) : (iso || '');
  const [text, setText] = React.useState(disp(value));
  const nativeRef = React.useRef(null);
  React.useEffect(() => { setText(disp(value)); }, [value]);
  const commit = (raw) => { const r = parseDateInput(raw, fmt); if (r === null){ setText(disp(value)); return; } if (r !== (value || '')) onChange(r); else setText(disp(value)); };
  const openPicker = () => { const n = nativeRef.current; if (!n) return; try { n.showPicker(); } catch (e) { n.focus(); } };
  return (
    <div style={{ position:'relative', width:'100%' }}>
      <input type="text" inputMode="numeric" disabled={disabled} className={className} placeholder={ph}
        style={{ width:'100%', ...style, paddingRight:28 }} value={text}
        onChange={e => setText(e.target.value)} onBlur={e => commit(e.target.value)}
        onKeyDown={e => { if (e.key === 'Enter'){ e.preventDefault(); commit(e.target.value); } }} />
      <input ref={nativeRef} type="date" value={value || ''} tabIndex={-1} aria-hidden="true" disabled={disabled}
        onChange={e => onChange(e.target.value)}
        style={{ position:'absolute', right:0, bottom:0, width:1, height:1, opacity:0, padding:0, margin:0, border:0, pointerEvents:'none' }} />
      <button type="button" tabIndex={-1} disabled={disabled} onClick={openPicker} title="Chọn ngày"
        style={{ position:'absolute', right:0, top:0, height:'100%', width:28, display:'flex', alignItems:'center', justifyContent:'center', background:'none', border:0, cursor:disabled ? 'default' : 'pointer', color:'var(--text-muted)' }}>
        <i className="bi bi-calendar3" style={{ fontSize:13 }}></i>
      </button>
    </div>
  );
}

/* ---------- sort bảng danh sách (dùng chung mọi module) ----------
   3 trạng thái mỗi cột: tăng → giảm → về thứ tự mặc định. So sánh: số theo số,
   chữ theo locale vi (numeric:true — "HD-2" < "HD-10"). Dùng:
     const srt = useTableSort();
     <ThSort k="name" sort={srt}>Tên</ThSort> ... rows = srt.sortRows(list, { name:x=>x.name, total:x=>tot(x) }) */
function useTableSort(){
  const [s, setS] = React.useState(null);   // {key, dir:'asc'|'desc'} | null = thứ tự mặc định
  const toggle = (key) => setS(cur => (cur && cur.key === key) ? (cur.dir === 'asc' ? { key, dir:'desc' } : null) : { key, dir:'asc' });
  const sortRows = (rows, getters) => {
    if (!s || !getters || !getters[s.key]) return rows;
    const g = getters[s.key];
    const sorted = [...rows].sort((a, b) => {
      const va = g(a), vb = g(b);
      const na = (va == null || va === ''), nb = (vb == null || vb === '');
      if (na || nb) return na && nb ? 0 : (na ? 1 : -1);                    // ô trống luôn xuống cuối
      const r = (typeof va === 'number' && typeof vb === 'number') ? va - vb
        : String(va).localeCompare(String(vb), 'vi', { numeric:true, sensitivity:'base' });
      return s.dir === 'asc' ? r : -r;
    });
    return sorted;
  };
  return { s, toggle, sortRows };
}
function ThSort({ k, sort, children, className, style }){
  const active = sort.s && sort.s.key === k;
  return (
    <th className={className} style={{ cursor:'pointer', userSelect:'none', ...style }} onClick={() => sort.toggle(k)}>
      <span style={{ display:'inline-flex', alignItems:'center', gap:3 }}>
        {children}
        <i className={'bi ' + (active ? (sort.s.dir === 'asc' ? 'bi-caret-up-fill' : 'bi-caret-down-fill') : 'bi-arrow-down-up')}
          style={{ fontSize:9, opacity:active ? 1 : .35, flex:'none' }}></i>
      </span>
    </th>
  );
}

/* ---------- tabs / segmented ---------- */
function Tabs({ tabs, active, onChange }) {
  return (
    <div className="tabs">
      {tabs.map(t => (
        <button key={t.id} className={'tab' + (active === t.id ? ' is-active' : '')} onClick={() => onChange(t.id)}>
          {t.icon && <i className={'bi ' + t.icon}></i>}{t.label}{t.count != null && <span className="badge badge-neutral" style={{ marginLeft:2 }}>{t.count}</span>}
        </button>
      ))}
    </div>
  );
}
function Segmented({ options, value, onChange }) {
  return (
    <div className="seg">
      {options.map(o => (
        <button key={o.value} className={value === o.value ? 'is-active' : ''} onClick={() => onChange(o.value)}>{o.label}</button>
      ))}
    </div>
  );
}

/* ---------- misc ---------- */
function Kpi({ label, value, icon, delta, deltaDir }) {
  return (
    <div className="kpi">
      <div className="k-label">{icon && <i className={'bi ' + icon}></i>}{label}</div>
      <div className="k-val">{value}</div>
      {delta && <div className={'k-delta ' + (deltaDir || 'up')}><i className={'bi ' + (deltaDir === 'down' ? 'bi-arrow-down-right' : 'bi-arrow-up-right')}></i>{delta}</div>}
    </div>
  );
}
function EmptyState({ icon = 'bi-inbox', text }) {
  return <div className="empty"><i className={'bi ' + icon}></i><div>{text}</div></div>;
}
function StatusBadge({ status }) {
  const map = { active:'badge-success', prospect:'badge-warning', inactive:'badge-neutral' };
  const { t } = useLang();
  return <span className={'badge ' + (map[status] || 'badge-neutral')}><i className="dot"></i>{t('st.' + status)}</span>;
}
function DistribBars({ rows, max }) {
  const m = max || Math.max(...rows.map(r => r.value), 1);
  return (
    <div className="distrib">
      {rows.map(r => (
        <div key={r.label} className="distrib-row">
          <span className="truncate" title={r.label}>{r.label}</span>
          <span className="distrib-bar"><i style={{ width:(r.value / m * 100) + '%', background:r.color || 'var(--primary)' }}></i></span>
          <span className="num muted">{r.value}</span>
        </div>
      ))}
    </div>
  );
}

/* ---------- recent interactions (dùng chung mọi module overview) ----------
   items: [{ icon, color, title, sub, date, onClick }] — đã sắp xếp mới→cũ. */
function RecentInteractions({ items, onViewAll, max = 8 }) {
  const { t, lang } = useLang();
  const list = (items || []).slice(0, max);
  return (
    <div className="card" style={{ marginTop:14 }}>
      <div className="card-head">
        <h3>{t('c.recent')}</h3>
        {onViewAll && <button className="btn btn-ghost btn-sm" onClick={onViewAll}>{t('c.viewAll')}</button>}
      </div>
      <div className="card-body" style={{ paddingTop:6 }}>
        {list.length === 0 && <EmptyState icon="bi-clock-history" text={lang === 'vi' ? 'Chưa có tương tác' : 'No activity yet'} />}
        {list.map((it, i) => (
          <div key={i} className={'recent-row' + (it.onClick ? ' is-link' : '')} onClick={it.onClick}>
            <span className="recent-ic" style={{ color:it.color || 'var(--primary)' }}><i className={'bi ' + (it.icon || 'bi-dot')}></i></span>
            <div className="grow" style={{ minWidth:0 }}>
              <div className="truncate" style={{ fontWeight:500 }}>{it.title}</div>
              {it.sub && <div className="subtle truncate" style={{ fontSize:'var(--fs-sm)' }}>{it.sub}</div>}
            </div>
            <span className="subtle mono" style={{ fontSize:'var(--fs-sm)', flex:'none' }}>{fmtDate(it.date)}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ---------- relative time (dùng chung module overviews) ----------
   "Hôm nay / Hôm qua / N ngày trước / N tuần trước…" — neo theo TODAY (mock). */
function fmtAgo(dateStr, lang){
  if (!dateStr) return '';
  const ds = String(dateStr).slice(0, 10);
  const today = (typeof TODAY !== 'undefined' ? TODAY : ds);
  const d = new Date(ds + 'T00:00:00'), now = new Date(today + 'T00:00:00');
  let days = Math.round((now - d) / 86400000);
  if (isNaN(days)) return ds;
  if (days < 0) days = 0;
  if (days === 0) return lang === 'vi' ? 'Hôm nay' : 'Today';
  if (days === 1) return lang === 'vi' ? 'Hôm qua' : 'Yesterday';
  if (days < 7) return lang === 'vi' ? days + ' ngày trước' : days + 'd ago';
  if (days < 28) { const w = Math.round(days / 7); return lang === 'vi' ? w + ' tuần trước' : w + 'w ago'; }
  if (days < 365) { const mo = Math.round(days / 30); return lang === 'vi' ? mo + ' tháng trước' : mo + 'mo ago'; }
  return ds;
}
/* ---------- recent activity feed (module overviews) ----------
   items: [{ icon, color, title, sub, tag, tagCls, time, onClick }] — đã sắp xếp mới→cũ. */
function RecentActivity({ items, title, emptyText, max = 7 }){
  const { lang } = useLang();
  const list = (items || []).filter(Boolean).slice(0, max);
  return (
    <div className="card recent-act" style={{ marginTop:14 }}>
      <div className="card-head">
        <h3><i className="bi bi-activity" style={{ marginRight:7, color:'var(--accent)' }}></i>{title || (lang === 'vi' ? 'Tương tác mới nhất' : 'Latest activity')}</h3>
        {list.length > 0 && <span className="badge badge-neutral">{list.length}</span>}
      </div>
      <div className="card-body" style={{ paddingTop:4 }}>
        {list.length === 0 && <EmptyState icon="bi-clock-history" text={emptyText || (lang === 'vi' ? 'Chưa có hoạt động gần đây' : 'No recent activity')} />}
        {list.map((it, i) => (
          <div key={i} className={'act-row' + (it.onClick ? ' clickable' : '')} onClick={it.onClick || undefined}>
            <span className="act-ic" style={{ color:it.color || 'var(--primary)', background:'color-mix(in srgb, ' + (it.color || 'var(--primary)') + ' 14%, transparent)' }}><i className={'bi ' + (it.icon || 'bi-dot')}></i></span>
            <div className="grow" style={{ minWidth:0 }}>
              <div className="act-title truncate">{it.title}</div>
              {it.sub && <div className="act-sub truncate">{it.sub}</div>}
            </div>
            {it.tag && <span className={'badge ' + (it.tagCls || 'badge-neutral')} style={{ flex:'none' }}>{it.tag}</span>}
            <span className="act-time mono">{it.time}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// Sự kiện "Tạo / Cập nhật bản ghi" cho dòng Hoạt động gần đây — dùng created_at/updated_at từ DB.
// Trả null nếu bản ghi chưa có mốc thời gian (vd dữ liệu mock chưa đăng nhập) → không hiện.
function recordActivity(rec, labels, onClick){
  const ts = rec.updatedAt || rec.createdAt; if (!ts) return null;
  const isNew = !rec.createdAt || rec.createdAt === rec.updatedAt;
  return { ts, date:String(ts).slice(0, 10), icon:isNew ? 'bi-plus-circle' : 'bi-pencil-square', color:'var(--primary)', title:isNew ? labels.create : labels.update, sub:labels.sub, onClick };
}

Object.assign(window, {
  useOutside, usePersist, ToastProvider, useToast, ConfirmProvider, useConfirm, CatalogEditor, Avatar, PlaceholderImg,
  Popover, Modal, Field, ChipInput, MoneyInput, DateInput, parseDateInput, Tabs, Segmented, Kpi, EmptyState, StatusBadge, DistribBars, RecentInteractions,
  RecentActivity, fmtAgo, recordActivity, useTableSort, ThSort,
});
