// workspace.js — Workspace panel, all job class CRUD, modals, grants, legislation
// Extracted from dashboard.html (lines 7573-11647)

// ── Workspace Panel ─────────────────────────────────
// WorkspaceState.config / .activeTab (namespaced above)

async function updateWorkspaceNav() {
    try {
        const res = await apiFetch(API + '/api/workspace');
        const data = await res.json();
        const nav = document.getElementById('workspace-nav');
        if (data.workspace) {
            WorkspaceState.config = data.workspace;
            nav.style.display = '';
            document.getElementById('workspace-nav-icon').className = 'fas ' + (data.workspace.icon || 'fa-th-large');
            document.getElementById('workspace-nav-label').textContent = data.workspace.label || 'Workspace';
            updateSidebarQuickActions(data.workspace);
        } else {
            WorkspaceState.config = null;
            nav.style.display = 'none';
            updateSidebarQuickActions(null);
        }
    } catch (e) {
        document.getElementById('workspace-nav').style.display = 'none';
        WorkspaceState.config = null;
        updateSidebarQuickActions(null);
    }
}

async function loadWorkspace() {
    try {
        const res = await apiFetch(API + '/api/workspace');
        const data = await res.json();
        if (!data.workspace) {
            document.getElementById('workspace-content').innerHTML = '<div class="ws-empty">No workspace configured for current job class.</div>';
            return;
        }
        WorkspaceState.config = data.workspace;
        document.getElementById('workspace-title').textContent = data.workspace.label || 'Workspace';
        document.getElementById('workspace-description').textContent = data.workspace.description || '';
        renderWorkspaceTabs(data.workspace);
        renderWorkspaceOverview(data.workspace.overview, data.workspace.alerts || []);
        WorkspaceState.activeTab = 'overview';
    } catch (e) {
        document.getElementById('workspace-content').innerHTML = '<div class="ws-empty">Failed to load workspace.</div>';
    }
}

function renderWorkspaceTabs(ws) {
    const tabs = document.getElementById('workspace-tabs');
    let html = '<div class="workspace-tab active" onclick="switchWorkspaceTab(\'overview\')"><i class="fas fa-chart-bar"></i> Overview</div>';
    // Setup Wizard tab — always first after overview
    html += '<div class="workspace-tab" data-ws-tab="setup_wizard" onclick="switchWorkspaceTab(\'setup_wizard\')">' +
        '<i class="fas fa-wand-magic-sparkles"></i> Setup</div>';
    for (const sec of (ws.sections || [])) {
        html += '<div class="workspace-tab" data-ws-tab="' + escapeHtml(sec.key) + '" onclick="switchWorkspaceTab(\'' + escapeHtml(sec.key) + '\')">' +
            '<i class="fas ' + escapeHtml(sec.icon || 'fa-table') + '"></i> ' + escapeHtml(sec.label) + '</div>';
    }
    tabs.innerHTML = html;
}

function renderWorkspaceOverview(overview, alerts) {
    const el = document.getElementById('workspace-overview');
    if (!overview || !overview.computed_cards || !overview.computed_cards.length) {
        el.innerHTML = '';
        return;
    }
    let html = '';

    // Smart alerts panel
    if (alerts && alerts.length) {
        html += '<div style="padding:0 16px 8px">';
        for (const a of alerts) {
            const colors = {
                urgent: {bg: '#dc262615', border: '#ef4444', icon: '#f87171'},
                warn: {bg: '#f59e0b15', border: '#f59e0b', icon: '#fbbf24'},
                info: {bg: '#3b82f615', border: '#3b82f6', icon: '#60a5fa'},
            };
            const c = colors[a.level] || colors.info;
            html += '<div style="display:flex;align-items:center;gap:12px;padding:10px 16px;margin-bottom:8px;' +
                'background:' + c.bg + ';border:1px solid ' + c.border + '40;border-radius:10px;font-size:0.85rem">' +
                '<i class="fas ' + escapeHtml(a.icon || 'fa-bell') + '" style="color:' + c.icon + ';font-size:1rem;flex-shrink:0"></i>' +
                '<span style="flex:1;color:var(--text)">' + escapeHtml(a.message) + '</span>';
            if (a.action_fn) {
                html += '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 12px;white-space:nowrap" ' +
                    'onclick="' + a.action_fn + '">' + escapeHtml(a.action_label || 'Action') + '</button>';
            }
            html += '</div>';
        }
        html += '</div>';
    }

    html += '<div class="ws-overview-grid">';
    for (const card of overview.computed_cards) {
        if (card.type === 'cta') {
            const svcKeysAttr = card.service_keys ? ' data-service-keys=\'' + JSON.stringify(card.service_keys) + '\'' : '';
            html += '<div class="ws-cta-card"' + svcKeysAttr + '>' +
                '<div class="ws-cta-icon" style="color:' + escapeHtml(card.color || 'var(--accent)') + '"><i class="fas ' + escapeHtml(card.icon || 'fa-server') + '"></i></div>' +
                '<div class="ws-cta-body">' +
                '<div class="ws-cta-title">' + escapeHtml(card.label) + '</div>' +
                '<div class="ws-cta-message">' + escapeHtml(card.message || '') + '</div>' +
                '</div>' +
                '<button class="ws-cta-btn" onclick="wsCtaAction(\'' + escapeHtml(card.button_action || '') + '\')">' +
                '<i class="fas fa-rocket" style="margin-right:6px"></i>' + escapeHtml(card.button_label || 'Set Up') +
                '</button></div>';
        } else if (card.type === 'service_status') {
            let badges = '';
            for (const svc of (card.services || [])) {
                const isOnline = svc.status === 'running';
                const isConfigured = svc.configured;
                // Three states: green (running+configured), amber (running+unconfigured), gray/red (offline)
                let dotCls, badgeCls, statusText;
                if (isOnline && isConfigured) {
                    dotCls = 'green'; badgeCls = 'online'; statusText = 'Online';
                } else if (isOnline && !isConfigured && svc.connect_key) {
                    dotCls = 'amber'; badgeCls = 'needs-setup'; statusText = 'Needs Setup';
                } else if (isOnline) {
                    dotCls = 'green'; badgeCls = 'online'; statusText = 'Online';
                } else if (svc.status === 'not_installed') {
                    dotCls = 'gray'; badgeCls = 'offline';
                    statusText = svc.service_type === 'native' ? 'Not installed' : 'Not deployed';
                } else if (svc.status === 'stopped') {
                    dotCls = 'red'; badgeCls = 'offline'; statusText = 'Stopped';
                } else {
                    dotCls = 'red'; badgeCls = 'offline'; statusText = svc.status;
                }
                badges += '<div class="ws-svc-badge ' + badgeCls + '">' +
                    '<span class="ws-svc-dot ' + dotCls + '"></span>' +
                    '<i class="fas ' + escapeHtml(svc.icon || 'fa-server') + '" style="color:' + escapeHtml(card.color || 'var(--text-dim)') + ';font-size:0.9em"></i>' +
                    '<span class="ws-svc-name">' + escapeHtml(svc.display_name) + '</span>' +
                    '<span style="font-size:0.75rem;color:var(--text-dim)">' + escapeHtml(statusText) + '</span>';
                if (isOnline && isConfigured && svc.url) {
                    badges += ' <a class="ws-svc-url" href="' + escapeHtml(svc.url) + '" target="_blank" onclick="event.stopPropagation()">Open &rarr;</a>';
                } else if (isOnline && !isConfigured && svc.connect_key) {
                    badges += ' <a class="ws-svc-url" href="#" onclick="event.stopPropagation();event.preventDefault();switchPanel(\'connect\');setTimeout(function(){if(typeof showTarotCard===\'function\')showTarotCard(\'' + escapeHtml(svc.connect_key) + '\');},400)" style="color:var(--warning-dim)">Connect &rarr;</a>';
                }
                badges += '</div>';
            }
            html += '<div class="ws-svc-card">' +
                '<div class="ws-svc-title"><i class="fas ' + escapeHtml(card.icon || 'fa-server') + '" style="margin-right:6px"></i>' + escapeHtml(card.label) + '</div>' +
                '<div class="ws-svc-grid">' + badges + '</div></div>';
        } else {
            html += '<div class="ws-stat-card" data-card-key="' + escapeHtml(card.key) + '" onclick="wsOverviewCardClick(\'' + escapeHtml(card.key) + '\')">' +
                '<div class="ws-stat-icon" style="color:' + escapeHtml(card.color || 'var(--accent)') + '"><i class="fas ' + escapeHtml(card.icon || 'fa-circle') + '"></i></div>' +
                '<div class="ws-stat-value" style="color:' + escapeHtml(card.color || 'var(--text)') + '">' + _wsFormatValue(card.value, card.format) + '</div>' +
                '<div class="ws-stat-label">' + escapeHtml(card.label) + '</div>' +
                '</div>';
        }
    }
    html += '</div>';
    el.innerHTML = html;

    // Resolve client-side cards (email count, calendar count) async
    for (const card of overview.computed_cards) {
        if (!card.client_api) continue;
        (async (c) => {
            try {
                const r = await apiFetch(API + c.client_api);
                const d = await r.json();
                let val = 0;
                if (c.client_metric === 'total') val = d.total || 0;
                else if (c.client_metric === 'events.length') val = (d.events || []).length;
                else val = d[c.client_metric] || 0;
                const cardEl = el.querySelector('[data-card-key="' + c.key + '"] .ws-stat-value');
                if (cardEl) cardEl.textContent = _wsFormatValue(val, c.format);
            } catch(e) {}
        })(card);
    }
}

function wsOverviewCardClick(key) {
    // Try to find a section that matches this card key
    if (!WorkspaceState.config) return;
    for (const sec of (WorkspaceState.config.sections || [])) {
        if (sec.key === key || key.includes(sec.key) || sec.key.includes(key)) {
            switchWorkspaceTab(sec.key);
            return;
        }
    }
}

async function wsCtaAction(action) {
    if (action === 'full_server_setup') {
        const cta = document.querySelector('.ws-cta-card');

        // Read job-specific service keys from the CTA card's data attribute
        let ctaServiceKeys = null;
        if (cta && cta.dataset.serviceKeys) {
            try { ctaServiceKeys = JSON.parse(cta.dataset.serviceKeys); } catch (_) {}
        }

        // Step 1: Check if runtime is usable before attempting setup
        try {
            const rtResp = await apiFetch(API + '/api/server/runtime');
            const rtData = await rtResp.json();

            if (rtData.runtime === 'none' && rtData.unusable_runtime) {
                if (cta) {
                    cta.innerHTML =
                        '<div class="ws-cta-icon" style="color:var(--warning)"><i class="fas fa-terminal"></i></div>' +
                        '<div class="ws-cta-body">' +
                        '<div class="ws-cta-title">Container runtime needed</div>' +
                        '<div class="ws-cta-message">' +
                        escapeHtml(rtData.unusable_runtime) + ' is installed but your user doesn\'t have permission to use it. ' +
                        'We\'ll install <strong>Podman</strong> (rootless — no permission issues, more secure).<br><br>' +
                        'Click below to open the Shell tab with the install command ready to run.' +
                        '</div>' +
                        '<button class="ws-cta-btn" onclick="wsShellBootstrap()">Open Shell &amp; Install Podman</button>' +
                        '</div>';
                }
                return;
            }

            if (rtData.runtime === 'none') {
                if (cta) {
                    cta.innerHTML =
                        '<div class="ws-cta-icon" style="color:var(--warning)"><i class="fas fa-terminal"></i></div>' +
                        '<div class="ws-cta-body">' +
                        '<div class="ws-cta-title">Container runtime needed</div>' +
                        '<div class="ws-cta-message">' +
                        'No container runtime found. We\'ll install <strong>Podman</strong> (lightweight, rootless, secure).<br><br>' +
                        'Click below to open the Shell tab with the install command ready to run.' +
                        '</div>' +
                        '<button class="ws-cta-btn" onclick="wsShellBootstrap()">Open Shell &amp; Install Podman</button>' +
                        '</div>';
                }
                return;
            }
        } catch (_) {
            // If runtime check fails, proceed anyway and let full-setup handle it
        }

        // Step 2: Runtime is available — show spinner and deploy
        if (cta) {
            cta.innerHTML = '<div class="ws-cta-icon" style="color:var(--accent)"><i class="fas fa-spinner fa-spin"></i></div>' +
                '<div class="ws-cta-body"><div class="ws-cta-title">Setting up your server...</div>' +
                '<div class="ws-cta-message">Deploying services with hardened containers. This may take a few minutes.</div></div>';
        }
        try {
            const body = ctaServiceKeys ? JSON.stringify({services: ctaServiceKeys}) : '{}';
            const resp = await apiFetch(API + '/api/server/full-setup', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: body,
            });
            const data = await resp.json();
            if (data.error) {
                appendMessage('assistant', 'Server setup failed: ' + data.error);
            } else {
                const summary = data.summary || {};
                const services = data.services || [];
                const ok = services.filter(s => s.ok);
                const failed = services.filter(s => !s.ok);

                let msg = '';
                if (summary.ok > 0 && summary.failed > 0) {
                    msg = '**Server partially deployed** — ' + summary.ok + ' service' + (summary.ok > 1 ? 's' : '') +
                        ' running, ' + summary.failed + ' failed.';
                } else if (summary.ok > 0) {
                    msg = '**Server setup complete!** ' + summary.ok + ' service' + (summary.ok > 1 ? 's' : '') + ' running.';
                } else {
                    msg = '**Server setup failed** — no services could be started.';
                }

                if (ok.length) {
                    msg += '\n\n**Running:**';
                    ok.forEach(s => {
                        msg += '\n- ' + (s.display_name || s.service) + (s.url ? ' → `' + s.url + '`' : '');
                    });
                }

                // Show setup notes for services that need first-time configuration
                const withNotes = services.filter(s => s.setup_notes);
                if (withNotes.length) {
                    msg += '\n\n**Setup required:**';
                    withNotes.forEach(s => {
                        msg += '\n- **' + (s.display_name || s.service) + '**: ' + s.setup_notes;
                    });
                }

                if (failed.length) {
                    msg += '\n\n**Failed:**';
                    failed.forEach(s => {
                        const reason = (s.error || 'unknown error').split('\n').pop().substring(0, 120);
                        msg += '\n- ' + (s.display_name || s.service) + ': ' + reason;
                    });
                }

                if (data.credentials && data.credentials.length) {
                    msg += '\n\n**Credentials** stored securely in `~/.familiar/services/secrets.json`.';
                }
                if (data.security) {
                    msg += '\n\nSecurity posture: **' + data.security.score + '/' + data.security.score_max + '** checks passing.';
                }
                appendMessage('assistant', msg);
            }
        } catch (e) {
            appendMessage('assistant', 'Server setup error: ' + e.message);
        } finally {
            // ALWAYS refresh workspace to stop spinner — even on error
            loadWorkspace();
        }
    }
}

function shellRunCommand(cmd) {
    // Switch to Shell tab and pre-fill a command for the user to execute
    switchPanel('shell');
    setTimeout(() => {
        const input = document.getElementById('shell-input');
        if (input) {
            input.value = cmd;
            input.focus();
        }
    }, 200);
}

function wsShellBootstrap() {
    // Show a password prompt, then run the install via the shell with sudo -S
    const installCmd = 'sudo apt-get install -y podman || sudo dnf install -y podman';

    // Create a modal password prompt
    const overlay = document.createElement('div');
    overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
    overlay.innerHTML =
        '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:420px;width:90%;">' +
        '<div style="color:var(--accent);font-weight:600;font-size:1.1rem;margin-bottom:8px;">🔒 Install Podman</div>' +
        '<div style="color:var(--text-dim);font-size:0.85rem;margin-bottom:16px;">' +
        'Installing Podman requires your system password. This is the same password you use to log in to your computer.<br><br>' +
        'Your password is sent directly to sudo and is <strong>never stored or logged</strong>.</div>' +
        '<div style="font-family:monospace;font-size:0.8rem;color:var(--warning);background:var(--bg);padding:8px 12px;border-radius:6px;margin-bottom:16px;">' +
        escapeHtml(installCmd) + '</div>' +
        '<input type="password" id="sudo-pw-input" placeholder="System password" autocomplete="off" ' +
        'style="width:100%;padding:10px 12px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:0.95rem;margin-bottom:12px;box-sizing:border-box;">' +
        '<div style="display:flex;gap:8px;justify-content:flex-end;">' +
        '<button id="sudo-pw-cancel" style="padding:8px 16px;background:var(--bg);color:var(--text-dim);border:1px solid var(--border);border-radius:6px;cursor:pointer;">Cancel</button>' +
        '<button id="sudo-pw-ok" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600;">Install</button>' +
        '</div></div>';
    document.body.appendChild(overlay);

    const pwInput = document.getElementById('sudo-pw-input');
    pwInput.focus();

    document.getElementById('sudo-pw-cancel').onclick = () => overlay.remove();
    pwInput.addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('sudo-pw-ok').click(); });

    document.getElementById('sudo-pw-ok').onclick = async () => {
        const pw = pwInput.value;
        overlay.remove();
        if (!pw) return;

        // Switch to shell tab and run the command with the password
        switchPanel('shell');
        await new Promise(r => setTimeout(r, 200));

        shellAddLine('── Familiar Server Setup ──', 'output');
        shellAddLine('Installing Podman...', 'output');
        shellAddLine('', 'output');
        shellAddLine(installCmd, 'command');

        // Run via the shell API with sudo_password
        shellRunning = true;
        const shellInputEl = document.getElementById('shell-input');
        if (shellInputEl) shellInputEl.disabled = true;
        document.getElementById('shell-status').textContent = 'installing...';
        document.getElementById('shell-kill-btn').style.display = 'inline-block';

        try {
            const res = await apiFetch(API + '/api/shell/stream', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({command: installCmd, sudo_password: pw}),
            });
            const reader = res.body.getReader();
            const decoder = new TextDecoder();
            let buf = '';
            let exitCode = null;

            while (true) {
                const {done, value} = await reader.read();
                if (done) break;
                buf += decoder.decode(value, {stream: true});
                const lines = buf.split('\n');
                buf = lines.pop();
                for (const line of lines) {
                    if (!line.startsWith('data: ')) continue;
                    try {
                        const ev = JSON.parse(line.slice(6));
                        if (ev.type === 'output') {
                            shellAddLine(stripAnsi(ev.content.replace(/\n$/, '')), 'output');
                        } else if (ev.type === 'exit') {
                            exitCode = ev.code;
                        }
                    } catch (_) {}
                }
            }

            shellRunning = false;
            if (shellInputEl) shellInputEl.disabled = false;
            document.getElementById('shell-kill-btn').style.display = 'none';
            document.getElementById('shell-status').textContent = '';

            if (exitCode === 0) {
                shellAddLine('', 'output');
                shellAddLine('✓ Podman installed successfully!', 'exit-ok');
                shellAddLine('Detecting runtime and switching back to setup...', 'output');
                // Invalidate runtime cache, then navigate back to workspace
                setTimeout(async () => {
                    // Force re-detection of container runtime
                    await apiFetch(API + '/api/server/runtime?refresh=true').catch(() => {});
                    switchPanel('workspace');
                    setTimeout(() => loadWorkspace(), 500);
                }, 1500);
            } else {
                shellAddLine('', 'output');
                shellAddLine('Installation failed (exit ' + exitCode + '). Check the output above.', 'exit-fail');
                shellAddLine('You can try running the command manually in a terminal.', 'output');
            }
        } catch (e) {
            shellRunning = false;
            if (shellInputEl) shellInputEl.disabled = false;
            shellAddLine('Error: ' + e.message, 'error');
        }
    };
}

async function switchWorkspaceTab(key) {
    WorkspaceState.activeTab = key;
    // Update tab active state
    document.querySelectorAll('.workspace-tab').forEach(t => {
        t.classList.toggle('active', (key === 'overview' && !t.dataset.wsTab) || t.dataset.wsTab === key);
    });
    const content = document.getElementById('workspace-content');
    const overview = document.getElementById('workspace-overview');

    if (key === 'overview') {
        overview.style.display = '';
        content.innerHTML = '';
        return;
    }
    overview.style.display = 'none';
    content.innerHTML = '<div class="ws-empty">Loading...</div>';

    // Setup Wizard — special tab, not from workspace sections
    if (key === 'setup_wizard') {
        await _renderSetupWizard(content);
        return;
    }

    try {
        const sec = (WorkspaceState.config.sections || []).find(s => s.key === key);
        if (!sec) { content.innerHTML = '<div class="ws-empty">Section not found.</div>'; return; }

        // Client-rendered sections — call existing APIs directly, no workspace pipeline
        if (sec.type === 'email') { await renderWorkspaceEmail(content, sec); return; }
        if (sec.type === 'calendar') { await renderWorkspaceCalendar(content, sec); return; }

        const sort = sec.default_sort || '';
        const dir = sec.default_sort_dir || 'asc';
        const res = await apiFetch(API + '/api/workspace/section/' + encodeURIComponent(key) + '?sort=' + sort + '&dir=' + dir);
        const data = await res.json();

        if (data.error) { content.innerHTML = '<div class="ws-empty">' + escapeHtml(data.error) + '</div>'; return; }

        if (sec.type === 'table') renderWorkspaceTable(content, sec, data);
        else if (sec.type === 'pipeline') renderWorkspacePipeline(content, sec, data);
        else if (sec.type === 'summary') renderWorkspaceSummary(content, sec, data);
        else content.innerHTML = '<div class="ws-empty">Unknown section type.</div>';
    } catch (e) {
        content.innerHTML = '<div class="ws-empty">Failed to load section.</div>';
    }
}

// ── Social Worker CRUD functions ──

function swShowCaseModal(existing) {
    document.getElementById('sw-case-id').value = (existing && existing.id) || '';
    document.getElementById('sw-case-modal-title').textContent = existing ? 'Edit Case' : 'New Case';
    document.getElementById('sw-case-name').value = (existing && existing.name) || '';
    document.getElementById('sw-case-category').value = (existing && existing.category) || 'general';
    document.getElementById('sw-case-priority').value = (existing && existing.priority) || 'normal';
    document.getElementById('sw-case-phone').value = (existing && existing.phone) || '';
    document.getElementById('sw-case-email').value = (existing && existing.email) || '';
    document.getElementById('sw-case-notes').value = (existing && existing.notes) || '';
    document.getElementById('sw-case-file-input').value = '';
    const filesSection = document.getElementById('sw-case-files-section');
    if (existing && existing.id) {
        filesSection.style.display = 'block';
        flLoadFiles('sw-case-files-list', 'cases', existing.id, 'social_worker');
    } else {
        filesSection.style.display = 'none';
        document.getElementById('sw-case-files-list').innerHTML = '';
    }
    showModal('sw-case-modal');
}

