// main.jsx — App root: hoists state, provides PortalContext, routes, mounts.
// Danh mục cấu hình (catalog): string-catalog = mảng nhãn; còn lại = {id,vi,en}.
const CATALOG_STRING = ['industries', 'vendorFields', 'units'];
const catItemId = (cat, item) => CATALOG_STRING.includes(cat) ? item : item.id;
function buildDefaultCatalogs() {
  return {
    industries: INDUSTRIES.slice(),
    vendorFields: VENDOR_FIELDS.slice(),
    contractCats: CONTRACT_CATEGORIES.map(c => ({ id:c.id, vi:c.vi, en:c.en })),
    productCats: PRODUCT_CATEGORIES.map(c => ({ id:c.id, vi:c.vi, en:c.en })),
    expenseCats: EXPENSE_CATEGORIES.map(c => ({ id:c.id, vi:c.vi, en:c.en })),
    contactTypes: Object.entries(CONTACT_TYPES).map(([id, v]) => ({ id, vi:v.vi, en:v.en })),
    units: PRODUCT_UNITS.slice(),
    websiteCats: WEBSITE_CATS.slice(),
  };
}

function App() {
  const [lang, setLang] = usePersist('lang', 'vi');
  return (
    <ToastProvider>
      <LangProvider lang={lang} setLang={setLang}>
        <ConfirmProvider>
          <PortalApp />
          <PwaUpdateBanner />
        </ConfirmProvider>
      </LangProvider>
    </ToastProvider>
  );
}

