/* QuoteFlow Prototype · Installer admin, wired to window.QFStore. Dashboard metrics, kanban (working stage moves), contacts, lead drawer. */ function AdminApp({ onGoCustomer, onGoPortal, jump }) { const { Card, MetricCard, Button, IconButton, StatusBadge, Avatar, Tag, Select } = window.QuoteFlowDesignSystem_41788d; const store = useStore(); const [active, setActive] = React.useState('dashboard'); const [selId, setSelId] = React.useState(null); // Lifted screen-local UI state so it survives store-driven re-renders // (inline screen components get new identities on each notify()). const [range, setRange] = React.useState(0); // Dashboard date-range (days; 0 = all) const [q, setQ] = React.useState(''); // Contacts search const [sel, setSel] = React.useState({}); // Contacts bulk selection {id:true} const [editing, setEditing] = React.useState(false); // Lead drawer edit mode React.useEffect(() => { if (jump && jump.id) setSelId(jump.id); }, [jump && jump.n]); React.useEffect(() => { setEditing(false); }, [selId]); const nav = [ { id: 'dashboard', label: 'Dashboard', icon: 'layout-dashboard' }, { id: 'pipeline', label: 'Sales Pipeline', icon: 'git-branch' }, { id: 'proposals', label: 'Proposals', icon: 'file-text' }, { id: 'contacts', label: 'Contacts', icon: 'users' }, { id: 'aftercare', label: 'After Care', icon: 'heart-handshake' }, { id: 'tools', label: 'Quote Tools', icon: 'sliders-horizontal' }, { id: 'settings', label: 'Settings', icon: 'settings' }, ]; const leads = store.getLeads(); const lead = selId ? store.getLead(selId) : null; const profile = store.getProfile(); // ---------------- Sidebar ---------------- const Sidebar = () => ( ); // ---------------- Dashboard ---------------- const Dashboard = () => { const m = store.getMetrics(range || undefined); const recent = leads.slice(0, 6); const ranges = [['All time', 0], ['7 days', 7], ['30 days', 30], ['90 days', 90]]; const exportCSV = () => window.downloadText('quoteflow-leads.csv', store.leadsCSV()); return (

Welcome back, Greenline

Every quote your tools generate lands here live. You currently have {m.open} open leads.

{ranges.map(([label, days]) => ( ))}
} /> } /> } /> } /> } /> } />

Recent leads

{['Customer', 'Quote tool', 'Value', 'Status', 'Created'].map(h => )} {recent.map((l, i) => ( setSelId(l.id)} style={{ borderBottom: i < recent.length - 1 ? '1px solid var(--slate-100)' : 'none', cursor: 'pointer' }}> ))}
{h}
{l.name}
{l.system} {gbp(l.value)} {store.fmtDate(l.createdAt)}
); }; // ---------------- Contacts ---------------- const Contacts = () => { const rows = leads.filter(l => q === '' || (l.name + l.email + l.loc).toLowerCase().includes(q.toLowerCase())); const selIds = Object.keys(sel).filter(k => sel[k]); const selCount = selIds.length; const allChecked = rows.length > 0 && rows.every(l => sel[l.id]); const toggleAll = () => { const next = {}; if (!allChecked) rows.forEach(l => { next[l.id] = true; }); setSel(next); }; const toggleOne = (id) => setSel(s => Object.assign({}, s, { [id]: !s[id] })); const clearSel = () => setSel({}); const Check = ({ on }) => ( {on && } ); return (

Contacts

Everyone who's come through your quote tools. Select rows for bulk actions.

setQ(e.target.value)} placeholder="Search name, email or area…" style={{ width: '100%', height: 44, boxSizing: 'border-box', padding: '0 18px 0 42px', fontFamily: 'var(--font-sans)', fontSize: 14, color: 'var(--text-strong)', background: 'var(--surface-card)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-full)', outline: 'none' }} />
{/* bulk action bar */} {selCount > 0 && (
{selCount} selected Move to Assign
)} {['Contact', 'Interest', 'Area', 'Value', 'Status', ''].map((h, i) => )} {rows.map((l, i) => ( ))} {rows.length === 0 && }
{h}
toggleOne(l.id)}> setSelId(l.id)}>
{l.name}
{l.email}
setSelId(l.id)}>{l.system} {l.loc} {gbp(l.value)} setSelId(l.id)}>
No contacts match.
); }; // ---------------- Lead drawer ---------------- const Drawer = () => { if (!lead) return null; const acts = lead.activity.slice().reverse(); return (
setSelId(null)} style={{ position: 'fixed', inset: 0, background: 'rgba(22,14,42,0.4)', zIndex: 60 }}>
e.stopPropagation()} style={{ position: 'absolute', top: 0, right: 0, width: 440, maxWidth: '94vw', height: '100%', background: 'var(--surface-card)', boxShadow: 'var(--shadow-xl)', display: 'flex', flexDirection: 'column' }}>
{lead.name}
{lead.loc} · {lead.system}
setEditing(e => !e)} style={editing ? { background: 'var(--primary-soft)', color: 'var(--primary)' } : {}}> setSelId(null)}>
{gbp(lead.value)}
{/* working stage control */}
Stage
store.assign(lead.id, e.target.value)} options={store.OWNERS} />
{editing ? setEditing(false)} /> : (
{[['Quote tool', lead.system], ['Email', lead.email || '—'], ['Phone', lead.phone || '—'], ['Postcode', lead.postcode || '—'], ['Source', lead.source], ['Created', store.fmtDate(lead.createdAt) + ' 2026']].map(r => (
{r[0]}
{r[1]}
))}
)} {lead.config && lead.config.panels && (
Configured system
{lead.config.panels} panels{lead.config.battery ? ' · 9.5kWh battery' : ''}{lead.config.ev ? ' · EV charger' : ''}{lead.config.bird ? ' · bird protection' : ''}
)} {/* proposal + deposit status */} {lead.proposal && (
Proposal
Total{gbp(lead.proposal.total)}
Deposit (15%){gbp(lead.proposal.deposit)}
Deposit status {lead.proposal.payment ? Paid ····{lead.proposal.payment.last4} : Awaiting payment}
)} {/* survey + install milestones */} {/* email / SMS notification log */}
Activity
{acts.map((a, i) => (
{a.label}
{store.fmtTime(a.ts)}
))}
{ if (confirm('Delete ' + lead.name + '? This cannot be undone.')) { var id = lead.id; setSelId(null); store.deleteLead(id); } }} style={{ color: 'var(--error)', borderColor: 'var(--error)' }}> {lead.proposal ? : }
); }; const Screen = { dashboard: Dashboard, pipeline: function () { return ; }, contacts: Contacts, proposals: function () { return ; }, aftercare: function () { return ; }, tools: function () { return ; }, settings: function () { return ; }, profile: function () { return setActive('dashboard')} />; } }[active]; return (
); } window.AdminApp = AdminApp;