async function swSaveCase() {
    const id = document.getElementById('sw-case-id').value;
    const data = {
        name: document.getElementById('sw-case-name').value,
        category: document.getElementById('sw-case-category').value,
        priority: document.getElementById('sw-case-priority').value,
        phone: document.getElementById('sw-case-phone').value,
        email: document.getElementById('sw-case-email').value,
        notes: document.getElementById('sw-case-notes').value,
    };
    try {
        if (id) {
            await apiFetch(API + '/api/cases/' + id, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
        } else {
            await apiFetch(API + '/api/cases', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
        }
        hideModal('sw-case-modal');
        showToast(id ? 'Case updated!' : 'Case created!', 'success');
        if (WorkspaceState.activeTab === 'cases') switchWorkspaceTab('cases');
        else if (WorkspaceState.activeTab === 'overview') renderWorkspaceOverview(WorkspaceState.config.overview, WorkspaceState.config.alerts || []);
    } catch (e) {
        showToast('Error saving case: ' + e.message, 'error');
    }
}

async function swDeleteCase(caseId, name) {
    if (!confirm('Delete case "' + (name || caseId) + '"? This cannot be undone.')) return;
    try {
        await apiFetch(API + '/api/cases/' + caseId, {method: 'DELETE'});
        showToast('Case deleted.', 'success');
        if (WorkspaceState.activeTab === 'cases') switchWorkspaceTab('cases');
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

function swShowResourceModal(existing) {
    document.getElementById('sw-resource-id').value = (existing && existing.id) || '';
    document.getElementById('sw-resource-modal-title').textContent = existing ? 'Edit Resource' : 'Add Resource';
    document.getElementById('sw-resource-name').value = (existing && existing.name) || '';
    document.getElementById('sw-resource-category').value = (existing && existing.category) || 'general';
    document.getElementById('sw-resource-phone').value = (existing && existing.phone) || '';
    document.getElementById('sw-resource-address').value = (existing && existing.address) || '';
    document.getElementById('sw-resource-website').value = (existing && existing.website) || '';
    document.getElementById('sw-resource-notes').value = (existing && existing.notes) || '';
    const filesSection = document.getElementById('sw-resource-files-section');
    if (existing && existing.id) {
        filesSection.style.display = 'block';
        flLoadFiles('sw-resource-files-list', 'resources', existing.id, 'social_worker');
    } else {
        filesSection.style.display = 'none';
        document.getElementById('sw-resource-files-list').innerHTML = '';
    }
    showModal('sw-resource-modal');
}

async function swSaveResource() {
    const id = document.getElementById('sw-resource-id').value;
    const data = {
        name: document.getElementById('sw-resource-name').value,
        category: document.getElementById('sw-resource-category').value,
        phone: document.getElementById('sw-resource-phone').value,
        address: document.getElementById('sw-resource-address').value,
        website: document.getElementById('sw-resource-website').value,
        notes: document.getElementById('sw-resource-notes').value,
    };
    try {
        if (id) {
            await apiFetch(API + '/api/cases/resources/' + id, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
        } else {
            const resp = await apiFetch(API + '/api/cases/resources', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
            const result = await resp.json();
            const savedId = result.id || result.resource?.id || '';
            if (savedId) {
                document.getElementById('sw-resource-id').value = savedId;
                document.getElementById('sw-resource-files-section').style.display = 'block';
                document.getElementById('sw-resource-modal-title').textContent = 'Edit Resource';
                flLoadFiles('sw-resource-files-list', 'resources', savedId, 'social_worker');
                showToast('Resource added! You can now attach files.', 'success');
                if (WorkspaceState.activeTab === 'resources') switchWorkspaceTab('resources');
                return;
            }
        }
        hideModal('sw-resource-modal');
        showToast(id ? 'Resource updated!' : 'Resource added!', 'success');
        if (WorkspaceState.activeTab === 'resources') switchWorkspaceTab('resources');
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

async function swDeleteResource(resId, name) {
    if (!confirm('Delete resource "' + (name || resId) + '"?')) return;
    try {
        await apiFetch(API + '/api/cases/resources/' + resId, {method: 'DELETE'});
        showToast('Resource deleted.', 'success');
        if (WorkspaceState.activeTab === 'resources') switchWorkspaceTab('resources');
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

async function swShowNoteModal() {
    // Populate case dropdown
    const sel = document.getElementById('sw-note-case');
    sel.innerHTML = '<option value="">General</option>';
    try {
        const res = await apiFetch(API + '/api/cases');
        const data = await res.json();
        for (const c of (data.cases || [])) {
            sel.innerHTML += '<option value="' + escapeHtml(c.id) + '" data-name="' + escapeHtml(c.name) + '">' + escapeHtml(c.name) + '</option>';
        }
    } catch (e) { /* ignore */ }
    document.getElementById('sw-note-category').value = 'progress';
    document.getElementById('sw-note-content').value = '';
    document.getElementById('sw-note-saved-id').value = '';
    document.getElementById('sw-note-files-section').style.display = 'none';
    document.getElementById('sw-note-files-list').innerHTML = '';
    showModal('sw-note-modal');
}

async function swSaveNote() {
    const caseSel = document.getElementById('sw-note-case');
    const caseOpt = caseSel.options[caseSel.selectedIndex];
    const data = {
        case_id: caseSel.value,
        case_name: caseOpt ? caseOpt.getAttribute('data-name') || caseOpt.textContent : '',
        category: document.getElementById('sw-note-category').value,
        content: document.getElementById('sw-note-content').value,
    };
    try {
        const resp = await apiFetch(API + '/api/cases/notes', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
        const result = await resp.json();
        const savedId = result.note?.id || result.id || '';
        if (savedId) {
            document.getElementById('sw-note-saved-id').value = savedId;
            document.getElementById('sw-note-files-section').style.display = 'block';
            flLoadFiles('sw-note-files-list', 'case_notes', savedId, 'social_worker');
            showToast('Note saved! You can now attach files.', 'success');
            if (WorkspaceState.activeTab === 'case_notes') switchWorkspaceTab('case_notes');
            return;
        }
        hideModal('sw-note-modal');
        showToast('Note saved!', 'success');
        if (WorkspaceState.activeTab === 'case_notes') switchWorkspaceTab('case_notes');
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

async function swDeleteNote(noteId) {
    if (!confirm('Delete this case note?')) return;
    try {
        await apiFetch(API + '/api/cases/notes/' + noteId, {method: 'DELETE'});
        showToast('Note deleted.', 'success');
        if (WorkspaceState.activeTab === 'case_notes') switchWorkspaceTab('case_notes');
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

async function swShowCrisisModal() {
    const sel = document.getElementById('sw-crisis-case');
    sel.innerHTML = '<option value="">Unlinked</option>';
    try {
        const res = await apiFetch(API + '/api/cases');
        const data = await res.json();
        for (const c of (data.cases || [])) {
            sel.innerHTML += '<option value="' + escapeHtml(c.id) + '" data-name="' + escapeHtml(c.name) + '">' + escapeHtml(c.name) + '</option>';
        }
    } catch (e) { /* ignore */ }
    document.getElementById('sw-crisis-severity').value = 'moderate';
    document.getElementById('sw-crisis-summary').value = '';
    document.getElementById('sw-crisis-outcome').value = '';
    document.getElementById('sw-crisis-saved-id').value = '';
    document.getElementById('sw-crisis-files-section').style.display = 'none';
    document.getElementById('sw-crisis-files-list').innerHTML = '';
    showModal('sw-crisis-modal');
}

async function swSaveCrisis() {
    const caseSel = document.getElementById('sw-crisis-case');
    const caseOpt = caseSel.options[caseSel.selectedIndex];
    const data = {
        case_id: caseSel.value,
        case_name: caseOpt ? caseOpt.getAttribute('data-name') || caseOpt.textContent : '',
        severity: document.getElementById('sw-crisis-severity').value,
        summary: document.getElementById('sw-crisis-summary').value,
        outcome: document.getElementById('sw-crisis-outcome').value,
    };
    try {
        const resp = await apiFetch(API + '/api/cases/crisis', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
        const result = await resp.json();
        const savedId = result.event?.id || result.id || '';
        if (savedId) {
            document.getElementById('sw-crisis-saved-id').value = savedId;
            document.getElementById('sw-crisis-files-section').style.display = 'block';
            flLoadFiles('sw-crisis-files-list', 'crisis_events', savedId, 'social_worker');
            showToast('Crisis logged! You can now attach documents.', 'success');
            if (WorkspaceState.activeTab === 'crisis_log') switchWorkspaceTab('crisis_log');
            return;
        }
        hideModal('sw-crisis-modal');
        showToast('Crisis event logged.', 'success');
        if (WorkspaceState.activeTab === 'crisis_log') switchWorkspaceTab('crisis_log');
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

async function swDeleteCrisis(eventId) {
    if (!confirm('Delete this crisis event?')) return;
    try {
        await apiFetch(API + '/api/cases/crisis/' + eventId, {method: 'DELETE'});
        showToast('Crisis event deleted.', 'success');
        if (WorkspaceState.activeTab === 'crisis_log') switchWorkspaceTab('crisis_log');
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

async function swChangeStatus(caseId, newStatus) {
    try {
        await apiFetch(API + '/api/cases/' + caseId, {
            method: 'PUT',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({status: newStatus}),
        });
        showToast('Status updated to ' + newStatus, 'success');
        if (WorkspaceState.activeTab === 'cases') switchWorkspaceTab('cases');
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

// ── Clinical Assessment Modal ──

async function swShowAssessmentModal() {
    const sel = document.getElementById('sw-assess-case');
    sel.innerHTML = '<option value="">Select case...</option>';
    try {
        const r = await apiFetch(API + '/api/cases');
        const data = await r.json();
        (data.cases || []).forEach(c => {
            sel.innerHTML += '<option value="' + c.id + '">' + (c.name||'Unnamed') + '</option>';
        });
    } catch(e) {}
    document.getElementById('sw-assess-score').value = '';
    document.getElementById('sw-assess-notes').value = '';
    document.getElementById('sw-assessment-saved-id').value = '';
    document.getElementById('sw-assessment-files-section').style.display = 'none';
    document.getElementById('sw-assessment-files-list').innerHTML = '';
    swUpdateAssessmentFields();
    showModal('sw-assessment-modal');
}

function swUpdateAssessmentFields() {
    const tool = document.getElementById('sw-assess-tool').value;
    const isCSSRS = tool === 'C-SSRS';
    document.getElementById('sw-assess-score-group').style.display = isCSSRS ? 'none' : '';
    document.getElementById('sw-assess-severity-group').style.display = isCSSRS ? 'none' : '';
    document.getElementById('sw-assess-cssrs-group').style.display = isCSSRS ? '' : 'none';
}

async function swSaveAssessment() {
    const caseId = document.getElementById('sw-assess-case').value;
    if (!caseId) { showToast('Please select a case', 'error'); return; }
    const tool = document.getElementById('sw-assess-tool').value;
    const body = {tool};
    if (tool === 'C-SSRS') {
        body.risk_level = document.getElementById('sw-assess-risk').value;
        body.ideation = document.getElementById('sw-assess-ideation').checked;
        body.plan = document.getElementById('sw-assess-plan').checked;
        body.intent = document.getElementById('sw-assess-intent').checked;
    } else {
        body.score = parseInt(document.getElementById('sw-assess-score').value) || 0;
        body.severity = document.getElementById('sw-assess-severity').value;
    }
    body.notes = document.getElementById('sw-assess-notes').value;
    try {
        const resp = await apiFetch(API + '/api/cases/' + caseId + '/assessments', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(body),
        });
        const result = await resp.json();
        const savedId = result.assessment?.id || result.id || '';
        if (savedId) {
            document.getElementById('sw-assessment-saved-id').value = savedId;
            document.getElementById('sw-assessment-files-section').style.display = 'block';
            flLoadFiles('sw-assessment-files-list', 'assessments', savedId, 'social_worker');
            showToast('Assessment saved! You can now attach signed forms.', 'success');
            return;
        }
        showToast('Assessment saved', 'success');
        hideModal('sw-assessment-modal');
    } catch(e) { showToast('Error: ' + e.message, 'error'); }
}

// ── Safety Plan Modal ──

async function swShowSafetyPlanModal() {
    const sel = document.getElementById('sw-sp-case');
    sel.innerHTML = '<option value="">Select case...</option>';
    try {
        const r = await apiFetch(API + '/api/cases');
        const data = await r.json();
        (data.cases || []).forEach(c => {
            sel.innerHTML += '<option value="' + c.id + '">' + (c.name||'Unnamed') + '</option>';
        });
    } catch(e) {}
    ['sw-sp-warnings','sw-sp-coping','sw-sp-social','sw-sp-professional','sw-sp-environment','sw-sp-reasons'].forEach(id => {
        document.getElementById(id).value = '';
    });
    document.getElementById('sw-safety-saved-id').value = '';
    document.getElementById('sw-safety-files-section').style.display = 'none';
    document.getElementById('sw-safety-files-list').innerHTML = '';
    showModal('sw-safety-plan-modal');
}

async function swSaveSafetyPlan() {
    const caseId = document.getElementById('sw-sp-case').value;
    if (!caseId) { showToast('Please select a case', 'error'); return; }
    const lines = id => document.getElementById(id).value.split('\n').filter(l => l.trim());
    const body = {
        warning_signs: lines('sw-sp-warnings'),
        coping_strategies: lines('sw-sp-coping'),
        social_contacts: lines('sw-sp-social'),
        professional_contacts: lines('sw-sp-professional'),
        environment_safety: document.getElementById('sw-sp-environment').value,
        reasons_for_living: lines('sw-sp-reasons'),
    };
    try {
        const resp = await apiFetch(API + '/api/cases/' + caseId + '/safety-plan', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(body),
        });
        const result = await resp.json();
        const savedId = result.plan?.id || result.id || caseId;
        if (savedId) {
            document.getElementById('sw-safety-saved-id').value = savedId;
            document.getElementById('sw-safety-files-section').style.display = 'block';
            flLoadFiles('sw-safety-files-list', 'safety_plans', savedId, 'social_worker');
            showToast('Safety plan saved! You can now attach signed copies.', 'success');
            return;
        }
        showToast('Safety plan saved', 'success');
        hideModal('sw-safety-plan-modal');
    } catch(e) { showToast('Error: ' + e.message, 'error'); }
}

// ── Artist Modal JS ──

function _wsGenericShow(prefix, source, key, record) {
    // Generic show/hide for workspace data modals
    const fields = document.querySelectorAll('[id^="' + prefix + '-"]');
    const idEl = document.getElementById(prefix + '-id');
    const titleEl = document.getElementById(prefix + '-modal-title');
    const saveBtn = document.getElementById(prefix + '-save-btn');
    if (idEl) idEl.value = record ? (record.id || '') : '';
    if (titleEl) titleEl.textContent = record ? 'Edit' : 'Add';
    if (saveBtn) saveBtn.textContent = record ? 'Update' : 'Save';
    const filesSection = document.getElementById(prefix + '-files-section');
    const filesList = document.getElementById(prefix + '-files-list');
    if (record && record.id && filesSection) {
        filesSection.style.display = 'block';
        flLoadFiles(prefix + '-files-list', key, record.id, source === 'studio' ? 'artist' : 'chef');
    } else if (filesSection) {
        filesSection.style.display = 'none';
        if (filesList) filesList.innerHTML = '';
    }
}

async function _wsGenericSave(prefix, source, key, fields, modalId, extraPayload) {
    const id = document.getElementById(prefix + '-id').value;
    const payload = {};
    for (const [field, type] of Object.entries(fields)) {
        const el = document.getElementById(prefix + '-' + field);
        if (!el) continue;
        if (type === 'number') payload[field] = parseFloat(el.value) || null;
        else payload[field] = el.value.trim();
    }
    if (extraPayload) Object.assign(payload, extraPayload);
    const jobClass = source === 'studio' ? 'artist' : 'chef';
    try {
        let savedId = id;
        if (id) {
            await apiFetch(API + '/api/workspace/data/' + source + '/' + key + '/' + id, {
                method: 'PUT',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(payload),
            });
        } else {
            const resp = await apiFetch(API + '/api/workspace/data/' + source + '/' + key, {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(payload),
            });
            const result = await resp.json();
            savedId = result.id || '';
            if (savedId) {
                document.getElementById(prefix + '-id').value = savedId;
                const filesSection = document.getElementById(prefix + '-files-section');
                if (filesSection) {
                    filesSection.style.display = 'block';
                    flLoadFiles(prefix + '-files-list', key, savedId, jobClass);
                }
                const titleEl = document.getElementById(prefix + '-modal-title');
                if (titleEl) titleEl.textContent = 'Edit';
                const saveBtn = document.getElementById(prefix + '-save-btn');
                if (saveBtn) saveBtn.textContent = 'Update';
                showToast('Saved! You can now attach files.', 'success');
                loadWorkspace();
                return;
            }
        }
        hideModal(modalId);
        showToast(id ? 'Updated!' : 'Created!', 'success');
        loadWorkspace();
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}

// Artist: Piece
function artShowPieceModal(piece) {
    _wsGenericShow('art-piece', 'studio', 'pieces', piece);
    document.getElementById('art-piece-name').value = piece ? (piece.name || '') : '';
    document.getElementById('art-piece-type').value = piece ? (piece.type || 'bowl') : 'bowl';
    document.getElementById('art-piece-status').value = piece ? (piece.status || 'concept') : 'concept';
    document.getElementById('art-piece-price').value = piece ? (piece.price || '') : '';
    document.getElementById('art-piece-notes').value = piece ? (piece.notes || '') : '';
    document.getElementById('art-piece-modal-title').textContent = piece ? 'Edit Piece' : 'Add Piece';
    showModal('art-piece-modal');
}
function artSavePiece() {
    _wsGenericSave('art-piece', 'studio', 'pieces', {
        name: 'text', type: 'text', status: 'text', price: 'number', notes: 'text'
    }, 'art-piece-modal');
}

// Artist: Generate Image
function artShowGenModal() {
    document.getElementById('art-gen-prompt').value = '';
    document.getElementById('art-gen-style').value = '';
    document.getElementById('art-gen-width').value = '1024';
    document.getElementById('art-gen-height').value = '1024';
    document.getElementById('art-gen-provider').value = 'auto';
    document.getElementById('art-gen-register').checked = false;
    document.getElementById('art-gen-result').style.display = 'none';
    document.getElementById('art-gen-btn').disabled = false;
    document.getElementById('art-gen-btn').innerHTML = '<i class="fas fa-wand-magic-sparkles"></i> Generate';
    showModal('art-gen-modal');
}

function artGenerateImage() {
    const prompt = document.getElementById('art-gen-prompt').value.trim();
    if (!prompt) { document.getElementById('art-gen-prompt').focus(); return; }

    const btn = document.getElementById('art-gen-btn');
    btn.disabled = true;
    btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...';
    document.getElementById('art-gen-result').style.display = 'none';

    const payload = {
        mode: 'tool',
        tool: 'generate_image',
        tool_args: {
            prompt: prompt,
            style: document.getElementById('art-gen-style').value,
            provider: document.getElementById('art-gen-provider').value,
            width: parseInt(document.getElementById('art-gen-width').value),
            height: parseInt(document.getElementById('art-gen-height').value),
            register_in_gallery: document.getElementById('art-gen-register').checked,
        }
    };

    apiFetch('/api/workspace/action', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(payload)
    })
    .then(r => r.json())
    .then(data => {
        btn.disabled = false;
        btn.innerHTML = '<i class="fas fa-wand-magic-sparkles"></i> Generate';
        const resultDiv = document.getElementById('art-gen-result');
        const preview = document.getElementById('art-gen-preview');
        const text = document.getElementById('art-gen-result-text');

        if (data.error) {
            resultDiv.style.display = 'block';
            preview.style.display = 'none';
            text.textContent = 'Error: ' + data.error;
            return;
        }

        resultDiv.style.display = 'block';
        const result = data.result || data;
        const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
        // Tool output: "File: /path/to/image.png"
        const match = resultStr.match(/File:\s*(.+\.(png|jpg|jpeg|webp))/i);
        if (match) {
            const filename = match[1].trim().split('/').pop();
            preview.style.display = 'block';
            preview.src = '/api/gallery/generated/' + encodeURIComponent(filename) + '?t=' + Date.now();
            preview.onerror = function() { preview.style.display = 'none'; };
        } else {
            preview.style.display = 'none';
        }
        text.textContent = resultStr;
    })
    .catch(err => {
        btn.disabled = false;
        btn.innerHTML = '<i class="fas fa-wand-magic-sparkles"></i> Generate';
        document.getElementById('art-gen-result').style.display = 'block';
        document.getElementById('art-gen-preview').style.display = 'none';
        document.getElementById('art-gen-result-text').textContent = 'Error: ' + err.message;
    });
}

// Artist: Commission
function artShowCommissionModal(comm) {
    _wsGenericShow('art-commission', 'studio', 'commissions', comm);
    document.getElementById('art-commission-client').value = comm ? (comm.client || '') : '';
    document.getElementById('art-commission-desc').value = comm ? (comm.description || '') : '';
    document.getElementById('art-commission-price').value = comm ? (comm.price || '') : '';
    document.getElementById('art-commission-deposit').value = comm ? (comm.deposit || '') : '';
    document.getElementById('art-commission-due').value = comm ? (comm.due_date || '').slice(0, 10) : '';
    document.getElementById('art-commission-status').value = comm ? (comm.status || 'inquiry') : 'inquiry';
    document.getElementById('art-commission-modal-title').textContent = comm ? 'Edit Commission' : 'New Commission';
    showModal('art-commission-modal');
}
function artSaveCommission() {
    const extra = {
        due_date: document.getElementById('art-commission-due').value,
        description: document.getElementById('art-commission-desc').value.trim(),
    };
    _wsGenericSave('art-commission', 'studio', 'commissions', {
        client: 'text', price: 'number', deposit: 'number', status: 'text'
    }, 'art-commission-modal', extra);
}

// Artist: Show
function artShowShowModal(show) {
    _wsGenericShow('art-show', 'studio', 'shows', show);
    document.getElementById('art-show-name').value = show ? (show.name || '') : '';
    document.getElementById('art-show-venue').value = show ? (show.venue || '') : '';
    document.getElementById('art-show-date').value = show ? (show.date || '').slice(0, 10) : '';
    document.getElementById('art-show-type').value = show ? (show.type || 'exhibition') : 'exhibition';
    document.getElementById('art-show-status').value = show ? (show.status || 'planned') : 'planned';
    document.getElementById('art-show-modal-title').textContent = show ? 'Edit Show' : 'Add Show';
    showModal('art-show-modal');
}
function artSaveShow() {
    _wsGenericSave('art-show', 'studio', 'shows', {
        name: 'text', venue: 'text', date: 'text', type: 'text', status: 'text'
    }, 'art-show-modal');
}

// Artist: Client
function artShowClientModal(client) {
    _wsGenericShow('art-client', 'studio', 'clients', client);
    document.getElementById('art-client-name').value = client ? (client.name || '') : '';
    document.getElementById('art-client-email').value = client ? (client.email || '') : '';
    document.getElementById('art-client-notes').value = client ? (client.notes || '') : '';
    document.getElementById('art-client-modal-title').textContent = client ? 'Edit Client' : 'Add Client';
    showModal('art-client-modal');
}
function artSaveClient() {
    _wsGenericSave('art-client', 'studio', 'clients', {
        name: 'text', email: 'text', notes: 'text'
    }, 'art-client-modal');
}

// ── Chef Modal JS ──

// Chef: Recipe
function chefShowRecipeModal(recipe) {
    _wsGenericShow('chef-recipe', 'kitchen', 'recipes', recipe);
    document.getElementById('chef-recipe-name').value = recipe ? (recipe.name || '') : '';
    document.getElementById('chef-recipe-category').value = recipe ? (recipe.category || 'entree') : 'entree';
    document.getElementById('chef-recipe-servings').value = recipe ? (recipe.servings || '') : '';
    document.getElementById('chef-recipe-cost').value = recipe ? (recipe.cost_per_serving || '') : '';
    document.getElementById('chef-recipe-prep').value = recipe ? (recipe.prep_time || '') : '';
    document.getElementById('chef-recipe-notes').value = recipe ? (recipe.notes || '') : '';
    document.getElementById('chef-recipe-modal-title').textContent = recipe ? 'Edit Recipe' : 'Add Recipe';
    showModal('chef-recipe-modal');
}
function chefSaveRecipe() {
    const extra = {
        cost_per_serving: parseFloat(document.getElementById('chef-recipe-cost').value) || null,
        prep_time: parseInt(document.getElementById('chef-recipe-prep').value) || null,
    };
    _wsGenericSave('chef-recipe', 'kitchen', 'recipes', {
        name: 'text', category: 'text', servings: 'number', notes: 'text'
    }, 'chef-recipe-modal', extra);
}

// Chef: Menu
function chefShowMenuModal(menu) {
    _wsGenericShow('chef-menu', 'kitchen', 'menus', menu);
    document.getElementById('chef-menu-name').value = menu ? (menu.name || '') : '';
    document.getElementById('chef-menu-type').value = menu ? (menu.type || 'dinner') : 'dinner';
    document.getElementById('chef-menu-status').value = menu ? (menu.status || 'draft') : 'draft';
    document.getElementById('chef-menu-notes').value = menu ? (menu.notes || '') : '';
    document.getElementById('chef-menu-modal-title').textContent = menu ? 'Edit Menu' : 'Add Menu';
    showModal('chef-menu-modal');
}
function chefSaveMenu() {
    _wsGenericSave('chef-menu', 'kitchen', 'menus', {
        name: 'text', type: 'text', status: 'text', notes: 'text'
    }, 'chef-menu-modal');
}

// Chef: Vendor
function chefShowVendorModal(vendor) {
    _wsGenericShow('chef-vendor', 'kitchen', 'vendors', vendor);
    document.getElementById('chef-vendor-name').value = vendor ? (vendor.name || '') : '';
    document.getElementById('chef-vendor-category').value = vendor ? (vendor.category || 'produce') : 'produce';
    document.getElementById('chef-vendor-contact').value = vendor ? (vendor.contact || '') : '';
    document.getElementById('chef-vendor-phone').value = vendor ? (vendor.phone || '') : '';
    document.getElementById('chef-vendor-order-day').value = vendor ? (vendor.order_day || '') : '';
    document.getElementById('chef-vendor-notes').value = vendor ? (vendor.notes || '') : '';
    document.getElementById('chef-vendor-modal-title').textContent = vendor ? 'Edit Vendor' : 'Add Vendor';
    showModal('chef-vendor-modal');
}
function chefSaveVendor() {
    const extra = {order_day: document.getElementById('chef-vendor-order-day').value};
    _wsGenericSave('chef-vendor', 'kitchen', 'vendors', {
        name: 'text', category: 'text', contact: 'text', phone: 'text', notes: 'text'
    }, 'chef-vendor-modal', extra);
}

// ── Security Incident Modal ──
function secShowIncidentModal(inc) {
    _wsGenericShow('sec-incident', 'security', 'incidents', inc);
    document.getElementById('sec-incident-name').value = inc ? (inc.name || '') : '';
    document.getElementById('sec-incident-severity').value = inc ? (inc.severity || 'medium') : 'medium';
    document.getElementById('sec-incident-status').value = inc ? (inc.status || 'open') : 'open';
    document.getElementById('sec-incident-notes').value = inc ? (inc.notes || '') : '';
    document.getElementById('sec-incident-modal-title').textContent = inc ? 'Edit Incident' : 'Log Incident';
    showModal('sec-incident-modal');
}
function secSaveIncident() {
    _wsGenericSave('sec-incident', 'security', 'incidents', {
        name: 'text', severity: 'text', status: 'text', notes: 'text'
    }, 'sec-incident-modal');
}

// ── OSINT Investigation Modal ──
function osintShowInvestigationModal(inv) {
    _wsGenericShow('osint-investigation', 'osint', 'investigations', inv);
    document.getElementById('osint-investigation-name').value = inv ? (inv.name || '') : '';
    document.getElementById('osint-investigation-type').value = inv ? (inv.type || 'domain') : 'domain';
    document.getElementById('osint-investigation-status').value = inv ? (inv.status || 'open') : 'open';
    document.getElementById('osint-investigation-notes').value = inv ? (inv.notes || '') : '';
    document.getElementById('osint-investigation-modal-title').textContent = inv ? 'Edit Investigation' : 'New Investigation';
    showModal('osint-investigation-modal');
}
function osintSaveInvestigation() {
    _wsGenericSave('osint-investigation', 'osint', 'investigations', {
        name: 'text', type: 'text', status: 'text', notes: 'text'
    }, 'osint-investigation-modal');
}

// ── Grant Search Modal ──

let _gsCurrentOffset = 0;
let _gsCurrentTotal = 0;

async function npShowGrantSearchModal() {
    document.getElementById('np-gs-keyword').value = '';
    document.getElementById('np-gs-category').value = '';
    document.getElementById('np-gs-posted').value = '';
    document.getElementById('np-gs-sort').value = '';
    document.getElementById('np-gs-results').innerHTML = '';
    _gsCurrentOffset = 0;
    _gsCurrentTotal = 0;
    showModal('np-grant-search-modal');
    document.getElementById('np-gs-keyword').focus();
}

function _gsRenderRow(g) {
    const floor = g.award_floor ? '$' + Number(g.award_floor).toLocaleString() : '';
    const ceil = g.award_ceiling ? '$' + Number(g.award_ceiling).toLocaleString() : '';
    const range = (floor || ceil) ? (floor && ceil ? floor + ' – ' + ceil : (ceil || floor)) : '—';
    const closeDate = g.close_date || '<span style="color:var(--text-dim)">Open</span>';
    const openDate = g.open_date || '—';
    const title = escapeHtml(g.title || 'Untitled');
    const oppUrl = g.number ? 'https://grants.gov/search-results-detail/' + encodeURIComponent(g.number) : '';
    const titleLink = oppUrl
        ? '<a href="' + oppUrl + '" target="_blank" rel="noopener" style="color:var(--accent)" title="View on Grants.gov">' + title + '</a>'
        : title;
    const statusBadge = g.status === 'forecasted'
        ? ' <span style="font-size:0.7em;padding:1px 5px;border-radius:3px;background:var(--warning-dim);color:var(--warning)">Forecasted</span>'
        : '';
    return '<tr>' +
        '<td style="max-width:280px;overflow:hidden;text-overflow:ellipsis">' + titleLink + statusBadge + '</td>' +
        '<td style="white-space:nowrap">' + escapeHtml(g.agency || '') + '</td>' +
        '<td style="white-space:nowrap">' + openDate + '</td>' +
        '<td style="white-space:nowrap">' + closeDate + '</td>' +
        '<td style="white-space:nowrap">' + range + '</td>' +
        '<td><button class="btn btn-secondary" style="font-size:0.75em;padding:3px 8px;white-space:nowrap" ' +
        'onclick="npTrackGrantFromSearch(\'' + (g.title||'').replace(/'/g,"\\'") + '\',\'' +
        (g.agency||'').replace(/'/g,"\\'") + '\',\'' + (g.close_date||'') + '\',' +
        (g.award_ceiling||0) + ',\'' + (g.number||'') + '\')"><i class="fas fa-plus"></i> Track</button></td>' +
        '</tr>';
}

async function npSearchGrants(loadMore) {
    const keyword = document.getElementById('np-gs-keyword').value.trim();
    const category = document.getElementById('np-gs-category').value;
    const dateRange = document.getElementById('np-gs-posted').value;
    const sortBy = document.getElementById('np-gs-sort').value;
    if (!keyword && !category) { showToast('Enter a keyword or select a category', 'error'); return; }

    const btn = document.getElementById('np-gs-btn');
    const resultsDiv = document.getElementById('np-gs-results');

    if (!loadMore) {
        _gsCurrentOffset = 0;
        btn.disabled = true;
        btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
        resultsDiv.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:24px"><i class="fas fa-spinner fa-spin" style="font-size:1.5em"></i><br>Searching Grants.gov...</p>';
    }

    try {
        const params = new URLSearchParams();
        if (keyword) params.set('keyword', keyword);
        if (category) params.set('category', category);
        if (dateRange) params.set('dateRange', dateRange);
        if (sortBy) params.set('sortBy', sortBy);
        params.set('limit', '25');
        params.set('offset', String(_gsCurrentOffset));

        const r = await apiFetch(API + '/api/grants-gov/search?' + params.toString());
        const data = await r.json();
        if (data.error) throw new Error(data.error);

        const grants = data.grants || [];
        const total = data.total || grants.length;
        _gsCurrentTotal = total;

        if (!grants.length && _gsCurrentOffset === 0) {
            resultsDiv.innerHTML =
                '<div style="text-align:center;padding:32px;color:var(--text-dim)">' +
                '<i class="fas fa-search" style="font-size:2em;opacity:0.3;margin-bottom:12px;display:block"></i>' +
                '<p style="margin:0 0 8px;font-weight:500">No matching opportunities found</p>' +
                '<p style="font-size:0.85em;margin:0">Try broader keywords, a different category, or check <a href="https://grants.gov/search-grants" target="_blank" rel="noopener" style="color:var(--accent)">Grants.gov</a> directly.</p>' +
                '</div>';
            return;
        }

        // Build or append rows
        let tbody;
        if (loadMore) {
            tbody = resultsDiv.querySelector('tbody');
            // Remove old Load More button
            const oldBtn = document.getElementById('np-gs-load-more');
            if (oldBtn) oldBtn.remove();
        } else {
            let html = '<p id="np-gs-count" style="color:var(--text-dim);font-size:0.82em;margin:0 0 8px"></p>';
            html += '<div style="overflow-x:auto"><table class="ws-table" style="font-size:0.82em"><thead><tr>' +
                '<th style="min-width:200px">Title</th><th>Agency</th><th>Posted</th><th>Close Date</th><th>Award Range</th><th></th>' +
                '</tr></thead><tbody id="np-gs-tbody"></tbody></table></div>';
            resultsDiv.innerHTML = html;
            tbody = document.getElementById('np-gs-tbody');
        }

        grants.forEach(g => { tbody.insertAdjacentHTML('beforeend', _gsRenderRow(g)); });
        _gsCurrentOffset += grants.length;

        // Update count
        const countEl = document.getElementById('np-gs-count');
        if (countEl) countEl.textContent = 'Showing ' + _gsCurrentOffset + ' of ' + total.toLocaleString() + ' opportunities';

        // Load More button
        if (data.has_more) {
            const loadMoreHtml = '<div id="np-gs-load-more" style="text-align:center;padding:12px">' +
                '<button class="btn btn-secondary" onclick="npSearchGrants(true)" id="np-gs-more-btn">' +
                '<i class="fas fa-arrow-down"></i> Load More (' + (_gsCurrentTotal - _gsCurrentOffset).toLocaleString() + ' remaining)</button></div>';
            resultsDiv.insertAdjacentHTML('beforeend', loadMoreHtml);
        }
    } catch(e) {
        if (_gsCurrentOffset === 0) {
            resultsDiv.innerHTML =
                '<div style="text-align:center;padding:24px;color:var(--error)">' +
                '<i class="fas fa-exclamation-triangle" style="font-size:1.5em;margin-bottom:8px;display:block"></i>' +
                '<p style="margin:0">Search failed: ' + escapeHtml(e.message) + '</p>' +
                '<p style="font-size:0.82em;color:var(--text-dim);margin:4px 0 0">Check your network connection or try again.</p>' +
                '</div>';
        } else {
            showToast('Load more failed: ' + e.message, 'error');
        }
    } finally {
        btn.disabled = false;
        btn.innerHTML = '<i class="fas fa-search"></i> Search';
    }
}

async function npTrackGrantFromSearch(title, agency, deadline, amount, oppNumber) {
    try {
        await apiFetch(API + '/api/nonprofit/grants', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                name: title,
                funder: agency,
                deadline: deadline,
                amount: amount,
                status: 'prospect',
                opportunity_number: oppNumber,
                grant_type: 'federal',
            }),
        });
        showToast('Grant added to pipeline!', 'success');
    } catch(e) { showToast('Error: ' + e.message, 'error'); }
}

// ── Legislation Search ──

let _legCurrentOffset = 0;
let _legWatchKeywords = [];  // cached from server

function _legLevelChanged(level) {
    document.getElementById('np-leg-congress-wrap').style.display = level === 'federal' ? '' : 'none';
    document.getElementById('np-leg-state-wrap').style.display = level === 'state' ? '' : 'none';
    document.getElementById('np-leg-city-wrap').style.display = level === 'city' ? '' : 'none';
}

async function npShowLegislationModal() {
    document.getElementById('np-leg-query').value = '';
    document.getElementById('np-leg-results').innerHTML = '';
    _legCurrentOffset = 0;
    // Pre-load watchlist keywords for highlighting
    try {
        const r = await apiFetch(API + '/api/nonprofit/watchlist');
        const d = await r.json();
        _legWatchKeywords = (d.watchlist || []).map(w => w.keyword.toLowerCase());
    } catch(e) { _legWatchKeywords = []; }
    showModal('np-legis-modal');
    document.getElementById('np-leg-query').focus();
}

function _legMatchesWatchlist(title) {
    const t = (title || '').toLowerCase();
    return _legWatchKeywords.filter(kw => t.includes(kw));
}

function _legRenderRow(b) {
    const title = escapeHtml(b.title || 'Untitled');
    const matches = _legMatchesWatchlist(b.title);
    const watchBadge = matches.length
        ? ' <span style="font-size:0.7em;padding:1px 5px;border-radius:3px;background:var(--accent);color:#fff" title="Matches: ' + escapeHtml(matches.join(', ')) + '">&#x1f514; Watched</span>'
        : '';
    const billUrl = 'https://congress.gov/bill/' + b.congress + 'th-congress/' +
        (b.chamber === 'House' ? 'house' : 'senate') + '-bill/' + b.number;
    const titleLink = '<a href="' + billUrl + '" target="_blank" rel="noopener" style="color:var(--accent)">' + title + '</a>';
    const actionDate = b.latest_action_date || '—';
    const action = escapeHtml((b.latest_action || '').substring(0, 80));
    const bSafe = JSON.stringify(b).replace(/'/g, '&#39;').replace(/"/g, '&quot;');
    return '<tr>' +
        '<td style="white-space:nowrap;font-weight:500">' + escapeHtml(b.bill_id) + '</td>' +
        '<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis">' + titleLink + watchBadge + '</td>' +
        '<td style="white-space:nowrap">' + escapeHtml(b.chamber || '') + '</td>' +
        '<td style="white-space:nowrap;font-size:0.85em">' + actionDate + '</td>' +
        '<td style="font-size:0.82em;color:var(--text-dim);max-width:200px;overflow:hidden;text-overflow:ellipsis">' + action + '</td>' +
        '<td><button class="btn btn-secondary" style="font-size:0.75em;padding:3px 8px;white-space:nowrap" ' +
        'onclick=\'npTrackBill(' + bSafe + ')\'><i class="fas fa-plus"></i> Track</button></td>' +
        '</tr>';
}

let _legCurrentPage = 1;

async function npSearchLegislation(loadMore) {
    const query = document.getElementById('np-leg-query').value.trim();
    const level = document.getElementById('np-leg-level').value;
    const congress = document.getElementById('np-leg-congress').value;
    const state = document.getElementById('np-leg-state').value;
    const city = document.getElementById('np-leg-city').value;
    if (!query) { showToast('Enter a search keyword', 'error'); return; }
    if (level === 'state' && !state) { showToast('Select a state', 'error'); return; }

    const btn = document.getElementById('np-leg-btn');
    const resultsDiv = document.getElementById('np-leg-results');

    if (!loadMore) {
        _legCurrentOffset = 0;
        _legCurrentPage = 1;
        btn.disabled = true;
        btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
        const source = level === 'state' ? 'state legislature' : 'Congress.gov';
        resultsDiv.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:24px"><i class="fas fa-spinner fa-spin" style="font-size:1.5em"></i><br>Searching ' + source + '...</p>';
    }

    try {
        let r;
        if (level === 'city') {
            const params = new URLSearchParams({
                query, city, limit: '25', page: String(_legCurrentPage),
            });
            r = await apiFetch(API + '/api/legislation/search/city?' + params.toString());
        } else if (level === 'state') {
            const params = new URLSearchParams({
                query, state, limit: '25', page: String(_legCurrentPage),
            });
            r = await apiFetch(API + '/api/legislation/search/state?' + params.toString());
        } else {
            const params = new URLSearchParams({
                query, congress, limit: '25', offset: String(_legCurrentOffset),
            });
            r = await apiFetch(API + '/api/legislation/search?' + params.toString());
        }
        const data = await r.json();
        if (data.error) {
            if (data.setup_hint) {
                resultsDiv.innerHTML =
                    '<div style="text-align:center;padding:32px;color:var(--text-dim)">' +
                    '<i class="fas fa-key" style="font-size:2em;opacity:0.3;margin-bottom:12px;display:block"></i>' +
                    '<p style="margin:0 0 8px;font-weight:500">State search needs an API key</p>' +
                    '<p style="font-size:0.85em;margin:0">' + escapeHtml(data.error) + '</p></div>';
                return;
            }
            throw new Error(data.error);
        }

        const bills = data.bills || [];
        const total = data.total || bills.length;

        if (!bills.length && _legCurrentOffset === 0) {
            resultsDiv.innerHTML =
                '<div style="text-align:center;padding:32px;color:var(--text-dim)">' +
                '<i class="fas fa-landmark" style="font-size:2em;opacity:0.3;margin-bottom:12px;display:block"></i>' +
                '<p style="margin:0 0 8px;font-weight:500">No matching bills found</p>' +
                '<p style="font-size:0.85em;margin:0">Try different keywords or a different Congress session.</p></div>';
            return;
        }

        let tbody;
        if (loadMore) {
            tbody = resultsDiv.querySelector('tbody');
            const oldBtn = document.getElementById('np-leg-load-more');
            if (oldBtn) oldBtn.remove();
        } else {
            let html = '<p id="np-leg-count" style="color:var(--text-dim);font-size:0.82em;margin:0 0 8px"></p>';
            html += '<div style="overflow-x:auto"><table class="ws-table" style="font-size:0.82em"><thead><tr>' +
                '<th>Bill</th><th style="min-width:200px">Title</th><th>Chamber</th><th>Last Action</th><th>Status</th><th></th>' +
                '</tr></thead><tbody id="np-leg-tbody"></tbody></table></div>';
            resultsDiv.innerHTML = html;
            tbody = document.getElementById('np-leg-tbody');
        }

        bills.forEach(b => { tbody.insertAdjacentHTML('beforeend', _legRenderRow(b)); });
        _legCurrentOffset += bills.length;
        _legCurrentPage += 1;

        const countEl = document.getElementById('np-leg-count');
        if (countEl) {
            const watchNote = _legWatchKeywords.length
                ? ' &bull; <span style="color:var(--accent)">&#x1f514; ' + _legWatchKeywords.length + ' watched keyword' + (_legWatchKeywords.length !== 1 ? 's' : '') + '</span>'
                : '';
            const sourceNote = data.source ? ' <span style="font-size:0.85em;color:var(--text-dim)">via ' + data.source + '</span>' : '';
            countEl.innerHTML = 'Showing ' + _legCurrentOffset + ' of ' + total.toLocaleString() + ' bills' + sourceNote + watchNote;
        }

        if (data.has_more) {
            const remaining = total - _legCurrentOffset;
            resultsDiv.insertAdjacentHTML('beforeend',
                '<div id="np-leg-load-more" style="text-align:center;padding:12px">' +
                '<button class="btn btn-secondary" onclick="npSearchLegislation(true)">' +
                '<i class="fas fa-arrow-down"></i> Load More' + (remaining > 0 ? ' (' + remaining.toLocaleString() + ' remaining)' : '') + '</button></div>');
        }
    } catch(e) {
        if (_legCurrentOffset === 0) {
            resultsDiv.innerHTML =
                '<div style="text-align:center;padding:24px;color:var(--error)">' +
                '<i class="fas fa-exclamation-triangle" style="font-size:1.5em;margin-bottom:8px;display:block"></i>' +
                '<p style="margin:0">Search failed: ' + escapeHtml(e.message) + '</p></div>';
        } else {
            showToast('Load more failed: ' + e.message, 'error');
        }
    } finally {
        btn.disabled = false;
        btn.innerHTML = '<i class="fas fa-search"></i> Search';
    }
}

async function npTrackBill(bill) {
    try {
        const jurisdiction = bill.level === 'state' ? (bill.state || '') : 'federal';
        await apiFetch(API + '/api/nonprofit/legislation', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                bill_id: bill.bill_id,
                title: bill.title,
                chamber: bill.chamber,
                congress: bill.congress,
                latest_action: bill.latest_action,
                latest_action_date: bill.latest_action_date,
                url: bill.url || '',
                level: bill.level || 'federal',
                state: bill.state || '',
                sponsor: bill.sponsor || '',
            }),
        });
        showToast('Bill tracked: ' + bill.bill_id, 'success');
    } catch(e) {
        if (e.message && e.message.includes('already tracked')) {
            showToast('Already tracking ' + bill.bill_id, 'info');
        } else {
            showToast('Error: ' + e.message, 'error');
        }
    }
}

// ── Bill Keyword Watchlist ──

async function npShowWatchlistModal() {
    showModal('np-watchlist-modal');
    document.getElementById('np-wl-keyword').value = '';
    await npLoadWatchlist();
    document.getElementById('np-wl-keyword').focus();
}

async function npLoadWatchlist() {
    const listDiv = document.getElementById('np-wl-list');
    try {
        const r = await apiFetch(API + '/api/nonprofit/watchlist');
        const data = await r.json();
        const watchlist = data.watchlist || [];
        _legWatchKeywords = watchlist.map(w => w.keyword.toLowerCase());

        if (!watchlist.length) {
            listDiv.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:16px">No keywords yet. Add keywords to flag matching bills in search results.</p>';
            return;
        }

        let html = '<div style="display:flex;flex-direction:column;gap:6px">';
        watchlist.forEach(w => {
            html += '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--bg-card, rgba(255,255,255,0.03));border-radius:6px">' +
                '<i class="fas fa-bell" style="color:var(--accent);font-size:0.85em"></i>' +
                '<span style="flex:1;font-weight:500">' + escapeHtml(w.keyword) + '</span>' +
                '<span style="color:var(--text-dim);font-size:0.8em">' + (w.created_at || '').substring(0, 10) + '</span>' +
                '<button class="em-trash" onclick="npRemoveWatchKeyword(\'' + w.id + '\')" title="Remove"><i class="fas fa-times"></i></button>' +
                '</div>';
        });
        html += '</div>';
        listDiv.innerHTML = html;
    } catch(e) {
        listDiv.innerHTML = '<p style="color:var(--error)">Failed to load watchlist.</p>';
    }
}

async function npAddWatchKeyword() {
    const input = document.getElementById('np-wl-keyword');
    const keyword = input.value.trim();
    if (!keyword) { showToast('Enter a keyword', 'error'); return; }
    try {
        const r = await apiFetch(API + '/api/nonprofit/watchlist', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({keyword}),
        });
        const data = await r.json();
        showToast(data.message || 'Added!', 'success');
        input.value = '';
        await npLoadWatchlist();
    } catch(e) { showToast('Error: ' + e.message, 'error'); }
}

async function npRemoveWatchKeyword(id) {
    try {
        await apiFetch(API + '/api/nonprofit/watchlist/' + id, {method: 'DELETE'});
        showToast('Keyword removed', 'success');
        await npLoadWatchlist();
    } catch(e) { showToast('Error: ' + e.message, 'error'); }
}

// Section-level action buttons (Add, Import) for nonprofit/bookkeeping/cases tables
const _WS_SECTION_ACTIONS = {
    donors: {
        add: {label: 'Add Donor', icon: 'fa-user-plus', fn: 'npShowDonorModal()'},
        donate: {label: 'Log Donation', icon: 'fa-hand-holding-dollar', fn: 'npShowDonationModal()'},
        import: {label: 'Import CSV', icon: 'fa-file-import', fn: 'npShowImportModal()'},
        export: {label: 'Export CSV', icon: 'fa-download', fn: 'npExport("donors")', secondary: true},
        thankyou: {label: 'Thank-You Letters', icon: 'fa-envelope-open-text', fn: 'npShowThankYouModal()', secondary: true},
    },
    grants: {
        add: {label: 'Add Grant', icon: 'fa-plus', fn: 'npShowGrantModal()'},
        export: {label: 'Export CSV', icon: 'fa-download', fn: 'npExport("grants")', secondary: true},
    },
    notes: {
        add: {label: 'Add Note', icon: 'fa-plus', fn: 'npShowNoteModal()'},
    },
    financials: {
        income: {label: 'Log Donation', icon: 'fa-hand-holding-dollar', fn: 'npShowDonationModal()'},
        expense: {label: 'Log Expense', icon: 'fa-receipt', fn: 'npShowExpenseModal()'},
        export: {label: 'Export CSV', icon: 'fa-download', fn: 'npExport("transactions")', secondary: true},
        boardrpt: {label: 'Board Report', icon: 'fa-file-export', fn: 'npGenerateBoardReport()', secondary: true},
        data990: {label: '990 Data', icon: 'fa-file-invoice', fn: 'npGenerate990()', secondary: true},
    },
    // Grant search section
    grant_search: {
        search: {label: 'Search Grants', icon: 'fa-magnifying-glass-dollar', fn: 'npShowGrantSearchModal()'},
    },
    legislation: {
        search: {label: 'Search Bills', icon: 'fa-landmark', fn: 'npShowLegislationModal()'},
        watchlist: {label: 'Watchlist', icon: 'fa-bell', fn: 'npShowWatchlistModal()'},
    },
    // Email section
    email_inbox: {
        sort: {label: 'Sort Inbox', icon: 'fa-inbox', fn: 'quickCommand("Triage_my_inbox_—_what_emails_need_a_reply?_Flag_anything_from_donors,_board_members,_or_grant_program_officers.")'},
        draft: {label: 'Draft Email', icon: 'fa-pen-to-square', fn: 'quickCommand("Help_me_draft_an_email.")'},
        full: {label: 'Open Email', icon: 'fa-arrow-up-right-from-square', fn: 'switchPanel(\"email\")', secondary: true},
    },
    // Schedule section
    schedule: {
        briefing: {label: 'Morning Briefing', icon: 'fa-sun', fn: 'quickCommand("Give_me_my_morning_briefing.")'},
        full: {label: 'Open Calendar', icon: 'fa-arrow-up-right-from-square', fn: 'switchPanel(\"calendar\")', secondary: true},
    },
    // Social Worker sections
    cases: {
        add: {label: 'New Case', icon: 'fa-folder-plus', fn: 'swShowCaseModal()'},
        assess: {label: 'Assessment', icon: 'fa-clipboard-check', fn: 'swShowAssessmentModal()', secondary: true},
        safety: {label: 'Safety Plan', icon: 'fa-shield-heart', fn: 'swShowSafetyPlanModal()', secondary: true},
    },
    resources: {
        add: {label: 'Add Resource', icon: 'fa-plus', fn: 'swShowResourceModal()'},
    },
    case_notes: {
        add: {label: 'Add Note', icon: 'fa-pen-to-square', fn: 'swShowNoteModal()'},
    },
    crisis_log: {
        add: {label: 'Log Crisis', icon: 'fa-triangle-exclamation', fn: 'swShowCrisisModal()'},
    },
};

function _renderSectionActionBar(sectionKey) {
    const actions = _WS_SECTION_ACTIONS[sectionKey];
    if (!actions) return '';
    let html = '<div style="display:flex;gap:8px;padding:0 0 12px;flex-wrap:wrap;align-items:center">';
    for (const [key, act] of Object.entries(actions)) {
        const btnClass = act.secondary ? 'btn btn-secondary' : 'btn btn-primary';
        html += '<button class="' + btnClass + '" style="font-size:0.8rem;padding:6px 14px" onclick="' + act.fn + '">' +
            '<i class="fas ' + act.icon + '"></i> ' + escapeHtml(act.label) + '</button>';
    }
    html += '</div>';
    return html;
}

function npExport(type) {
    const urls = {
        donors: '/api/nonprofit/donors/export',
        grants: '/api/nonprofit/grants/export',
        transactions: '/api/bookkeeping/transactions/export',
    };
    const url = urls[type];
    if (!url) return;
    // Download via hidden link with auth
    const a = document.createElement('a');
    a.href = API + url + (_apiKey ? '?api_key=' + encodeURIComponent(_apiKey) : '');
    a.download = type + '.csv';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
}

// ── Document generation functions ──

function npShowThankYouModal(donorName, amount, date) {
    document.getElementById('np-ty-donor').value = donorName || '';
    document.getElementById('np-ty-amount').value = amount || '';
    document.getElementById('np-ty-date').value = date || new Date().toISOString().slice(0, 10);
    document.getElementById('np-ty-org').value = '';
    document.getElementById('np-ty-impact').value = '';
    document.getElementById('np-ty-note').value = '';
    document.getElementById('np-ty-signer').value = '';
    const st = document.getElementById('np-ty-status');
    st.style.display = 'none';
    showModal('np-thankyou-modal');
}

async function npGenerateThankYou() {
    const st = document.getElementById('np-ty-status');
    st.style.display = 'block';
    st.style.background = 'var(--bg-input)';
    st.style.color = 'var(--text-dim)';
    st.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating letter...';
    try {
        const res = await apiFetch(API + '/api/nonprofit/generate/thankyou', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                donor_name: document.getElementById('np-ty-donor').value,
                amount: document.getElementById('np-ty-amount').value,
                gift_date: document.getElementById('np-ty-date').value,
                organization: document.getElementById('np-ty-org').value,
                impact_area: document.getElementById('np-ty-impact').value,
                personal_note: document.getElementById('np-ty-note').value,
                signer_name: document.getElementById('np-ty-signer').value,
            }),
        });
        if (res.ok) {
            const blob = await res.blob();
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'thank_you_letter.docx';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            st.style.background = 'rgba(22,163,74,0.13)';
            st.style.color = 'var(--success)';
            st.innerHTML = '<i class="fas fa-check"></i> Letter downloaded!';
        } else {
            const data = await res.json();
            st.style.background = 'rgba(220,38,38,0.13)';
            st.style.color = 'var(--error)';
            st.textContent = data.error || data.message || 'Generation failed';
        }
    } catch (e) {
        st.style.background = 'rgba(220,38,38,0.13)';
        st.style.color = 'var(--error)';
        st.textContent = 'Error: ' + e.message;
    }
}

function npShowReceiptModal(donorName, amount, date) {
    document.getElementById('np-rcpt-donor').value = donorName || '';
    document.getElementById('np-rcpt-amount').value = amount || '';
    document.getElementById('np-rcpt-date').value = date || new Date().toISOString().slice(0, 10);
    document.getElementById('np-rcpt-desc').value = 'Charitable donation';
    const st = document.getElementById('np-rcpt-status');
    st.style.display = 'none';
    showModal('np-receipt-modal');
}

async function npGenerateReceipt() {
    const st = document.getElementById('np-rcpt-status');
    st.style.display = 'block';
    st.style.background = 'var(--bg-input)';
    st.style.color = 'var(--text-dim)';
    st.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating receipt...';
    try {
        const res = await apiFetch(API + '/api/nonprofit/generate/receipt', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                donor_name: document.getElementById('np-rcpt-donor').value,
                amount: parseFloat(document.getElementById('np-rcpt-amount').value) || 0,
                date: document.getElementById('np-rcpt-date').value,
                description: document.getElementById('np-rcpt-desc').value,
            }),
        });
        if (res.ok) {
            const blob = await res.blob();
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'donation_receipt.docx';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            st.style.background = 'rgba(22,163,74,0.13)';
            st.style.color = 'var(--success)';
            st.innerHTML = '<i class="fas fa-check"></i> Receipt downloaded!';
        } else {
            const data = await res.json();
            st.style.background = 'rgba(220,38,38,0.13)';
            st.style.color = 'var(--error)';
            st.textContent = data.error || data.message || 'Generation failed';
        }
    } catch (e) {
        st.style.background = 'rgba(220,38,38,0.13)';
        st.style.color = 'var(--error)';
        st.textContent = 'Error: ' + e.message;
    }
}

async function npGenerateBoardReport() {
    showToast('Generating board report...', 'info');
    try {
        const res = await apiFetch(API + '/api/nonprofit/generate/board-report', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({}),
        });
        if (res.ok) {
            const blob = await res.blob();
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'board_report.docx';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            showToast('Board report downloaded!', 'success');
        } else {
            const data = await res.json();
            showToast(data.error || data.message || 'Generation failed', 'error');
        }
    } catch (e) {
        showToast('Error generating board report: ' + e.message, 'error');
    }
}

async function npGenerate990() {
    showToast('Generating 990 data export...', 'info');
    try {
        const res = await apiFetch(API + '/api/nonprofit/generate/990-data', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({}),
        });
        if (res.ok) {
            const blob = await res.blob();
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = '990_data_export.docx';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            showToast('990 data export downloaded!', 'success');
        } else {
            const data = await res.json();
            showToast(data.error || data.message || 'Generation failed', 'error');
        }
    } catch (e) {
        showToast('Error generating 990 data: ' + e.message, 'error');
    }
}

function renderWorkspaceTable(container, config, data) {
    // Action bar for nonprofit sections
    let actionBarHtml = _renderSectionActionBar(config.key);

    if (!data.rows || !data.rows.length) {
        let emptyHtml = actionBarHtml + '<div class="ws-empty">';
        if (data.service_status) {
            const ss = data.service_status;
            if (ss.status === 'running' && ss.url) {
                emptyHtml += '<i class="fas fa-circle" style="color:#22c55e;font-size:0.6em;vertical-align:middle;margin-right:6px"></i>' +
                    '<strong>' + escapeHtml(ss.display_name) + '</strong> is running at ' +
                    '<a href="' + escapeHtml(ss.url) + '" target="_blank" style="color:var(--accent)">' + escapeHtml(ss.url) + '</a>.<br>' +
                    '<span style="margin-top:8px;display:inline-block">' + escapeHtml(config.empty_message || 'No data yet.') + '</span>';
            } else if (ss.status === 'not_installed') {
                emptyHtml += '<i class="fas fa-circle" style="color:var(--text-dim);font-size:0.6em;vertical-align:middle;margin-right:6px"></i>' +
                    '<strong>' + escapeHtml(ss.display_name) + '</strong> is not deployed yet. ' +
                    'Use the workspace overview to deploy your server stack.';
            } else {
                emptyHtml += '<i class="fas fa-circle" style="color:var(--error-strong);font-size:0.6em;vertical-align:middle;margin-right:6px"></i>' +
                    '<strong>' + escapeHtml(ss.display_name) + '</strong> is ' + escapeHtml(ss.status) + '. ' +
                    escapeHtml(config.empty_message || 'No data.');
            }
        } else {
            emptyHtml += escapeHtml(config.empty_message || 'No data.');
        }
        emptyHtml += '</div>';
        container.innerHTML = emptyHtml;
        return;
    }
    const cols = config.columns || [];
    const actions = config.row_actions || [];
    // Nonprofit sections get inline edit/delete actions added dynamically
    const _npInlineActions = {
        donors: {edit: 'npShowDonorModal', del: 'npDeleteDonor', nameField: 'name', idField: 'id'},
        notes: {del: 'npDeleteNote', idField: 'id'},
        // Social Worker inline actions
        resources: {edit: 'swShowResourceModal', del: 'swDeleteResource', nameField: 'name', idField: 'id'},
        case_notes: {del: 'swDeleteNote', idField: 'id'},
        crisis_log: {del: 'swDeleteCrisis', idField: 'id'},
    };
    const npInline = _npInlineActions[config.key];
    let html = actionBarHtml + '<div class="ws-section-wrap"><table class="ws-table"><thead><tr>';
    for (const col of cols) {
        const sortable = col.sortable ? ' onclick="wsTableSort(\'' + escapeHtml(config.key) + '\',\'' + escapeHtml(col.key) + '\')"' : '';
        html += '<th' + sortable + '>' + escapeHtml(col.label);
        if (col.sortable) html += ' <span class="sort-arrow">&#x25B4;&#x25BE;</span>';
        html += '</th>';
    }
    const hasActions = actions.length || npInline;
    if (hasActions) html += '<th>Actions</th>';
    html += '</tr></thead><tbody>';
    for (const row of data.rows) {
        html += '<tr>';
        for (const col of cols) {
            const val = row[col.key];
            if (col.type === 'currency') html += '<td>' + _wsFormatValue(val, 'currency') + '</td>';
            else if (col.type === 'date') html += '<td>' + _wsFormatValue(val, 'date') + '</td>';
            else if (col.type === 'badge') {
                const bval = String(val || '');
                const isUp = bval === 'up' || bval === 'online' || bval === 'ok' || bval === 'pass' || bval === 'running';
                const isDown = bval === 'down' || bval === 'offline' || bval === 'fail' || bval === 'error' || bval === 'critical';
                const badgeStyle = isUp ? 'background:rgba(34,197,94,0.15);color:var(--success)' : isDown ? 'background:rgba(239,68,68,0.15);color:var(--error-strong)' : '';
                html += '<td><span class="ws-badge" style="' + badgeStyle + '">' + escapeHtml(bval) + '</span></td>';
            }
            else if (col.type === 'number') html += '<td>' + _wsFormatValue(val, 'number') + '</td>';
            else if (col.type === 'percent') {
                const pct = parseFloat(val) || 0;
                const barColor = pct > 90 ? 'var(--error-strong)' : pct > 70 ? '#f97316' : pct > 50 ? 'var(--warning)' : 'var(--success)';
                html += '<td><div style="display:flex;align-items:center;gap:8px;min-width:100px">' +
                    '<div style="flex:1;height:6px;background:var(--bg-dark);border-radius:3px;overflow:hidden">' +
                    '<div style="width:' + Math.min(pct, 100) + '%;height:100%;background:' + barColor + ';border-radius:3px;transition:width 0.3s"></div></div>' +
                    '<span style="font-size:0.8rem;min-width:40px;text-align:right">' + pct.toFixed(1) + '%</span></div></td>';
            }
            else if (config.key === 'donors' && col.key === 'name' && row.id) {
                html += '<td><a href="#" onclick="npShowDonorDetail(\'' + escapeHtml(row.id) + '\');return false" ' +
                    'style="color:var(--accent);text-decoration:none;font-weight:500">' + escapeHtml(String(val || '')) + '</a></td>';
            }
            else html += '<td>' + escapeHtml(String(val || '')) + '</td>';
        }
        if (hasActions) {
            html += '<td><div class="ws-row-actions">';
            // Standard row actions from workspace config
            for (const act of actions) {
                if (act.show_when_status && act.status_field) {
                    const rowStatus = String(row[act.status_field] || '');
                    if (!act.show_when_status.includes(rowStatus)) continue;
                }
                let actLabel = act.label || '';
                let actIcon = act.icon || 'fa-play';
                if (act.label_when && act.status_field) {
                    const st = String(row[act.status_field] || '');
                    actLabel = act.label_when[st] || actLabel;
                }
                if (act.icon_when && act.status_field) {
                    const st = String(row[act.status_field] || '');
                    actIcon = act.icon_when[st] || actIcon;
                }
                const actionId = 'wsa_' + Math.random().toString(36).slice(2, 10);
                if (!window._wsActions) window._wsActions = {};
                window._wsActions[actionId] = {act: act, row: row};
                html += '<button class="ws-row-action" onclick="wsRowAction(\'' + actionId + '\', event)">' +
                    '<i class="fas ' + escapeHtml(actIcon) + '"></i> ' + escapeHtml(actLabel) + '</button>';
            }
            // Nonprofit inline edit/delete
            if (npInline) {
                const rowId = row[npInline.idField] || '';
                const rowJson = JSON.stringify(row).replace(/'/g, "\\'").replace(/"/g, '&quot;');
                if (npInline.edit) {
                    html += '<button class="ws-row-action" onclick="' + npInline.edit + '(' + rowJson + ')">' +
                        '<i class="fas fa-pen"></i> Edit</button>';
                }
                if (npInline.del) {
                    const nameVal = npInline.nameField ? escapeHtml(String(row[npInline.nameField] || '')) : '';
                    html += '<button class="ws-row-action" style="color:var(--error-strong)" onclick="' + npInline.del +
                        '(\'' + escapeHtml(rowId) + '\',\'' + nameVal + '\')">' +
                        '<i class="fas fa-trash"></i></button>';
                }
            }
            html += '</div></td>';
        }
        html += '</tr>';
    }
    html += '</tbody></table>';
    if (data.pages > 1) {
        html += '<div style="padding:12px; text-align:center; color:var(--text-dim); font-size:0.8rem;">Page ' + data.page + ' of ' + data.pages + ' (' + data.total + ' total)</div>';
    }
    html += '</div>';
    container.innerHTML = html;
}

async function wsTableSort(sectionKey, colKey) {
    const sec = (WorkspaceState.config.sections || []).find(s => s.key === sectionKey);
    if (!sec) return;
    // Toggle sort direction
    const curDir = sec._sortDir || sec.default_sort_dir || 'asc';
    const curCol = sec._sortCol || sec.default_sort || '';
    let newDir = 'asc';
    if (curCol === colKey) newDir = curDir === 'asc' ? 'desc' : 'asc';
    sec._sortCol = colKey;
    sec._sortDir = newDir;
    const content = document.getElementById('workspace-content');
    try {
        const res = await apiFetch(API + '/api/workspace/section/' + encodeURIComponent(sectionKey) + '?sort=' + colKey + '&dir=' + newDir);
        const data = await res.json();
        renderWorkspaceTable(content, sec, data);
    } catch (e) {}
}

function renderWorkspacePipeline(container, config, data) {
    const stages = config.stages || [];
    const grouped = data.stages || {};
    let actionBarHtml = _renderSectionActionBar(config.key);
    if (!stages.length) { container.innerHTML = actionBarHtml + '<div class="ws-empty">' + escapeHtml(config.empty_message || 'No data.') + '</div>'; return; }
    let html = actionBarHtml + '<div class="ws-pipeline">';
    for (const stage of stages) {
        const items = grouped[stage] || [];
        html += '<div class="ws-pipeline-column"><h4>' + escapeHtml(stage) + '<span class="ws-count">' + items.length + '</span></h4>';
        if (!items.length) {
            const isGrantEmpty = config.key === 'grants';
            const isCaseEmpty = config.key === 'cases';
            const addFn = isGrantEmpty ? 'npShowGrantModal()' : (isCaseEmpty ? 'swShowCaseModal()' : '');
            if (addFn) {
                html += '<div onclick="' + addFn + '" style="color:var(--text-dim); font-size:0.75rem; text-align:center; padding:20px 0; cursor:pointer; border:2px dashed var(--border); border-radius:6px; margin:4px; transition:border-color 0.15s, color 0.15s" onmouseover="this.style.borderColor=\'var(--accent)\';this.style.color=\'var(--accent)\'" onmouseout="this.style.borderColor=\'var(--border)\';this.style.color=\'var(--text-dim)\'"><i class="fas fa-plus" style="margin-right:4px"></i>Add</div>';
            } else {
                html += '<div style="color:var(--text-dim); font-size:0.75rem; text-align:center; padding:20px 0;">Empty</div>';
            }
        }
        for (const item of items) {
            const isGrant = config.key === 'grants';
            const isCase = config.key === 'cases';
            html += '<div class="ws-pipeline-card">';
            if (isGrant || isCase) {
                // Pipeline card with edit button and status dropdown
                const editFn = isGrant ? 'npShowGrantModal' : 'swShowCaseModal';
                const delFn = isGrant ? 'npDeleteGrant' : 'swDeleteCase';
                const statusFn = isGrant ? 'npChangeGrantStatus' : 'swChangeStatus';
                html += '<div style="display:flex;justify-content:space-between;align-items:start">';
                html += '<div class="ws-card-title" style="cursor:pointer" onclick="' + editFn + '(' +
                    JSON.stringify(item).replace(/"/g, '&quot;') + ')">' +
                    escapeHtml(String(item[config.card_title_field] || '')) + '</div>';
                html += '<div style="display:flex;gap:4px">';
                html += '<button style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:2px;font-size:0.75rem" ' +
                    'onclick="' + editFn + '(' + JSON.stringify(item).replace(/"/g, '&quot;') + ')" title="Edit">' +
                    '<i class="fas fa-pen"></i></button>';
                html += '<button style="background:none;border:none;color:var(--error-strong);cursor:pointer;padding:2px;font-size:0.75rem" ' +
                    'onclick="' + delFn + '(\'' + escapeHtml(item.id || '') + '\',\'' + escapeHtml(String(item.name || '')) + '\')" title="Delete">' +
                    '<i class="fas fa-trash"></i></button>';
                html += '</div></div>';
                if (config.card_subtitle_field) html += '<div class="ws-card-subtitle">' + escapeHtml(String(item[config.card_subtitle_field] || '')) + '</div>';
                if (config.card_badge_field && item[config.card_badge_field] != null) {
                    html += '<div class="ws-card-badge">' + _wsFormatValue(item[config.card_badge_field], config.card_badge_format || 'number') + '</div>';
                }
                if (item.deadline) {
                    html += '<div style="font-size:0.7rem;color:var(--text-dim);margin-top:4px"><i class="fas fa-clock"></i> ' + escapeHtml(item.deadline) + '</div>';
                }
                if (isCase && item.follow_up_date) {
                    html += '<div style="font-size:0.7rem;color:var(--text-dim);margin-top:4px"><i class="fas fa-phone-flip"></i> Follow-up: ' + escapeHtml(item.follow_up_date) + '</div>';
                }
                // Status dropdown — move to different stage
                html += '<div style="margin-top:6px">';
                html += '<select style="font-size:0.7rem;padding:2px 6px;background:var(--bg-dark);color:var(--text);border:1px solid var(--border);border-radius:4px;cursor:pointer" ' +
                    'onchange="' + statusFn + '(\'' + escapeHtml(item.id || '') + '\',this.value)">';
                for (const s of stages) {
                    const sel = s === stage ? ' selected' : '';
                    html += '<option value="' + escapeHtml(s) + '"' + sel + '>' + escapeHtml(s) + '</option>';
                }
                html += '</select></div>';
            } else {
                html += '<div class="ws-card-title">' + escapeHtml(String(item[config.card_title_field] || '')) + '</div>';
                if (config.card_subtitle_field) html += '<div class="ws-card-subtitle">' + escapeHtml(String(item[config.card_subtitle_field] || '')) + '</div>';
                if (config.card_badge_field && item[config.card_badge_field] != null) {
                    html += '<div class="ws-card-badge">' + _wsFormatValue(item[config.card_badge_field], config.card_badge_format || 'number') + '</div>';
                }
            }
            html += '</div>';
        }
        html += '</div>';
    }
    html += '</div>';
    container.innerHTML = html;
}

function renderWorkspaceSummary(container, config, data) {
    let actionBarHtml = _renderSectionActionBar(config.key);
    if (!data.stats || !data.stats.length) { container.innerHTML = actionBarHtml + '<div class="ws-empty">No data yet. Log your first income or expense to see financial summaries here.</div>'; return; }
    let html = actionBarHtml + '<div class="ws-overview-grid">';
    for (const stat of data.stats) {
        html += '<div class="ws-stat-card">' +
            '<div class="ws-stat-icon" style="color:' + escapeHtml(stat.color || 'var(--accent)') + '"><i class="fas ' + escapeHtml(stat.icon || 'fa-circle') + '"></i></div>' +
            '<div class="ws-stat-value" style="color:' + escapeHtml(stat.color || 'var(--text)') + '">' + _wsFormatValue(stat.value, stat.format) + '</div>' +
            '<div class="ws-stat-label">' + escapeHtml(stat.label) + '</div>' +
            '</div>';
    }
    html += '</div>';

    // Financial charts section (only for financials)
    if (config.key === 'financials') {
        html += '<div style="margin-top:20px;padding:0 16px">';
        html += '<h4 style="font-size:0.85rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:12px">Charts</h4>';
        html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:16px">';
        html += '<div id="np-chart-monthly" style="background:var(--bg-input);border-radius:10px;padding:16px;border:1px solid var(--border);min-height:200px;display:flex;align-items:center;justify-content:center"><span style="color:var(--text-dim);font-size:0.8rem"><i class="fas fa-spinner fa-spin"></i> Loading chart...</span></div>';
        html += '<div id="np-chart-income" style="background:var(--bg-input);border-radius:10px;padding:16px;border:1px solid var(--border);min-height:200px;display:flex;align-items:center;justify-content:center"><span style="color:var(--text-dim);font-size:0.8rem"><i class="fas fa-spinner fa-spin"></i> Loading chart...</span></div>';
        html += '<div id="np-chart-expense" style="background:var(--bg-input);border-radius:10px;padding:16px;border:1px solid var(--border);min-height:200px;display:flex;align-items:center;justify-content:center"><span style="color:var(--text-dim);font-size:0.8rem"><i class="fas fa-spinner fa-spin"></i> Loading chart...</span></div>';
        html += '</div></div>';
    }

    container.innerHTML = html;

    // Load charts async
    if (config.key === 'financials') {
        _npLoadChart('monthly', 'np-chart-monthly');
        _npLoadChart('category_income', 'np-chart-income');
        _npLoadChart('category_expense', 'np-chart-expense');
    }
}

// ── Workspace Email section — reuses /api/email/triage ──
// WorkspaceState.emailTab (namespaced above)

async function renderWorkspaceEmail(container, config) {
    let html = _renderSectionActionBar(config.key);
    // Sub-tabs: Inbox | Settings | Weekly Review
    html += '<div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:12px">';
    for (const [key, label, icon] of [['inbox','Inbox','fa-inbox'],['settings','Settings','fa-sliders'],['review','Weekly Review','fa-chart-bar']]) {
        const active = WorkspaceState.emailTab === key;
        html += '<button class="em-ws-tab' + (active ? ' active' : '') + '" onclick="WorkspaceState.emailTab=\'' + key + '\';renderWorkspaceEmail(document.getElementById(\'workspace-content\'),' + JSON.stringify(config) + ')">' +
            '<i class="fas ' + icon + '" style="margin-right:4px"></i>' + label + '</button>';
    }
    html += '</div>';
    container.innerHTML = html;

    if (WorkspaceState.emailTab === 'settings') { await _renderEmailSettings(container); return; }
    if (WorkspaceState.emailTab === 'review') { await _renderEmailWeeklyReview(container); return; }

    // Inbox tab
    container.innerHTML += '<div class="em-summary" style="font-size:0.85em;padding:4px 0 8px"><i class="fas fa-spinner fa-spin"></i> Loading inbox...</div>';
    try {
        const res = await apiFetch(API + '/api/email/triage?limit=30&days_back=7');
        const data = await res.json();
        const emails = data.emails || [];
        // Populate EmailState.triageEmails so openEmailDetail() has metadata
        if (emails.length && !EmailState.triageEmails.length) EmailState.triageEmails = emails;
        if (!emails.length) {
            container.innerHTML += '<div class="ws-empty">' + escapeHtml(config.empty_message || 'No emails found.') + '</div>';
            return;
        }
        const groups = {};
        for (const em of emails) {
            const cat = em.category || 'Other';
            if (!groups[cat]) groups[cat] = [];
            groups[cat].push(em);
        }
        const prioOrder = {urgent: 0, action: 1, fyi: 2};
        const sortedCats = Object.keys(groups).sort((a, b) => {
            const pa = Math.min(...groups[a].map(e => prioOrder[e.priority] ?? 2));
            const pb = Math.min(...groups[b].map(e => prioOrder[e.priority] ?? 2));
            return pa - pb || a.localeCompare(b);
        });
        let inboxHtml = '<div class="em-summary" style="font-size:0.8em;padding:0 0 8px">' + emails.length + ' emails in the last 7 days</div>';
        for (const cat of sortedCats) {
            const catEmails = groups[cat];
            inboxHtml += '<div style="margin-bottom:12px">';
            inboxHtml += '<div class="em-ws-category-hdr">' +
                escapeHtml(cat) + ' <span class="em-ws-category-count">(' + catEmails.length + ')</span></div>';
            for (const em of catEmails) {
                const dot = em.priority === 'urgent' ? '🔴' : em.priority === 'action' ? '🟡' : '🟢';
                const cal = em.has_calendar_event ? ' 📅' : '';
                const rowId = 'ws-email-' + (em.id || em.index);
                inboxHtml += '<div id="' + rowId + '" class="em-ws-row" onclick="openEmailDetail(\'' + escapeHtml(em.id || '') + '\',\'' + escapeHtml(em.id_source || 'gmail') + '\')">' +
                    '<span style="flex-shrink:0">' + dot + '</span>' +
                    '<span class="em-ws-sender">' + escapeHtml(em.from_name || em.from_email || '?') + '</span>' +
                    '<span class="em-ws-subject">' + escapeHtml(em.subject || '(no subject)') + cal + '</span>' +
                    '<button class="em-ws-trash" onclick="event.stopPropagation(); wsTrashEmail(\'' + escapeHtml(em.id || '') + '\',\'' + escapeHtml(em.id_source || 'gmail') + '\',\'' + rowId + '\')" title="Move to Trash" aria-label="Move to Trash">' +
                    '<i class="fas fa-trash"></i></button>' +
                    '</div>';
            }
            inboxHtml += '</div>';
        }
        // Replace the loading spinner
        const spinner = container.querySelector('.fa-spinner');
        if (spinner) spinner.closest('div').outerHTML = inboxHtml;
        else container.innerHTML += inboxHtml;
    } catch (e) {
        container.innerHTML += '<div class="ws-empty">' + escapeHtml(config.empty_message || 'Could not load email.') + '</div>';
    }
}

async function _renderEmailSettings(container) {
    let html = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;padding:0 4px">';

    // Topic Flags panel
    html += '<div class="em-settings-panel">';
    html += '<h4><i class="fas fa-flag em-flag-icon"></i>Topic Flags</h4>';
    html += '<p>Keywords that auto-escalate emails to urgent. E.g. "trans pride", "training", "board vote".</p>';
    html += '<div id="ws-topic-flags-list" style="margin-bottom:10px"><i class="fas fa-spinner fa-spin" style="color:var(--text-dim)"></i></div>';
    html += '<div style="display:flex;gap:6px">';
    html += '<input type="text" id="ws-topic-flag-input" placeholder="Add topic flag..." style="flex:1;font-size:0.85em" onkeydown="if(event.key===\'Enter\')wsAddTopicFlag()">';
    html += '<button class="btn btn-primary" style="font-size:0.8em;padding:6px 12px" onclick="wsAddTopicFlag()"><i class="fas fa-plus"></i></button>';
    html += '</div></div>';

    // VIP Senders panel
    html += '<div class="em-settings-panel">';
    html += '<h4><i class="fas fa-star em-vip-icon"></i>VIP Senders</h4>';
    html += '<p>People or domains whose emails always get priority. E.g. "chair@board.org", "foundation.org".</p>';
    html += '<div id="ws-vip-list" style="margin-bottom:10px"><i class="fas fa-spinner fa-spin" style="color:var(--text-dim)"></i></div>';
    html += '<div style="display:flex;gap:6px">';
    html += '<input type="text" id="ws-vip-input" placeholder="Add email or domain..." style="flex:1;font-size:0.85em" onkeydown="if(event.key===\'Enter\')wsAddVip()">';
    html += '<button class="btn btn-primary" style="font-size:0.8em;padding:6px 12px" onclick="wsAddVip()"><i class="fas fa-plus"></i></button>';
    html += '</div></div>';

    html += '</div>';
    container.innerHTML += html;

    // Load current flags and VIPs
    _wsLoadTopicFlags();
    _wsLoadVips();
}

async function _wsLoadTopicFlags() {
    const el = document.getElementById('ws-topic-flags-list');
    if (!el) return;
    try {
        const r = await apiFetch(API + '/api/email/topic-flags');
        const d = await r.json();
        const flags = d.flags || [];
        if (!flags.length) { el.innerHTML = '<span style="font-size:0.8em;color:var(--text-dim)">No topic flags set yet.</span>'; return; }
        el.innerHTML = flags.map(f =>
            '<span style="display:inline-flex;align-items:center;gap:4px;background:var(--bg);padding:4px 10px;border-radius:20px;font-size:0.8em;margin:2px 4px 2px 0;border:1px solid var(--border)">' +
            '<i class="fas fa-flag em-flag-icon" style="font-size:0.7em"></i> ' + escapeHtml(f) +
            ' <button style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:0 2px;font-size:0.9em" onclick="wsRemoveTopicFlag(\'' + escapeHtml(f) + '\')" title="Remove">×</button></span>'
        ).join('');
    } catch(e) { el.innerHTML = '<span style="color:var(--text-dim);font-size:0.8em">Could not load flags.</span>'; }
}

async function _wsLoadVips() {
    const el = document.getElementById('ws-vip-list');
    if (!el) return;
    try {
        const r = await apiFetch(API + '/api/email/vip-senders');
        const d = await r.json();
        const vips = d.vips || [];
        if (!vips.length) { el.innerHTML = '<span style="font-size:0.8em;color:var(--text-dim)">No VIP senders set yet.</span>'; return; }
        el.innerHTML = vips.map(v =>
            '<span style="display:inline-flex;align-items:center;gap:4px;background:var(--bg);padding:4px 10px;border-radius:20px;font-size:0.8em;margin:2px 4px 2px 0;border:1px solid var(--border)">' +
            '<i class="fas fa-star em-vip-icon" style="font-size:0.7em"></i> ' + escapeHtml(v) +
            ' <button style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:0 2px;font-size:0.9em" onclick="wsRemoveVip(\'' + escapeHtml(v) + '\')" title="Remove">×</button></span>'
        ).join('');
    } catch(e) { el.innerHTML = '<span style="color:var(--text-dim);font-size:0.8em">Could not load VIPs.</span>'; }
}

async function wsAddTopicFlag() {
    const input = document.getElementById('ws-topic-flag-input');
    const kw = (input.value || '').trim();
    if (!kw) return;
    await apiFetch(API + '/api/email/topic-flags', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({keyword: kw})});
    input.value = '';
    _wsLoadTopicFlags();
    showToast('Topic flag added: ' + kw, 'success');
}

async function wsRemoveTopicFlag(kw) {
    await apiFetch(API + '/api/email/topic-flags', {method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({keyword: kw})});
    _wsLoadTopicFlags();
    showToast('Topic flag removed', 'success');
}

async function wsAddVip() {
    const input = document.getElementById('ws-vip-input');
    const sender = (input.value || '').trim();
    if (!sender) return;
    await apiFetch(API + '/api/email/vip-senders', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({sender})});
    input.value = '';
    _wsLoadVips();
    showToast('VIP added: ' + sender, 'success');
}

async function wsRemoveVip(sender) {
    await apiFetch(API + '/api/email/vip-senders', {method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({sender})});
    _wsLoadVips();
    showToast('VIP removed', 'success');
}

async function _renderEmailWeeklyReview(container) {
    container.innerHTML += '<div class="em-summary" style="font-size:0.85em;padding:4px 0 8px"><i class="fas fa-spinner fa-spin"></i> Analyzing the past week...</div>';
    try {
        const r = await apiFetch(API + '/api/email/weekly-digest?days=7');
        const d = await r.json();
        if (d.error) { container.innerHTML += '<div class="ws-empty">' + escapeHtml(d.error) + '</div>'; return; }

        let html = '<div style="padding:0 4px">';

        // Summary stats
        html += '<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px">';
        html += '<div class="ws-stat-card em-stat-accent" style="flex:1;min-width:100px"><div class="ws-stat-value">' + (d.total || 0) + '</div><div class="ws-stat-label">Emails This Week</div></div>';
        html += '<div class="ws-stat-card em-stat-learned" style="flex:1;min-width:100px"><div class="ws-stat-value">' + (d.learned_sender_count || 0) + '</div><div class="ws-stat-label">Learned Senders</div></div>';
        html += '<div class="ws-stat-card em-stat-vip" style="flex:1;min-width:100px"><div class="ws-stat-value">' + (d.current_vips || []).length + '</div><div class="ws-stat-label">VIPs</div></div>';
        html += '<div class="ws-stat-card em-stat-flag" style="flex:1;min-width:100px"><div class="ws-stat-value">' + (d.current_topic_flags || []).length + '</div><div class="ws-stat-label">Topic Flags</div></div>';
        html += '</div>';

        // Category breakdown
        const cats = d.categories || {};
        if (Object.keys(cats).length) {
            html += '<div class="em-settings-panel" style="margin-bottom:16px">';
            html += '<h4><i class="fas fa-chart-pie" style="margin-right:6px;color:var(--accent)"></i>Category Breakdown</h4>';
            const sorted = Object.entries(cats).sort((a,b) => b[1] - a[1]);
            for (const [cat, count] of sorted) {
                const pct = d.total ? Math.round(count / d.total * 100) : 0;
                html += '<div class="em-bar">' +
                    '<span class="em-bar-label">' + escapeHtml(cat) + '</span>' +
                    '<div class="em-bar-track"><div class="em-bar-fill" style="width:' + pct + '%"></div></div>' +
                    '<span class="em-bar-count">' + count + '</span></div>';
            }
            html += '</div>';
        }

        // Top unlearned senders — suggest VIPs
        const unlearned = d.unlearned_senders || [];
        if (unlearned.length) {
            html += '<div class="em-settings-panel" style="margin-bottom:16px">';
            html += '<h4><i class="fas fa-user-plus em-vip-icon"></i>New Senders This Week</h4>';
            html += '<p>These senders aren\'t categorized yet. Add important ones as VIPs or teach them a category.</p>';
            for (const s of unlearned) {
                html += '<div class="em-sender-row">' +
                    '<span class="em-sender-email">' + escapeHtml(s.email) + '</span>' +
                    '<span class="em-sender-count">' + s.count + ' email' + (s.count > 1 ? 's' : '') + '</span>' +
                    '<button class="btn btn-secondary" style="font-size:0.7em;padding:3px 8px" onclick="wsAddVipDirect(\'' + escapeHtml(s.email) + '\')"><i class="fas fa-star"></i> VIP</button>' +
                    '</div>';
            }
            html += '</div>';
        }

        // Top senders
        const top = d.top_senders || [];
        if (top.length) {
            html += '<div class="em-settings-panel">';
            html += '<h4><i class="fas fa-ranking-star" style="margin-right:6px;color:var(--accent)"></i>Top Senders</h4>';
            for (const s of top.slice(0, 10)) {
                const isVip = (d.current_vips || []).includes(s.email);
                html += '<div class="em-sender-row" style="border-bottom:none;padding:3px 0">' +
                    (isVip ? '<i class="fas fa-star em-vip-icon" style="font-size:0.7em"></i>' : '<span style="width:12px"></span>') +
                    '<span class="em-sender-email">' + escapeHtml(s.email) + '</span>' +
                    '<span class="em-sender-count">' + s.count + '</span></div>';
            }
            html += '</div>';
        }

        html += '</div>';
        // Remove spinner, add content
        const spinner = container.querySelector('.fa-spinner');
        if (spinner) spinner.closest('div').outerHTML = html;
        else container.innerHTML += html;
    } catch(e) {
        container.innerHTML += '<div class="ws-empty">Could not generate weekly digest.</div>';
    }
}

async function wsAddVipDirect(email) {
    await apiFetch(API + '/api/email/vip-senders', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({sender: email})});
    showToast('VIP added: ' + email, 'success');
    // Refresh the review tab
    WorkspaceState.emailTab = 'review';
    renderWorkspaceEmail(document.getElementById('workspace-content'), (WorkspaceState.config.sections || []).find(s => s.type === 'email'));
}

async function wsTrashEmail(msgId, source, rowId) {
    if (!msgId) return;
    const row = document.getElementById(rowId);
    if (row) row.style.opacity = '0.4';
    try {
        const res = await apiFetch(API + '/api/email/delete/' + encodeURIComponent(msgId), {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({source: source})
        });
        const d = await res.json();
        if (d.status === 'ok') {
            if (row) {
                row.style.transition = 'opacity 0.3s, max-height 0.3s';
                row.style.opacity = '0';
                row.style.maxHeight = '0';
                row.style.overflow = 'hidden';
                setTimeout(() => row.remove(), 350);
            }
            showToast('Email moved to trash', 'success');
        } else {
            if (row) row.style.opacity = '1';
            showToast('Delete failed: ' + (d.error || 'unknown error'), 'error');
        }
    } catch (e) {
        if (row) row.style.opacity = '1';
        showToast('Delete failed: ' + e.message, 'error');
    }
}

// ── Workspace Calendar section — reuses /api/calendar/events ──
async function renderWorkspaceCalendar(container, config) {
    let html = _renderSectionActionBar(config.key);
    html += '<div style="color:var(--text-dim);font-size:0.85em;padding:4px 0 8px"><i class="fas fa-spinner fa-spin"></i> Loading calendar...</div>';
    container.innerHTML = html;
    try {
        const res = await apiFetch(API + '/api/calendar/events?days=7&view=week');
        const data = await res.json();
        const events = data.events || [];
        if (!events.length) {
            container.innerHTML = _renderSectionActionBar(config.key) +
                '<div class="ws-empty">' + escapeHtml(config.empty_message || 'No upcoming events.') + '</div>';
            return;
        }
        // Group events by date
        const byDate = {};
        for (const ev of events) {
            const startStr = String(ev.start || '');
            const dateKey = startStr.includes('T') ? startStr.split('T')[0] : startStr;
            if (!byDate[dateKey]) byDate[dateKey] = [];
            byDate[dateKey].push(ev);
        }
        html = _renderSectionActionBar(config.key);
        html += '<div style="font-size:0.8em;color:var(--text-dim);padding:0 0 8px">' + events.length + ' events this week</div>';
        const today = new Date().toISOString().split('T')[0];
        for (const dateKey of Object.keys(byDate).sort()) {
            const isToday = dateKey === today;
            const dayLabel = isToday ? 'Today' : formatDate(dateKey + 'T00:00:00', 'day');
            html += '<div style="margin-bottom:12px">';
            html += '<div style="font-size:0.75em;text-transform:uppercase;letter-spacing:0.05em;color:' + (isToday ? 'var(--accent)' : 'var(--text-dim)') + ';padding:6px 0;border-bottom:1px solid var(--border);font-weight:' + (isToday ? '600' : '400') + '">' +
                escapeHtml(dayLabel) + '</div>';
            for (const ev of byDate[dateKey]) {
                let timeStr = 'All day';
                if (!ev.all_day && ev.start && String(ev.start).includes('T')) {
                    try {
                        timeStr = formatDate(ev.start, 'time');
                        if (ev.end) {
                            timeStr += ' – ' + formatDate(ev.end, 'time');
                        }
                    } catch(e) { timeStr = String(ev.start); }
                }
                const loc = ev.location ? ' · ' + ev.location : '';
                const attendees = (ev.attendees || []).length;
                const attStr = attendees ? ' · ' + attendees + ' attendee' + (attendees > 1 ? 's' : '') : '';
                html += '<div style="display:flex;align-items:baseline;gap:10px;padding:6px 4px;border-bottom:1px solid var(--border-dim);cursor:pointer" ' +
                    'onclick="switchPanel(\'calendar\')" title="Open in Calendar">' +
                    '<span style="flex-shrink:0;width:130px;color:var(--text-dim);font-size:0.85em">' + escapeHtml(timeStr) + '</span>' +
                    '<span style="flex:1;font-size:0.9em">' + escapeHtml(ev.summary || '(No title)') +
                    '<span style="color:var(--text-dim);font-size:0.85em">' + escapeHtml(loc) + escapeHtml(attStr) + '</span></span>' +
                    '</div>';
            }
            html += '</div>';
        }
        container.innerHTML = html;
    } catch (e) {
        container.innerHTML = _renderSectionActionBar(config.key) +
            '<div class="ws-empty">' + escapeHtml(config.empty_message || 'Could not load calendar.') + '</div>';
    }
}

async function _npLoadChart(chartType, containerId) {
    const el = document.getElementById(containerId);
    if (!el) return;
    try {
        const res = await apiFetch(API + '/api/bookkeeping/chart/' + chartType);
        const data = await res.json();
        if (data.error) {
            el.innerHTML = '<span style="color:var(--text-dim);font-size:0.8rem">' + escapeHtml(data.error) + '</span>';
        } else if (data.image) {
            el.innerHTML = '<img src="' + data.image + '" style="max-width:100%;height:auto;border-radius:6px" alt="' + escapeHtml(chartType) + '">';
        }
    } catch (e) {
        el.innerHTML = '<span style="color:var(--text-dim);font-size:0.8rem">Chart unavailable</span>';
    }
}

function _wsFormatValue(value, format) {
    if (value == null || value === '') return '—';
    switch (format) {
        case 'currency': return '$' + Number(value).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
        case 'percent': return Number(value).toFixed(1) + '%';
        case 'date':
            if (!value) return '—';
            try { return formatDate(value, 'date'); } catch(e) { return String(value); }
        case 'number': return Number(value).toLocaleString();
        default: return escapeHtml(String(value));
    }
}

// ── Setup Wizard renderer ──────────────────────────────
const _SWIZ_ICONS = {
    llm: 'fa-brain', integrations: 'fa-plug', channels: 'fa-share-nodes',
    'self-hosted': 'fa-server',
};
const _SWIZ_CAT_LABELS = {
    llm: 'AI Provider', integrations: 'Integration', channels: 'Channel',
    'self-hosted': 'Self-Hosted',
};

async function _renderSetupWizard(container) {
    try {
        const res = await apiFetch(API + '/api/connect/status');
        const data = await res.json();
        const services = data.services || data || [];

        // Separate connected and not-connected
        const connected = services.filter(s => s.connected);
        const missing = services.filter(s => !s.connected);

        let html = '<div class="swiz-summary">' +
            '<span><b>' + connected.length + '</b> connected</span>' +
            '<span><b>' + missing.length + '</b> available to set up</span>' +
            '</div>';

        if (!missing.length) {
            html += '<div class="ws-empty" style="padding:32px; text-align:center;">' +
                '<i class="fas fa-check-circle" style="font-size:2em; color:var(--success); margin-bottom:12px; display:block;"></i>' +
                'All services are connected! You\'re all set.</div>';
            container.innerHTML = html;
            return;
        }

        // Not connected first, then connected
        html += '<h3 style="padding:0 16px; margin:8px 0 4px; font-size:0.9rem; color:var(--text-dim);">Available to Set Up</h3>';
        html += '<div class="swiz-grid">';
        for (const svc of missing) {
            const icon = _SWIZ_ICONS[svc.category] || 'fa-circle';
            const cat = _SWIZ_CAT_LABELS[svc.category] || svc.category || '';
            html += '<div class="swiz-card">' +
                '<div class="swiz-card-head"><i class="fas ' + escapeHtml(icon) + '" style="color:var(--accent);"></i>' +
                '<span class="swiz-name">' + escapeHtml(svc.display_name) + '</span></div>' +
                '<div class="swiz-card-cat">' + escapeHtml(cat) + '</div>' +
                '<div class="swiz-card-status"><span class="swiz-dot red"></span> Not connected</div>' +
                '<button class="btn btn-primary" onclick="swizSetup(\'' + escapeHtml(svc.connect_command || '/connect ' + svc.name) + '\')"><i class="fas fa-bolt"></i> Set Up</button>' +
                '</div>';
        }
        html += '</div>';

        if (connected.length) {
            html += '<h3 style="padding:0 16px; margin:16px 0 4px; font-size:0.9rem; color:var(--text-dim);">Already Connected</h3>';
            html += '<div class="swiz-grid">';
            for (const svc of connected) {
                const icon = _SWIZ_ICONS[svc.category] || 'fa-circle';
                const cat = _SWIZ_CAT_LABELS[svc.category] || svc.category || '';
                html += '<div class="swiz-card swiz-done">' +
                    '<div class="swiz-card-head"><i class="fas ' + escapeHtml(icon) + '" style="color:var(--success);"></i>' +
                    '<span class="swiz-name">' + escapeHtml(svc.display_name) + '</span></div>' +
                    '<div class="swiz-card-cat">' + escapeHtml(cat) + '</div>' +
                    '<div class="swiz-card-status"><span class="swiz-dot green"></span> Connected' +
                    (svc.detail ? ' — ' + escapeHtml(svc.detail) : '') + '</div>' +
                    '</div>';
            }
            html += '</div>';
        }

        // ── Server Stack section ──
        try {
            // Check container runtime
            let hasRuntime = true;
            let runtimeName = '';
            try {
                const rtRes = await apiFetch(API + '/api/server/runtime');
                const rtData = await rtRes.json();
                runtimeName = rtData.runtime || 'none';
                if (!rtData.runtime || rtData.runtime === 'none') {
                    hasRuntime = false;
                }
            } catch(e) {}

            const srvRes = await apiFetch(API + '/api/onboard/server-recommendations');
            const srvData = await srvRes.json();
            const recs = srvData.recommendations || [];
            if (recs.length) {
                // Check which are already running
                let srvStatuses = {};
                try {
                    const stRes = await apiFetch(API + '/api/server/status');
                    const stArr = (await stRes.json()) || [];
                    // Convert array to key-based lookup
                    for (const s of stArr) {
                        if (s.key) srvStatuses[s.key] = s;
                    }
                } catch(e) {}

                const running = recs.filter(r => {
                    const st = srvStatuses[r.key];
                    return st && st.status === 'running';
                });
                const notRunning = recs.filter(r => {
                    const st = srvStatuses[r.key];
                    return !st || st.status !== 'running';
                });

                html += '<h3 style="padding:0 16px; margin:20px 0 4px; font-size:0.9rem; color:var(--text-dim);">' +
                    '<i class="fas fa-server" style="margin-right:6px;"></i>Server Stack for ' +
                    escapeHtml((srvData.job_class || 'your role').replace(/_/g, ' ')) + '</h3>';

                // Runtime status line
                if (!hasRuntime) {
                    html += '<div style="padding:4px 16px; font-size:0.82rem; color:var(--warning-dim);">' +
                        '<i class="fas fa-exclamation-triangle"></i> No container runtime — will install Podman automatically</div>';
                } else {
                    html += '<div style="padding:4px 16px; font-size:0.82rem; color:var(--text-dim);">' +
                        '<i class="fas fa-check-circle" style="color:var(--success);"></i> Runtime: ' + escapeHtml(runtimeName) + '</div>';
                }

                html += '<div class="swiz-summary">' +
                    '<span><b>' + running.length + '</b> running</span>' +
                    '<span><b>' + notRunning.length + '</b> not provisioned</span>';
                if (notRunning.length) {
                    const btnLabel = hasRuntime ? 'Deploy All' : 'Set Up Server';
                    const btnIcon = hasRuntime ? 'fa-rocket' : 'fa-magic';
                    html += '<button class="btn btn-primary" id="swiz-auto-deploy" onclick="swizFullSetup()" style="margin-left:auto; font-size:0.8rem; padding:5px 14px;">' +
                        '<i class="fas ' + btnIcon + '"></i> ' + btnLabel + '</button>';
                }
                html += '</div>';

                html += '<div class="swiz-grid">';
                for (const svc of recs) {
                    const isUp = running.some(r => r.key === svc.key);
                    const stInfo = srvStatuses[svc.key] || {};
                    const svcUrl = stInfo.url || '';
                    const hasWebUi = stInfo.web_ui && svcUrl;
                    const icon = svc.icon || 'fa-cube';
                    html += '<div class="swiz-card' + (isUp ? ' swiz-done' : '') + '">' +
                        '<div class="swiz-card-head"><i class="fas ' + escapeHtml(icon) + '" style="color:' + (isUp ? '#4ade80' : 'var(--accent)') + ';"></i>' +
                        '<span class="swiz-name">' + escapeHtml(svc.display_name) + '</span></div>' +
                        '<div class="swiz-card-cat">' + escapeHtml(svc.service_type === 'native' ? 'System Service' : 'Container') + '</div>' +
                        '<div class="swiz-card-status"><span class="swiz-dot ' + (isUp ? 'green' : 'red') + '"></span> ' +
                        (isUp ? 'Running' : 'Not provisioned') + '</div>' +
                        '<div style="font-size:0.8rem; color:var(--text-dim);">' + escapeHtml(svc.description || '') + '</div>';
                    if (isUp && hasWebUi) {
                        html += '<a href="' + escapeHtml(svcUrl) + '" target="_blank" class="btn btn-primary" style="font-size:0.78rem; padding:4px 12px; text-decoration:none;">' +
                            '<i class="fas fa-external-link-alt"></i> Open Dashboard</a>';
                    }
                    if (!isUp && svc.service_type !== 'native') {
                        html += '<button class="btn btn-secondary" onclick="swizProvisionOne(\'' + escapeHtml(svc.key) + '\', this)" style="font-size:0.78rem; padding:4px 12px;">' +
                            '<i class="fas fa-download"></i> Deploy</button>';
                    }
                    html += '</div>';
                }
                html += '</div>';
            }
        } catch(e) {
            // Server recommendations unavailable — skip section
        }

        container.innerHTML = html;
    } catch (e) {
        container.innerHTML = '<div class="ws-empty">Failed to load setup status.</div>';
    }
}

async function swizFullSetup() {
    const btn = document.getElementById('swiz-auto-deploy');
    if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Setting up server...'; }
    try {
        const res = await apiFetch(API + '/api/server/full-setup', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: '{}',
        });
        const data = await res.json();
        if (data.error) { alert('Error: ' + data.error); return; }

        // Build summary message
        const s = data.summary || {};
        let msg = 'Server Setup Complete\n\n';

        // Bootstrap result
        if (data.bootstrap && !data.bootstrap.already_installed) {
            msg += 'Installed ' + (data.bootstrap.runtime || 'Podman') + ' container runtime.\n\n';
        }

        msg += s.ok + ' services running, ' + s.failed + ' failed';
        if (s.skipped) msg += ', ' + s.skipped + ' native (skipped)';
        msg += '.\n';

        // Per-service details
        const services = data.services || [];
        const running = services.filter(sv => sv.ok && sv.status === 'running');
        const failed = services.filter(sv => !sv.ok);

        if (running.length) {
            msg += '\nRunning:\n' + running.map(sv =>
                '  \u2713 ' + (sv.display_name || sv.service) + (sv.url ? ' \u2014 ' + sv.url : '')
            ).join('\n');
        }
        if (failed.length) {
            msg += '\n\nFailed:\n' + failed.map(sv =>
                '  \u2717 ' + (sv.display_name || sv.service) + ': ' + (sv.error || sv.status)
            ).join('\n');
        }

        alert(msg);
        // Refresh
        const content = document.getElementById('workspace-content');
        if (content) await _renderSetupWizard(content);
    } catch(e) {
        alert('Server setup failed: ' + e.message);
    } finally {
        if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-rocket"></i> Deploy All'; }
    }
}

async function swizProvisionOne(serviceKey, btnEl) {
    if (btnEl) { btnEl.disabled = true; btnEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deploying...'; }
    try {
        const provRes = await apiFetch(API + '/api/server/provision/' + serviceKey, {
            method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{}',
        });
        const provData = await provRes.json();
        if (provData.error) { throw new Error(provData.error); }
        const startRes = await apiFetch(API + '/api/server/start/' + serviceKey, {
            method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{}',
        });
        const startData = await startRes.json();
        if (startData.error) { throw new Error(startData.error); }
        // Refresh
        const content = document.getElementById('workspace-content');
        if (content) await _renderSetupWizard(content);
    } catch(e) {
        alert('Failed to deploy ' + serviceKey + ': ' + e.message);
        if (btnEl) { btnEl.disabled = false; btnEl.innerHTML = '<i class="fas fa-download"></i> Deploy'; }
    }
}

function swizSetup(cmd) {
    // Navigate to Connect panel and trigger the wizard command
    document.querySelector('[data-panel="connect"]').click();
    setTimeout(() => {
        const input = document.querySelector('.wizard-text-input') || document.getElementById('chat-input');
        if (input) { input.value = cmd; }
        // Try to trigger the wizard
        if (typeof loadConnectWizard === 'function') {
            loadConnectWizard(cmd);
        }
    }, 300);
}

function wsRowAction(actionId, evt) {
    const entry = (window._wsActions || {})[actionId];
    if (!entry) return;
    workspaceAction(entry.act, entry.row, evt);
}

async function workspaceAction(actionConfig, rowData, evt) {
    if (actionConfig.mode === 'agent') {
        // Route through chat
        let prompt = actionConfig.prompt || '';
        if (actionConfig.prompt_template && rowData) {
            prompt = actionConfig.prompt_template;
            for (const [k, v] of Object.entries(rowData)) {
                prompt = prompt.replace(new RegExp('\\{' + k + '\\}', 'g'), String(v || ''));
            }
        }
        if (prompt) quickCommand(prompt.replace(/ /g, '_'));
    } else if (actionConfig.mode === 'api') {
        // Direct API call (e.g. start/stop/restart a service)
        const serviceKey = (rowData || {})[actionConfig.row_key || 'service_key'];
        if (!serviceKey) return;
        const endpoint = (actionConfig.endpoint || '').replace('{key}', encodeURIComponent(serviceKey));
        if (!endpoint) return;
        const btn = evt && evt.target ? evt.target.closest('.ws-row-action') : null;
        if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; }
        try {
            const res = await apiFetch(API + endpoint, {
                method: actionConfig.method || 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({key: serviceKey}),
            });
            const data = await res.json();
            if (data.error) {
                appendMessage('assistant', '**' + actionConfig.label + ' failed** for ' + (rowData.name || serviceKey) + ': ' + data.error);
            }
        } catch (e) {
            appendMessage('assistant', actionConfig.label + ' error: ' + e.message);
        } finally {
            // Reload the current tab (not loadWorkspace which resets to overview)
            if (WorkspaceState.activeTab && WorkspaceState.activeTab !== 'overview') {
                switchWorkspaceTab(WorkspaceState.activeTab);
            } else {
                loadWorkspace();
            }
        }
    } else if (actionConfig.mode === 'tool') {
        try {
            const res = await apiFetch(API + '/api/workspace/action', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    mode: 'tool',
                    tool: actionConfig.tool,
                    tool_args: actionConfig.tool_args || {},
                    row_data: rowData || {}
                })
            });
            const data = await res.json();
            if (data.result) {
                addMessage(data.result, 'assistant');
                switchPanel('chat');
            }
        } catch (e) {}
    }
}

function updateSidebarQuickActions(ws) {
    const defaultEl = document.getElementById('quick-actions-default');
    const wsEl = document.getElementById('quick-actions-workspace');
    if (!ws || !ws.quick_actions || !ws.quick_actions.length) {
        defaultEl.style.display = '';
        wsEl.style.display = 'none';
        wsEl.innerHTML = '';
        return;
    }
    defaultEl.style.display = 'none';
    wsEl.style.display = '';
    let html = '';
    for (const act of ws.quick_actions) {
        const isLast = act === ws.quick_actions[ws.quick_actions.length - 1];
        html += '<button class="btn btn-secondary" style="width:100%;' + (isLast ? '' : ' margin-bottom:8px;') + '" onclick="workspaceSidebarAction(\'' + escapeHtml(act.key) + '\',' + (act.confirm ? 'true' : 'false') + ')">' +
            '<i class="fas ' + escapeHtml(act.icon || 'fa-play') + '"></i> ' + escapeHtml(act.label) + '</button>';
    }
    wsEl.innerHTML = html;
}

function workspaceSidebarAction(key, needsConfirm) {
    if (!WorkspaceState.config) {
        showToast('Workspace not loaded yet', 5000);
        return;
    }
    const act = (WorkspaceState.config.quick_actions || []).find(a => a.key === key);
    if (!act) {
        showToast('Action "' + key + '" not found in workspace config', 5000);
        console.error('[workspace] Action not found:', key,
            'Available:', (WorkspaceState.config.quick_actions || []).map(a => a.key));
        return;
    }
    if (needsConfirm && !confirm('Run "' + act.label + '"?')) return;

    try {
        if (act.mode === 'js' && act.js_fn) {
            const fn = window[act.js_fn];
            if (typeof fn !== 'function') {
                showToast('Function "' + act.js_fn + '" not found', 5000);
                console.error('[workspace] js_fn not on window:', act.js_fn);
                return;
            }
            fn();
        } else if (act.mode === 'navigate') {
            if (act.target && typeof switchPanel === 'function') {
                switchPanel(act.target);
            } else {
                showToast('Navigate target "' + (act.target || '?') + '" not found', 5000);
            }
        } else if (act.mode === 'agent') {
            quickCommand((act.prompt || '').replace(/ /g, '_'));
        } else if (act.mode === 'tool') {
            workspaceAction(act, {});
        } else {
            showToast('Unknown action mode: ' + act.mode, 5000);
        }
    } catch (e) {
        showToast('Action failed: ' + e.message, 5000);
        console.error('[workspace] Action error:', key, e);
    }
}

// ── Nonprofit detail panel + inline editing ──────────

// WorkspaceState.currentDonor (namespaced above)

async function npShowDonorDetail(donorId) {
    try {
        const res = await apiFetch(API + '/api/nonprofit/donors');
        const data = await res.json();
        const donor = (data.donors || []).find(d => d.id === donorId);
        if (!donor) return;
        WorkspaceState.currentDonor = donor;

        document.getElementById('np-detail-name').textContent = donor.name;

        let html = '';

        // Contact info section
        html += '<div class="np-detail-section"><h4>Contact</h4>';
        html += '<div class="np-detail-field"><span class="np-field-label">Email</span>' +
            '<span class="np-field-value">' + escapeHtml(donor.email || '—') + '</span></div>';
        html += '<div class="np-detail-field"><span class="np-field-label">Phone</span>' +
            '<span class="np-field-value">' + escapeHtml(donor.phone || '—') + '</span></div>';
        if (donor.tags && donor.tags.length) {
            html += '<div class="np-detail-field"><span class="np-field-label">Tags</span>' +
                '<span class="np-field-value"><div class="np-detail-tags">';
            for (const tag of donor.tags) {
                html += '<span class="np-detail-tag">' + escapeHtml(tag) + '</span>';
            }
            html += '</div></span></div>';
        }
        if (donor.notes) {
            html += '<div class="np-detail-field"><span class="np-field-label">Notes</span>' +
                '<span class="np-field-value">' + escapeHtml(donor.notes) + '</span></div>';
        }
        html += '</div>';

        // Giving summary with segmentation
        const totalGiven = Number(donor.total_given || 0);
        const gifts = (donor.interactions || []).filter(i => i.type === 'gift');
        const giftCount = gifts.length;
        const avgGift = giftCount > 0 ? totalGiven / giftCount : 0;

        // Use server-computed donor level (AFP standard) with fallback
        const donorLevel = donor.donor_level || (totalGiven >= 10000 ? 'major' : totalGiven >= 1000 ? 'mid' : totalGiven > 0 ? 'small' : 'prospect');
        const segMap = {
            major: {label: 'Major Donor', color: '#f59e0b'},
            mid: {label: 'Mid-Level', color: '#60a5fa'},
            small: {label: 'Supporter', color: '#4ade80'},
            prospect: {label: 'Prospect', color: '#94a3b8'},
            lapsed: {label: 'Lapsed', color: '#ef4444'},
        };
        const seg = segMap[donorLevel] || {label: donorLevel, color: '#94a3b8'};

        // LYBUNT/SYBUNT/Lapse warnings
        let lapseWarn = '';
        if (donor.lapsed) {
            lapseWarn = '<div style="background:#dc262615;border:1px solid #ef444440;border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:var(--error)">' +
                '<i class="fas fa-exclamation-triangle" style="margin-right:6px"></i>Lapsed donor — no gift in 18+ months. Consider a re-engagement touchpoint.</div>';
        } else if (donor.lybunt) {
            lapseWarn = '<div style="background:#f59e0b15;border:1px solid #f59e0b40;border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:var(--warning)">' +
                '<i class="fas fa-clock" style="margin-right:6px"></i>LYBUNT — gave last year but not yet this year. Follow-up recommended.</div>';
        } else if (donor.sybunt) {
            lapseWarn = '<div style="background:#f59e0b15;border:1px solid #f59e0b40;border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:var(--warning)">' +
                '<i class="fas fa-clock" style="margin-right:6px"></i>SYBUNT — gave in a prior year but not recently. May need re-engagement.</div>';
        } else if (donor.last_gift) {
            const daysSince = Math.floor((Date.now() - new Date(donor.last_gift).getTime()) / 86400000);
            if (daysSince > 180) {
                lapseWarn = '<div style="background:#f59e0b15;border:1px solid #f59e0b40;border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:var(--warning)">' +
                    '<i class="fas fa-clock" style="margin-right:6px"></i>Last gift ' + daysSince + ' days ago — follow-up recommended.</div>';
            }
        }

        html += '<div class="np-detail-section"><h4>Giving Summary ' +
            '<span style="display:inline-block;background:' + seg.color + '22;color:' + seg.color + ';padding:2px 10px;border-radius:12px;font-size:0.7rem;font-weight:600;margin-left:8px;vertical-align:middle">' + seg.label + '</span></h4>';
        html += lapseWarn;
        html += '<div class="np-detail-field"><span class="np-field-label">Total Given</span>' +
            '<span class="np-field-value" style="color:var(--success);font-size:1.1rem">$' +
            totalGiven.toLocaleString('en-US', {minimumFractionDigits: 2}) + '</span></div>';

        // RFM Score badge
        const rfm = donor.rfm_score || 0;
        if (rfm > 0) {
            const rfmColor = rfm >= 12 ? '#4ade80' : rfm >= 8 ? '#60a5fa' : rfm >= 5 ? '#fbbf24' : '#94a3b8';
            html += '<div class="np-detail-field"><span class="np-field-label">RFM Score</span>' +
                '<span class="np-field-value"><span style="display:inline-block;background:' + rfmColor + '22;color:' + rfmColor + ';padding:2px 10px;border-radius:12px;font-size:0.8rem;font-weight:600">' +
                rfm + '/15</span> <span style="font-size:0.7rem;color:var(--text-dim)">(R:' + (donor.recency_score||0) + ' F:' + (donor.frequency_score||0) + ' M:' + (donor.monetary_score||0) + ')</span></span></div>';
        }

        if (donor.first_gift) {
            html += '<div class="np-detail-field"><span class="np-field-label">First Gift</span>' +
                '<span class="np-field-value">' + escapeHtml((donor.first_gift || '').slice(0, 10)) + '</span></div>';
        }
        if (donor.last_gift) {
            html += '<div class="np-detail-field"><span class="np-field-label">Last Gift</span>' +
                '<span class="np-field-value">' + escapeHtml((donor.last_gift || '').slice(0, 10)) + '</span></div>';
        }
        html += '<div class="np-detail-field"><span class="np-field-label">Total Gifts</span>' +
            '<span class="np-field-value">' + giftCount + '</span></div>';
        if (giftCount > 0) {
            html += '<div class="np-detail-field"><span class="np-field-label">Average Gift</span>' +
                '<span class="np-field-value">$' + avgGift.toLocaleString('en-US', {minimumFractionDigits: 2}) + '</span></div>';
        }
        if (donor.largest_gift) {
            html += '<div class="np-detail-field"><span class="np-field-label">Largest Gift</span>' +
                '<span class="np-field-value">$' + Number(donor.largest_gift).toLocaleString('en-US', {minimumFractionDigits: 2}) + '</span></div>';
        }
        if (donor.giving_frequency) {
            html += '<div class="np-detail-field"><span class="np-field-label">Giving Frequency</span>' +
                '<span class="np-field-value">' + escapeHtml(donor.giving_frequency) + '</span></div>';
        }
        if (donor.lifetime_value) {
            html += '<div class="np-detail-field"><span class="np-field-label">Projected LTV</span>' +
                '<span class="np-field-value">$' + Number(donor.lifetime_value).toLocaleString('en-US', {minimumFractionDigits: 2}) + '</span></div>';
        }
        html += '</div>';

        // Cultivation & Stewardship (only show if populated)
        const hasCultivation = donor.cultivation_stage || donor.acquisition_source || donor.communication_pref || donor.stewardship_plan;
        if (hasCultivation) {
            html += '<div class="np-detail-section"><h4>Stewardship</h4>';
            if (donor.cultivation_stage) {
                html += '<div class="np-detail-field"><span class="np-field-label">Stage</span>' +
                    '<span class="np-field-value">' + escapeHtml(donor.cultivation_stage) + '</span></div>';
            }
            if (donor.acquisition_source) {
                html += '<div class="np-detail-field"><span class="np-field-label">Source</span>' +
                    '<span class="np-field-value">' + escapeHtml(donor.acquisition_source) + '</span></div>';
            }
            if (donor.communication_pref) {
                html += '<div class="np-detail-field"><span class="np-field-label">Prefers</span>' +
                    '<span class="np-field-value">' + escapeHtml(donor.communication_pref) + '</span></div>';
            }
            if (donor.stewardship_plan) {
                html += '<div class="np-detail-field"><span class="np-field-label">Plan</span>' +
                    '<span class="np-field-value">' + escapeHtml(donor.stewardship_plan) + '</span></div>';
            }
            // Communication preferences flags
            const flags = [];
            if (donor.do_not_solicit) flags.push('No Solicit');
            if (donor.do_not_email) flags.push('No Email');
            if (donor.do_not_call) flags.push('No Call');
            if (donor.do_not_mail) flags.push('No Mail');
            if (flags.length) {
                html += '<div class="np-detail-field"><span class="np-field-label">Restrictions</span>' +
                    '<span class="np-field-value" style="color:var(--error)">' + flags.join(', ') + '</span></div>';
            }
            if (donor.employer_match) {
                html += '<div class="np-detail-field"><span class="np-field-label">Employer Match</span>' +
                    '<span class="np-field-value" style="color:var(--success)">Eligible</span></div>';
            }
            html += '</div>';
        }

        // Quick log interaction buttons
        html += '<div class="np-detail-section"><h4>Log Interaction</h4>' +
            '<div style="display:flex;gap:6px;flex-wrap:wrap">' +
            '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 10px" onclick="npLogInteraction(\'' + escapeHtml(donor.id) + '\',\'call\')"><i class="fas fa-phone"></i> Call</button>' +
            '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 10px" onclick="npLogInteraction(\'' + escapeHtml(donor.id) + '\',\'meeting\')"><i class="fas fa-handshake"></i> Meeting</button>' +
            '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 10px" onclick="npLogInteraction(\'' + escapeHtml(donor.id) + '\',\'email\')"><i class="fas fa-at"></i> Email</button>' +
            '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 10px" onclick="npLogInteraction(\'' + escapeHtml(donor.id) + '\',\'note\')"><i class="fas fa-sticky-note"></i> Note</button>' +
            '</div></div>';

        // Interaction timeline
        const interactions = (donor.interactions || []).slice().reverse().slice(0, 20);
        if (interactions.length) {
            html += '<div class="np-detail-section"><h4>Recent Activity</h4>';
            html += '<ul class="np-timeline">';
            for (const int of interactions) {
                const typeClass = 'np-tl-' + (int.type || 'note');
                const dateStr = (int.date || '').slice(0, 10);
                let desc = '';
                if (int.type === 'gift' && int.amount) {
                    desc = '<strong>Gift:</strong> $' + Number(int.amount).toLocaleString('en-US', {minimumFractionDigits: 2});
                } else {
                    desc = '<strong>' + escapeHtml((int.type || 'note').charAt(0).toUpperCase() + (int.type || 'note').slice(1)) + '</strong>';
                }
                if (int.description) desc += ' — ' + escapeHtml(int.description.slice(0, 80));
                html += '<li class="' + typeClass + '"><span class="np-tl-date">' + escapeHtml(dateStr) + '</span><br>' + desc + '</li>';
            }
            html += '</ul></div>';
        }

        document.getElementById('np-detail-body').innerHTML = html;

        // Action buttons
        let actHtml = '<button class="btn btn-primary" style="font-size:0.85rem" onclick="npShowDonationModal(\'' +
            escapeHtml(donor.id) + '\',\'' + escapeHtml(donor.name) + '\')"><i class="fas fa-hand-holding-dollar"></i> Log Donation</button>' +
            '<button class="btn btn-secondary" style="font-size:0.85rem" onclick="npLogInteraction(\'' +
            escapeHtml(donor.id) + '\',\'thanks\')"><i class="fas fa-envelope"></i> Mark Thanked</button>' +
            '<button class="btn btn-secondary" style="font-size:0.85rem" onclick="npShowThankYouModal(\'' +
            escapeHtml(donor.name) + '\',' + (donor.total_given || 0) + ')"><i class="fas fa-envelope-open-text"></i> Thank You Letter</button>' +
            '<button class="btn btn-secondary" style="font-size:0.85rem" onclick="npShowReceiptModal(\'' +
            escapeHtml(donor.name) + '\',' + (donor.total_given || 0) + ')"><i class="fas fa-file-invoice"></i> Receipt</button>' +
            '<button class="btn btn-secondary" style="font-size:0.85rem" onclick="npCloseDetail();npShowDonorModal(' +
            JSON.stringify(donor).replace(/"/g, '&quot;') + ')"><i class="fas fa-pen"></i> Edit</button>' +
            '<button class="btn btn-secondary" style="font-size:0.85rem;color:var(--error-strong)" onclick="npCloseDetail();npDeleteDonor(\'' +
            escapeHtml(donor.id) + '\',\'' + escapeHtml(donor.name) + '\')"><i class="fas fa-trash"></i> Delete</button>';
        document.getElementById('np-detail-actions').innerHTML = actHtml;

        // Show panel
        document.getElementById('np-detail-overlay').classList.add('show');
        document.getElementById('np-detail-panel').classList.add('show');
    } catch (e) {
        console.error('Failed to load donor detail:', e);
    }
}

function npCloseDetail() {
    document.getElementById('np-detail-overlay').classList.remove('show');
    document.getElementById('np-detail-panel').classList.remove('show');
    WorkspaceState.currentDonor = null;
}

async function npLogInteraction(donorId, type) {
    try {
        await apiFetch(API + '/api/nonprofit/donors/' + donorId + '/interaction', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({type: type, description: 'Logged from dashboard'}),
        });
        // Refresh the detail panel
        npShowDonorDetail(donorId);
        // Also refresh the table if it's showing
        if (WorkspaceState.activeTab === 'donors') switchWorkspaceTab('donors');
    } catch (e) {
        alert('Failed to log interaction: ' + e.message);
    }
}

async function npChangeGrantStatus(grantId, newStatus) {
    try {
        await apiFetch(API + '/api/nonprofit/grants/' + grantId, {
            method: 'PUT',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({status: newStatus}),
        });
        if (WorkspaceState.activeTab === 'grants' || WorkspaceState.activeTab === 'grants') switchWorkspaceTab(WorkspaceState.activeTab);
        else loadWorkspace();
    } catch (e) {
        alert('Failed to update grant status: ' + e.message);
    }
}

// Close detail panel with Escape key
document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
        if (document.getElementById('np-detail-panel').classList.contains('show')) {
            npCloseDetail();
            e.stopPropagation();
        }
    }
});