function PortalApp() {
  const { t, tt, lang } = useLang();
  const toast = useToast();

  const [session, setSession] = React.useState(null);
  const [booted, setBooted] = React.useState(!window.SB_READY);   // đã kiểm tra phiên ban đầu chưa
  const [profile, setProfile] = React.useState(null);   // hồ sơ user thật (khi đăng nhập)
  // Bố cục widget dashboard (thứ tự/ẩn/thu nhỏ/kích thước) — theo NGƯỜI DÙNG, đồng bộ đa máy qua profiles.meta.dashLayout; localStorage = cache tức thì/offline.
  const [dashLayout, setDashLayout] = React.useState(() => { try { const v = JSON.parse(localStorage.getItem('swo-portal-dash')); return v && typeof v === 'object' ? v : {}; } catch (e) { return {}; } });
  const dashTimer = React.useRef(null);
  const authed = !!session;                     // BẮT BUỘC đăng nhập Supabase (đã bỏ demo)
  const [route, setRoute] = React.useState({ name:'dashboard' });
  const [pinned, setPinned] = usePersist('pinnedv3', ['customers', 'contacts', 'products', 'quotes', 'contracts', 'support', 'finance']);
  const [hidden, setHidden] = usePersist('hidden', []);
  const [density, setDensity] = usePersist('density', 'comfortable');
  const [fontsize, setFontsize] = usePersist('fontsize', 'md');
  const [pageWidth, setPageWidth] = usePersist('pagewidth', '1280');   // độ rộng nội dung trang: 1280 | 1800 | full
  const [theme, setTheme] = usePersist('theme', 'system');   // light | dark | system (mặc định system)
  const [dateFmt, setDateFmt] = usePersist('datefmt', '');   // '' = theo hệ thống | 'dmy' | 'mdy' (đổi định dạng ngày hiển thị)
  const [collapsed, setCollapsed] = usePersist('collapsed', false);
  const [branding, setBranding] = usePersist('brandingv2', DEFAULT_BRANDING);
  const [drawerOpen, setDrawerOpen] = React.useState(false);

  const [customers, setCustomers] = React.useState(CUSTOMERS);
  const [contacts, setContacts] = React.useState(CONTACTS);
  const [sentEmails, setSentEmails] = React.useState(SENT_EMAILS);
  const [templates, setTemplates] = React.useState(EMAIL_TEMPLATES);
  const [mailSettings, setMailSettings] = React.useState({ signature:SIGNATURE, account:'no-reply@swo.vn', connected:true });
  const [users, setUsers] = React.useState(Object.values(USERS));   // demo: persona mock; đăng nhập: user thật từ profiles
  const [contracts, setContracts] = React.useState(CONTRACTS);
  const [expenses, setExpenses] = React.useState(EXPENSES);
  const [tasks, setTasks] = React.useState(TASKS);
  const [notifRead, setNotifRead] = usePersist('notifsRead', []);   // lưu ID đã đọc (giữ qua reload)
  const notifs = NOTIFS.map(n => ({ ...n, unread: n.unread && !notifRead.includes(n.id) }));
  // mock state cho các module mới (Báo giá · Sản phẩm · Người dùng)
  const [quotes, setQuotes] = React.useState(QUOTES);
  const [products, setProducts] = React.useState(PRODUCTS);
  const [vendors, setVendors] = React.useState(VENDORS);   // Nhà cung cấp (mock-only, in-memory)
  const [deals, setDeals] = React.useState(DEALS);         // Thương vụ (mock-only, in-memory)
  const [tickets, setTickets] = React.useState(TICKETS);   // Hỗ trợ sau bán — DB.loadTickets thay khi đăng nhập
  const [posts, setPosts] = React.useState([]);             // Quản trị website (tin tức www.swo.vn) — KHÔNG mock, DB.loadPosts khi đăng nhập
  const [moves, setMoves] = React.useState([]);            // Kho: sổ nhập/xuất (DB.loadMoves)
  const [issues, setIssues] = React.useState([]);          // Kho: phiếu xuất (P2)
  const [receipts, setReceipts] = React.useState([]);      // Kho: phiếu nhập
  const [catalogs, setCatalogs] = React.useState(buildDefaultCatalogs);   // danh mục Settings (DB khi đăng nhập + 0016)
  const [appSettings, setAppSettings] = React.useState({});               // cấu hình key-value (DB + 0017)
  const [accounts, setAccounts] = React.useState(USERS_LIST);       // tài khoản (module Người dùng) — KHÁC p.users
  const [accountRoles, setRoles] = usePersist('rolesv1', ROLES);   // phân quyền vai trò — giữ qua reload
  const [pendingData, setPendingData] = React.useState({ mail:null });

  const [cmdkOpen, setCmdkOpen] = React.useState(false);
  const [newCustomerOpen, setNewCustomerOpen] = React.useState(false);
  const [newContactOpen, setNewContactOpen] = React.useState(false);
  const [newContractOpen, setNewContractOpen] = React.useState(false);
  const [newQuoteOpen, setNewQuoteOpen] = React.useState(false);
  const [newProductOpen, setNewProductOpen] = React.useState(false);
  const [newVendorOpen, setNewVendorOpen] = React.useState(false);
  const [newDealOpen, setNewDealOpen] = React.useState(false);
  const [newTicketOpen, setNewTicketOpen] = React.useState(false);
  const [newPostOpen, setNewPostOpen] = React.useState(false);
  const [postEditing, setPostEditing] = React.useState(null);   // bài đang sửa (null = tạo mới)
  const [newReceiptOpen, setNewReceiptOpen] = React.useState(false);
  const [receiptPreset, setReceiptPreset] = React.useState(null);
  const [stockAdjustItem, setStockAdjustItem] = React.useState(null);
  const [newIssueOpen, setNewIssueOpen] = React.useState(false);
  const [issuePreset, setIssuePreset] = React.useState(null);
  const [quotePrefill, setQuotePrefill] = React.useState(null);
  const [quoteEditing, setQuoteEditing] = React.useState(null);
  const [formPrefill, setFormPrefill] = React.useState(null);   // {customer, direction} cho form liên hệ/hợp đồng
  const [sendQuoteTarget, setSendQuoteTarget] = React.useState(null);
  const [inviteUserOpen, setInviteUserOpen] = React.useState(false);
  const [newTaskOpen, setNewTaskOpen] = React.useState(false);
  const [taskEditing, setTaskEditing] = React.useState(null);

  /* apply appearance to <html> */
  React.useEffect(() => { document.documentElement.dataset.density = density; }, [density]);
  React.useEffect(() => { document.documentElement.dataset.fontsize = fontsize; }, [fontsize]);
  React.useEffect(() => { document.documentElement.dataset.pagewidth = pageWidth; }, [pageWidth]);
  React.useEffect(() => {
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    const apply = () => { document.documentElement.dataset.theme = theme === 'system' ? (mq.matches ? 'dark' : 'light') : theme; };
    apply();
    if (theme === 'system') { mq.addEventListener('change', apply); return () => mq.removeEventListener('change', apply); }
  }, [theme]);
  React.useEffect(() => { applyBranding(branding); }, [branding]);

  /* ⌘K */
  React.useEffect(() => {
    const h = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setCmdkOpen(o => !o); }
    };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, []);

  /* Supabase auth: theo dõi phiên. CHỈ cho phép email @swo.vn (enforce app-level —
     KHÔNG đặt cấp project vì Supabase dùng chung). Sai domain → signOut + báo lỗi. */
  React.useEffect(() => {
    if (!window.SB_READY) return;
    const ALLOW = '@swo.vn';
    const gate = (s) => {
      if (s && s.user && !(s.user.email || '').toLowerCase().endsWith(ALLOW)) {
        toast(lang === 'vi' ? 'Chỉ cho phép đăng nhập bằng email @swo.vn' : 'Only @swo.vn emails are allowed', 'error');
        sb.auth.signOut();
        return null;
      }
      return s || null;
    };
    sb.auth.getSession().then(({ data }) => { setSession(gate(data.session)); setBooted(true); });
    const { data:sub } = sb.auth.onAuthStateChange((_e, s) => setSession(gate(s)));
    return () => sub.subscription.unsubscribe();
  }, []);

  /* Có phiên → nạp dữ liệu thật (thay mock) + role từ profiles. Không phiên (demo) → giữ mock. */
  React.useEffect(() => {
    let alive = true;
    if (session && window.SB_READY) {
      DB.loadAll().then(d => {
        if (!alive || !d) return;
        setCustomers(d.customers); setContacts(d.contacts);
        setContracts(d.contracts); setExpenses(d.expenses);
        if (d.templates) setTemplates(d.templates);
        if (d.sentEmails) setSentEmails(d.sentEmails);
        if (d.mailSettings) setMailSettings(d.mailSettings);
      });
      DB.myProfile(session.user.id).then(pr => { if (alive && pr) setProfile(pr); });
      DB.listUsers().then(us => { if (alive && us && us.length) { setUsers(us); setAccounts(us); } });
      DB.loadVendors().then(vs => { if (alive && vs) setVendors(vs); });   // null nếu chưa migrate → giữ mock
      DB.loadDeals().then(ds => { if (alive && ds) setDeals(ds); });
      DB.loadTickets().then(ts => { if (alive && ts) setTickets(ts); });   // null nếu chưa migrate → giữ mock
      DB.loadPosts().then(ps => { if (alive && ps) setPosts(ps); });       // null nếu chưa migrate 0033 → giữ rỗng
      DB.loadMoves().then(ms => { if (alive && ms) setMoves(ms); });
      DB.loadIssues().then(is => { if (alive && is) setIssues(is); });
      DB.loadReceipts().then(rs => { if (alive && rs) setReceipts(rs); });
      DB.loadTasks().then(ts => { if (alive && ts) setTasks(ts); });   // null nếu chưa migrate → giữ mock

      DB.loadProducts().then(ps => { if (alive && ps) setProducts(ps); });
      DB.loadQuotes().then(qs => { if (alive && qs) setQuotes(qs); });
      DB.loadCatalogs().then(cat => { if (alive && cat) setCatalogs({ ...buildDefaultCatalogs(), ...cat }); });   // merge → catalog chưa seed (vd units) vẫn có mặc định
      DB.loadSettings().then(s => {
        if (!alive || !s) return;
        setAppSettings(s);
        // "Quản trị hệ thống" (branding) lưu chung trên DB (key 'branding') → ưu tiên hơn localStorage; áp ngay khi tải xong.
        if (s.branding) { try { const b = JSON.parse(s.branding); if (b && typeof b === 'object') setBranding(Object.assign({}, DEFAULT_BRANDING, b)); } catch (e) {} }
      });
    } else if (!session) {
      setProfile(null);
    }
    return () => { alive = false; };
  }, [session]);

  /* optimistic persist: ghi nền khi đã đăng nhập; lỗi → toast + rollback. Demo → chỉ in-memory. */
  const persist = (fn, rollback) => {
    if (!(window.SB_READY && session)) return;
    Promise.resolve().then(fn).catch(e => {
      console.warn('[persist]', e && e.message);
      toast(lang === 'vi' ? 'Lưu lên máy chủ thất bại' : 'Failed to save to server', 'error');
      if (rollback) rollback();
    });
  };

  /* expose query stores synchronously (ready before children first render) */
  window.CustomerStore = {
    all:() => customers,
    byCode:(code) => customers.find(c => c.code === code),
    byId:(code) => customers.find(c => c.code === code),
    search:(q) => customers.filter(c => (c.name + c.code + c.shortName).toLowerCase().includes((q || '').toLowerCase())),
    codes:() => customers.map(c => c.code),
  };
  window.ContactStore = {
    all:() => contacts,
    byCode:(code) => contacts.find(c => c.code === code),
    byId:(code) => contacts.find(c => c.code === code),
    byCustomer:(code) => contacts.filter(c => c.customer === code),
    search:(q) => contacts.filter(c => (c.name + c.code + c.email).toLowerCase().includes((q || '').toLowerCase())),
    codes:() => contacts.map(c => c.code),
  };
  window.ContractStore = {
    all:() => contracts,
    byCode:(code) => contracts.find(c => c.code === code),
    byCustomer:(code) => contracts.filter(c => c.customer === code),
    totalValue:() => contracts.reduce((s, c) => s + contractTotals(c).total, 0),
    codes:() => contracts.map(c => c.code),
  };
  window.QuoteStore = {
    all:() => quotes,
    byCode:(code) => quotes.find(q => q.code === code),
    byCustomer:(code) => quotes.filter(q => q.customer === code),
    totalValue:() => quotes.reduce((s, q) => s + quoteTotals(q).total, 0),
    codes:() => quotes.map(q => q.code),
  };
  window.ProductStore = {
    all:() => products,
    byCode:(code) => products.find(p => p.code === code),
    price:(code) => { const pr = products.find(p => p.code === code); return pr ? productPrice(pr) : 0; },
    codes:() => products.map(p => p.code),
  };
  window.VendorStore = {
    all:() => vendors,
    byCode:(code) => vendors.find(v => v.code === code),
    search:(q) => vendors.filter(v => (v.name + v.code + v.shortName).toLowerCase().includes((q || '').toLowerCase())),
    codes:() => vendors.map(v => v.code),
  };
  window.DealStore = {
    all:() => deals,
    byCode:(code) => deals.find(d => d.code === code),
    codes:() => deals.map(d => d.code),
  };
  window.CatalogData = catalogs;   // cho label helper (catLabel/catName/prCatLabel...) đọc danh mục live
  window.AppSettings = appSettings; // cho genQuoteCode... đọc cấu hình live

  // role áp dụng ĐÚNG theo tài khoản (profile.role); chưa nạp profile → 'staff' (quyền tối thiểu).
  const role = profile ? (profile.role || 'staff') : 'staff';
  const apps = APPS.filter(a => !hidden.includes(a.id) && (!a.adminOnly || role === 'admin'));
  const me = profile ? { id:profile.id, name:profile.name, email:profile.email, dept:profile.dept || '', title:profile.title || (USERS[role] && USERS[role].title) || '', role, meta:profile.meta || {} } : USERS[role];
  // Được xem/sửa giá vốn? (mirror RLS product_costs) → chỉ khi đủ quyền mới ghi cost sang DB.
  const canEditCost = canViewCost({ role, appSettings, me });

  // Nạp xong hồ sơ → lấy bố cục widget đã lưu của user (đa máy) đè lên cache localStorage.
  React.useEffect(() => {
    const dl = profile && profile.meta && profile.meta.dashLayout;
    if (dl && typeof dl === 'object') { setDashLayout(dl); try { localStorage.setItem('swo-portal-dash', JSON.stringify(dl)); } catch (e) {} }
  }, [profile]);

  const navigate = (r) => { setRoute(r); setDrawerOpen(false); document.querySelector('.main') && document.querySelector('.main').scrollTo(0, 0); };
  const openApp = (app) => {
    if (app.module) navigate({ name:'module', module:app.module });
    else if (app.id === 'audit') navigate({ name:'admin', tab:'log' });   // Nhật ký = tab log của Bảng điều khiển
    else toast(t('apps.noModule'), 'info');
  };
  const quick = (mod, data) => {
    if (mod === 'mail') { setPendingData(d => ({ ...d, mail:data || {} })); navigate({ name:'module', module:'mail', sub:'compose' }); }
    else if (mod === 'customers') setNewCustomerOpen(true);
    else if (mod === 'contacts') { setFormPrefill(data || null); setNewContactOpen(true); }
    else if (mod === 'contracts') { setFormPrefill(data || null); setNewContractOpen(true); }
    else if (mod === 'quotes') { setQuotePrefill(data || null); setNewQuoteOpen(true); }
    else if (mod === 'products') setNewProductOpen(true);
    else if (mod === 'vendors') setNewVendorOpen(true);
    else if (mod === 'deals') setNewDealOpen(true);
    else if (mod === 'users') setInviteUserOpen(true);
    else if (mod === 'tasks') { setTaskEditing(data || null); setNewTaskOpen(true); }
    else if (mod === 'support') { setFormPrefill(data || null); setNewTicketOpen(true); }
    else if (mod === 'inventory') setNewReceiptOpen(true);
    else if (mod === 'website') { setPostEditing(data || null); setNewPostOpen(true); }
  };
  const togglePin = (id) => setPinned(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);
  const toggleHidden = (id) => setHidden(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);

  const portal = {
    route, navigate, role, me, users, apps, lang, t, tt, toast,
    pinned, togglePin, hidden, toggleHidden,
    customers, contacts, sentEmails,
    addCustomer:(c) => { setCustomers(s => [c, ...s]); persist(() => DB.insertCustomer(c), () => setCustomers(s => s.filter(x => x !== c))); },
    updateCustomer:(code, next) => { const prev = customers.find(c => c.code === code); setCustomers(s => s.map(c => c.code === code ? next : c)); persist(() => DB.updateCustomer(next), () => prev && setCustomers(s => s.map(c => c.code === code ? prev : c))); },
    deleteCustomer:(code) => { const prev = customers; setCustomers(s => s.filter(c => c.code !== code)); persist(() => DB.deleteCustomer(code), () => setCustomers(prev)); },
    addContact:(c) => { setContacts(s => [c, ...s]); persist(() => DB.insertContact(c), () => setContacts(s => s.filter(x => x !== c))); },
    updateContact:(code, next) => { const prev = contacts.find(c => c.code === code); setContacts(s => s.map(c => c.code === code ? next : c)); persist(() => DB.updateContact(next), () => prev && setContacts(s => s.map(c => c.code === code ? prev : c))); },
    deleteContact:(code) => { const prev = contacts; setContacts(s => s.filter(c => c.code !== code)); persist(() => DB.deleteContact(code), () => setContacts(prev)); },
    addSentEmail:(e) => { setSentEmails(s => [e, ...s]); persist(() => DB.insertSentEmail(e), () => setSentEmails(s => s.filter(x => x !== e))); },
    templates, mailSettings,
    updateTemplate:(tpl) => {
      const prev = templates.find(x => x.id === tpl.id);
      setTemplates(s => s.map(x => x.id === tpl.id ? tpl : x));
      persist(() => DB.upsertTemplate(tpl), () => prev && setTemplates(s => s.map(x => x.id === tpl.id ? prev : x)));
    },
    addTemplate:(tpl) => {
      const t = { ...tpl, id: tpl.id || ('t' + Date.now()) };
      setTemplates(s => [...s, t]);
      persist(() => DB.upsertTemplate(t), () => setTemplates(s => s.filter(x => x.id !== t.id)));
      return t;
    },
    deleteTemplate:(id) => {
      const prev = templates;
      setTemplates(s => s.filter(x => x.id !== id));
      persist(() => DB.deleteTemplate(id), () => setTemplates(prev));
    },
    saveMailSettings:(patch) => {
      const prev = mailSettings, next = { ...mailSettings, ...patch };
      setMailSettings(next);
      persist(() => DB.saveSettings(next), () => setMailSettings(prev));
    },
    contracts, expenses,
    saveContract:(c) => { setContracts(s => [c, ...s]); persist(() => DB.upsertContract(c), () => setContracts(s => s.filter(x => x !== c))); },
    patchContract:(code, next) => {
      const prev = contracts.find(c => c.code === code);
      setContracts(s => s.map(c => c.code === code ? next : c));
      persist(() => DB.upsertContract(next), () => prev && setContracts(s => s.map(c => c.code === code ? prev : c)));
    },
    deleteContract:(code) => {
      const prev = contracts;
      setContracts(s => s.filter(c => c.code !== code));
      persist(() => DB.deleteContract(code), () => setContracts(prev));
      // HĐ sinh từ báo giá: trả báo giá nguồn về "Khách đồng ý" (accepted) — nếu không,
      // báo giá kẹt vĩnh viễn ở "Đã chuyển HĐ" trỏ tới HĐ đã xóa (không sửa/chuyển lại được).
      const src = quotes.find(q => q.convertedTo === code);
      if (src) {
        const prevQ = src;
        const next = qWithHist({ ...src, status:'accepted', convertedTo:null },
          qEvt('status', 'Hợp đồng ' + code + ' đã bị xóa — báo giá quay về bước đồng ý', ''));
        setQuotes(s => s.map(q => q.code === src.code ? next : q));
        persist(() => DB.upsertQuote(next), () => setQuotes(s => s.map(q => q.code === src.code ? prevQ : q)));
      }
    },
    addExpense:(e) => { setExpenses(s => [e, ...s]); persist(() => DB.insertExpense(e), () => setExpenses(s => s.filter(x => x !== e))); },
    updateExpense:(id, next) => { const prev = expenses.find(e => e.id === id); setExpenses(s => s.map(e => e.id === id ? next : e)); persist(() => DB.updateExpense(next), () => prev && setExpenses(s => s.map(e => e.id === id ? prev : e))); },
    deleteExpense:(id) => { const prev = expenses; setExpenses(s => s.filter(e => e.id !== id)); persist(() => DB.deleteExpense(id), () => setExpenses(prev)); },
    // ----- Công việc (Tasks) — optimistic + persist DB (task_tasks) -----
    tasks,
    addTask:(tk) => { setTasks(s => [tk, ...s]); persist(() => DB.upsertTask(tk), () => setTasks(s => s.filter(x => x !== tk))); },
    updateTask:(id, patch) => { const prev = tasks.find(t => t.id === id); const next = { ...prev, ...patch }; setTasks(s => s.map(t => t.id === id ? next : t)); persist(() => DB.upsertTask(next), () => prev && setTasks(s => s.map(t => t.id === id ? prev : t))); },
    deleteTask:(id) => { const prev = tasks; setTasks(s => s.filter(t => t.id !== id)); persist(() => DB.deleteTask(id), () => setTasks(prev)); },
    // ----- Thông báo (mock-only, in-memory) -----
    notifs,
    markNotifRead:(id) => setNotifRead(s => s.includes(id) ? s : [...s, id]),
    markAllNotifsRead:() => setNotifRead(NOTIFS.map(n => n.id)),
    // ----- Báo giá (DB khi đăng nhập + đã migrate 0015; mock khi chưa) -----
    quotes,
    saveQuote:(q) => { setQuotes(s => [q, ...s]); persist(() => DB.upsertQuote(q), () => setQuotes(s => s.filter(x => x !== q))); },
    patchQuote:(code, next) => { const prev = quotes.find(q => q.code === code); setQuotes(s => s.map(q => q.code === code ? next : q)); persist(() => DB.upsertQuote(next), () => prev && setQuotes(s => s.map(q => q.code === code ? prev : q))); },
    editQuote:(q) => { setQuoteEditing(q); setNewQuoteOpen(true); },   // mở form sửa báo giá
    deleteQuote:(code) => { const prev = quotes; setQuotes(s => s.filter(q => q.code !== code)); persist(() => DB.deleteQuote(code), () => setQuotes(prev)); },   // chỉ admin|manager gọi (gate ở UI)
    startQuoteFrom:(prod) => { setQuotePrefill({ items:[{ name:prod.name, unit:prod.unit, qty:1, price:productPrice(prod), vat:prod.vat }] }); setNewQuoteOpen(true); },
    reconcileProductsFromQuote:(items, code) => {
      let next = products.slice(); const changed = [];
      (items || []).forEach(it => {
        const name = (it.name || '').trim(); if (!name) return;
        const idx = next.findIndex(pr => sdeburr(pr.name) === sdeburr(name) || productMatches(pr, name));
        if (idx >= 0){
          const pr = next[idx]; const latest = productPrice(pr);
          if (it.price && it.price !== latest) { next[idx] = { ...pr, priceHistory:[...pr.priceHistory, { date:TODAY, price:it.price, note:'Theo báo giá ' + code }] }; changed.push(next[idx]); }
        } else {
          const ncode = genProductCode(next);
          const np = { code:ncode, name, category:'khac', unit:it.unit || 'gói', vat:it.vat || 8, status:'active', desc:'Tự thêm từ báo giá ' + code, match:[name], priceHistory:[{ date:TODAY, price:it.price || 0, note:'Khởi tạo từ báo giá ' + code }] };
          next = [np, ...next]; changed.push(np);
        }
      });
      setProducts(next);
      changed.forEach(pr => persist(() => DB.upsertProduct(pr)));
    },
    markQuoteSent:(code, to) => {
      const prev = quotes.find(q => q.code === code); if (!prev) return;
      const recips = Array.isArray(to) ? to.join(', ') : (to || '');
      const evt = qEvt('send', (lang === 'vi' ? 'Gửi báo giá' : 'Quote sent') + (recips ? ' → ' + recips : ''), me.name);
      const next = { ...prev, status:(prev.status === 'accepted' || prev.status === 'converted') ? prev.status : 'sent', sentDate:TODAY, sentTo:to, history:[...(prev.history || []), evt] };
      setQuotes(s => s.map(q => q.code === code ? next : q));
      persist(() => DB.upsertQuote(next), () => setQuotes(s => s.map(q => q.code === code ? prev : q)));
    },
    // Bảo đảm báo giá có confirm_token (mã bí mật cho link xác nhận online). Sinh + lưu nền nếu chưa có; trả token.
    ensureQuoteToken:(code) => {
      const q = quotes.find(x => x.code === code); if (!q) return '';
      if (q.confirmToken) return q.confirmToken;
      const a = new Uint8Array(16); (window.crypto || crypto).getRandomValues(a);
      const token = Array.from(a, b => b.toString(16).padStart(2, '0')).join('');
      const next = { ...q, confirmToken:token };
      setQuotes(s => s.map(x => x.code === code ? next : x));
      persist(() => DB.upsertQuote(next), () => setQuotes(s => s.map(x => x.code === code ? q : x)));
      return token;
    },
    sendQuote:(q) => setSendQuoteTarget(q),
    // ----- Sản phẩm & Dịch vụ (DB khi đăng nhập + đã migrate 0014; mock khi chưa) -----
    products,
    addProduct:(pr) => { setProducts(s => [pr, ...s]); persist(() => DB.upsertProduct(pr, canEditCost), () => setProducts(s => s.filter(x => x !== pr))); },
    updateProduct:(code, patch) => { const prev = products.find(pr => pr.code === code); if (!prev) return; const next = { ...prev, ...patch }; setProducts(s => s.map(pr => pr.code === code ? next : pr)); persist(() => DB.upsertProduct(next, canEditCost), () => setProducts(s => s.map(pr => pr.code === code ? prev : pr))); },
    deleteProduct:(code) => { const prev = products; setProducts(s => s.filter(pr => pr.code !== code)); persist(() => DB.deleteProduct(code), () => setProducts(prev)); },
    // ----- Nhà cung cấp (DB khi đăng nhập + đã migrate 0011; mock khi chưa) -----
    vendors,
    addVendor:(v) => { setVendors(s => [v, ...s]); persist(() => DB.insertVendor(v), () => setVendors(s => s.filter(x => x !== v))); },
    updateVendor:(code, next) => { const prev = vendors.find(v => v.code === code); setVendors(s => s.map(v => v.code === code ? next : v)); persist(() => DB.updateVendor(next), () => prev && setVendors(s => s.map(v => v.code === code ? prev : v))); },
    deleteVendor:(code) => { const prev = vendors; setVendors(s => s.filter(v => v.code !== code)); persist(() => DB.deleteVendor(code), () => setVendors(prev)); },
    // ----- Thương vụ (DB khi đăng nhập + đã migrate 0012; mock khi chưa) -----
    deals,
    addDeal:(d) => { setDeals(s => [d, ...s]); persist(() => DB.upsertDeal(d), () => setDeals(s => s.filter(x => x !== d))); },
    updateDeal:(code, next) => { const prev = deals.find(d => d.code === code); setDeals(s => s.map(d => d.code === code ? next : d)); persist(() => DB.upsertDeal(next), () => prev && setDeals(s => s.map(d => d.code === code ? prev : d))); },
    deleteDeal:(code) => { const prev = deals; setDeals(s => s.filter(d => d.code !== code)); persist(() => DB.deleteDeal(code), () => setDeals(prev)); },
    // ----- Hỗ trợ sau bán (DB khi đăng nhập + migrate support_tickets; mock khi chưa) -----
    tickets,
    addTicket:(tk) => { setTickets(s => [tk, ...s]); persist(() => DB.upsertTicket(tk), () => setTickets(s => s.filter(x => x !== tk))); },
    // ----- Quản trị website (tin tức www.swo.vn) — optimistic + DB nền -----
    posts,
    openPostEdit:(w) => { setPostEditing(w || null); setNewPostOpen(true); },
    addPost:(w) => { setPosts(s => [w, ...s]); persist(() => DB.upsertPost(w), () => setPosts(s => s.filter(x => x !== w))); },
    patchPost:(id, next) => { const prev = posts.find(x => x.id === id); setPosts(s => s.map(x => x.id === id ? next : x)); persist(() => DB.upsertPost(next), () => prev && setPosts(s => s.map(x => x.id === id ? prev : x))); },
    deletePost:(id) => { const prev = posts; setPosts(s => s.filter(x => x.id !== id)); persist(() => DB.deletePost(id), () => setPosts(prev)); },
    patchTicket:(code, next) => { const prev = tickets.find(t => t.code === code); setTickets(s => s.map(t => t.code === code ? next : t)); persist(() => DB.upsertTicket(next), () => prev && setTickets(s => s.map(t => t.code === code ? prev : t))); },
    // ----- Kho (Inventory) — tồn tính từ moves; nhập kho/điều chỉnh ghi moves -----
    moves, issues, receipts,
    openStockAdjust:(itemCode) => setStockAdjustItem(itemCode),
    openNewReceipt:(preset) => { setReceiptPreset(preset || null); setNewReceiptOpen(true); },
    addReceipt:(rec) => {
      const ms = (rec.lines || []).filter(l => +l.qty > 0).map((l, i) => ({ id:'M' + Date.now() + '-' + i, item:l.item, type:'in', qty:+l.qty, date:rec.date, refType:'receipt', refCode:rec.code, note:rec.note || ('Nhập kho ' + rec.code), by:rec.by }));
      setReceipts(s => [rec, ...s]); setMoves(s => [...ms, ...s]);
      persist(() => Promise.all([DB.upsertReceipt(rec), DB.insertMoves(ms)]), () => { setReceipts(s => s.filter(x => x !== rec)); setMoves(s => s.filter(m => !ms.includes(m))); });
    },
    addAdjust:(item, qty, note) => {
      const m = { id:'M' + Date.now(), item, type:'adjust', qty:+qty, date:TODAY, refType:'manual', refCode:'', note:note || 'Điều chỉnh', by:me.name };
      setMoves(s => [m, ...s]); persist(() => DB.insertMove(m), () => setMoves(s => s.filter(x => x !== m)));
    },
    openNewIssue:(contract) => { setIssuePreset(contract || null); setNewIssueOpen(true); },
    addIssue:(iss) => { setIssues(s => [iss, ...s]); persist(() => DB.upsertIssue(iss), () => setIssues(s => s.filter(x => x !== iss))); },
    patchIssue:(code, next) => { const prev = issues.find(i => i.code === code); setIssues(s => s.map(i => i.code === code ? next : i)); persist(() => DB.upsertIssue(next), () => prev && setIssues(s => s.map(i => i.code === code ? prev : i))); },
    commitIssue:(iss) => {
      const ms = (iss.lines || []).filter(l => +l.qty > 0).map((l, i) => ({ id:'M' + Date.now() + '-' + i, item:l.item, type:'out', qty:+l.qty, date:TODAY, refType:'issue', refCode:iss.code, note:'Xuất kho ' + iss.code + (iss.contract ? ' · HĐ ' + iss.contract : ''), by:me.name }));
      const next = { ...iss, status:'issued', issuedDate:TODAY };
      setIssues(s => s.map(i => i.code === iss.code ? next : i)); setMoves(s => [...ms, ...s]);
      persist(() => Promise.all([DB.upsertIssue(next), DB.insertMoves(ms)]), () => { setIssues(s => s.map(i => i.code === iss.code ? iss : i)); setMoves(s => s.filter(m => !ms.includes(m))); });
    },
    // ----- Danh mục Settings (DB khi đăng nhập + đã migrate 0016; mặc định hằng số khi chưa) -----
    catalogs,
    addCatalogItem:(cat, item) => {
      const isStr = CATALOG_STRING.includes(cat);
      setCatalogs(c => ({ ...c, [cat]:[...(c[cat] || []), item] }));
      const row = { catalog:cat, item_id:isStr ? item : item.id, label_vi:isStr ? item : item.vi, label_en:isStr ? item : (item.en || item.vi), sort:(catalogs[cat] || []).length };
      persist(() => DB.insertCatalogItem(row), () => setCatalogs(c => ({ ...c, [cat]:(c[cat] || []).filter(x => x !== item) })));
    },
    updateCatalogItem:(cat, id, patch) => {   // patch={vi,en} — chỉ id-catalog
      const prev = catalogs[cat];
      setCatalogs(c => ({ ...c, [cat]:(c[cat] || []).map(x => catItemId(cat, x) === id ? { ...x, ...patch } : x) }));
      persist(() => DB.updateCatalogItem(cat, id, patch), () => setCatalogs(c => ({ ...c, [cat]:prev })));
    },
    deleteCatalogItem:(cat, id) => {
      const prev = catalogs[cat];
      setCatalogs(c => ({ ...c, [cat]:(c[cat] || []).filter(x => catItemId(cat, x) !== id) }));
      persist(() => DB.deleteCatalogItem(cat, id), () => setCatalogs(c => ({ ...c, [cat]:prev })));
    },
    // ----- Cấu hình app (key-value, DB + 0017) -----
    appSettings,
    setSetting:(key, value) => {
      const prev = appSettings;
      setAppSettings(s => ({ ...s, [key]:value }));
      persist(() => DB.saveSetting(key, value), () => setAppSettings(prev));
    },
    // ----- Người dùng & phân quyền (mock-only) — dùng namespace accounts để KHÔNG đè p.users -----
    accounts, roles:accountRoles,
    addAccount:(u) => setAccounts(s => [u, ...s]),
    patchAccount:(id, patch) => {
      const prev = accounts.find(u => u.id === id);
      setAccounts(s => s.map(u => u.id === id ? { ...u, ...patch } : u));
      // Đổi vai trò → ghi lên Supabase để máy user nhận được (cần RLS admin — migration 0007).
      // Hiện ĐÚNG message lỗi (RLS/role) thay vì thông báo chung, để dễ chẩn đoán.
      if (patch.role !== undefined && window.SB_READY && session) {
        DB.updateUserRole(id, patch.role).catch(e => {
          toast((lang === 'vi' ? 'Đổi vai trò thất bại: ' : 'Role update failed: ') + (e.message || e), 'error');
          if (prev) setAccounts(s => s.map(u => u.id === id ? prev : u));
        });
      }
      // Sửa hồ sơ (họ tên / phòng ban / trạng thái) → ghi profiles (cần RLS 0007 + cột 0013).
      const meta = {};
      if (patch.name !== undefined) meta.name = patch.name;
      if (patch.dept !== undefined) meta.dept = patch.dept;
      if (patch.status !== undefined) meta.status = patch.status;
      if (Object.keys(meta).length && window.SB_READY && session) {
        DB.updateProfile(id, meta).catch(e => {
          toast((lang === 'vi' ? 'Lưu hồ sơ thất bại: ' : 'Profile save failed: ') + (e.message || e), 'error');
          if (prev) setAccounts(s => s.map(u => u.id === id ? prev : u));
        });
      }
    },
    // Hồ sơ của CHÍNH user đang đăng nhập (Cài đặt) — cập nhật live `me` + danh sách + Supabase.
    updateMyProfile:(patch) => {
      if (!profile) return;
      const id = profile.id;
      setProfile(pr => pr ? { ...pr, ...patch } : pr);
      setAccounts(s => s.map(u => u.id === id ? { ...u, ...patch } : u));
      const meta = {}; if (patch.name !== undefined) meta.name = patch.name; if (patch.dept !== undefined) meta.dept = patch.dept;
      if (Object.keys(meta).length && window.SB_READY && session) {
        DB.updateProfile(id, meta).catch(e => toast((lang === 'vi' ? 'Lưu hồ sơ thất bại: ' : 'Profile save failed: ') + (e.message || e), 'error'));
      }
    },
    // Lưu chữ ký namecard + hồ sơ (gộp): họ tên + phòng ban ghi vào profiles; chữ ký vào profiles.meta.namecard
    // (DB, đồng bộ đa máy) + cache localStorage.
    saveMyNamecard:(card) => {
      if (window.saveNamecard) saveNamecard(me, card);                 // cache cục bộ (hiển thị tức thì + offline/fallback)
      if (!profile) return;
      const id = profile.id, nextMeta = { ...(profile.meta || {}), namecard:card };
      const name = (card.name || '').trim() || profile.name, dept = (card.dept || '').trim();
      setProfile(pr => pr ? { ...pr, name, dept, meta:nextMeta } : pr);
      setAccounts(s => s.map(u => u.id === id ? { ...u, name, dept } : u));
      if (window.SB_READY && session) {
        DB.updateProfile(id, { name, dept, meta:nextMeta }).catch(e => toast((lang === 'vi' ? 'Lưu hồ sơ/chữ ký thất bại: ' : 'Profile/signature save failed: ') + (e.message || e), 'error'));
      }
    },
    updateRole:(id, patch) => setRoles(s => s.map(r => r.id === id ? { ...r, ...patch } : r)),
    addRole:(r) => setRoles(s => [...s, r]),
    quick, openApp, openCmdk:() => setCmdkOpen(true),
    branding, setBranding,
    // Lưu "Quản trị hệ thống" (branding) lên DB dùng chung — app_settings key 'branding' (chỉ admin ghi, RLS); localStorage = cache hiển thị tức thì + màn đăng nhập.
    saveBranding:(next) => {
      const prevB = branding, prevS = appSettings;
      setBranding(next);
      setAppSettings(a => ({ ...a, branding: JSON.stringify(next) }));
      persist(() => DB.saveSetting('branding', JSON.stringify(next)), () => { setBranding(prevB); setAppSettings(prevS); });
    },
    dashLayout,
    // Lưu bố cục widget theo NGƯỜI DÙNG: cache localStorage tức thì + ghi profiles.meta.dashLayout (đồng bộ đa máy), debounce 700ms tránh spam khi kéo/resize.
    saveDashLayout:(next) => {
      setDashLayout(next);
      try { localStorage.setItem('swo-portal-dash', JSON.stringify(next)); } catch (e) {}
      if (window.SB_READY && session && profile) {
        const id = profile.id, nextMeta = { ...(profile.meta || {}), dashLayout:next };
        if (dashTimer.current) clearTimeout(dashTimer.current);
        dashTimer.current = setTimeout(() => {
          setProfile(pr => pr ? { ...pr, meta:nextMeta } : pr);
          DB.updateProfile(id, { meta:nextMeta }).catch(() => {});
        }, 700);
      }
    },
    density, setDensity, fontsize, setFontsize, pageWidth, setPageWidth, theme, setTheme, dateFmt, setDateFmt, collapsed, setCollapsed, drawerOpen, setDrawerOpen,
    pendingData, clearPending:() => setPendingData(d => ({ ...d, mail:null })),
    logout:() => { if (window.SB_READY) sb.auth.signOut(); navigate({ name:'dashboard' }); },
  };

  // Định dạng ngày hiển thị: ưu tiên lựa chọn của user → mặc định hệ thống (app_settings) → DD/MM/YYYY.
  window.__DATEFMT = dateFmt || appSettings.dateFormat || 'dmy';

  if (!booted) return null;   // chờ kiểm tra phiên xong, tránh nháy màn login khi đã đăng nhập
  if (!authed) return <Login branding={branding} onLogin={() => { if (window.SB_READY) sb.auth.getSession().then(({ data }) => setSession(data.session || null)); }} />;

  const portalPages = {
    dashboard:Dashboard, allapps:AllApps, admin:AdminConsole,
    approvals:ApprovalsPage, notifs:NotifsPage, settings:SettingsPage,
  };

  let body;
  if (route.name === 'module') {
    body = route.module === 'mail' ? <MailModule />
      : route.module === 'customers' ? <CustomerModule />
      : route.module === 'contacts' ? <ContactModule />
      : route.module === 'contracts' ? <ContractModule />
      : route.module === 'quotes' ? <QuoteModule />
      : route.module === 'products' ? <ProductModule />
      : route.module === 'vendors' ? <VendorModule />
      : route.module === 'deals' ? <DealModule />
      : route.module === 'users' ? <UserModule />
      : route.module === 'tasks' ? <TaskModule />
      : route.module === 'support' ? <SupportModule />
      : route.module === 'inventory' ? <InventoryModule />
      : route.module === 'finance' ? <FinanceModule />
      : route.module === 'website' ? <WebsiteModule />
      : <div className="main"><EmptyState text={t('c.empty')} /></div>;
  } else {
    const Page = portalPages[route.name] || Dashboard;
    body = <><PortalSidebar /><main className="main"><Page /></main></>;
  }

  return (
    <PortalContext.Provider value={portal}>
      <div className={'shell' + (collapsed ? ' collapsed' : '') + (drawerOpen ? ' drawer-open' : '')}>
        <Brandbar />
        <Topbar />
        {body}
        <MobileTabBar />
      </div>
      {drawerOpen && <div className="drawer-scrim" onClick={() => setDrawerOpen(false)}></div>}
      {route.name === 'module' && <PortalFAB />}
      {cmdkOpen && <CommandPalette onClose={() => setCmdkOpen(false)} />}
      {newCustomerOpen && <NewCustomerModal onClose={() => setNewCustomerOpen(false)} />}
      {newContactOpen && <NewContactModal prefill={formPrefill} onClose={() => { setNewContactOpen(false); setFormPrefill(null); }} />}
      {newContractOpen && <NewContractModal prefill={formPrefill} onClose={() => { setNewContractOpen(false); setFormPrefill(null); }} />}
      {newQuoteOpen && <NewQuoteModal prefill={quotePrefill} editing={quoteEditing} onClose={() => { setNewQuoteOpen(false); setQuotePrefill(null); setQuoteEditing(null); }} />}
      {newProductOpen && <NewProductModal onClose={() => setNewProductOpen(false)} />}
      {newVendorOpen && <NewVendorModal onClose={() => setNewVendorOpen(false)} />}
      {newDealOpen && <NewDealModal onClose={() => setNewDealOpen(false)} />}
      {sendQuoteTarget && <SendQuoteModal quote={sendQuoteTarget} onClose={() => setSendQuoteTarget(null)} />}
      {inviteUserOpen && <InviteUserModal onClose={() => setInviteUserOpen(false)} />}
      {newTaskOpen && <NewTaskModal editing={taskEditing} onClose={() => { setNewTaskOpen(false); setTaskEditing(null); }} />}
      {newTicketOpen && <NewTicketModal presetContract={formPrefill && formPrefill.contract} presetCustomer={formPrefill && formPrefill.customer} onClose={() => { setNewTicketOpen(false); setFormPrefill(null); }} />}
      {newPostOpen && <PostEditModal editing={postEditing} onClose={() => { setNewPostOpen(false); setPostEditing(null); }} />}
      {newReceiptOpen && <NewReceiptModal preset={receiptPreset} onClose={() => { setNewReceiptOpen(false); setReceiptPreset(null); }} />}
      {stockAdjustItem && <StockAdjustModal itemCode={stockAdjustItem} onClose={() => setStockAdjustItem(null)} />}
      {newIssueOpen && <NewIssueModal presetContract={issuePreset} onClose={() => { setNewIssueOpen(false); setIssuePreset(null); }} />}
    </PortalContext.Provider>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
