356 lines
15 KiB
JavaScript
356 lines
15 KiB
JavaScript
// ============================================================
|
||
// page-dashboard.jsx — KPI strip + time-series chart +
|
||
// pipeline funnel + recent activity + team leaderboard.
|
||
// Theme-aware so it adapts to dark rail chrome.
|
||
// ============================================================
|
||
|
||
const DashboardBody = ({ theme = "light" }) => {
|
||
const dark = theme === "dark";
|
||
const c = dark ? {
|
||
bg: "#0f0f14", panel: "#13131a", border: "#ffffff10",
|
||
text: "#e8e8ee", subtext: "#9a9aa6", muted: "#6a6a78",
|
||
grid: "#ffffff08", accent: "#7a78ff", up: "#22c55e", down: "#ff4d5e",
|
||
} : {
|
||
bg: "#fafaf9", panel: "#ffffff", border: "#ebebe6",
|
||
text: "#111", subtext: "#5a5a5e", muted: "#8a8a90",
|
||
grid: "#eeeee9", accent: "#5e5cff", up: "#22c55e", down: "#ff4d5e",
|
||
};
|
||
|
||
// Synthetic but consistent daily series, weekday-shaped
|
||
const days = ["M","T","W","T","F","S","S","M","T","W","T","F","S","S"];
|
||
const series = [42,58,71,64,79,32,28, 51,68,82,75,90,38,33];
|
||
const max = Math.max(...series);
|
||
|
||
// Funnel data
|
||
const funnel = [
|
||
{ stage: "New", n: 184, v: "€2.1m" },
|
||
{ stage: "Qualified", n: 96, v: "€1.4m" },
|
||
{ stage: "Proposal", n: 42, v: "€780k" },
|
||
{ stage: "Negotiation", n: 19, v: "€420k" },
|
||
{ stage: "Closed-won", n: 11, v: "€286k" },
|
||
];
|
||
const fmax = funnel[0].n;
|
||
|
||
const Avatar = ({ name, color = "#d4b8a8", size = 22 }) => (
|
||
<div style={{
|
||
width: size, height: size, borderRadius: "50%", background: color,
|
||
fontSize: size * 0.42, fontWeight: 600, color: "#3a2820",
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
flexShrink: 0,
|
||
}}>{name}</div>
|
||
);
|
||
|
||
return (
|
||
<div style={{
|
||
height: "100%", background: c.bg, color: c.text, fontFamily: SANS,
|
||
display: "flex", flexDirection: "column", overflow: "hidden",
|
||
}}>
|
||
{/* Header */}
|
||
<div style={{
|
||
padding: "20px 28px 16px", borderBottom: `1px solid ${c.border}`,
|
||
display: "flex", alignItems: "flex-end", justifyContent: "space-between",
|
||
}}>
|
||
<div>
|
||
<div style={{
|
||
fontSize: 11, color: c.muted, letterSpacing: "0.06em",
|
||
textTransform: "uppercase", marginBottom: 4, fontWeight: 500,
|
||
}}>Workspace dashboard</div>
|
||
<h1 style={{
|
||
fontSize: 26, fontWeight: 600, margin: 0, letterSpacing: "-0.02em",
|
||
}}>Good afternoon, Mira</h1>
|
||
<div style={{ fontSize: 13, color: c.subtext, marginTop: 4 }}>
|
||
3 deals moved stage today · 12 unread in Inbox · 1 task overdue
|
||
</div>
|
||
</div>
|
||
<div style={{ display: "flex", gap: 8 }}>
|
||
<div style={{
|
||
display: "flex", alignItems: "center", padding: "6px 10px",
|
||
borderRadius: 6, background: c.panel, border: `1px solid ${c.border}`,
|
||
fontSize: 12, color: c.subtext, gap: 8,
|
||
}}>
|
||
<span style={{ fontWeight: 500, color: c.text }}>Last 14 days</span>
|
||
<Icon d={P.chevron} size={12} />
|
||
</div>
|
||
<button style={{
|
||
padding: "7px 12px", borderRadius: 6, fontSize: 12, fontFamily: SANS,
|
||
background: c.panel, border: `1px solid ${c.border}`, color: c.text,
|
||
cursor: "pointer",
|
||
}}>Export</button>
|
||
<button style={{
|
||
padding: "7px 14px", borderRadius: 6, fontSize: 12, fontFamily: SANS,
|
||
background: dark ? "#fff" : "#111", color: dark ? "#111" : "#fff",
|
||
border: "none", cursor: "pointer", fontWeight: 500,
|
||
display: "flex", alignItems: "center", gap: 6,
|
||
}}><Icon d={P.plus} size={12}/> New report</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
flex: 1, overflowY: "auto", padding: "20px 28px 28px",
|
||
display: "flex", flexDirection: "column", gap: 20,
|
||
}}>
|
||
{/* KPI strip */}
|
||
<div style={{
|
||
display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12,
|
||
}}>
|
||
{[
|
||
{ l: "Revenue · MTD", v: "€286,420", d: "+18.4%", up: true,
|
||
spark: [20,28,24,36,30,42,52,48,58,62,70,82] },
|
||
{ l: "Active deals", v: "168", d: "+12", up: true,
|
||
spark: [40,42,45,46,49,52,54,56,58,60,62,65] },
|
||
{ l: "Win rate · 30d", v: "34.2%", d: "−1.1%", up: false,
|
||
spark: [60,58,55,52,54,50,48,45,46,42,38,36] },
|
||
{ l: "Pipeline ratio", v: "4.8×", d: "healthy", up: true,
|
||
spark: [50,48,52,55,53,58,56,60,62,65,63,68] },
|
||
].map(k => {
|
||
const sm = Math.max(...k.spark), sn = Math.min(...k.spark);
|
||
const pts = k.spark.map((v, i) => {
|
||
const x = (i / (k.spark.length - 1)) * 100;
|
||
const y = 30 - ((v - sn) / (sm - sn || 1)) * 26 - 2;
|
||
return `${x},${y}`;
|
||
}).join(" ");
|
||
return (
|
||
<div key={k.l} style={{
|
||
padding: "16px 18px", borderRadius: 10,
|
||
background: c.panel, border: `1px solid ${c.border}`,
|
||
}}>
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "baseline",
|
||
marginBottom: 8,
|
||
}}>
|
||
<span style={{ fontSize: 12, color: c.muted }}>{k.l}</span>
|
||
<span style={{
|
||
fontSize: 11, color: k.up ? c.up : c.down, fontWeight: 500,
|
||
}}>{k.d}</span>
|
||
</div>
|
||
<div style={{
|
||
fontSize: 26, fontWeight: 600, letterSpacing: "-0.02em",
|
||
marginBottom: 6, fontVariantNumeric: "tabular-nums",
|
||
}}>{k.v}</div>
|
||
<svg viewBox="0 0 100 30" style={{
|
||
width: "100%", height: 26, display: "block",
|
||
}} preserveAspectRatio="none">
|
||
<polyline points={pts} fill="none"
|
||
stroke={k.up ? c.up : c.down} strokeWidth="1.5"
|
||
vectorEffect="non-scaling-stroke" />
|
||
</svg>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Chart + funnel */}
|
||
<div style={{
|
||
display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 16,
|
||
}}>
|
||
{/* Time-series */}
|
||
<div style={{
|
||
padding: "18px 20px", borderRadius: 12,
|
||
background: c.panel, border: `1px solid ${c.border}`,
|
||
}}>
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||
marginBottom: 14,
|
||
}}>
|
||
<div>
|
||
<div style={{ fontSize: 13, fontWeight: 600 }}>Revenue, daily</div>
|
||
<div style={{ fontSize: 11, color: c.muted, marginTop: 2 }}>
|
||
Bookings · GBP closed-won
|
||
</div>
|
||
</div>
|
||
<div style={{ display: "flex", gap: 4 }}>
|
||
{["Day", "Week", "Month"].map((t, i) => (
|
||
<span key={t} style={{
|
||
padding: "4px 10px", borderRadius: 5, fontSize: 11, fontWeight: 500,
|
||
background: i === 0 ? (dark ? "#ffffff10" : "#f1f0eb") : "transparent",
|
||
color: i === 0 ? c.text : c.muted, cursor: "pointer",
|
||
}}>{t}</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ position: "relative", height: 180 }}>
|
||
{/* Gridlines */}
|
||
{[0, 0.25, 0.5, 0.75, 1].map(p => (
|
||
<div key={p} style={{
|
||
position: "absolute", left: 0, right: 0,
|
||
bottom: `${p * 100}%`, height: 1, background: c.grid,
|
||
}}></div>
|
||
))}
|
||
{/* Bars */}
|
||
<div style={{
|
||
position: "absolute", inset: 0, display: "flex",
|
||
alignItems: "flex-end", gap: 6, paddingRight: 6,
|
||
}}>
|
||
{series.map((v, i) => (
|
||
<div key={i} style={{ flex: 1, position: "relative",
|
||
display: "flex", flexDirection: "column",
|
||
alignItems: "center", justifyContent: "flex-end",
|
||
height: "100%",
|
||
}}>
|
||
<div style={{
|
||
width: "100%", height: `${(v / max) * 100}%`,
|
||
background: i === 11
|
||
? `linear-gradient(180deg, ${c.accent}, ${dark ? "#3a38c0" : "#bfbeff"})`
|
||
: (dark ? "#ffffff14" : "#e8e7e0"),
|
||
borderRadius: 3,
|
||
}}></div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* Annotation */}
|
||
<div style={{
|
||
position: "absolute", right: 6, top: -6,
|
||
background: c.text, color: c.bg,
|
||
padding: "3px 8px", borderRadius: 4, fontSize: 11, fontWeight: 500,
|
||
}}>€42k · today</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", marginTop: 6,
|
||
fontSize: 10, color: c.muted, fontFamily: "ui-monospace, monospace",
|
||
}}>
|
||
{days.map((d, i) => (
|
||
<span key={i} style={{ flex: 1, textAlign: "center" }}>{d}</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Funnel */}
|
||
<div style={{
|
||
padding: "18px 20px", borderRadius: 12,
|
||
background: c.panel, border: `1px solid ${c.border}`,
|
||
}}>
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "baseline",
|
||
marginBottom: 14,
|
||
}}>
|
||
<div style={{ fontSize: 13, fontWeight: 600 }}>Pipeline funnel</div>
|
||
<span style={{ fontSize: 11, color: c.muted }}>Q2 · 168 deals</span>
|
||
</div>
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||
{funnel.map((f, i) => {
|
||
const w = (f.n / fmax) * 100;
|
||
const colors = ["#5e5cff", "#7a78ff", "#9b99ff", "#bcb9ff", "#22c55e"];
|
||
return (
|
||
<div key={f.stage} style={{ position: "relative" }}>
|
||
<div style={{
|
||
width: `${w}%`, height: 30, borderRadius: 5,
|
||
background: colors[i], display: "flex",
|
||
alignItems: "center", paddingLeft: 12, color: "#fff",
|
||
fontSize: 12, fontWeight: 500,
|
||
}}>{f.stage}</div>
|
||
<div style={{
|
||
position: "absolute", right: 0, top: 0, height: 30,
|
||
display: "flex", alignItems: "center", gap: 10,
|
||
fontSize: 12, color: c.muted,
|
||
}}>
|
||
<span style={{
|
||
fontFamily: "ui-monospace, monospace", color: c.text,
|
||
}}>{f.n}</span>
|
||
<span style={{ fontSize: 11 }}>{f.v}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Activity + leaderboard */}
|
||
<div style={{
|
||
display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 16,
|
||
}}>
|
||
<div style={{
|
||
padding: "18px 20px", borderRadius: 12,
|
||
background: c.panel, border: `1px solid ${c.border}`,
|
||
}}>
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "baseline",
|
||
marginBottom: 14,
|
||
}}>
|
||
<div style={{ fontSize: 13, fontWeight: 600 }}>Recent activity</div>
|
||
<span style={{ fontSize: 11, color: c.accent, cursor: "pointer" }}>View all →</span>
|
||
</div>
|
||
{[
|
||
{ who: "MR", c: "#d4b8a8", n: "Mira Reyes", v: "moved",
|
||
w: <><b>Q3 — Carrier API</b> to <span style={{ color: "#22c55e" }}>Negotiation</span></>,
|
||
t: "2m ago" },
|
||
{ who: "TR", c: "#c8e8a8", n: "Theo Roux", v: "logged a call with",
|
||
w: <><b>Sun Kim · Northstar</b></>, t: "14m" },
|
||
{ who: "DP", c: "#a8c8e8", n: "Devi Patel", v: "closed",
|
||
w: <><b>Halcyon · Pro renewal</b> · €24,000</>, t: "1h" },
|
||
{ who: "MR", c: "#d4b8a8", n: "Mira Reyes", v: "created a deal",
|
||
w: <><b>Brooke Foods — Q3 pilot</b></>, t: "2h" },
|
||
{ who: "SK", c: "#e8a87c", n: "Sun Kim", v: "added 4 contacts to",
|
||
w: <><b>Kestrel</b></>, t: "3h" },
|
||
].map((a, i) => (
|
||
<div key={i} style={{
|
||
display: "flex", alignItems: "center", gap: 10,
|
||
padding: "8px 0",
|
||
borderTop: i === 0 ? "none" : `1px solid ${c.border}`,
|
||
}}>
|
||
<Avatar name={a.who} color={a.c} size={26} />
|
||
<div style={{ flex: 1, fontSize: 13 }}>
|
||
<span style={{ fontWeight: 500 }}>{a.n}</span>
|
||
<span style={{ color: c.muted }}> {a.v} </span>
|
||
<span>{a.w}</span>
|
||
</div>
|
||
<span style={{ fontSize: 11, color: c.muted, whiteSpace: "nowrap" }}>{a.t}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{
|
||
padding: "18px 20px", borderRadius: 12,
|
||
background: c.panel, border: `1px solid ${c.border}`,
|
||
}}>
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "baseline",
|
||
marginBottom: 14,
|
||
}}>
|
||
<div style={{ fontSize: 13, fontWeight: 600 }}>Team · this month</div>
|
||
<span style={{ fontSize: 11, color: c.muted }}>By bookings</span>
|
||
</div>
|
||
{[
|
||
{ i: "MR", c: "#d4b8a8", n: "Mira Reyes", v: 124, d: "€124k", p: 100 },
|
||
{ i: "DP", c: "#a8c8e8", n: "Devi Patel", v: 86, d: "€86k", p: 70 },
|
||
{ i: "TR", c: "#c8e8a8", n: "Theo Roux", v: 62, d: "€62k", p: 50 },
|
||
{ i: "SK", c: "#e8a87c", n: "Sun Kim", v: 48, d: "€48k", p: 39 },
|
||
].map(t => (
|
||
<div key={t.i} style={{
|
||
display: "grid", gridTemplateColumns: "26px 1fr auto", gap: 10,
|
||
alignItems: "center", padding: "8px 0",
|
||
}}>
|
||
<Avatar name={t.i} color={t.c} size={26} />
|
||
<div style={{ minWidth: 0 }}>
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between",
|
||
fontSize: 12, marginBottom: 4,
|
||
}}>
|
||
<span style={{ fontWeight: 500 }}>{t.n}</span>
|
||
<span style={{
|
||
color: c.subtext, fontVariantNumeric: "tabular-nums",
|
||
}}>{t.d}</span>
|
||
</div>
|
||
<div style={{
|
||
height: 3, borderRadius: 2,
|
||
background: dark ? "#ffffff10" : "#eeeee9", overflow: "hidden",
|
||
}}>
|
||
<div style={{
|
||
width: `${t.p}%`, height: "100%",
|
||
background: `linear-gradient(90deg, ${c.accent}, ${dark ? "#9b99ff" : "#b15bff"})`,
|
||
}}></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
window.DashboardBody = DashboardBody;
|