// ── Nonprofit CRUD form handlers ──────────────────────

function npShowDonorModal(donor) {
    // donor is optional — if provided, we're editing
    document.getElementById('np-donor-id').value = donor ? donor.id : '';
    document.getElementById('np-donor-name').value = donor ? donor.name : '';
    document.getElementById('np-donor-email').value = donor ? (donor.email || '') : '';
    document.getElementById('np-donor-phone').value = donor ? (donor.phone || '') : '';
    document.getElementById('np-donor-tags').value = donor ? (donor.tags || []).join(', ') : '';
    document.getElementById('np-donor-notes').value = donor ? (donor.notes || '') : '';
    document.getElementById('np-donor-modal-title').textContent = donor ? 'Edit Donor' : 'Add Donor';
    document.getElementById('np-donor-save-btn').textContent = donor ? 'Update' : 'Save';
    // File section
    const filesSection = document.getElementById('np-donor-files-section');
    if (donor && donor.id) {
        filesSection.style.display = 'block';
        flLoadFiles('np-donor-files-list', 'donors', donor.id, 'nonprofit');
    } else {
        filesSection.style.display = 'none';
        document.getElementById('np-donor-files-list').innerHTML = '';
    }
    showModal('np-donor-modal');
}

async function npSaveDonor() {
    const id = document.getElementById('np-donor-id').value;
    const name = document.getElementById('np-donor-name').value.trim();
    if (!name) { alert('Name is required.'); return; }

    const tags = document.getElementById('np-donor-tags').value
        .split(',').map(t => t.trim()).filter(Boolean);

    const payload = {
        name: name,
        email: document.getElementById('np-donor-email').value.trim(),
        phone: document.getElementById('np-donor-phone').value.trim(),
        tags: tags,
        notes: document.getElementById('np-donor-notes').value.trim(),
    };

    try {
        if (id) {
            await apiFetch(API + '/api/nonprofit/donors/' + id, {
                method: 'PUT',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(payload),
            });
        } else {
            const resp = await apiFetch(API + '/api/nonprofit/donors', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(payload),
            });
            const result = await resp.json();
            const savedId = result.id || result.donor?.id || '';
            if (savedId) {
                document.getElementById('np-donor-id').value = savedId;
                document.getElementById('np-donor-files-section').style.display = 'block';
                document.getElementById('np-donor-modal-title').textContent = 'Edit Donor';
                document.getElementById('np-donor-save-btn').textContent = 'Update';
                flLoadFiles('np-donor-files-list', 'donors', savedId, 'nonprofit');
                showToast('Donor saved! You can now attach files.', 'success');
                if (WorkspaceState.activeTab === 'donors') switchWorkspaceTab('donors');
                return;
            }
        }
        hideModal('np-donor-modal');
        if (WorkspaceState.activeTab === 'donors') switchWorkspaceTab('donors');
        else loadWorkspace();
    } catch (e) {
        alert('Failed to save donor: ' + e.message);
    }
}

