// ── Nav registry — the ONE list the icon rail renders from. Adding a view
//    means adding a row here; nothing else defines the app's navigation.
//    `badge` names a key in railBadges (computed per render) — badges show
//    things needing action, never raw totals.
//    `pack` gates the item on the tenant's enabled packs (tenant.packs from
//    GET /api/tenant, checked via packEnabled) — no pack key = core, always on.
//    `module` gates a CORE item the tenant can have switched off (tenant.modules,
//    checked via moduleOn) — SPEC-lightweight tenants hide Billing + Inventory.
//    Both resolve through itemEnabled().
const NAV_ITEMS = [
    { id: 'dashboard', icon: 'fa-tachometer-alt',      label: 'Dashboard' },
    { id: 'tasks',     icon: 'fa-tasks',               label: 'Tasks',  badge: 'tasks' },
    { id: 'accounts',  icon: 'fa-building',            label: 'Accounts'  },
    // Sales & billing is core (folded back 2026-08-19 — no `pack` key), but
    // a tenant can have the MODULE switched off (SPEC lightweight, 2026-08-21).
    { id: 'leads',     icon: 'fa-inbox',               label: 'Leads',  badge: 'leads' },
    { id: 'billing',   icon: 'fa-file-invoice-dollar', label: 'Billing',   module: 'sales_billing' },
    { id: 'inventory', icon: 'fa-boxes',               label: 'Inventory', module: 'sales_billing' },
    // Pack-registered nav (PACK_UI populated before this module evaluates —
    // pack scripts are injected ahead of MainApp). Spread at module eval so
    // the hooks-in-a-loop below still iterates a stable constant.
    ...PACK_UI.navItems,
];

