689 lines
42 KiB
JavaScript
689 lines
42 KiB
JavaScript
// ============================================================
|
||
// crm-pages.jsx — Cadence CRM · in-app screens
|
||
// ------------------------------------------------------------
|
||
// Every page renders INSIDE SidebarShell (far-left sidebar).
|
||
// Built on vibn-ai-templates components. Light/minimal theme.
|
||
//
|
||
// Pages: CRMHome, CRMPeople, CRMRecord, CRMPipeline,
|
||
// CRMInbox, CRMReports, CRMSettings.
|
||
// ============================================================
|
||
|
||
const CRM_USER = { name: "Mira Reyes", email: "mira@northwind.io", color: "#d4b8a8" };
|
||
|
||
// Sidebar config — pass the active id, get the sections array.
|
||
const crmSections = (active) => [
|
||
{ items: [
|
||
{ id: "home", label: "Home", icon: "home", active: active === "home" },
|
||
{ id: "inbox", label: "Inbox", icon: "inbox", count: 8, active: active === "inbox" },
|
||
{ id: "tasks", label: "My tasks", icon: "check", count: 3, active: active === "tasks" },
|
||
]},
|
||
{ title: "Records", items: [
|
||
{ id: "companies", label: "Companies", icon: "building", active: active === "companies" },
|
||
{ id: "people", label: "People", icon: "people", active: active === "people" },
|
||
{ id: "deals", label: "Deals", icon: "target", count: 12, active: active === "deals" },
|
||
]},
|
||
{ title: "Workspace", items: [
|
||
{ id: "reports", label: "Reports", icon: "bar", active: active === "reports" },
|
||
{ id: "automations", label: "Automations", icon: "workflow", active: active === "automations" },
|
||
{ id: "settings", label: "Settings", icon: "settings", active: active === "settings" },
|
||
]},
|
||
];
|
||
|
||
// Wrap a page body in the shell with the right nav item active
|
||
const CRMShell = ({ active, children }) => (
|
||
<SidebarShell brand={{ name: "Northwind", mark: <CadenceMark size={22}/> }}
|
||
sections={crmSections(active)} user={CRM_USER} search="Search or jump to…">
|
||
{children}
|
||
</SidebarShell>
|
||
);
|
||
|
||
// Reusable page header bar (title + actions row)
|
||
const PageBar = ({ title, sub, breadcrumb, actions }) => (
|
||
<div style={{
|
||
padding: "16px 28px", borderBottom: "1px solid var(--divider)",
|
||
background: "var(--surface)", display: "flex",
|
||
justifyContent: "space-between", alignItems: "center", gap: 16,
|
||
}}>
|
||
<div style={{ minWidth: 0 }}>
|
||
{breadcrumb && <div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 3 }}>{breadcrumb}</div>}
|
||
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 600, letterSpacing: "-0.01em" }}>{title}</h1>
|
||
{sub && <div style={{ fontSize: 13, color: "var(--text-2)", marginTop: 2 }}>{sub}</div>}
|
||
</div>
|
||
{actions && <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>{actions}</div>}
|
||
</div>
|
||
);
|
||
|
||
const Scroll = ({ children, pad = 28 }) => (
|
||
<div style={{ flex: 1, overflowY: "auto", padding: pad }}>{children}</div>
|
||
);
|
||
|
||
// ── small stat tile ──────────────────────────────────────────
|
||
const Stat = ({ label, value, delta, up, spark }) => (
|
||
<Card padding={18}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
|
||
<span style={{ fontSize: 12, color: "var(--text-2)" }}>{label}</span>
|
||
{delta && <span style={{ fontSize: 11, fontWeight: 600, color: up ? "var(--success)" : "var(--danger)" }}>
|
||
{up ? "↑" : "↓"} {delta}</span>}
|
||
</div>
|
||
<div style={{ fontSize: 28, fontWeight: 600, letterSpacing: "-0.02em", marginTop: 8 }}>{value}</div>
|
||
{spark && (
|
||
<svg viewBox="0 0 100 28" preserveAspectRatio="none" style={{ width: "100%", height: 24, marginTop: 6, display: "block" }}>
|
||
<polyline points={spark} fill="none" stroke={up ? "var(--success)" : "var(--danger)"} strokeWidth="1.5" vectorEffect="non-scaling-stroke"/>
|
||
</svg>
|
||
)}
|
||
</Card>
|
||
);
|
||
|
||
const sparkUp = "0,24 12,20 24,22 36,15 48,17 60,10 72,12 84,6 100,2";
|
||
const sparkDn = "0,6 14,8 28,7 42,12 56,11 70,16 84,15 100,20";
|
||
|
||
// ============================================================
|
||
// 1 · HOME
|
||
// ============================================================
|
||
const CRMHome = () => (
|
||
<CRMShell active="home">
|
||
<PageBar title="Good morning, Mira"
|
||
sub="3 deals need a nudge today · 8 unread in Inbox · 1 task overdue"
|
||
actions={<>
|
||
<Button variant="secondary" leadingIcon={<Icon name="bell" size={13}/>}>Notifications</Button>
|
||
<Button leadingIcon={<Icon name="plus" size={13}/>}>New deal</Button>
|
||
</>}/>
|
||
<Scroll>
|
||
{/* KPI row */}
|
||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14, marginBottom: 20 }}>
|
||
<Stat label="Open pipeline" value="$1.24M" delta="12%" up spark={sparkUp}/>
|
||
<Stat label="Won · this month" value="$284K" delta="8%" up spark={sparkUp}/>
|
||
<Stat label="Win rate · 90d" value="31%" delta="2pt" up spark={sparkUp}/>
|
||
<Stat label="Avg. response" value="2.4h" delta="0.6h" up={false} spark={sparkDn}/>
|
||
</div>
|
||
|
||
<div style={{ display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 20 }}>
|
||
{/* Pipeline by stage */}
|
||
<Card padding={0}>
|
||
<CardHeader title="Pipeline by stage" subtitle="Q2 · 12 active deals"
|
||
style={{ padding: "16px 20px", margin: 0, borderBottom: "1px solid var(--divider)" }}
|
||
action={<Button variant="ghost" size="sm">View board →</Button>}/>
|
||
<div style={{ padding: "12px 20px 18px" }}>
|
||
{[
|
||
["Lead", 5, "$420K", 100],
|
||
["Qualified", 3, "$365K", 78],
|
||
["Proposal", 2, "$280K", 60],
|
||
["Negotiation", 1, "$148K", 32],
|
||
["Won", 1, "$96K", 22],
|
||
].map(([name, n, val, w]) => (
|
||
<div key={name} style={{ padding: "8px 0" }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6, fontSize: 13 }}>
|
||
<span>{name}</span>
|
||
<span style={{ color: "var(--text-2)" }}>{n} deals · <b style={{ color: "var(--text)" }}>{val}</b></span>
|
||
</div>
|
||
<div style={{ height: 8, borderRadius: 4, background: "var(--surface-alt)", overflow: "hidden" }}>
|
||
<div style={{ width: `${w}%`, height: "100%", background: "linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 55%, transparent))", borderRadius: 4 }}/>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Today's tasks */}
|
||
<Card padding={0}>
|
||
<CardHeader title="Today" subtitle="3 tasks"
|
||
style={{ padding: "16px 20px", margin: 0, borderBottom: "1px solid var(--divider)" }}
|
||
action={<Button variant="ghost" size="sm">All tasks</Button>}/>
|
||
<div style={{ padding: "6px 12px 12px" }}>
|
||
{[
|
||
["Follow up with Acme Robotics", "Overdue · 2d", true, "danger"],
|
||
["Send proposal to Halcyon", "Due 2:00pm", false, "warn"],
|
||
["Call Sun at Northstar", "Due 4:30pm", false, "neutral"],
|
||
["Prep QBR deck for Kestrel", "Tomorrow", false, "neutral"],
|
||
].map(([t, when, overdue, tone], i) => (
|
||
<div key={i} style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 8px", borderBottom: i < 3 ? "1px solid var(--divider)" : "none" }}>
|
||
<Checkbox checked={false}/>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ fontSize: 13, fontWeight: 500 }}>{t}</div>
|
||
<div style={{ fontSize: 11, color: overdue ? "var(--danger)" : "var(--text-3)", marginTop: 1 }}>{when}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Recent activity */}
|
||
<Card padding={0} style={{ marginTop: 20 }}>
|
||
<CardHeader title="Recent activity"
|
||
style={{ padding: "16px 20px", margin: 0, borderBottom: "1px solid var(--divider)" }}
|
||
action={<Tabs variant="pill" items={[{ label: "All" }, { label: "Deals" }, { label: "Emails" }]} active="All"/>}/>
|
||
<div style={{ padding: "8px 20px 16px" }}>
|
||
{[
|
||
["MR", "#d4b8a8", "Mira Reyes", "moved", "Acme — Renewal '26 to Negotiation", "12m"],
|
||
["TR", "#c8e8a8", "Theo Roux", "logged a call with", "Sun Kim · Northstar", "1h"],
|
||
["DP", "#a8c8e8", "Devi Patel", "won", "Halcyon · Pro renewal · $24K", "3h"],
|
||
["SK", "#e8a87c", "Sun Ortiz", "added 4 contacts to", "Kestrel", "Yesterday"],
|
||
].map(([i, c, who, verb, obj, t], idx) => (
|
||
<div key={idx} style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 0", borderTop: idx ? "1px solid var(--divider)" : "none" }}>
|
||
<Avatar name={who} color={c} size={28}/>
|
||
<div style={{ flex: 1, fontSize: 13 }}>
|
||
<b style={{ fontWeight: 600 }}>{who}</b>
|
||
<span style={{ color: "var(--text-2)" }}> {verb} </span>
|
||
<b style={{ fontWeight: 500 }}>{obj}</b>
|
||
</div>
|
||
<span style={{ fontSize: 11, color: "var(--text-3)" }}>{t}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
</Scroll>
|
||
</CRMShell>
|
||
);
|
||
|
||
// ============================================================
|
||
// 2 · PEOPLE (contacts table)
|
||
// ============================================================
|
||
const CRMPeople = () => {
|
||
const people = [
|
||
{ id: 1, name: "Iris Tanaka", color: "#e8a87c", title: "Head of Engineering", company: "Acme Robotics", stage: "Customer", owner: "MR", last: "2h" },
|
||
{ id: 2, name: "Daniel Owusu", color: "#a8c8e8", title: "VP Product", company: "Acme Robotics", stage: "Customer", owner: "MR", last: "Yesterday" },
|
||
{ id: 3, name: "Sun Kim", color: "#c8e8a8", title: "VP Operations", company: "Northstar", stage: "Lead", owner: "TR", last: "3d" },
|
||
{ id: 4, name: "Priya Nair", color: "#c8a8e8", title: "COO", company: "Halcyon", stage: "Prospect", owner: "DP", last: "1w" },
|
||
{ id: 5, name: "Marco Lindqvist", color: "#e8c8a8", title: "Procurement", company: "Kestrel", stage: "Customer", owner: "MR", last: "4d" },
|
||
{ id: 6, name: "Naila Choudhury", color: "#a8e8c8", title: "CFO", company: "Mossbank", stage: "Lead", owner: "TR", last: "2w" },
|
||
{ id: 7, name: "Henri Lamarck", color: "#e8a8c8", title: "Founder", company: "Verra", stage: "Prospect", owner: "DP", last: "5d" },
|
||
{ id: 8, name: "Emi Hara", color: "#d4b8a8", title: "Head of Sales", company: "Tide Co.", stage: "Customer", owner: "MR", last: "1d" },
|
||
];
|
||
const stageTone = { Customer: "success", Lead: "accent", Prospect: "warn" };
|
||
return (
|
||
<CRMShell active="people">
|
||
<PageBar title="People" sub="2,418 contacts"
|
||
actions={<>
|
||
<Button variant="secondary" leadingIcon={<Icon name="doc" size={13}/>}>Import</Button>
|
||
<Button leadingIcon={<Icon name="plus" size={13}/>}>New contact</Button>
|
||
</>}/>
|
||
{/* Filter row */}
|
||
<div style={{ padding: "12px 28px", borderBottom: "1px solid var(--divider)", display: "flex", alignItems: "center", gap: 10, background: "var(--surface)" }}>
|
||
<Input placeholder="Search people" leadingIcon={<Icon name="search" size={13}/>} style={{ width: 260, padding: "6px 10px" }}/>
|
||
{["Stage", "Owner", "Company", "Last activity"].map(f => (
|
||
<div key={f} style={{
|
||
display: "flex", alignItems: "center", gap: 6, padding: "6px 10px",
|
||
border: "1px dashed var(--border)", borderRadius: "var(--radius-sm)",
|
||
fontSize: 12, color: "var(--text-2)", cursor: "pointer",
|
||
}}>{f}<Icon name="chevDown" size={11}/></div>
|
||
))}
|
||
<div style={{ flex: 1 }}/>
|
||
<FieldGroup options={["Table", "Board"]} value="Table"/>
|
||
</div>
|
||
<Scroll pad={20}>
|
||
<Table
|
||
selectable selected={[1, 5]}
|
||
columns={[
|
||
{ key: "name", label: "Name", render: r => (
|
||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||
<Avatar name={r.name} color={r.color} size={28}/>
|
||
<div>
|
||
<div style={{ fontWeight: 500 }}>{r.name}</div>
|
||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{r.title}</div>
|
||
</div>
|
||
</div>
|
||
)},
|
||
{ key: "company", label: "Company", render: r => (
|
||
<span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
|
||
<span style={{ width: 20, height: 20, borderRadius: 5, background: "var(--surface-alt)", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 700, color: "var(--text-2)" }}>{r.company[0]}</span>
|
||
{r.company}
|
||
</span>
|
||
)},
|
||
{ key: "stage", label: "Stage", render: r => <Badge tone={stageTone[r.stage]} dot>{r.stage}</Badge> },
|
||
{ key: "owner", label: "Owner", render: r => <Avatar name={r.owner} size={24}/> },
|
||
{ key: "last", label: "Last activity" },
|
||
{ key: "act", label: "", align: "right", width: 40, render: () => <IconButton name="more" size="sm" label="More"/> },
|
||
]}
|
||
rows={people}
|
||
/>
|
||
</Scroll>
|
||
</CRMShell>
|
||
);
|
||
};
|
||
|
||
// ============================================================
|
||
// 3 · COMPANY RECORD (detail)
|
||
// ============================================================
|
||
const CRMRecord = () => (
|
||
<CRMShell active="companies">
|
||
<PageBar breadcrumb="Companies › Acme Robotics" title="Acme Robotics"
|
||
actions={<>
|
||
<Button variant="secondary" leadingIcon={<Icon name="star" size={13}/>}>Follow</Button>
|
||
<Button variant="secondary">Share</Button>
|
||
<Button leadingIcon={<Icon name="plus" size={13}/>}>Log activity</Button>
|
||
</>}/>
|
||
<div style={{ display: "grid", gridTemplateColumns: "320px 1fr", flex: 1, overflow: "hidden" }}>
|
||
{/* Details rail */}
|
||
<div style={{ borderRight: "1px solid var(--divider)", overflowY: "auto", padding: "20px 22px" }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 18 }}>
|
||
<div style={{ width: 52, height: 52, borderRadius: 12, background: "linear-gradient(135deg, #ff8a3a, #f43f5e)", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22, fontWeight: 700 }}>A</div>
|
||
<div>
|
||
<div style={{ display: "flex", gap: 6 }}>
|
||
<Badge tone="success" dot>Customer</Badge>
|
||
<Badge tone="accent">Tier 1</Badge>
|
||
</div>
|
||
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 6 }}>acme-robotics.io</div>
|
||
</div>
|
||
</div>
|
||
{[
|
||
["Industry", "Industrial automation"],
|
||
["Employees", "210"],
|
||
["Owner", "Mira Reyes"],
|
||
["Renewal", "Sept 1, 2026"],
|
||
["ARR", "$148,000"],
|
||
["Health", "Strong"],
|
||
].map(([k, v]) => (
|
||
<div key={k} style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: 10, padding: "8px 0", fontSize: 13, borderBottom: "1px solid var(--divider)" }}>
|
||
<span style={{ color: "var(--text-3)" }}>{k}</span>
|
||
<span style={{ fontWeight: 500 }}>{v}</span>
|
||
</div>
|
||
))}
|
||
<div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 500, margin: "18px 0 8px" }}>Open deals · 3</div>
|
||
{[
|
||
["Renewal '26", "$148K", "Negotiation", 70],
|
||
["Vision platform", "$62K", "Discovery", 30],
|
||
["Edge SDK pilot", "$24K", "Proposal", 45],
|
||
].map(([n, v, s, p]) => (
|
||
<div key={n} style={{ padding: "10px 12px", borderRadius: "var(--radius)", background: "var(--surface)", border: "1px solid var(--border)", marginBottom: 8 }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", gap: 8, fontSize: 13, marginBottom: 6 }}>
|
||
<span style={{ fontWeight: 500, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{n}</span>
|
||
<span style={{ color: "var(--text-2)", flexShrink: 0 }}>{v}</span>
|
||
</div>
|
||
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--text-3)", marginBottom: 4 }}>
|
||
<span>{s}</span><span>{p}%</span>
|
||
</div>
|
||
<div style={{ height: 3, borderRadius: 2, background: "var(--surface-alt)", overflow: "hidden" }}>
|
||
<div style={{ width: `${p}%`, height: "100%", background: p > 60 ? "var(--success)" : "var(--accent)" }}/>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Main: tabs + activity */}
|
||
<div style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||
<div style={{ padding: "0 28px", borderBottom: "1px solid var(--divider)", background: "var(--surface)" }}>
|
||
<Tabs items={[{ label: "Activity", count: 28 }, { label: "Notes", count: 7 }, { label: "People", count: 4 }, { label: "Files" }, { label: "Emails" }]} active="Activity"/>
|
||
</div>
|
||
<Scroll>
|
||
{/* KPIs */}
|
||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12, marginBottom: 22 }}>
|
||
{[["Pipeline", "$234K", "+$12K 30d"], ["Lifetime", "$420K", "won"], ["Open deals", "3", "1 stalled"], ["Health", "82/100", "stable"]].map(([l, v, s]) => (
|
||
<Card key={l} padding={14}>
|
||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{l}</div>
|
||
<div style={{ fontSize: 20, fontWeight: 600, marginTop: 4 }}>{v}</div>
|
||
<div style={{ fontSize: 11, color: "var(--text-2)", marginTop: 2 }}>{s}</div>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
{/* Composer */}
|
||
<div style={{ display: "flex", gap: 10, padding: 12, borderRadius: "var(--radius)", border: "1px solid var(--border)", background: "var(--surface)", marginBottom: 20 }}>
|
||
<Avatar name="Mira Reyes" color="#d4b8a8" size={28}/>
|
||
<input placeholder="Log a note, call, or email…" style={{ flex: 1, border: "none", outline: "none", background: "transparent", fontSize: 13, fontFamily: "var(--font-sans)", color: "var(--text)" }}/>
|
||
<Button size="sm">Log</Button>
|
||
</div>
|
||
{/* Timeline */}
|
||
<div style={{ position: "relative", paddingLeft: 22 }}>
|
||
<div style={{ position: "absolute", left: 9, top: 6, bottom: 6, width: 1, background: "var(--border)" }}/>
|
||
{[
|
||
["var(--success)", "Deal moved to Negotiation", "Mira · Renewal '26 · $148,000", "2h ago"],
|
||
["var(--accent)", "Email sent · proposal v4", "To Iris, Daniel — opened 6 times", "Yesterday"],
|
||
["var(--warn)", "Call logged · 32 min", "Theo — walkthrough with ops lead", "2d ago"],
|
||
["var(--text-3)", "Note added", "They need SSO + SCIM by Sept — gating item", "4d ago"],
|
||
].map(([dot, t, sub, when], i) => (
|
||
<div key={i} style={{ position: "relative", marginBottom: 18 }}>
|
||
<span style={{ position: "absolute", left: -19, top: 3, width: 11, height: 11, borderRadius: "50%", background: dot, boxShadow: "0 0 0 3px var(--bg)" }}/>
|
||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||
<span style={{ fontSize: 13, fontWeight: 500 }}>{t}</span>
|
||
<span style={{ fontSize: 11, color: "var(--text-3)" }}>{when}</span>
|
||
</div>
|
||
<div style={{ fontSize: 12, color: "var(--text-2)", marginTop: 2 }}>{sub}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Scroll>
|
||
</div>
|
||
</div>
|
||
</CRMShell>
|
||
);
|
||
|
||
// ============================================================
|
||
// 4 · PIPELINE (deals kanban)
|
||
// ============================================================
|
||
const CRMPipeline = () => {
|
||
const columns = [
|
||
{ name: "Lead", total: "$420K", tone: "var(--text-3)", deals: [
|
||
{ co: "Mossbank", title: "New logo · Platform", val: "$120K", owner: "TR", color: "#a8e8c8", days: 3, tags: ["Inbound"] },
|
||
{ co: "Verra", title: "Vision pilot", val: "$84K", owner: "DP", color: "#e8a8c8", days: 8 },
|
||
{ co: "Tide Co.", title: "Expansion", val: "$96K", owner: "MR", color: "#d4b8a8", days: 1, tags: ["Warm"] },
|
||
]},
|
||
{ name: "Qualified", total: "$365K", tone: "var(--accent)", deals: [
|
||
{ co: "Northstar", title: "Carrier API", val: "$148K", owner: "MR", color: "#c8e8a8", days: 5, tags: ["Champion"] },
|
||
{ co: "Kestrel", title: "Renewal + seats", val: "$120K", owner: "TR", color: "#a8c8e8", days: 12 },
|
||
]},
|
||
{ name: "Proposal", total: "$280K", tone: "var(--warn)", deals: [
|
||
{ co: "Halcyon", title: "Pro renewal", val: "$180K", owner: "DP", color: "#c8a8e8", days: 2, tags: ["Sent"] },
|
||
{ co: "Acme", title: "Edge SDK pilot", val: "$24K", owner: "MR", color: "#e8c8a8", days: 6 },
|
||
]},
|
||
{ name: "Negotiation", total: "$148K", tone: "#f59e0b", deals: [
|
||
{ co: "Acme Robotics", title: "Renewal '26", val: "$148K", owner: "MR", color: "#e8a87c", days: 1, tags: ["Hot", "Closing"] },
|
||
]},
|
||
{ name: "Won", total: "$96K", tone: "var(--success)", deals: [
|
||
{ co: "Lowell Works", title: "Annual plan", val: "$96K", owner: "TR", color: "#a8c8e8", days: 0, tags: ["Closed"] },
|
||
]},
|
||
];
|
||
return (
|
||
<CRMShell active="deals">
|
||
<PageBar title="Deals" sub="12 active · $1.24M open pipeline"
|
||
actions={<>
|
||
<Button variant="secondary" leadingIcon={<Icon name="bar" size={13}/>}>Forecast</Button>
|
||
<Button leadingIcon={<Icon name="plus" size={13}/>}>New deal</Button>
|
||
</>}/>
|
||
<div style={{ padding: "12px 28px", borderBottom: "1px solid var(--divider)", display: "flex", alignItems: "center", gap: 10, background: "var(--surface)" }}>
|
||
<Input placeholder="Search deals" leadingIcon={<Icon name="search" size={13}/>} style={{ width: 240, padding: "6px 10px" }}/>
|
||
{["Owner", "Close date", "Value"].map(f => (
|
||
<div key={f} style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 10px", border: "1px dashed var(--border)", borderRadius: "var(--radius-sm)", fontSize: 12, color: "var(--text-2)", cursor: "pointer" }}>{f}<Icon name="chevDown" size={11}/></div>
|
||
))}
|
||
</div>
|
||
<div style={{ flex: 1, overflow: "auto", padding: 20, background: "var(--bg)" }}>
|
||
<div style={{ display: "grid", gridTemplateColumns: "repeat(5, minmax(240px, 1fr))", gap: 14, height: "100%", minWidth: "max-content" }}>
|
||
{columns.map(col => (
|
||
<div key={col.name} style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8, padding: "0 4px 10px" }}>
|
||
<span style={{ width: 8, height: 8, borderRadius: "50%", background: col.tone }}/>
|
||
<span style={{ fontSize: 13, fontWeight: 600 }}>{col.name}</span>
|
||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{col.deals.length}</span>
|
||
<span style={{ flex: 1 }}/>
|
||
<span style={{ fontSize: 12, color: "var(--text-2)", fontVariantNumeric: "tabular-nums" }}>{col.total}</span>
|
||
</div>
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 10, flex: 1, overflowY: "auto", paddingBottom: 8 }}>
|
||
{col.deals.map((d, i) => (
|
||
<div key={i} style={{ padding: 14, borderRadius: "var(--radius)", background: "var(--surface)", border: "1px solid var(--border)", boxShadow: "var(--shadow-sm)", cursor: "grab" }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
|
||
<span style={{ width: 22, height: 22, borderRadius: 6, background: "var(--surface-alt)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 700, color: "var(--text-2)" }}>{d.co[0]}</span>
|
||
<span style={{ fontSize: 12, color: "var(--text-2)", flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{d.co}</span>
|
||
<IconButton name="more" size="sm" label="More"/>
|
||
</div>
|
||
<div style={{ fontSize: 14, fontWeight: 600, letterSpacing: "-0.01em" }}>{d.title}</div>
|
||
<div style={{ fontSize: 18, fontWeight: 600, marginTop: 6, fontVariantNumeric: "tabular-nums" }}>{d.val}</div>
|
||
{d.tags && <div style={{ display: "flex", gap: 6, marginTop: 10, flexWrap: "wrap" }}>
|
||
{d.tags.map(t => <Badge key={t} tone={t === "Hot" || t === "Closing" ? "danger" : t === "Closed" ? "success" : "neutral"}>{t}</Badge>)}
|
||
</div>}
|
||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: 12, paddingTop: 10, borderTop: "1px solid var(--divider)" }}>
|
||
<Avatar name={d.owner} color={d.color} size={22}/>
|
||
<span style={{ fontSize: 11, color: d.days <= 1 ? "var(--danger)" : "var(--text-3)" }}>
|
||
{d.days === 0 ? "Closed" : d.days === 1 ? "Closes today" : `${d.days}d to close`}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
<button style={{ padding: "8px", borderRadius: "var(--radius)", border: "1px dashed var(--border)", background: "transparent", color: "var(--text-3)", fontSize: 12, fontFamily: "var(--font-sans)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 6 }}>
|
||
<Icon name="plus" size={12}/> Add deal
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</CRMShell>
|
||
);
|
||
};
|
||
|
||
// ============================================================
|
||
// 5 · INBOX
|
||
// ============================================================
|
||
const CRMInbox = () => {
|
||
const threads = [
|
||
["IT", "#e8a87c", "Iris Tanaka", "Acme Robotics", "Re: Renewal terms — forwarding to Marco to start paper.", "10:42", 1, true],
|
||
["SK", "#c8e8a8", "Sun Kim", "Northstar", "Could we move the demo to Thursday?", "9:18", 1, false],
|
||
["DP", "#a8c8e8", "Devi Patel", "Internal", "Closed Halcyon! 🎉 Logging it now.", "Tue", 0, false],
|
||
["PN", "#c8a8e8", "Priya Nair", "Halcyon", "Thanks for the deck — a few questions inside.", "Mon", 0, false],
|
||
["ML", "#e8c8a8", "Marco Lindqvist", "Kestrel", "Procurement needs the security packet.", "May 28", 0, false],
|
||
];
|
||
return (
|
||
<CRMShell active="inbox">
|
||
<div style={{ display: "grid", gridTemplateColumns: "340px 1fr", flex: 1, overflow: "hidden" }}>
|
||
{/* List */}
|
||
<div style={{ borderRight: "1px solid var(--divider)", display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||
<div style={{ padding: "16px 18px 10px" }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
|
||
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 600 }}>Inbox</h1>
|
||
<Button size="sm" leadingIcon={<Icon name="plus" size={12}/>}>Compose</Button>
|
||
</div>
|
||
<Input placeholder="Search messages" leadingIcon={<Icon name="search" size={13}/>}/>
|
||
</div>
|
||
<div style={{ padding: "0 12px 6px", display: "flex", gap: 6 }}>
|
||
{["All", "Unread", "Assigned", "Deals"].map((t, i) => (
|
||
<span key={t} style={{ padding: "5px 10px", borderRadius: 999, fontSize: 12, fontWeight: 500, cursor: "pointer", background: i === 0 ? "var(--text)" : "transparent", color: i === 0 ? "var(--bg)" : "var(--text-2)" }}>{t}</span>
|
||
))}
|
||
</div>
|
||
<div style={{ flex: 1, overflowY: "auto", padding: "8px 8px" }}>
|
||
{threads.map((th, i) => (
|
||
<div key={i} style={{ display: "flex", gap: 12, padding: 12, borderRadius: "var(--radius)", cursor: "pointer", marginBottom: 2, background: th[7] ? "var(--surface)" : "transparent", boxShadow: th[7] ? "var(--shadow-sm)" : "none" }}>
|
||
<Avatar name={th[2]} color={th[1]} size={38}/>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", gap: 6 }}>
|
||
<span style={{ fontSize: 13, fontWeight: th[6] ? 700 : 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{th[2]}</span>
|
||
<span style={{ fontSize: 11, color: "var(--text-3)", flexShrink: 0 }}>{th[5]}</span>
|
||
</div>
|
||
<div style={{ fontSize: 11, color: "var(--accent)", fontWeight: 500, marginTop: 1 }}>{th[3]}</div>
|
||
<div style={{ fontSize: 12, color: th[6] ? "var(--text)" : "var(--text-2)", marginTop: 2, display: "-webkit-box", WebkitLineClamp: 1, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{th[4]}</div>
|
||
</div>
|
||
{th[6] > 0 && <span style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--accent)", alignSelf: "center", flexShrink: 0 }}/>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Conversation */}
|
||
<div style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||
<div style={{ padding: "14px 24px", borderBottom: "1px solid var(--divider)", display: "flex", alignItems: "center", gap: 14, background: "var(--surface)" }}>
|
||
<Avatar name="Iris Tanaka" color="#e8a87c" size={40}/>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ fontSize: 15, fontWeight: 600 }}>Iris Tanaka</div>
|
||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>iris@acme.io · Head of Engineering</div>
|
||
</div>
|
||
<Button variant="secondary" size="sm">Open record</Button>
|
||
<IconButton name="more" variant="secondary" label="More"/>
|
||
</div>
|
||
{/* Linked deal */}
|
||
<div style={{ padding: "12px 24px 0" }}>
|
||
<div style={{ display: "flex", gap: 12, padding: 12, borderRadius: "var(--radius)", background: "var(--surface-2)", border: "1px solid var(--border)", alignItems: "center" }}>
|
||
<span style={{ width: 36, height: 36, borderRadius: 8, background: "linear-gradient(135deg, #ff8a3a, #f43f5e)", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 700 }}>A</span>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontSize: 13, fontWeight: 600 }}>Acme — Renewal '26</div>
|
||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>Negotiation · $148,000 · closes Jun 12</div>
|
||
</div>
|
||
<Badge tone="warn" dot>Closing soon</Badge>
|
||
</div>
|
||
</div>
|
||
{/* Messages */}
|
||
<div style={{ flex: 1, overflowY: "auto", padding: "20px 24px" }}>
|
||
<div style={{ textAlign: "center", fontSize: 11, color: "var(--text-3)", margin: "0 0 16px" }}>Today</div>
|
||
{[
|
||
[false, "Hi Mira — the team reviewed the renewal terms and they look great. I'm forwarding to Marco in procurement to start the paperwork.", "10:14"],
|
||
[true, "That's wonderful news, Iris. I'll send Marco the order form today. Anything he needs from our side to move quickly?", "10:28"],
|
||
[false, "Just the security packet (SOC 2 + the SSO/SCIM details). If you can get that over, we should be able to close by the 12th.", "10:42"],
|
||
].map(([mine, body, time], i) => (
|
||
<div key={i} style={{ display: "flex", flexDirection: mine ? "row-reverse" : "row", gap: 10, alignItems: "flex-end", marginBottom: 12 }}>
|
||
{!mine && <Avatar name="Iris Tanaka" color="#e8a87c" size={28}/>}
|
||
<div style={{ maxWidth: "62%" }}>
|
||
<div style={{ padding: "10px 14px", borderRadius: 16, background: mine ? "var(--text)" : "var(--surface-2)", color: mine ? "var(--bg)" : "var(--text)", fontSize: 13, lineHeight: 1.45, borderBottomRightRadius: mine ? 4 : 16, borderBottomLeftRadius: mine ? 16 : 4, boxShadow: "var(--shadow-sm)" }}>{body}</div>
|
||
<div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 4, textAlign: mine ? "right" : "left" }}>{time}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* Composer */}
|
||
<div style={{ padding: 16, borderTop: "1px solid var(--divider)" }}>
|
||
<div style={{ display: "flex", alignItems: "flex-end", gap: 10, padding: 8, borderRadius: 14, background: "var(--surface)", border: "1px solid var(--border)" }}>
|
||
<IconButton name="plus" size="sm" variant="ghost" label="Attach"/>
|
||
<textarea placeholder="Reply to Iris…" rows="1" style={{ flex: 1, border: "none", outline: "none", background: "transparent", fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--text)", resize: "none", padding: "6px 0" }}/>
|
||
<Button size="sm">Send</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CRMShell>
|
||
);
|
||
};
|
||
|
||
// ============================================================
|
||
// 6 · REPORTS
|
||
// ============================================================
|
||
const CRMReports = () => {
|
||
const months = ["Dec","Jan","Feb","Mar","Apr","May"];
|
||
const won = [180, 220, 195, 260, 240, 284];
|
||
const goal = 250;
|
||
const maxV = 320;
|
||
return (
|
||
<CRMShell active="reports">
|
||
<PageBar title="Reports" sub="Sales performance · last 6 months"
|
||
actions={<>
|
||
<Select value="Last 6 months"/>
|
||
<Button variant="secondary">Export</Button>
|
||
<Button leadingIcon={<Icon name="plus" size={13}/>}>New report</Button>
|
||
</>}/>
|
||
<Scroll>
|
||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14, marginBottom: 20 }}>
|
||
<Stat label="Bookings · MTD" value="$284K" delta="18%" up spark={sparkUp}/>
|
||
<Stat label="New pipeline" value="$612K" delta="9%" up spark={sparkUp}/>
|
||
<Stat label="Avg deal size" value="$84K" delta="2%" up={false} spark={sparkDn}/>
|
||
<Stat label="Sales cycle" value="34d" delta="3d" up spark={sparkUp}/>
|
||
</div>
|
||
|
||
<div style={{ display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 20 }}>
|
||
{/* Bookings vs goal */}
|
||
<Card padding={24}>
|
||
<CardHeader title="Bookings vs goal" subtitle="Closed-won by month"
|
||
action={<Tabs variant="pill" items={[{ label: "Monthly" }, { label: "Quarterly" }]} active="Monthly"/>}/>
|
||
<div style={{ position: "relative", height: 220, marginTop: 8 }}>
|
||
{/* goal line */}
|
||
<div style={{ position: "absolute", left: 0, right: 0, bottom: `${(goal / maxV) * 100}%`, borderTop: "2px dashed var(--accent)", zIndex: 1 }}>
|
||
<span style={{ position: "absolute", right: 0, top: -18, fontSize: 11, color: "var(--accent)", fontWeight: 600, background: "var(--surface)", padding: "0 4px" }}>Goal ${goal}K</span>
|
||
</div>
|
||
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "flex-end", gap: 18, paddingRight: 4 }}>
|
||
{won.map((v, i) => (
|
||
<div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-end", height: "100%", gap: 8 }}>
|
||
<div style={{ width: "100%", maxWidth: 44, height: `${(v / maxV) * 100}%`, borderRadius: "6px 6px 0 0", background: v >= goal ? "linear-gradient(180deg, var(--success), color-mix(in srgb, var(--success) 50%, transparent))" : "linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 45%, transparent))" }}/>
|
||
<span style={{ fontSize: 11, color: "var(--text-3)" }}>{months[i]}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Leaderboard */}
|
||
<Card padding={0}>
|
||
<CardHeader title="Team leaderboard" subtitle="This month"
|
||
style={{ padding: "16px 20px", margin: 0, borderBottom: "1px solid var(--divider)" }}/>
|
||
<div style={{ padding: "6px 16px 12px" }}>
|
||
{[
|
||
["MR", "#d4b8a8", "Mira Reyes", "$124K", 100],
|
||
["DP", "#a8c8e8", "Devi Patel", "$86K", 70],
|
||
["TR", "#c8e8a8", "Theo Roux", "$62K", 50],
|
||
["SK", "#e8a87c", "Sun Ortiz", "$48K", 39],
|
||
].map(([i, c, n, v, p], idx) => (
|
||
<div key={idx} style={{ display: "grid", gridTemplateColumns: "20px 28px 1fr auto", gap: 10, alignItems: "center", padding: "9px 0", borderBottom: idx < 3 ? "1px solid var(--divider)" : "none" }}>
|
||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>#{idx + 1}</span>
|
||
<Avatar name={n} color={c} size={28}/>
|
||
<div style={{ minWidth: 0 }}>
|
||
<div style={{ fontSize: 13, fontWeight: 500 }}>{n}</div>
|
||
<div style={{ height: 3, borderRadius: 2, background: "var(--surface-alt)", overflow: "hidden", marginTop: 4 }}>
|
||
<div style={{ width: `${p}%`, height: "100%", background: "var(--accent)" }}/>
|
||
</div>
|
||
</div>
|
||
<span style={{ fontSize: 13, fontWeight: 600 }}>{v}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
</Scroll>
|
||
</CRMShell>
|
||
);
|
||
};
|
||
|
||
// ============================================================
|
||
// 7 · SETTINGS · members (admin)
|
||
// ============================================================
|
||
const CRMSettings = () => {
|
||
const members = [
|
||
{ id: 1, name: "Mira Reyes", email: "mira@northwind.io", color: "#d4b8a8", role: "Owner", status: "Active", last: "now" },
|
||
{ id: 2, name: "Theo Roux", email: "theo@northwind.io", color: "#c8e8a8", role: "Admin", status: "Active", last: "12 min" },
|
||
{ id: 3, name: "Devi Patel", email: "devi@northwind.io", color: "#a8c8e8", role: "Admin", status: "Active", last: "1 hour" },
|
||
{ id: 4, name: "Sun Ortiz", email: "sun@northwind.io", color: "#e8a87c", role: "Member", status: "Active", last: "today" },
|
||
{ id: 5, name: "Linnea Berg", email: "linnea@northwind.io", color: "#c8a8e8", role: "Member", status: "Invited", last: "—" },
|
||
{ id: 6, name: "Jamal Frost", email: "jamal@partner.co", color: "#a8e8c8", role: "Guest", status: "Active", last: "3 days" },
|
||
];
|
||
const roleTone = { Owner: "accent", Admin: "info", Member: "success", Guest: "neutral" };
|
||
return (
|
||
<CRMShell active="settings">
|
||
<div style={{ display: "grid", gridTemplateColumns: "200px 1fr", flex: 1, overflow: "hidden" }}>
|
||
{/* Settings subnav */}
|
||
<div style={{ borderRight: "1px solid var(--divider)", padding: "18px 12px", overflowY: "auto" }}>
|
||
<div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 500, padding: "0 10px 8px" }}>Workspace</div>
|
||
{["General", "Members", "Roles", "Pipeline stages", "Integrations", "Billing", "API"].map((s, i) => (
|
||
<div key={s} style={{ padding: "7px 10px", borderRadius: "var(--radius-sm)", fontSize: 13, cursor: "pointer", marginBottom: 2, color: i === 1 ? "var(--text)" : "var(--text-2)", fontWeight: i === 1 ? 500 : 400, background: i === 1 ? "var(--surface)" : "transparent", boxShadow: i === 1 ? "var(--shadow-sm)" : "none" }}>{s}</div>
|
||
))}
|
||
<div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 500, padding: "16px 10px 8px" }}>Personal</div>
|
||
{["Profile", "Notifications", "Sessions"].map(s => (
|
||
<div key={s} style={{ padding: "7px 10px", borderRadius: "var(--radius-sm)", fontSize: 13, color: "var(--text-2)", cursor: "pointer", marginBottom: 2 }}>{s}</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Members panel */}
|
||
<div style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||
<PageBar breadcrumb="Settings" title="Members"
|
||
sub="Manage who can access the Northwind workspace."
|
||
actions={<>
|
||
<Button variant="secondary">Export CSV</Button>
|
||
<Button leadingIcon={<Icon name="plus" size={13}/>}>Invite people</Button>
|
||
</>}/>
|
||
<div style={{ padding: "12px 28px", borderBottom: "1px solid var(--divider)", display: "flex", alignItems: "center", gap: 10, background: "var(--surface)" }}>
|
||
<Input placeholder="Search members" leadingIcon={<Icon name="search" size={13}/>} style={{ width: 260, padding: "6px 10px" }}/>
|
||
<div style={{ flex: 1 }}/>
|
||
<span style={{ fontSize: 12, color: "var(--text-2)" }}><b style={{ color: "var(--text)" }}>6</b> members · 1 invited</span>
|
||
</div>
|
||
<Scroll pad={20}>
|
||
<Table
|
||
columns={[
|
||
{ key: "name", label: "Name", render: r => (
|
||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||
<Avatar name={r.name} color={r.color} size={28}/>
|
||
<div>
|
||
<div style={{ fontWeight: 500 }}>{r.name}</div>
|
||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{r.email}</div>
|
||
</div>
|
||
</div>
|
||
)},
|
||
{ key: "role", label: "Role", render: r => (
|
||
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "3px 9px", borderRadius: "var(--radius-sm)", background: "var(--accent-soft)", color: "var(--accent)", fontSize: 12, fontWeight: 500, cursor: "pointer" }}>
|
||
{r.role}<Icon name="chevDown" size={11}/>
|
||
</span>
|
||
)},
|
||
{ key: "status", label: "Status", render: r => (
|
||
<Badge dot tone={r.status === "Active" ? "success" : "warn"}>{r.status}</Badge>
|
||
)},
|
||
{ key: "last", label: "Last active" },
|
||
{ key: "act", label: "", align: "right", width: 40, render: () => <IconButton name="more" size="sm" label="More"/> },
|
||
]}
|
||
rows={members}
|
||
/>
|
||
<div style={{ marginTop: 18 }}>
|
||
<Banner tone="warn" title="1 invitation pending"
|
||
action={<Button size="sm" variant="secondary">Resend</Button>}>
|
||
linnea@northwind.io hasn't accepted yet — sent 3 days ago.
|
||
</Banner>
|
||
</div>
|
||
</Scroll>
|
||
</div>
|
||
</div>
|
||
</CRMShell>
|
||
);
|
||
};
|
||
|
||
Object.assign(window, {
|
||
CRMHome, CRMPeople, CRMRecord, CRMPipeline, CRMInbox, CRMReports, CRMSettings,
|
||
});
|