async function npDeleteDonor(donorId, donorName) {
    if (!confirm('Delete donor "' + donorName + '"? This cannot be undone.')) return;
    try {
        await apiFetch(API + '/api/nonprofit/donors/' + donorId, {method: 'DELETE'});
        if (WorkspaceState.activeTab === 'donors') switchWorkspaceTab('donors');
        else loadWorkspace();
    } catch (e) {
        alert('Failed to delete donor: ' + e.message);
    }
}

async function npShowDonationModal(donorId, donorName) {
    document.getElementById('np-donation-donor-id').value = donorId || '';
    document.getElementById('np-donation-donor').value = donorName || '';
    document.getElementById('np-donation-amount').value = '';
    document.getElementById('np-donation-date').value = new Date().toISOString().slice(0, 10);
    document.getElementById('np-donation-desc').value = '';
    document.getElementById('np-donation-saved-id').value = '';
    document.getElementById('np-donation-files-section').style.display = 'none';
    document.getElementById('np-donation-files-list').innerHTML = '';

    // Load donor list for autocomplete
    try {
        const res = await apiFetch(API + '/api/nonprofit/donors');
        const data = await res.json();
        const dl = document.getElementById('np-donor-list');
        dl.innerHTML = '';
        for (const d of (data.donors || [])) {
            const opt = document.createElement('option');
            opt.value = d.name;
            opt.dataset.id = d.id;
            dl.appendChild(opt);
        }
    } catch (e) {}

    // Load funds
    try {
        const res = await apiFetch(API + '/api/bookkeeping/funds');
        const data = await res.json();
        const sel = document.getElementById('np-donation-fund');
        sel.innerHTML = '';
        for (const f of (data.funds || [{name: 'General'}])) {
            sel.innerHTML += '<option value="' + escapeHtml(f.name) + '">' + escapeHtml(f.name) + '</option>';
        }
    } catch (e) {}

    showModal('np-donation-modal');
}