const MainApp = ({ currentUser, onLogout, appVersion }) => {
    const [showGlobalSearch, setShowGlobalSearch] = React.useState(false);
    const [searchTerm, setSearchTerm]             = React.useState('');
    const [selectedAccount, setSelectedAccount]   = React.useState(null);
    const [accountTabHint, setAccountTabHint]     = React.useState(null); // { tab } — which detail tab to land on
    const [data, setData]                 = React.useState({ accounts: [], followUpTasks: [], followups: [], unreadEmails: [], users: [], tags: [] });
    const [loadingData, setLoadingData]   = React.useState(true);
    const [showNewAccount, setShowNewAccount] = React.useState(false);
    // Workspace identity — name/letterhead/branding from GET /api/tenant.
    // Loaded once; Settings edits flow back through handleTenantChange so the
    // nav logo and custom CSS update live without a refresh.
    const [tenant, setTenant]                 = React.useState(null);
    // Land on Settings → Email automatically when returning from the OAuth
    // redirect (/api/email/callback bounces back to /?email=connected|declined|error).
    // Keep the flag's *value* too — the Email section uses it to tell the user
    // whether the connect actually worked (a silent bounce back to "Connect"
    // reads as success-then-amnesia when the server side actually failed).
    const [emailOAuthResult] = React.useState(() => {
        const flag = new URLSearchParams(window.location.search).get('email');
        if (flag) window.history.replaceState({}, '', '/'); // clean the URL so refresh doesn't re-trigger
        return flag; // 'connected' | 'declined' | 'error' | null
    });
    // accounts | dashboard | tasks | leads | billing | inventory | settings
    // Deep links: the URL is parsed once at boot (src/router.js) and seeds the
    // initial state; the sync effect below writes state → URL from then on.
    const initialRoute = React.useMemo(() => parseRoute(), []);
    // Per-user landing view (users.settings.default_view) — Dashboard unless
    // this user picked another nav view in My Preferences. An id that no
    // longer resolves (retired view, stale setting) falls back rather than
    // stranding the UI; Settings is never a landing.
    const landingView = React.useMemo(() => {
        const pick = currentUser.settings?.default_view;
        return pick && pick !== 'settings' && NAV_ITEMS.some(v => v.id === pick) ? pick : 'dashboard';
    }, [currentUser.settings?.default_view]);
    const [currentView, setCurrentView] = React.useState(initialRoute.view || (emailOAuthResult ? 'settings' : landingView));
    // Billing groups the old Quotes/Invoices nav tabs behind one button.
    const [billingTab, setBillingTabState] = React.useState(initialRoute.billingTab === 'invoices' ? 'invoices' : 'quotes');
    // Doc open in the global Billing view (modal). ref = human number (Q-…/INV-…).
    const [billingDoc, setBillingDoc] = React.useState(initialRoute.view === 'billing' ? initialRoute.doc : null);
    const setBillingTab = (tab) => { setBillingTabState(tab); setBillingDoc(null); };
    // The docked quote/invoice pane. Lives here (not in AccountDetailView)
    // so deep links can set account + doc together at boot.
    const [openDoc, setOpenDoc] = React.useState(null); // { kind: 'quote'|'invoice', id } | null — id is the human number when we have it

    // A doc pane belongs to the account it was opened from — switching (or
    // clearing) the account closes it. The ref suppresses exactly one clear:
    // a deep link / popstate that sets account + doc together.
    const keepDocOnNextAccountChange = React.useRef(false);
    React.useEffect(() => {
        if (keepDocOnNextAccountChange.current) { keepDocOnNextAccountChange.current = false; return; }
        setOpenDoc(null);
    }, [selectedAccount?.id]);

    // Pack gating safety net: a deep link (or stale bookmark) can land on a
    // view whose pack this tenant doesn't have — once the tenant payload
    // arrives, bounce to Tasks rather than render a view whose API refuses.
    React.useEffect(() => {
        const item = NAV_ITEMS.find(v => v.id === currentView);
        if (item && !itemEnabled(tenant, item)) {
            // Bounce to the user's landing view — unless that's itself a
            // pack view this tenant lost, in which case Dashboard (core,
            // always on) breaks the loop.
            const land = NAV_ITEMS.find(v => v.id === landingView);
            setCurrentView(land && itemEnabled(tenant, land) ? landingView : 'dashboard');
        }
    }, [tenant, currentView]);

    // ── Deep links: boot + back/forward ───────────────────────────
    // Apply the account/doc part of a route (view/tab are plain setters).
    const applyAccountRoute = (route) => {
        if (route.view === 'accounts' && route.accountRef) {
            api.getAccount(route.accountRef).then(full => {
                keepDocOnNextAccountChange.current = true;
                setSelectedAccount(full);
                setOpenDoc(route.doc ? { kind: route.doc.kind, id: route.doc.ref } : null);
            }).catch(err => console.error('Deep-linked account load failed:', err));
        } else if (route.view === 'accounts') {
            setSelectedAccount(null);
        }
    };
    React.useEffect(() => { applyAccountRoute(initialRoute); }, []);
    // Leaving the Accounts view drops the open account (owner's call
    // 2026-08-23): coming back via the rail lands on the list, not on
    // whatever record was open last. Deep links and search-to-account
    // set the account and the view in the same batch, so they're untouched;
    // back/forward re-resolves the account from the URL anyway.
    React.useEffect(() => {
        if (currentView !== 'accounts' && selectedAccount) setSelectedAccount(null);
    }, [currentView]);
    React.useEffect(() => {
        const onPop = () => {
            const r = parseRoute();
            setCurrentView(r.view || landingView);
            setBillingTabState(r.billingTab === 'invoices' ? 'invoices' : 'quotes');
            setBillingDoc(r.view === 'billing' ? r.doc : null);
            applyAccountRoute(r);
        };
        window.addEventListener('popstate', onPop);
        return () => window.removeEventListener('popstate', onPop);
    }, []);

    // ── URL sync: state → address bar ─────────────────────────────
    // First run replaces (boot must not grow the back stack); later changes
    // push, so browser Back walks through views/accounts/docs naturally.
    const firstUrlSync = React.useRef(true);
    React.useEffect(() => {
        if (initialRoute.focus) return; // popout window: URL is fixed to its doc
        syncRoute({
            view: currentView,
            billingTab,
            accountRef: currentView === 'accounts' && selectedAccount
                ? (selectedAccount.customer_number || String(selectedAccount.id)) : null,
            doc: currentView === 'accounts'
                ? (openDoc ? { kind: openDoc.kind, ref: String(openDoc.id) } : null)
                : (currentView === 'billing' ? billingDoc : null),
        }, firstUrlSync.current);
        firstUrlSync.current = false;
    }, [currentView, billingTab, selectedAccount?.id, openDoc, billingDoc]);
    const [myAccountsOnly, setMyAccountsOnly] = React.useState(false);
    // CSV import (admin): two hidden file inputs share one result line.
    const importAccountsRef = React.useRef(null);
    const importContactsRef = React.useRef(null);
    const [importMsg, setImportMsg] = React.useState('');
    const [importPick, setImportPick] = React.useState(false); // Import button → chip pair (which kind?)
    // Deleted-accounts review surface (0048): privileged-only toggle — the
    // server refuses the param for reps, so this is display state, not the rule.
    const [showDeleted, setShowDeleted] = React.useState(false);
    const [tagFilter, setTagFilter]           = React.useState([]); // selected tag ids — OR-match (account shows if it has ANY selected tag)
    const [isFullscreen, setIsFullscreen]     = React.useState(Boolean(document.fullscreenElement));

    // ── Theme: the account's saved theme wins over the local cache ───
    // (localStorage is just a pre-paint hint; the DB value is the truth,
    // so signing in on a new machine brings your theme with you.)
    React.useEffect(() => {
        if (currentUser.settings?.theme) applyTheme(currentUser.settings.theme);
    }, []);

    // ── Workspace branding ────────────────────────────────────────
    React.useEffect(() => {
        api.getTenant()
            .then(t => { setTenant(t); applyBranding(t.branding); })
            .catch(err => console.error('Tenant load failed:', err));
    }, []);

    const handleTenantChange = (t) => { setTenant(t); applyBranding(t.branding); };

    // ── Fullscreen ────────────────────────────────────────────────
    // Track via the browser event, not our own flag — Esc exits fullscreen
    // without ever touching our button, and the icon must follow reality.
    React.useEffect(() => {
        const handler = () => setIsFullscreen(Boolean(document.fullscreenElement));
        document.addEventListener('fullscreenchange', handler);
        return () => document.removeEventListener('fullscreenchange', handler);
    }, []);

    const toggleFullscreen = () => {
        if (document.fullscreenElement) document.exitFullscreen();
        else document.documentElement.requestFullscreen().catch(err => console.error('Fullscreen refused:', err));
    };

    React.useEffect(() => {
        const handler = (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); setShowGlobalSearch(s => !s); } };
        document.addEventListener('keydown', handler);
        return () => document.removeEventListener('keydown', handler);
    }, []);

    // ── Keyboard shortcuts (src/hooks/shortcuts.js) ───────────────
    // Popout doc windows (?focus=1) get none of these — they're a single
    // document, not the app.
    const [showShortcutHelp, setShowShortcutHelp] = React.useState(false);
    const inApp = () => !initialRoute.focus;
    useShortcut({ key: '/', description: 'Search everything', section: 'General', when: inApp },
        () => setShowGlobalSearch(true), []);
    useShortcut({ key: '?', description: 'Show this shortcut list', section: 'General', when: inApp },
        () => setShowShortcutHelp(true), []);
    // g-then-x nav chords — derived from NAV_ITEMS so navigation shortcuts
    // can't drift from the rail. First letter of the label is the key
    // (all currently unique; a collision would shadow, not break).
    // Hooks-in-a-loop is safe here because NAV_ITEMS is a module constant —
    // same length and order every render.
    NAV_ITEMS.forEach(v => {
        // Pack-gated items keep their hook slot (stable hook count) but the
        // chord goes inert — and invisible in the "?" help — when the pack is off.
        useShortcut({ key: v.label[0].toLowerCase(), chord: 'g', description: `Go to ${v.label}`, section: 'Navigation',
                      when: () => inApp() && itemEnabled(tenant, v) },
            () => { if (v.id === 'accounts') setSelectedAccount(null); setCurrentView(v.id); }, [tenant]);
    });
    useShortcut({ key: 's', chord: 'g', description: 'Go to Settings', section: 'Navigation', when: inApp },
        () => setCurrentView('settings'), []);
    // Accounts landing: new account + arrow selection over the table.
    const onAccountsLanding = () => inApp() && currentView === 'accounts' && !selectedAccount;
    useShortcut({ key: 'n', description: 'New account', section: 'Accounts', when: onAccountsLanding },
        () => setShowNewAccount(true), [currentView, selectedAccount]);
    const [accountSel] = useListNav({
        length: data.accounts.length,
        isActive: onAccountsLanding(),
        section: 'Accounts',
        onOpen: (i) => setSelectedAccount(data.accounts[i]),
    });
    // Esc steps back from an account detail to the list — the overlay guard
    // in shortcuts.js keeps this from firing under an open modal.
    useShortcut({ key: 'Escape', description: 'Back to the account list', section: 'Accounts', hidden: true,
                  when: () => inApp() && currentView === 'accounts' && Boolean(selectedAccount) },
        () => setSelectedAccount(null), [currentView, selectedAccount]);

    const [accountTotal, setAccountTotal] = React.useState(0);

    // Rail badge: leads awaiting review. Refetched on every view change —
    // promote/dismiss happen inside LeadsView, which this component can't
    // see, and one small GET per nav click is cheap enough to stay fresh.
    const [newLeadsCount, setNewLeadsCount] = React.useState(0);
    React.useEffect(() => {
        // Leads are core (sales & billing folded in 2026-08-19) — always poll.
        api.getLeads({ status: 'new' }).then(rows => setNewLeadsCount(rows.length)).catch(() => {});
    }, [currentView, tenant]);

    React.useEffect(() => { loadAll(); }, []);

    // Search-first account list: the server filters and pages; the browser
    // never holds the whole account table. Debounced so typing doesn't fire
    // a request per keystroke.
    const accountParams = () => {
        const params = { paged: 'true', limit: 100 };
        if (myAccountsOnly) params.mine = 'true';
        if (showDeleted) params.deleted = 'true';
        if (searchTerm.trim()) params.search = searchTerm.trim();
        if (tagFilter.length) params.tag_ids = tagFilter.join(',');
        return params;
    };

    // A failed accounts/tasks load must not render as "no accounts" — the
    // landing view is exactly where a rep would believe it (swallowed-error rule).
    const [dataLoadError, setDataLoadError] = React.useState(false);

    const loadAccounts = async () => {
        const { rows, total } = await api.getAccounts(accountParams());
        setData(p => ({ ...p, accounts: rows }));
        setAccountTotal(total);
        setDataLoadError(false);
    };

    // Infinite scroll (2026-08-25, hit on stage at 3,000 accounts): the list
    // used to stop dead at the first page of 100 with a "search to narrow"
    // hint at the TOP. Now a sentinel row at the bottom appends the next page
    // when it scrolls into view. Still search-first — the browser only ever
    // holds what's been scrolled to, never the whole table.
    const loadingMoreRef = React.useRef(false);
    const loadMoreAccounts = async () => {
        if (loadingMoreRef.current) return;
        loadingMoreRef.current = true;
        try {
            const { rows, total } = await api.getAccounts({ ...accountParams(), offset: data.accounts.length });
            setData(p => ({ ...p, accounts: [...p.accounts, ...rows] }));
            setAccountTotal(total);
        } catch (_) { /* next scroll retries; the banner covers hard failures */ }
        finally { loadingMoreRef.current = false; }
    };
    const accountsSentinelRef = React.useRef(null);
    React.useEffect(() => {
        const el = accountsSentinelRef.current;
        if (!el || currentView !== 'accounts') return;
        const io = new IntersectionObserver((entries) => {
            if (entries.some(e => e.isIntersecting) && data.accounts.length < accountTotal) loadMoreAccounts();
        }, { rootMargin: '400px' }); // start fetching before the user actually reaches the end
        io.observe(el);
        return () => io.disconnect();
    }, [currentView, data.accounts.length, accountTotal, myAccountsOnly, showDeleted, searchTerm, tagFilter]);

    React.useEffect(() => {
        if (loadingData) return;
        const t = setTimeout(() => { loadAccounts().catch(() => setDataLoadError(true)); }, 250);
        return () => clearTimeout(t);
    }, [myAccountsOnly, showDeleted, searchTerm, tagFilter]);

    // Refetch on view entry (owner's call 2026-08-10): the self-fetching views
    // (Dashboard/Billing/Inventory/Leads) remount and refetch on every nav
    // click already — Accounts and Tasks are MainApp-fed and went stale in a
    // long-lived tab. One GET per entry, same budget as the leads rail badge.
    // loadingData guard skips the mount firing (loadAll covers first load).
    React.useEffect(() => {
        if (loadingData) return;
        if (currentView === 'accounts') loadAccounts().catch(() => setDataLoadError(true));
        if (currentView === 'tasks') {
            Promise.all([api.getTasks({ completed: false }), api.getFollowups(), api.getUnreadEmails()])
                .then(([tasks, followups, unreadEmails]) => { setData(p => ({ ...p, followUpTasks: tasks, followups, unreadEmails })); setDataLoadError(false); })
                .catch(() => setDataLoadError(true));
        }
    }, [currentView]);

    const loadAll = async () => {
        setLoadingData(true);
        try {
            const isManager = currentUser.role === 'admin' || currentUser.role === 'manager';
            const [accountsPage, tasks, followups, unreadEmails, users, tags] = await Promise.all([
                api.getAccounts(accountParams()),
                api.getTasks({ completed: false }),
                api.getFollowups(),
                api.getUnreadEmails(),
                isManager ? api.getUsers() : Promise.resolve([]),
                api.getTags(),
            ]);
            setData(p => ({ ...p, accounts: accountsPage.rows, followUpTasks: tasks, followups, unreadEmails, users, tags }));
            setAccountTotal(accountsPage.total);
            setDataLoadError(false);
        } catch (err) {
            console.error('Failed to load data:', err);
            if (err.message.includes('token') || err.message.includes('401')) onLogout();
            else setDataLoadError(true);
        } finally {
            setLoadingData(false);
        }
    };

    const handleCreateAccount = async (formData) => {
        const account = await api.createAccount(formData);
        setData(p => ({ ...p, accounts: [...p.accounts, account] }));
        setShowNewAccount(false);
    };

    // signoff is present when the task requires it (TasksView collects it);
    // the server enforces the rule either way.
    const handleCompleteTask = async (taskId, signoff) => {
        await api.completeTask(taskId, signoff);
        setData(p => ({ ...p, followUpTasks: p.followUpTasks.filter(t => t.id !== taskId) }));
    };

    // Log-or-clear completion for a DERIVED follow-up (no task row exists):
    // optionally log the contact as a comm, then clear/advance the account's
    // callback_date — the date is the record, this just edits it.
    const handleCompleteFollowup = async ({ accountId, comm, nextDate, nextNote }) => {
        if (comm) await api.createComm(comm);
        // Note rides with the date (0058): a cleared date clears its note too.
        await api.updateAccount(accountId, { callback_date: nextDate || null, callback_note: nextDate ? (nextNote || null) : null });
        const followups = await api.getFollowups();
        setData(p => ({ ...p, followups }));
    };

    const handleUpdateTask = async (taskId, fd) => {
        await api.updateTask(taskId, fd);
        // Refetch rather than merge: the PATCH returns a bare row without the
        // joined account_name, and the edit may have moved the task.
        const tasks = await api.getTasks({ completed: false });
        setData(p => ({ ...p, followUpTasks: tasks }));
    };

    const handleDeleteTask = async (taskId) => {
        if (!await confirmAction('Delete this task?')) return;
        await api.deleteTask(taskId);
        setData(p => ({ ...p, followUpTasks: p.followUpTasks.filter(t => t.id !== taskId) }));
    };

    const handleMergeAccount = async (sourceId, targetId) => {
        await api.mergeAccounts(sourceId, targetId);
        setSelectedAccount(null);
        await loadAll();
    };

    // Move-as-contact lands you ON the business — the natural next step is
    // checking the new contact, not staring at the vanished personal account.
    const handleConvertToContact = async (sourceId, targetId) => {
        await api.convertToContact(sourceId, targetId);
        await loadAll();
        try { setSelectedAccount(await api.getAccount(targetId)); }
        catch { setSelectedAccount(null); }
    };

    const handleSelectAccountFromSearch = async (accountStub) => {
        setCurrentView('accounts');
        setShowGlobalSearch(false);
        // Optional tab hint (e.g. unread-email rows open straight to Activity).
        // Fresh object per call so the detail view reacts even for the same account.
        setAccountTabHint(accountStub.tab ? { tab: accountStub.tab } : null);
        try {
            const full = await api.getAccount(accountStub.id);
            setSelectedAccount(full);
        } catch(e) { console.error(e); }
    };

    // Filtering happens server-side now (search + tags + mine, paged) —
    // data.accounts IS the filtered page.
    const filteredAccounts = data.accounts;

    // Rail badges — action counts only (owner's call 2026-08-09): tasks =
    // overdue + due today, leads = awaiting review. Never raw totals — a
    // badge that's always lit is noise.
    // Derived follow-ups count too (owner's call 2026-08-10): a due callback
    // is exactly the "action needed" signal the badge exists for.
    // Every unread inbound email counts — an unread message IS the action,
    // same day or three days old (it only leaves the badge by being read).
    const tasksDueCount =
        data.followUpTasks.filter(t => ['overdue', 'today'].includes(getTaskStatusClass(t.due_date))).length
        + data.followups.filter(f => ['overdue', 'today'].includes(getTaskStatusClass(f.callback_date))).length
        + data.unreadEmails.length;
    const railBadges = { tasks: tasksDueCount, leads: newLeadsCount };
    // Catalog style (tenant.catalog): a Menu-style workspace calls its
    // inventory "Menu" — same view, same rows, different name on the door.
    const navLabel = (v) => (v.id === 'inventory' && tenant?.catalog?.style === 'menu') ? 'Menu' : v.label;
    const viewTitle = currentView === 'settings' ? 'Settings'
        : (() => { const v = NAV_ITEMS.find(v => v.id === currentView); return v ? navLabel(v) : ''; })();

    // ── Popout window mode (?focus=1 on a doc URL) ────────────────
    // Just the document, no app chrome — the compact floating window.
    // Placed after every hook so the hooks order never changes between modes.
    if (initialRoute.focus && initialRoute.doc) {
        document.title = `${initialRoute.doc.ref} — ${PRODUCT_TAB_TITLE}`;
        return (
            <div className="focus-shell">
                <DocDetailModal
                    inline
                    allowPopout={false}
                    kind={initialRoute.doc.kind}
                    docId={initialRoute.doc.ref}
                    currentUser={currentUser}
                    onClose={() => window.close()}
                    onChanged={() => {}}
                />
            </div>
        );
    }

    return (
        <div className="crm-container">

            {/* ── Icon rail — the app's only nav (rail UI, 2026-08-09). Renders
                NAV_ITEMS so navigation can't drift from the registry. Real <a>
                links so cmd/middle-click opens a view in its own tab; plain
                click stays SPA (navClick, src/router.js). Settings/fullscreen/
                logout pin to the bottom — the old top-nav icon cluster. ── */}
            <nav className="rail">
                <div className="rail-logo" title={tenant?.name || 'CRM'}>
                    <img src={resolveTenantLogo(tenant?.branding, 'app') || '/assets/logo.svg'} alt={tenant?.name || 'CRM'} />
                </div>
                {NAV_ITEMS.filter(v => itemEnabled(tenant, v)).map(v => {
                    const badge = v.badge ? railBadges[v.badge] : 0;
                    return (
                        <a key={v.id} href={routeUrl({ view: v.id, billingTab })}
                           className={`rail-btn ${currentView === v.id ? 'active' : ''}`}
                           data-tip={navLabel(v)} aria-label={navLabel(v)}
                           onClick={(e) => navClick(e, () => {
                               // Clicking Accounts while inside an account = back to the list.
                               if (v.id === 'accounts') setSelectedAccount(null);
                               setCurrentView(v.id);
                           })}>
                            <i className={`fas ${v.icon}`}></i>
                            {badge > 0 && <span className="rail-badge">{badge > 99 ? '99+' : badge}</span>}
                        </a>
                    );
                })}
            </nav>

            <div className="main-col">

            <div className="topbar">
                <h1 className="topbar-title">{viewTitle}</h1>
                {currentView === 'accounts' && selectedAccount && (
                    <span className="topbar-crumb">/ {selectedAccount.name}</span>
                )}
                <button className="topbar-search" onClick={() => setShowGlobalSearch(true)} title="Search (Ctrl+K / ⌘K)">
                    <i className="fas fa-search"></i>
                    <span className="topbar-search-label">Search everything…</span>
                    <kbd className="topbar-search-kbd">⌘K</kbd>
                </button>
                {tenant?.name && <span className="nav-workspace-name">{tenant.name}</span>}
                {appVersion && <span className="nav-version-pill" title="CRM version">v{appVersion}</span>}
                <div className="nav-user-chip" title={`${currentUser.first_name} ${currentUser.last_name} (${currentUser.role})`}>
                    <span className="nav-avatar">{currentUser.first_name[0]}{currentUser.last_name?.[0] || ''}</span>
                    <span className="nav-user-name">{currentUser.first_name}</span>
                    <span className="nav-role-tag">{currentUser.role}</span>
                </div>
                <button className="topbar-icon-btn" onClick={toggleFullscreen} title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}>
                    <i className={`fas ${isFullscreen ? 'fa-compress' : 'fa-expand'}`}></i>
                </button>
                {/* One settings door for everyone — the role filter inside
                    SettingsView decides what each user sees. */}
                <button className={`topbar-icon-btn ${currentView === 'settings' ? 'active' : ''}`}
                        onClick={() => setCurrentView('settings')} title="Settings">
                    <i className="fas fa-cog"></i>
                </button>
                <button className="topbar-icon-btn" onClick={onLogout} title="Sign out">
                    <i className="fas fa-sign-out-alt"></i>
                </button>
            </div>

            {currentView === 'dashboard' && <DashboardView onSelectAccount={handleSelectAccountFromSearch} tenant={tenant} />}
            {/* No accounts prop on TasksView: data.accounts is the server-paged
                Accounts view page, not a lookup table — tasks carry
                account_name from their own API. */}
            {currentView === 'tasks'     && (
                <TasksView
                    tasks={data.followUpTasks}
                    followups={data.followups}
                    unreadEmails={data.unreadEmails}
                    onCompleteFollowup={handleCompleteFollowup}
                    loadError={dataLoadError}
                    onRetryLoad={loadAll}
                    onSelectAccount={handleSelectAccountFromSearch}
                    onComplete={handleCompleteTask}
                    onDelete={handleDeleteTask}
                    onUpdateTask={handleUpdateTask}
                    onCreateTask={async (fd) => {
                        const task = await api.createTask(fd);
                        setData(p => ({ ...p, followUpTasks: [...p.followUpTasks, task] }));
                    }}
                />
            )}
            {currentView === 'inventory' && <ProductsView  currentUser={currentUser} tenant={tenant} onTenantChange={handleTenantChange} />}
            {PACK_UI.views[currentView] && React.createElement(PACK_UI.views[currentView],
                { currentUser, tenant, onSelectAccount: handleSelectAccountFromSearch })}
            {currentView === 'leads'     && <LeadsView     onSelectAccount={handleSelectAccountFromSearch} />}
            {currentView === 'billing'   && (
                <div className="billing-view">
                    <div className="billing-toggle">
                        <button className={`toggle-btn ${billingTab === 'quotes' ? 'active' : ''}`} onClick={() => setBillingTab('quotes')}>
                            <i className="fas fa-file-alt" style={{ marginRight: '0.25rem' }}></i>Quotes
                        </button>
                        <button className={`toggle-btn ${billingTab === 'invoices' ? 'active' : ''}`} onClick={() => setBillingTab('invoices')}>
                            <i className="fas fa-file-invoice-dollar" style={{ marginRight: '0.25rem' }}></i>Invoices
                        </button>
                    </div>
                    {billingTab === 'quotes'
                        ? <QuotesView   currentUser={currentUser}
                                        initialDetailId={billingDoc?.kind === 'quote' ? billingDoc.ref : null}
                                        onDetailChange={(ref) => setBillingDoc(ref ? { kind: 'quote', ref: String(ref) } : null)} />
                        : <InvoicesView currentUser={currentUser}
                                        initialDetailId={billingDoc?.kind === 'invoice' ? billingDoc.ref : null}
                                        onDetailChange={(ref) => setBillingDoc(ref ? { kind: 'invoice', ref: String(ref) } : null)} />}
                </div>
            )}
            {currentView === 'settings'  && (
                <SettingsView
                    currentUser={currentUser}
                    allTags={data.tags}
                    onTagsChange={() => api.getTags().then(tags => setData(p => ({ ...p, tags })))}
                    tenant={tenant}
                    onTenantChange={handleTenantChange}
                    initialSection={emailOAuthResult ? 'email' : null}
                    emailOAuthResult={emailOAuthResult}
                    landingOptions={NAV_ITEMS.filter(v => v.id !== 'settings' && itemEnabled(tenant, v))
                        .map(v => ({ id: v.id, label: v.label, icon: v.icon }))}
                />
            )}

            {/* ── Accounts: the list IS the view (rail UI) — a full-width table
                landing; picking a row swaps to the full-width detail with Back.
                No side list pane anywhere (owner's call, 2026-08-09). ── */}
            {currentView === 'accounts' && (selectedAccount ? (
                <AccountDetailView
                    account={selectedAccount}
                    tabHint={accountTabHint}
                    currentUser={currentUser}
                    tenant={tenant}
                    openDoc={openDoc}
                    onOpenDoc={setOpenDoc}
                    allTags={data.tags}
                    allUsers={data.users}
                    onBack={() => setSelectedAccount(null)}
                    onOpenAccount={async (id) => {
                        try { setSelectedAccount(await api.getAccount(id)); }
                        catch (e) { console.error(e); }
                    }}
                    onAccountUpdate={(updated) => {
                        setData(p => ({ ...p, accounts: p.accounts.map(a => a.id === updated.id ? updated : a) }));
                        setSelectedAccount(updated);
                    }}
                    onAccountDelete={(id) => {
                        setData(p => ({ ...p, accounts: p.accounts.filter(a => a.id !== id) }));
                        setSelectedAccount(null);
                    }}
                    onMerge={handleMergeAccount}
                    onConvertToContact={handleConvertToContact}
                />
            ) : (
                <div className="accounts-landing">
                    <div className="accounts-toolbar">
                        <input
                            type="text"
                            className="search-box accounts-search"
                            placeholder="Search accounts…"
                            value={searchTerm}
                            onChange={(e) => setSearchTerm(e.target.value)}
                        />
                        {data.tags.map(t => {
                            const active = tagFilter.includes(t.id);
                            return (
                                <button
                                    key={t.id}
                                    className={`tag-pill tag-filter-chip ${active ? 'active' : ''}`}
                                    style={active
                                        ? { background: t.color, color: '#fff', border: `1px solid ${t.color}` }
                                        : { background: t.color + '22', color: t.color, border: `1px solid ${t.color}44` }}
                                    onClick={() => setTagFilter(p => active ? p.filter(id => id !== t.id) : [...p, t.id])}
                                    title={active ? `Stop filtering by ${t.name}` : `Show ${t.name} accounts`}
                                >
                                    {t.name}
                                </button>
                            );
                        })}
                        {tagFilter.length > 0 && (
                            <button className="tag-filter-clear" onClick={() => setTagFilter([])}>clear</button>
                        )}
                        {currentUser.role !== 'rep' && (
                            <div className="my-accounts-toggle accounts-toolbar-seg">
                                <button className={`toggle-btn ${myAccountsOnly ? 'active' : ''}`} onClick={() => setMyAccountsOnly(true)}>
                                    <i className="fas fa-user" style={{ marginRight: '0.25rem' }}></i>My
                                </button>
                                <button className={`toggle-btn ${!myAccountsOnly ? 'active' : ''}`} onClick={() => setMyAccountsOnly(false)}>
                                    <i className="fas fa-users" style={{ marginRight: '0.25rem' }}></i>All
                                </button>
                            </div>
                        )}
                        {currentUser.role !== 'rep' && (
                            <button
                                className={`toggle-btn accounts-toolbar-seg ${showDeleted ? 'active' : ''}`}
                                onClick={() => setShowDeleted(d => !d)}
                                title={showDeleted ? 'Back to live accounts' : 'Review deleted accounts (restorable)'}
                            >
                                <i className="fas fa-trash" style={{ marginRight: '0.25rem' }}></i>Deleted
                            </button>
                        )}
                        {currentUser.role === 'admin' && (() => {
                            // Shared handler: read the file, post it, report the
                            // outcome inline (never alert — no-browser-dialogs rule).
                            const runImport = async (file, apiCall, label, unit = 'line') => {
                                const lineErrs = (errs) => errs.slice(0, 3).map(x => `${unit} ${x.line} (${x.error})`).join('; ')
                                    + (errs.length > 3 ? `; +${errs.length - 3} more` : '');
                                try {
                                    const out = await apiCall(await file.text());
                                    setImportMsg(`${label}: ${out.created} new, ${out.updated} updated` +
                                        (out.accounts_created ? `, ${out.accounts_created} account(s) created` : '') +
                                        (out.errors.length ? `. ${out.errors.length} ${unit} error(s): ${lineErrs(out.errors)}` : ''));
                                    await loadAll();
                                } catch (err) {
                                    const errs = err.body?.errors || [];
                                    setImportMsg(`${label} failed: ${err.message}` + (errs.length ? ` — ${lineErrs(errs)}` : ''));
                                }
                            };
                            const onPick = (apiCall, label) => async (e) => {
                                const f = e.target.files[0]; e.target.value = '';
                                if (f) await runImport(f, apiCall, label);
                            };
                            // Contacts accept CSV or vCard (.vcf — what phones / Outlook /
                            // Google Contacts export); the file name picks the door.
                            const onPickContacts = async (e) => {
                                const f = e.target.files[0]; e.target.value = '';
                                if (!f) return;
                                if (/\.vcf$/i.test(f.name) || /vcard/i.test(f.type)) {
                                    await runImport(f, api.importContactsVcf, 'Contacts import (vCard)', 'card');
                                } else {
                                    await runImport(f, api.importContactsCsv, 'Contacts import');
                                }
                            };
                            // ONE Import button; a chip pair picks the kind (owner flagged two
                            // look-alike buttons 2026-08-20). Inline chips, not a floating menu —
                            // the app has no menu pattern and the chip pair is already the house
                            // idiom for a binary choice (invite form, filters).
                            const pick = (ref) => { setImportPick(false); ref.current && ref.current.click(); };
                            return <>
                                <button className={`btn btn-secondary${importPick ? ' active' : ''}`}
                                    title="Import accounts or contacts from a file"
                                    onClick={() => setImportPick(v => !v)}>
                                    <i className="fas fa-file-import"></i> Import
                                </button>
                                {importPick && (
                                    <span style={{ display: 'inline-flex', gap: '0.375rem', alignItems: 'center' }}>
                                        <button type="button" className="filter-chip"
                                            title="Import/update accounts from a CSV file (matches by email, then name)"
                                            onClick={() => pick(importAccountsRef)}>Accounts · CSV</button>
                                        <button type="button" className="filter-chip"
                                            title="Import/update contacts from a CSV file (rows name their account by CUS number or name) or a vCard (.vcf) export from your phone, Outlook or Google Contacts"
                                            onClick={() => pick(importContactsRef)}>Contacts · CSV or vCard</button>
                                    </span>
                                )}
                                <input ref={importAccountsRef} type="file" accept=".csv,text/csv" style={{ display: 'none' }}
                                    onChange={onPick(api.importAccountsCsv, 'Accounts import')} />
                                <input ref={importContactsRef} type="file" accept=".csv,text/csv,.vcf,text/vcard,text/x-vcard" style={{ display: 'none' }}
                                    onChange={onPickContacts} />
                            </>;
                        })()}
                        <button className="btn btn-primary" onClick={() => setShowNewAccount(true)}>
                            <i className="fas fa-plus"></i> New Account
                        </button>
                    </div>

                    {importMsg && <div className="api-success" style={{ margin: '0.5rem 0' }}>{importMsg}</div>}

                    {dataLoadError && !loadingData && (
                        <LoadErrorBanner what="accounts" hasData={filteredAccounts.length > 0} onRetry={loadAll} />
                    )}

                    {loadingData ? (
                        <div className="loading-state"><i className="fas fa-spinner fa-spin"></i> Loading…</div>
                    ) : filteredAccounts.length === 0 ? (
                        <div className="empty-state" style={{ marginTop: '3rem' }}>
                            <i className="fas fa-building empty-state-icon"></i>
                            <p className="empty-state-message">{searchTerm ? `No results for "${searchTerm}"` : tagFilter.length > 0 ? 'No accounts match the selected tags.' : 'No accounts yet.'}</p>
                        </div>
                    ) : (<>
                        <table className="data-table accounts-table">
                            <thead>
                                <tr>
                                    <th>Name</th>
                                    <th>Type</th>
                                    <th>Contact</th>
                                    <th>Status</th>
                                    <th>Rep</th>
                                </tr>
                            </thead>
                            <tbody>
                                {filteredAccounts.map((account, i) => (
                                    <tr key={account.id} className={`row-clickable ${i === accountSel ? 'kb-selected' : ''}`} onClick={() => setSelectedAccount(account)}>
                                        <td>
                                            {/* Real link so cmd/middle-click opens the account
                                                in its own tab; plain click selects in-app. */}
                                            <a
                                                href={'/accounts/' + encodeURIComponent(account.customer_number || account.id)}
                                                className="doc-link accounts-table-name"
                                                onClick={(e) => { e.stopPropagation(); navClick(e, () => setSelectedAccount(account)); }}
                                            >
                                                {account.name}
                                            </a>
                                            {/* Mirrors the mailbox via the poller — clears itself when read. */}
                                            {account.has_unread_email && (
                                                <span className="unread-dot" style={{ marginLeft: '0.4rem' }}
                                                      title={`${account.unread_email_count} unread email${account.unread_email_count === 1 ? '' : 's'}`}></span>
                                            )}
                                            {account.customer_number && (
                                                /* List view shows the bare number — the CUS- prefix
                                                   is noise at a glance; full number stays on the
                                                   detail view and pickers. */
                                                <span className="customer-number-badge" style={{ marginLeft: '0.5rem' }}>{account.customer_number.replace(/^CUS-/, '')}</span>
                                            )}
                                        </td>
                                        <td className="accounts-table-type">{account.type}</td>
                                        <td className="accounts-table-contact">
                                            {account.main_email || account.main_phone || '—'}
                                        </td>
                                        <td>
                                            <div className="accounts-table-status">
                                                {/* Stage is derived, never hand-set. */}
                                                {billingStage(account.billing_stage) && (
                                                    <span className={`badge stage-badge ${billingStage(account.billing_stage).cls}`}
                                                          title={billingStage(account.billing_stage).title}>
                                                        {billingStage(account.billing_stage).label}
                                                    </span>
                                                )}
                                                {account.urgent_task_count > 0 && (
                                                    <span className="badge urgent-flare"
                                                          title={`${account.urgent_task_count} urgent open task(s)`}>
                                                        <i className="fas fa-exclamation-circle"></i> {account.urgent_task_count}
                                                    </span>
                                                )}
                                                {/* Lifecycle pills (0048): pending request / erased. */}
                                                {account.delete_requested_at && !account.deleted_at && (
                                                    <span className="badge secondary" title="Deletion requested — awaiting manager approval">
                                                        <i className="fas fa-hourglass-half"></i> delete pending
                                                    </span>
                                                )}
                                                {account.erased_at && (
                                                    <span className="badge secondary" title="Personal data permanently erased">erased</span>
                                                )}
                                                {/* Pipeline service pills only — manual tags show inside
                                                    the record. Muted tag treatment (owner's call): tinted
                                                    text + thin border, no solid fill. */}
                                                {pipelineTags(account.tags).map(t => (
                                                    <span key={t.id} className="badge service-pill"
                                                          style={{ color: t.color, borderColor: t.color + '55', background: t.color + '14' }}>
                                                        {pipelinePill(t).text}
                                                    </span>
                                                ))}
                                            </div>
                                        </td>
                                        <td className="accounts-table-rep">{account.assigned_to_name || '—'}</td>
                                    </tr>
                                ))}
                            </tbody>
                        </table>
                        {/* Scroll sentinel: observed above; the count reads as a footer */}
                        <div ref={accountsSentinelRef} className="accounts-count-note" style={{ marginTop: '0.5rem', textAlign: 'center' }}>
                            {accountTotal > filteredAccounts.length
                                ? <><i className="fas fa-spinner fa-spin"></i> Showing {filteredAccounts.length.toLocaleString()} of {accountTotal.toLocaleString()} — loading more as you scroll</>
                                : <>Showing all {filteredAccounts.length.toLocaleString()}</>}
                        </div>
                    </>)}
                </div>
            ))}

            </div>{/* /main-col */}

            <Modal isOpen={showNewAccount} onClose={() => setShowNewAccount(false)} title="Create New Account">
                <AccountForm onSubmit={handleCreateAccount} onCancel={() => setShowNewAccount(false)} />
            </Modal>

            {showGlobalSearch && (
                <GlobalSearch onClose={() => setShowGlobalSearch(false)} onSelectAccount={handleSelectAccountFromSearch} />
            )}

            {showShortcutHelp && <ShortcutHelpModal onClose={() => setShowShortcutHelp(false)} />}

            <WhatsNewModal currentUser={currentUser} tenant={tenant} />
        </div>
    );
};