async function npSaveDonation() {
    const donorName = document.getElementById('np-donation-donor').value.trim();
    const amount = parseFloat(document.getElementById('np-donation-amount').value);
    if (!donorName) { alert('Donor name is required.'); return; }
    if (!amount || amount <= 0) { alert('Please enter a valid amount.'); return; }

    // First log the donation as a donor interaction
    const donorId = document.getElementById('np-donation-donor-id').value;
    try {
        if (donorId) {
            await apiFetch(API + '/api/nonprofit/donors/' + donorId + '/interaction', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    type: 'gift',
                    amount: amount,
                    date: document.getElementById('np-donation-date').value,
                    description: document.getElementById('np-donation-desc').value.trim(),
                }),
            });
        } else {
            // Create donor + log gift via log_donor
            await apiFetch(API + '/api/nonprofit/donors', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    name: donorName,
                    type: 'gift',
                    amount: amount,
                    description: document.getElementById('np-donation-desc').value.trim(),
                }),
            });
        }

        // Also log as a bookkeeping transaction
        const txResp = await apiFetch(API + '/api/bookkeeping/transactions', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                type: 'income',
                amount: amount,
                date: document.getElementById('np-donation-date').value,
                category: document.getElementById('np-donation-category').value,
                fund: document.getElementById('np-donation-fund').value,
                source: donorName,
                description: document.getElementById('np-donation-desc').value.trim(),
            }),
        });
        const txResult = await txResp.json();
        const txId = txResult.transaction?.id || '';
        if (txId) {
            document.getElementById('np-donation-saved-id').value = txId;
            document.getElementById('np-donation-files-section').style.display = 'block';
            flLoadFiles('np-donation-files-list', 'donations', txId, 'nonprofit');
            showToast('Donation logged! You can now attach receipts.', 'success');
            if (WorkspaceState.activeTab === 'donors') switchWorkspaceTab('donors');
            return;
        }

        hideModal('np-donation-modal');
        if (WorkspaceState.activeTab === 'donors') switchWorkspaceTab('donors');
        else loadWorkspace();
    } catch (e) {
        alert('Failed to log donation: ' + e.message);
    }
}

function npShowExpenseModal() {
    document.getElementById('np-expense-amount').value = '';
    document.getElementById('np-expense-date').value = new Date().toISOString().slice(0, 10);
    document.getElementById('np-expense-vendor').value = '';
    document.getElementById('np-expense-desc').value = '';
    document.getElementById('np-expense-saved-id').value = '';
    document.getElementById('np-expense-files-section').style.display = 'none';
    document.getElementById('np-expense-files-list').innerHTML = '';

    // Load funds
    apiFetch(API + '/api/bookkeeping/funds').then(r => r.json()).then(data => {
        const sel = document.getElementById('np-expense-fund');
        sel.innerHTML = '';
        for (const f of (data.funds || [{name: 'General'}])) {
            sel.innerHTML += '<option value="' + escapeHtml(f.name) + '">' + escapeHtml(f.name) + '</option>';
        }
    }).catch(() => {});

    showModal('np-expense-modal');
}

async function npSaveExpense() {
    const amount = parseFloat(document.getElementById('np-expense-amount').value);
    if (!amount || amount <= 0) { alert('Please enter a valid amount.'); return; }

    try {
        const resp = await apiFetch(API + '/api/bookkeeping/transactions', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                type: 'expense',
                amount: amount,
                date: document.getElementById('np-expense-date').value,
                category: document.getElementById('np-expense-category').value,
                fund: document.getElementById('np-expense-fund').value,
                vendor: document.getElementById('np-expense-vendor').value.trim(),
                description: document.getElementById('np-expense-desc').value.trim(),
            }),
        });
        const result = await resp.json();
        const txId = result.transaction?.id || '';
        if (txId) {
            document.getElementById('np-expense-saved-id').value = txId;
            document.getElementById('np-expense-files-section').style.display = 'block';
            flLoadFiles('np-expense-files-list', 'expenses', txId, 'nonprofit');
            showToast('Expense logged! You can now attach receipts.', 'success');
            if (WorkspaceState.activeTab === 'financials') switchWorkspaceTab('financials');
            return;
        }
        hideModal('np-expense-modal');
        if (WorkspaceState.activeTab === 'financials') switchWorkspaceTab('financials');
        else loadWorkspace();
    } catch (e) {
        alert('Failed to log expense: ' + e.message);
    }
}

function npShowGrantModal(grant) {
    document.getElementById('np-grant-id').value = grant ? grant.id : '';
    document.getElementById('np-grant-name').value = grant ? grant.name : '';
    document.getElementById('np-grant-funder').value = grant ? (grant.funder || '') : '';
    document.getElementById('np-grant-amount').value = grant ? (grant.amount || '') : '';
    document.getElementById('np-grant-status').value = grant ? (grant.status || 'prospect') : 'prospect';
    document.getElementById('np-grant-deadline').value = grant ? (grant.deadline || '').slice(0, 10) : '';
    document.getElementById('np-grant-report-due').value = grant ? (grant.report_due || '').slice(0, 10) : '';
    document.getElementById('np-grant-notes').value = grant ? (grant.notes || '') : '';
    document.getElementById('np-grant-modal-title').textContent = grant ? 'Edit Grant' : 'Add Grant';
    document.getElementById('np-grant-save-btn').textContent = grant ? 'Update' : 'Save';
    document.getElementById('np-grant-file-input').value = '';
    // Show files section for existing grants
    const filesSection = document.getElementById('np-grant-files-section');
    if (grant && grant.id) {
        filesSection.style.display = 'block';
        flLoadFiles('np-grant-files-list', 'grants', grant.id, 'nonprofit');
    } else {
        filesSection.style.display = 'none';
        document.getElementById('np-grant-files-list').innerHTML = '';
    }
    showModal('np-grant-modal');
}

async function npSaveGrant() {
    const id = document.getElementById('np-grant-id').value;
    const name = document.getElementById('np-grant-name').value.trim();
    if (!name) { alert('Grant name is required.'); return; }

    const payload = {
        name: name,
        funder: document.getElementById('np-grant-funder').value.trim(),
        amount: parseFloat(document.getElementById('np-grant-amount').value) || null,
        status: document.getElementById('np-grant-status').value,
        deadline: document.getElementById('np-grant-deadline').value || null,
        report_due: document.getElementById('np-grant-report-due').value || null,
        notes: document.getElementById('np-grant-notes').value.trim(),
    };

    try {
        let savedId = id;
        if (id) {
            await apiFetch(API + '/api/nonprofit/grants/' + id, {
                method: 'PUT',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(payload),
            });
        } else {
            const resp = await apiFetch(API + '/api/nonprofit/grants', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(payload),
            });
            const result = await resp.json();
            savedId = result.id || result.grant?.id || '';
            // Show files section now that we have an ID
            if (savedId) {
                document.getElementById('np-grant-id').value = savedId;
                document.getElementById('np-grant-files-section').style.display = 'block';
                document.getElementById('np-grant-modal-title').textContent = 'Edit Grant';
                document.getElementById('np-grant-save-btn').textContent = 'Update';
                flLoadFiles('np-grant-files-list', 'grants', savedId, 'nonprofit');
                showToast('Grant saved! You can now attach files.', 'success');
                if (WorkspaceState.activeTab === 'grants') switchWorkspaceTab('grants');
                return; // Keep modal open for file attachment
            }
        }
        hideModal('np-grant-modal');
        if (WorkspaceState.activeTab === 'grants') switchWorkspaceTab('grants');
        else loadWorkspace();
    } catch (e) {
        alert('Failed to save grant: ' + e.message);
    }
}

// ── Reusable FileStore UI functions (fl- prefix) ──

async function flLoadFiles(containerId, recordType, recordId, jobClass) {
    const container = document.getElementById(containerId);
    if (!container) return;
    container.innerHTML = '<span style="color:var(--text-dim);font-size:0.8em"><i class="fas fa-spinner fa-spin"></i> Loading files...</span>';
    try {
        const res = await apiFetch(API + '/api/files?record_type=' + encodeURIComponent(recordType) + '&record_id=' + encodeURIComponent(recordId) + (jobClass ? '&job_class=' + encodeURIComponent(jobClass) : ''));
        const data = await res.json();
        const files = data.files || [];
        if (!files.length) {
            container.innerHTML = '<span style="color:var(--text-dim);font-size:0.8em">No files attached yet.</span>';
            return;
        }
        let html = '';
        const mimeIcons = {
            'application/pdf': 'fa-file-pdf',
            'image/': 'fa-file-image',
            'text/': 'fa-file-lines',
            'application/vnd.openxmlformats': 'fa-file-word',
            'application/zip': 'fa-file-zipper',
        };
        for (const f of files) {
            let icon = 'fa-file';
            for (const [prefix, ic] of Object.entries(mimeIcons)) {
                if ((f.mime_type || '').startsWith(prefix)) { icon = ic; break; }
            }
            const size = f.size_bytes < 1024 ? f.size_bytes + ' B' :
                f.size_bytes < 1048576 ? (f.size_bytes / 1024).toFixed(1) + ' KB' :
                (f.size_bytes / 1048576).toFixed(1) + ' MB';
            html += '<div style="display:flex;align-items:center;gap:8px;padding:4px 0;font-size:0.85em;border-bottom:1px solid var(--border)">';
            html += '<i class="fas ' + icon + '" style="color:var(--accent);width:16px;text-align:center"></i>';
            html += '<a href="' + API + '/api/files/' + f.file_id + '" target="_blank" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)" title="' + escapeHtml(f.description || f.filename) + '">' + escapeHtml(f.filename) + '</a>';
            html += '<span style="color:var(--text-dim);font-size:0.8em;flex-shrink:0">' + size + '</span>';
            html += '<button onclick="flDeleteFile(\'' + f.file_id + '\',\'' + containerId + '\',\'' + encodeURIComponent(recordType) + '\',\'' + encodeURIComponent(recordId) + '\',\'' + encodeURIComponent(jobClass || '') + '\')" style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:2px;font-size:0.8em" title="Delete" onmouseover="this.style.color=\'var(--error-strong)\'" onmouseout="this.style.color=\'var(--text-dim)\'"><i class="fas fa-trash"></i></button>';
            html += '</div>';
        }
        container.innerHTML = html;
    } catch (e) {
        container.innerHTML = '<span style="color:var(--error-strong);font-size:0.8em">Failed to load files.</span>';
    }
}

async function flUploadFiles(inputId, recordType, recordId, jobClass, listContainerId) {
    const input = document.getElementById(inputId);
    if (!input || !input.files.length) { showToast('Select files first', 'error'); return; }
    let uploaded = 0;
    for (const file of input.files) {
        const form = new FormData();
        form.append('file', file);
        form.append('record_type', recordType);
        form.append('record_id', recordId);
        form.append('job_class', jobClass);
        try {
            const res = await apiFetch(API + '/api/files/upload', {method: 'POST', body: form});
            const data = await res.json();
            if (data.status === 'ok') uploaded++;
            else showToast('Upload failed: ' + (data.error || file.name), 'error');
        } catch (e) {
            showToast('Upload failed: ' + file.name, 'error');
        }
    }
    if (uploaded) {
        showToast(uploaded + ' file' + (uploaded > 1 ? 's' : '') + ' uploaded', 'success');
        input.value = '';
        flLoadFiles(listContainerId, recordType, recordId, jobClass);
    }
}

async function flDeleteFile(fileId, listContainerId, recordType, recordId, jobClass) {
    if (!confirm('Delete this file?')) return;
    try {
        await apiFetch(API + '/api/files/' + fileId, {method: 'DELETE'});
        showToast('File deleted', 'success');
        flLoadFiles(listContainerId, decodeURIComponent(recordType), decodeURIComponent(recordId), decodeURIComponent(jobClass));
    } catch (e) {
        showToast('Delete failed', 'error');
    }
}

async function npUploadGrantFiles() {
    const grantId = document.getElementById('np-grant-id').value;
    if (!grantId) { showToast('Save the grant first', 'error'); return; }
    await flUploadFiles('np-grant-file-input', 'grants', grantId, 'nonprofit', 'np-grant-files-list');
}

async function swUploadCaseFiles() {
    const caseId = document.getElementById('sw-case-id').value;
    if (!caseId) { showToast('Save the case first', 'error'); return; }
    await flUploadFiles('sw-case-file-input', 'cases', caseId, 'social_worker', 'sw-case-files-list');
}

async function npDeleteGrant(grantId, grantName) {
    if (!confirm('Delete grant "' + grantName + '"? This cannot be undone.')) return;
    try {
        await apiFetch(API + '/api/nonprofit/grants/' + grantId, {method: 'DELETE'});
        if (WorkspaceState.activeTab === 'grants') switchWorkspaceTab('grants');
        else loadWorkspace();
    } catch (e) {
        alert('Failed to delete grant: ' + e.message);
    }
}

function npShowNoteModal(note) {
    document.getElementById('np-note-id').value = note ? (note.id || '') : '';
    document.getElementById('np-note-content').value = note ? (note.content || '') : '';
    document.getElementById('np-note-category').value = note ? (note.category || 'general') : 'general';
    document.getElementById('np-note-modal-title').textContent = note ? 'Edit Note' : 'Add Note';
    // File section
    const filesSection = document.getElementById('np-note-files-section');
    if (note && note.id) {
        filesSection.style.display = 'block';
        flLoadFiles('np-note-files-list', 'notes', note.id, 'nonprofit');
    } else {
        filesSection.style.display = 'none';
        document.getElementById('np-note-files-list').innerHTML = '';
    }
    showModal('np-note-modal');
}

async function npSaveNote() {
    const id = document.getElementById('np-note-id').value;
    const content = document.getElementById('np-note-content').value.trim();
    if (!content) { alert('Note content is required.'); return; }

    try {
        const resp = await apiFetch(API + '/api/nonprofit/notes', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                content: content,
                category: document.getElementById('np-note-category').value,
            }),
        });
        const result = await resp.json();
        const savedId = result.note?.id || id || '';
        if (savedId && !id) {
            document.getElementById('np-note-id').value = savedId;
            document.getElementById('np-note-files-section').style.display = 'block';
            document.getElementById('np-note-modal-title').textContent = 'Edit Note';
            flLoadFiles('np-note-files-list', 'notes', savedId, 'nonprofit');
            showToast('Note saved! You can now attach files.', 'success');
            if (WorkspaceState.activeTab === 'notes') switchWorkspaceTab('notes');
            return;
        }
        hideModal('np-note-modal');
        if (WorkspaceState.activeTab === 'notes') switchWorkspaceTab('notes');
        else loadWorkspace();
    } catch (e) {
        alert('Failed to save note: ' + e.message);
    }
}

async function npDeleteNote(noteId) {
    if (!confirm('Delete this note?')) return;
    try {
        await apiFetch(API + '/api/nonprofit/notes/' + noteId, {method: 'DELETE'});
        if (WorkspaceState.activeTab === 'notes') switchWorkspaceTab('notes');
        else loadWorkspace();
    } catch (e) {
        alert('Failed to delete note: ' + e.message);
    }
}

// CSV import
document.addEventListener('DOMContentLoaded', function() {
    const fileInput = document.getElementById('np-import-file');
    if (fileInput) {
        fileInput.addEventListener('change', function() {
            const file = this.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = function(e) {
                const text = e.target.result;
                const lines = text.split('\n').filter(l => l.trim());
                if (lines.length < 2) { alert('CSV must have a header row and at least one data row.'); return; }
                const headers = lines[0].split(',').map(h => h.trim().replace(/^["']|["']$/g, ''));
                const preview = document.getElementById('np-import-preview');
                const countEl = document.getElementById('np-import-count');
                const rowsEl = document.getElementById('np-import-rows');
                countEl.textContent = (lines.length - 1) + ' rows found. Columns: ' + headers.join(', ');
                // Show first 5 rows
                let html = '<table style="width:100%;font-size:0.75rem;"><thead><tr>';
                for (const h of headers) html += '<th style="padding:2px 6px;text-align:left">' + escapeHtml(h) + '</th>';
                html += '</tr></thead><tbody>';
                for (let i = 1; i < Math.min(lines.length, 6); i++) {
                    html += '<tr>';
                    const vals = lines[i].split(',').map(v => v.trim().replace(/^["']|["']$/g, ''));
                    for (const v of vals) html += '<td style="padding:2px 6px">' + escapeHtml(v) + '</td>';
                    html += '</tr>';
                }
                html += '</tbody></table>';
                if (lines.length > 6) html += '<div style="color:var(--text-dim);margin-top:4px">...and ' + (lines.length - 6) + ' more rows</div>';
                rowsEl.innerHTML = html;
                preview.style.display = '';
                document.getElementById('np-import-btn').disabled = false;
                window._npImportFile = file;
            };
            reader.readAsText(file);
        });
    }
});

async function npDoImport() {
    const file = window._npImportFile;
    if (!file) return;
    const btn = document.getElementById('np-import-btn');
    btn.disabled = true;
    btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Importing...';

    try {
        const formData = new FormData();
        formData.append('file', file);

        const res = await fetch(API + '/api/nonprofit/import', {
            method: 'POST',
            headers: _apiKey ? {'X-API-Key': _apiKey} : {},
            body: formData,
        });
        const data = await res.json();
        const resultEl = document.getElementById('np-import-result');
        if (data.error) {
            resultEl.style.display = '';
            resultEl.style.background = 'rgba(239,68,68,0.1)';
            resultEl.innerHTML = '<span style="color:var(--error-strong)">Error: ' + escapeHtml(data.error) + '</span>';
        } else {
            resultEl.style.display = '';
            resultEl.style.background = 'rgba(34,197,94,0.1)';
            let msg = '<span style="color:var(--success)">Imported ' + data.imported + ' of ' + data.total_rows + ' donors.</span>';
            if (data.errors && data.errors.length) {
                msg += '<br><span style="color:#f97316;font-size:0.8rem">' + data.errors.join('; ') + '</span>';
            }
            resultEl.innerHTML = msg;
        }
    } catch (e) {
        alert('Import failed: ' + e.message);
    } finally {
        btn.disabled = false;
        btn.innerHTML = 'Import';
    }
}

function npShowImportModal() {
    document.getElementById('np-import-file').value = '';
    document.getElementById('np-import-preview').style.display = 'none';
    document.getElementById('np-import-result').style.display = 'none';
    document.getElementById('np-import-btn').disabled = true;
    window._npImportFile = null;
    showModal('np-import-modal');
}

// Call updateWorkspaceNav on init
updateWorkspaceNav();

_routingHealth = {};  // provider name → healthy bool

async function loadRoutingConfig() {
    try {
        const fetches = [
            apiFetch('/api/config/routing'),
            apiFetch('/api/activity/health'),
        ];
        // Pre-fetch Ollama model status for per-model colors
        if (!_modelsData) fetches.push(apiFetch('/api/models/ollama'));
        const [routeResp, healthResp, modelsResp] = await Promise.all(fetches);
        if (modelsResp) {
            try { _modelsData = await modelsResp.json(); } catch(e) {}
        }
        _routingConfig = await routeResp.json();
        const healthData = await healthResp.json();
        // Build provider health map
        _routingHealth = {};
        const activeProv = (healthData.routing || {}).active_provider || '';
        _routingHealth._active = activeProv.toLowerCase();
        _routingHealth._activeKey = ((healthData.routing || {}).active_provider_key || '').toLowerCase();
        _routingHealth._activeModel = ((healthData.routing || {}).active_model || '').toLowerCase();
        for (const dep of (healthData.dependencies || [])) {
            if (dep.type === 'llm_provider') {
                _routingHealth[dep.name.toLowerCase()] = dep.healthy;
            }
        }
        renderRoutingCard(_routingConfig);
        loadPresets();
        loadOllamaLoaded();
    } catch(e) {
        document.getElementById('routing-content').innerHTML =
            '<div class="routing-note">Could not load routing config.</div>';
    }
}

async function loadOllamaLoaded() {
    const wrap = document.getElementById('ollama-loaded-content');
    if (!wrap) return;
    try {
        const resp = await apiFetch('/api/models/ollama/loaded');
        const d = await resp.json();
        const models = d.loaded || [];
        if (!models.length) {
            wrap.innerHTML = '<div class="olm-empty">No models loaded — Ollama is idle.</div>';
            return;
        }
        // Estimate system RAM for meter (default 32GB if unknown)
        const sysRam = 32 * 1024; // MB
        const pct = Math.min(100, Math.round(d.total_ram_mb / sysRam * 100));
        let html = '<div class="olm-bar">'
            + `<span><strong>${models.length}</strong> model${models.length > 1 ? 's' : ''} loaded</span>`
            + `<span class="olm-total">${d.total_ram_mb} MB / ~${sysRam} MB RAM</span></div>`
            + `<div class="olm-meter"><div class="olm-meter-fill" style="width:${pct}%"></div></div>`
            + '<div style="height:8px"></div>';
        for (const m of models) {
            const sizeMB = Math.round(m.size_bytes / 1024 / 1024);
            const ttl = m.expires_at ? new Date(m.expires_at) : null;
            const remaining = ttl ? Math.max(0, Math.round((ttl - Date.now()) / 60000)) : '?';
            html += '<div class="olm-model">'
                + `<span class="olm-name">${m.name}</span>`
                + `<span class="olm-params">${m.parameter_size || ''}</span>`
                + (m.quantization ? `<span class="olm-quant">${m.quantization}</span>` : '')
                + `<span class="olm-size">${sizeMB} MB</span>`
                + `<span class="olm-ctx">ctx ${m.context_length || '?'} · ${remaining}m left</span>`
                + '</div>';
        }
        wrap.innerHTML = html;
    } catch(e) {
        wrap.innerHTML = '<div class="olm-empty">Could not reach Ollama.</div>';
    }
}

const _MODEL_HINTS = {
    // Cloud providers
    'claude-opus': 'most capable, deep reasoning',
    'claude-sonnet': 'strong reasoning, code, analysis',
    'claude-haiku': 'fast, lightweight tasks',
    'gpt-4o': 'versatile, vision, long context',
    'gpt-4o-mini': 'fast, cost-efficient',
    'gemini-2.5-flash': 'fast, free tier, long context',
    // Ollama — original models
    'ollama/llama3.2': 'local, private, general purpose',
    'ollama/llama3.2:latest': 'local, private, general purpose',
    'ollama/deepseek-r1:14b': 'deep reasoning, chain-of-thought',
    'ollama/qwen2.5:0.5b': 'tiny, background tasks',
    'ollama/qwen2.5:3b': 'light reasoning, fast',
    'ollama/qwen2.5:7b': 'solid reasoning, local',
    'ollama/qwen2.5:14b': 'strong reasoning and code',
    'ollama/qwen3:8b': 'hybrid thinking, local',
    'ollama/qwen3.5:27b': 'advanced reasoning, local',
    'ollama/mistral': 'balanced, multilingual',
    'ollama/gemma3:4b': 'compact, efficient',
    'ollama/phi4:14b': 'math, logic, reasoning',
    'ollama/smollm2:1.7b': 'ultra-light, simple tasks',
    'ollama/nomic-embed-text': 'embeddings only',
    'ollama/nomic-embed-text:latest': 'embeddings only',
    'ollama/tinyllama': 'minimal, edge tasks',
    // Ollama — 2026 best-in-class
    'ollama/glm-5': '#1 open-source reasoning, 200K context',
    'ollama/deepseek-v3.2': 'top-tier agent, MoE architecture',
    'ollama/gpt-oss:20b': 'open-weight reasoning, agentic',
    'ollama/gpt-oss:120b': 'full-size open-weight, o4-mini class',
    'ollama/llama3.3': 'major upgrade, ecosystem leader',
    'ollama/qwen3-coder:30b': 'agentic coding, 1M context',
    'ollama/devstral-small-2': 'coding agent, codebase exploration',
    'ollama/granite4:3b': 'enterprise, strong tool calling',
    'ollama/nemotron-3-nano': 'efficient agentic, 1M context',
    'ollama/lfm2': 'hybrid, on-device deployment',
    'ollama/ministral-3:8b': 'edge deployment, compact',
    'ollama/glm-4.7-flash': 'lightweight, balanced performance',
    'ollama/functiongemma': 'ultra-light, function calling',
    'ollama/lfm2.5-thinking': 'reasoning on edge devices',
};

function _modelHint(modelName) {
    const key = modelName.toLowerCase();
    if (_MODEL_HINTS[key]) return _MODEL_HINTS[key];
    // Fallback: check model catalog data for description
    if (_modelsData && _modelsData.models) {
        const tag = key.replace(/^ollama\//i, '');
        const match = _modelsData.models.find(m => m.tag === tag || m.tag === tag + ':latest');
        if (match && match.description) return match.description;
    }
    return '';
}

function _modelProvider(modelName) {
    const m = modelName.toLowerCase();
    if (m.startsWith('ollama/') || m.startsWith('ollama-')) return 'ollama';
    if (m.includes('claude') || m.includes('anthropic')) return 'anthropic';
    if (m.includes('gpt') || m.includes('openai')) return 'openai';
    if (m.includes('gemini') || m.includes('google')) return 'gemini';
    return '';
}

function _modelColor(modelName) {
    const provKey = _modelProvider(modelName);
    // For Ollama models, check individual model install/active status
    if (provKey === 'ollama' && _modelsData && _modelsData.models) {
        const ollamaTag = modelName.replace(/^ollama\//i, '');
        const match = _modelsData.models.find(m =>
            m.tag === ollamaTag || m.tag === ollamaTag + ':latest'
            || ollamaTag === m.tag + ':latest');
        if (match) {
            if (match.active) return 'var(--success)';
            if (match.installed) return 'var(--warning)';
            return 'var(--error)';
        }
        // Not in catalog — check if Ollama is running at all
        if (!_modelsData.ollama_running) return 'var(--error)';
        return 'var(--warning)';
    }
    // Cloud providers — check active model and provider health
    const activeModel = _routingHealth._activeModel || '';
    const activeKey = _routingHealth._activeKey || '';
    // Normalize route name to model ID for comparison
    const normalized = _normalizeModelId(modelName.toLowerCase());
    // Exact model match = active (green)
    if (provKey && activeModel && (normalized === activeModel || modelName.toLowerCase() === activeModel)) return 'var(--success)';
    // Same provider, different model = available (yellow)
    if (provKey && provKey === activeKey) return 'var(--warning)';
    // Provider healthy but not active = available (yellow)
    if (provKey && provKey in _routingHealth) {
        return _routingHealth[provKey] ? 'var(--warning)' : 'var(--error)';
    }
    return 'var(--text-dim)';
}

// Map route names (claude-sonnet) to actual model IDs (claude-sonnet-4-6)
const _MODEL_ID_MAP = {
    'claude-opus': 'claude-opus-4-6',
    'claude-sonnet': 'claude-sonnet-4-6',
    'claude-haiku': 'claude-haiku-4-5-20251001',
    'gpt-4o': 'gpt-4o',
    'gpt-4o-mini': 'gpt-4o-mini',
    'gemini-2.5-flash': 'gemini-2.5-flash',
};
function _normalizeModelId(name) {
    return _MODEL_ID_MAP[name] || name;
}

function _modelStatus(modelName) {
    const provKey = _modelProvider(modelName);
    // For Ollama models, check individual model install/active status
    if (provKey === 'ollama' && _modelsData && _modelsData.models) {
        const ollamaTag = modelName.replace(/^ollama\//i, '');
        const match = _modelsData.models.find(m =>
            m.tag === ollamaTag || m.tag === ollamaTag + ':latest'
            || ollamaTag === m.tag + ':latest');
        if (match) {
            if (match.active) return 'active';
            if (match.installed) return 'available';
            return 'unavailable';
        }
        if (_modelsData.ollama_running) return 'available';
        return 'unavailable';
    }
    // Cloud providers — check exact model
    const activeModel = _routingHealth._activeModel || '';
    const activeKey = _routingHealth._activeKey || '';
    const normalized = _normalizeModelId(modelName.toLowerCase());
    if (provKey && activeModel && (normalized === activeModel || modelName.toLowerCase() === activeModel)) return 'active';
    if (provKey && provKey === activeKey) return 'available';
    if (provKey && provKey in _routingHealth) {
        return _routingHealth[provKey] ? 'available' : 'unavailable';
    }
    return '';
}

function _buildColorDropdown(container, items, selectedValue, onSelect) {
    // items: [{value, label, color, hint?}]
    container.innerHTML = '';
    const wrap = document.createElement('div');
    wrap.className = 'cdrop';
    const btn = document.createElement('div');
    btn.className = 'cdrop-btn';
    const list = document.createElement('div');
    list.className = 'cdrop-list';

    const selItem = items.find(i => i.value === selectedValue) || items[0];

    function setBtn(item) {
        let h = '<span style="display:flex;align-items:center;gap:8px;flex:1;min-width:0;">'
            + '<span class="cdrop-dot" style="background:' + item.color + ';"></span>'
            + '<span style="color:' + item.color + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + item.label + '</span>';
        if (item.hint) h += '<span style="color:var(--text-dim);font-size:0.7rem;margin-left:auto;white-space:nowrap;">' + item.hint + '</span>';
        h += '</span>';
        btn.innerHTML = h;
    }
    setBtn(selItem);

    for (const item of items) {
        const row = document.createElement('div');
        row.className = 'cdrop-item' + (item.value === selectedValue ? ' selected' : '');
        let rowH = '<span class="cdrop-dot" style="background:' + item.color + ';"></span>'
            + '<span style="color:' + item.color + ';flex:1;">' + item.label + '</span>';
        if (item.hint) rowH += '<span style="color:var(--text-dim);font-size:0.7rem;white-space:nowrap;">' + item.hint + '</span>';
        row.innerHTML = rowH;
        row.addEventListener('click', () => {
            setBtn(item);
            wrap.classList.remove('open');
            list.querySelectorAll('.cdrop-item').forEach(r => r.classList.remove('selected'));
            row.classList.add('selected');
            onSelect(item.value);
        });
        list.appendChild(row);
    }

    btn.addEventListener('click', (e) => {
        e.stopPropagation();
        document.querySelectorAll('.cdrop.open').forEach(d => { if (d !== wrap) d.classList.remove('open'); });
        const opening = !wrap.classList.contains('open');
        wrap.classList.toggle('open');
        if (opening) {
            const rect = btn.getBoundingClientRect();
            const spaceBelow = window.innerHeight - rect.bottom;
            list.style.width = rect.width + 'px';
            list.style.left = rect.left + 'px';
            if (spaceBelow < 200 && rect.top > 200) {
                list.style.bottom = (window.innerHeight - rect.top + 2) + 'px';
                list.style.top = 'auto';
            } else {
                list.style.top = (rect.bottom + 2) + 'px';
                list.style.bottom = 'auto';
            }
        }
    });

    wrap.appendChild(btn);
    wrap.appendChild(list);
    container.appendChild(wrap);
}

// Close dropdowns on outside click
document.addEventListener('click', () => {
    document.querySelectorAll('.cdrop.open').forEach(d => d.classList.remove('open'));
});

function _updateProviderSelect() {
    const models = [
        { value: 'claude-opus', label: 'Claude Opus', key: 'anthropic', hint: 'most capable, deep reasoning' },
        { value: 'claude-sonnet', label: 'Claude Sonnet', key: 'anthropic', hint: 'strong reasoning, code, analysis' },
        { value: 'claude-haiku', label: 'Claude Haiku', key: 'anthropic', hint: 'fast, lightweight tasks' },
        { value: 'gpt-4o', label: 'GPT-4o', key: 'openai', hint: 'versatile, vision, long context' },
        { value: 'gpt-4o-mini', label: 'GPT-4o Mini', key: 'openai', hint: 'fast, cost-efficient' },
        { value: 'gemini', label: 'Gemini 2.5 Flash', key: 'gemini', hint: 'fast, free tier, long context' },
        { value: 'ollama', label: 'Ollama (Local)', key: 'ollama', hint: 'local, private, no API cost' },
    ];
    const activeKey = _routingHealth._activeKey || '';
    const activeModel = _routingHealth._activeModel || '';
    const wrap = document.getElementById('provider-select-wrap');
    if (!wrap) return;

    // Map active model ID to dropdown value for precise matching
    const modelToValue = {
        'claude-opus-4-6': 'claude-opus',
        'claude-sonnet-4-6': 'claude-sonnet',
        'claude-haiku-4-5-20251001': 'claude-haiku',
        'gpt-4o': 'gpt-4o',
        'gpt-4o-mini': 'gpt-4o-mini',
        'gemini-2.5-flash': 'gemini',
        'gemini-2.5-flash-preview-05-20': 'gemini',
    };
    // Also match by provider key when model ID doesn't match
    const keyToValue = {
        'anthropic': 'claude-sonnet',
        'openai': 'gpt-4o',
        'gemini': 'gemini',
        'ollama': 'ollama',
    };
    const activeVal = modelToValue[activeModel] || keyToValue[activeKey] || '';

    // Check if Ollama has any installed models
    const ollamaHasModels = _modelsData && _modelsData.ollama_running
        && _modelsData.models && _modelsData.models.some(m => m.installed);

    let selectedVal = '';
    const items = models.map(m => {
        const isThisModel = m.value === activeVal;
        const isThisProvider = activeKey === m.key;
        const healthy = _routingHealth[m.key];
        let suffix = '', color = 'var(--text-dim)';
        if (isThisModel) {
            color = 'var(--success)'; suffix = ' (active)'; selectedVal = m.value;
        } else if (m.key === 'ollama' && activeKey === 'ollama') {
            color = 'var(--success)'; suffix = ' (active)'; selectedVal = m.value;
        } else if (isThisProvider) {
            // Same provider, different model — it's available
            color = 'var(--warning)'; suffix = ' (available)';
        } else if (m.key === 'ollama' && ollamaHasModels) {
            color = 'var(--warning)'; suffix = ' (available)';
        } else if (healthy === true) {
            color = 'var(--warning)'; suffix = ' (available)';
        } else if (healthy === false) {
            color = 'var(--error)'; suffix = ' (unavailable)';
        }
        return { value: m.value, label: m.label + suffix, color, hint: m.hint };
    });

    _buildColorDropdown(wrap, items, selectedVal || models[0].value, (val) => {
        changeProvider(val);
    });
}

function renderRoutingCard(cfg) {
    // Update provider select with health colors
    _updateProviderSelect();

    const el = document.getElementById('routing-content');
    if (!cfg.router_active) {
        el.innerHTML = '<div class="routing-note">' +
            '<i class="fas fa-info-circle"></i> ' +
            'Add a second LLM provider to enable model routing.</div>';
        return;
    }
    const isManual = cfg.mode === 'manual';
    let html = '<div class="routing-toggle-bar">' +
        '<div><span class="label">Mode:</span>' +
        '<span class="mode-value">' + (isManual ? 'Manual' : 'Auto') + '</span></div>' +
        '<div class="toggle ' + (isManual ? 'on' : '') + '" ' +
        'onclick="toggleRoutingMode()"></div></div>';

    // Legend
    html += '<div style="font-size:0.7rem;margin-bottom:8px;color:var(--text-dim);">'
        + '<span style="color:var(--success);">&#9679;</span> active &nbsp;'
        + '<span style="color:var(--warning);">&#9679;</span> available &nbsp;'
        + '<span style="color:var(--error);">&#9679;</span> unavailable'
        + '</div>';

    // Fallback provider selector + explanation
    const activeName = _routingHealth._active || '';
    html += '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;">'
        + '<span style="font-size:0.8rem;color:var(--text-dim);white-space:nowrap;">Fallback Provider:</span>'
        + '<div id="routing-fallback-provider" style="flex:1;"></div>'
        + '</div>';
    if (isManual && activeName) {
        html += '<div style="font-size:0.75rem;color:var(--text-dim);background:var(--bg-input);border-radius:6px;padding:8px 10px;margin-bottom:10px;line-height:1.5;">'
            + '<i class="fas fa-info-circle" style="color:var(--accent);margin-right:4px;"></i>'
            + 'Each query is classified by type and sent to the model below. '
            + 'If the routed model is unreachable, the fallback provider above is used.'
            + '</div>';
    }

    html += '<table class="routing-table"><thead><tr>' +
        '<th class="col-cat">Category</th>' +
        '<th class="col-desc">Description</th>' +
        '<th class="col-model">Model</th></tr></thead><tbody>';

    const cats = cfg.categories || {};
    for (const [key, cat] of Object.entries(cats)) {
        const modelName = isManual ? cat.model : cat.auto_model;
        const color = _modelColor(modelName);
        const status = _modelStatus(modelName);
        html += '<tr><td class="col-cat">' +
            key.charAt(0).toUpperCase() + key.slice(1) +
            '</td><td class="col-desc">' + (cat.description || '') + '</td>' +
            '<td class="col-model">';
        if (isManual) {
            html += '<div id="route-drop-' + key + '"></div>';
        } else {
            const dot = '&#9679; ';
            const hint = _modelHint(cat.auto_model);
            html += '<span style="color:' + color + ';font-size:0.6rem;">' + dot + '</span>'
                + '<span class="auto-label" style="color:' + color + ';font-weight:600;">' + cat.auto_model + '</span>';
            if (hint) html += '<div style="font-size:0.65rem;color:var(--text-dim);margin-top:1px;">' + hint + '</div>';
        }
        html += '</td></tr>';
    }
    html += '</tbody></table>';
    el.innerHTML = html;

    // Build fallback provider dropdown inside routing card
    const fbWrap = document.getElementById('routing-fallback-provider');
    if (fbWrap) {
        const provs = [
            { value: 'anthropic', label: 'Claude (Anthropic)', key: 'anthropic' },
            { value: 'openai', label: 'GPT (OpenAI)', key: 'openai' },
            { value: 'gemini', label: 'Gemini (Google)', key: 'gemini' },
            { value: 'ollama', label: 'Ollama (Local)', key: 'ollama' },
        ];
        const fbActiveKey = _routingHealth._activeKey || '';
        const ollamaReady = _modelsData && _modelsData.ollama_running
            && _modelsData.models && _modelsData.models.some(m => m.installed);
        let fbSelected = '';
        const fbItems = provs.map(p => {
            const isAct = p.key === fbActiveKey;
            const healthy = _routingHealth[p.key];
            let color = 'var(--text-dim)', suffix = '';
            if (isAct) { color = 'var(--success)'; suffix = ' (active)'; fbSelected = p.value; }
            else if (p.key === 'ollama' && ollamaReady) { color = 'var(--warning)'; suffix = ' (available)'; }
            else if (healthy === true) { color = 'var(--warning)'; suffix = ' (available)'; }
            else if (healthy === false) { color = 'var(--error)'; suffix = ' (unavailable)'; }
            return { value: p.value, label: p.label + suffix, color, hint: '' };
        });
        _buildColorDropdown(fbWrap, fbItems, fbSelected || 'anthropic', (val) => {
            changeProvider(val);
        });
    }

    // Build custom dropdowns for manual route selects
    if (isManual) {
        for (const [key, cat] of Object.entries(cats)) {
            const ctr = document.getElementById('route-drop-' + key);
            if (!ctr) continue;
            const assigned = cat.model;
            const items = cfg.available_models.map(m => {
                const isAssigned = m === assigned;
                const provKey = _modelProvider(m);
                // Route-aware color: assigned model with healthy provider = green
                let color, statusLabel;
                if (isAssigned) {
                    // Check if the provider for this model is healthy
                    const healthy = provKey === 'ollama'
                        ? (_modelsData && _modelsData.ollama_running)
                        : _routingHealth[provKey];
                    if (healthy === false) {
                        color = 'var(--error)'; statusLabel = 'assigned, unavailable';
                    } else {
                        color = 'var(--success)'; statusLabel = 'active';
                    }
                } else {
                    // Non-assigned: use standard availability color
                    const s = _modelStatus(m);
                    statusLabel = s;
                    color = _modelColor(m);
                }
                const suffix = statusLabel ? ' (' + statusLabel + ')' : '';
                return { value: m, label: m + suffix, color, hint: _modelHint(m) };
            });
            _buildColorDropdown(ctr, items, assigned, (val) => {
                setManualRoute(key, val);
            });
        }
    }
}

async function toggleRoutingMode() {
    if (!_routingConfig) return;
    const newMode = _routingConfig.mode === 'manual' ? 'auto' : 'manual';
    const body = { mode: newMode };
    if (newMode === 'manual' && _routingConfig.categories) {
        const routes = {};
        for (const [k, v] of Object.entries(_routingConfig.categories)) {
            routes[k] = v.auto_model;
        }
        body.routes = routes;
    }
    try {
        const resp = await apiFetch('/api/config/routing', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(body)
        });
        _routingConfig = await resp.json();
        renderRoutingCard(_routingConfig);
    } catch(e) { console.error('Routing toggle failed', e); }
}

async function setManualRoute(category, model) {
    if (!_routingConfig) return;
    const routes = {};
    for (const [k, v] of Object.entries(_routingConfig.categories)) {
        routes[k] = v.model;
    }
    routes[category] = model;
    try {
        const resp = await apiFetch('/api/config/routing', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({ mode: 'manual', routes })
        });
        _routingConfig = await resp.json();
        renderRoutingCard(_routingConfig);
    } catch(e) { console.error('Route update failed', e); }
}

