Replace dashboard with Profile, Support Center & Rules modules

- Remove old dashboard stats/charts content; keep sidebar + header shell
  which now routes between Profile, Support Center and Rules views.
- Profile: hero card + 6 tabs (Personal Info with verify-to-reveal +
  editable contacts via OTP, KYC two-doc rule + completion %, Security
  password policy + 2FA, Notifications matrix + contact windows, Privacy
  consents, Devices + activity timeline).
- Support Center: 5 channel cards, support team, Message Center with
  typing, New Ticket category->department routing + attachment rules,
  My Tickets + status-timeline modal, Live Chat handshake, Callback/Email
  modals, Help Center search + accordion.
- Rules: the 1-13 business-rules checklist.
- Shared design system (Icon, Avatar, PageHead, Pill, Toggle, OtpField,
  Modal, Toast, Field, Tabs) + full mock data in account-data.ts.
- Premium dark visual pass: brand gradients, depth, glow accents, pill tabs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 21:52:34 +05:30
parent 5ff655d786
commit 3ab6768c1c
13 changed files with 2663 additions and 512 deletions
+23 -228
View File
@@ -1,258 +1,53 @@
"use client";
// ============================================================
// Dashboard shell — keeps the sidebar + header and swaps the
// inner content between the Profile, Support and Rules views.
// ============================================================
import { useEffect, useState } from "react";
import { TrendingUp, Cloud, MoreHorizontal, ArrowUpRight } from "lucide-react";
import { Sidebar } from "./sidebar";
import { Topbar } from "./topbar";
import { FigIcon } from "./figicon";
import { BarsChart, Spark, Donut, Gauge, Progress, HBar } from "./charts";
import { ToastProvider } from "./ui";
import { Profile } from "./profile";
import { Support } from "./support";
import { Rules } from "./rules";
import "../../app/dashboard/dashboard.css";
const C = {
orange: "#fda913", orange2: "#fd6d13", yellow: "#ffd60a",
cyan: "#09b9c6", green: "#14bc83", blue: "#285ef0", purple: "#9036e9", red: "#f0563f",
const HEADINGS: Record<string, { title: string; subtitle: string }> = {
profile: { title: "Profile", subtitle: "Your account, identity and preferences" },
support: { title: "Support Center", subtitle: "Get help, track requests and find answers" },
rules: { title: "Rules Checklist", subtitle: "The 13 rules behind Profile & Support" },
};
const rnd = (v: number, p = 0.12) => Math.max(1, Math.round(v * (1 + (Math.random() * 2 - 1) * p)));
const arr = (base: number[], p = 0.14) => base.map((v) => rnd(v, p));
export function Dashboard() {
const [theme, setTheme] = useState<"dark" | "light">("dark");
const [active, setActive] = useState("dashboard");
const [active, setActive] = useState("profile");
useEffect(() => {
// Sync the persisted theme from localStorage (an external system) on mount.
// eslint-disable-next-line react-hooks/set-state-in-effect
try { const t = localStorage.getItem("lup_dash_theme"); if (t === "light" || t === "dark") setTheme(t); } catch {}
}, []);
function toggle() {
setTheme((t) => { const n = t === "dark" ? "light" : "dark"; try { localStorage.setItem("lup_dash_theme", n); } catch {} return n; });
}
// ---- live data ----
const [pnl, setPnl] = useState({
revenue: [4, 6, 3, 7, 5, 6, 4], gross: [5, 3, 6, 4, 7, 5, 6],
comm: [3, 5, 4, 6, 5, 7, 4], net: [4, 6, 5, 3, 6, 4, 7],
});
const [rev, setRev] = useState([5, 7, 4, 8, 6, 9, 16, 7, 5, 8]);
const [sparks, setSparks] = useState({
revenue: [4, 7, 5, 9, 6, 10, 8], leads: [6, 4, 8, 5, 9, 6, 10], meets: [5, 8, 6, 9, 7, 10, 8],
});
useEffect(() => {
const id = setInterval(() => {
setPnl((p) => ({ revenue: arr(p.revenue), gross: arr(p.gross), comm: arr(p.comm), net: arr(p.net) }));
setRev((r) => r.map((v, i) => (i === 6 ? 16 : rnd(v, 0.18))));
setSparks((s) => ({ revenue: arr(s.revenue), leads: arr(s.leads), meets: arr(s.meets) }));
}, 2600);
return () => clearInterval(id);
}, []);
const days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
const yk = ["8k", "6k", "4k", "2k", "0"];
const head = HEADINGS[active] ?? HEADINGS.profile;
return (
<div className="dash-root" data-theme={theme}>
<Sidebar active={active} onSelect={setActive} />
<div className="dash-main">
<Topbar theme={theme} onToggle={toggle} />
<Topbar theme={theme} onToggle={toggle} title={head.title} subtitle={head.subtitle} />
<div className="dash-content">
{/* ===== Statistics ===== */}
<h2 className="sec-title">Statistics</h2>
<div className="grid" style={{ gridTemplateColumns: "1.55fr 1fr", marginBottom: 16 }}>
<div className="grid" style={{ gridTemplateColumns: "1fr 1fr 1fr" }}>
<BigStat icon={<FigIcon name="i_revenue" size={18} />} ic={C.orange} tag="GROSS PROFIT $114K" value="$626K" label="Revenue" spark={sparks.revenue} sc={C.orange} />
<BigStat icon={<FigIcon name="i_hotleads" size={18} />} ic={C.red} tag="REQUIRES ACTION" value="20k" label="Hot Leads" spark={sparks.leads} sc={C.purple} />
<BigStat icon={<FigIcon name="i_meets" size={18} />} ic={C.cyan} tag="NEXT 7 DAYS" value="70k" label="Upcoming Meets" spark={sparks.meets} sc={C.cyan} />
</div>
<div className="grid" style={{ gridTemplateColumns: "1fr 1fr" }}>
<MiniStat icon={<FigIcon name="i_collected" size={16} />} ic={C.cyan} value="$311.3k" label="Collected to Date" sub="32 payments" pill="pill-cyan" />
<MiniStat icon={<FigIcon name="i_ar" size={16} />} ic={C.orange} value="$325.9k" label="Outstanding AR" sub="25 pending" pill="pill-orange" />
<MiniStat icon={<FigIcon name="i_payouts" size={16} />} ic={C.purple} value="$203.7k" label="Pending Payouts" sub="20 pending" pill="pill-blue" />
<MiniStat icon={<FigIcon name="i_vendor" size={16} />} ic={C.blue} value="$432.4k" label="YTD Vendor Spend" sub="57 invoices" pill="pill-blue" />
</div>
</div>
{/* ===== Pipeline / Win / Storm ===== */}
<div className="grid" style={{ gridTemplateColumns: "1fr 1fr 1fr", marginBottom: 16 }}>
<div className="dcard">
<div className="card-h"><h3>Open Pipeline</h3><span className="pill pill-orange">27 leads</span></div>
<div className="big-val" style={{ marginBottom: 14 }}>$396K</div>
{[["New", 6], ["Contacted", 7], ["Appointment", 7], ["Estimate", 3]].map(([l, n]) => (
<div className="pl-row" key={l as string}>
<span className="lab">{l}</span>
<HBar value={n as number} max={8} color={C.orange} />
<span className="n">{n}</span>
</div>
))}
</div>
<div className="dcard">
<div className="card-h"><h3>Win Rate</h3><TrendingUp size={16} color={C.green} /></div>
<div className="big-val" style={{ marginBottom: 14 }}>94%</div>
<Progress value={94} color={C.cyan} />
<div className="row" style={{ gap: 16, marginTop: 14, flexWrap: "wrap" }}>
<Legend dot={C.cyan} text="17 Won" />
<Legend dot="var(--muted)" text="18 Decided" />
<Legend dot={C.red} text="1 Lost" />
</div>
</div>
<div className="dcard">
<div className="card-h"><h3>Storm-Attributed Revenue</h3><span className="pill pill-blue">19 Storm Leads</span></div>
<div className="big-val" style={{ marginBottom: 14 }}>24%</div>
<Progress value={24} color={C.blue} />
<div className="lbl" style={{ marginTop: 12 }}>$56K of $626K</div>
</div>
</div>
{/* ===== Roof / Risk / Weather ===== */}
<div className="grid" style={{ gridTemplateColumns: "1fr 1.15fr 1.25fr", marginBottom: 16 }}>
<div className="dcard">
<div className="card-h"><div><h3>Roof Condition</h3><div className="card-sub">Territory health snapshot</div></div></div>
<div className="row" style={{ justifyContent: "center", margin: "6px 0 10px" }}>
<Donut segments={[{ value: 30, color: C.orange }, { value: 14, color: C.cyan }]} total="44" label="Total" />
</div>
<div className="row" style={{ gap: 18, justifyContent: "center" }}>
<Legend dot={C.orange} text="Good" />
<Legend dot={C.cyan} text="Bad" />
</div>
</div>
<div className="dcard">
<div className="card-h"><div><h3>Risk Index</h3><div className="card-sub">Storm Impact Probability</div></div></div>
<div className="row" style={{ justifyContent: "center", margin: "4px 0" }}>
<Gauge value={51} max={100} color={C.blue} size={170} label="51" sub="of 100" />
</div>
<div className="row between" style={{ marginTop: 6 }}>
<Readout big="10 mph" small="Wind Exp." />
<Readout big="52%" small="Vulnerable" color={C.orange} />
</div>
</div>
<div className="dcard">
<div className="row between">
<div>
<div className="row gap-2"><Cloud size={20} color={C.cyan} /><span className="big-val">80&deg;F</span></div>
<div className="lbl">Plano, US</div>
</div>
<div style={{ textAlign: "right" }}>
<div className="card-sub">Updated: 02:48 PM</div>
<div className="row gap-3" style={{ marginTop: 8, justifyContent: "flex-end" }}>
<span className="row gap-2" style={{ fontSize: 12 }}><span style={{ color: "var(--muted)", display: "inline-flex" }}><FigIcon name="i_wind" size={14} /></span> 10 mph</span>
<span className="row gap-2" style={{ fontSize: 12 }}><span style={{ color: C.blue, display: "inline-flex" }}><FigIcon name="i_humidity" size={14} /></span> 82%</span>
</div>
</div>
</div>
<div className="wx-hours">
{[["12 PM", 94], ["5 PM", 79], ["8 PM", 82], ["10 PM", 90], ["1 AM", 92], ["5 AM", 79]].map(([t, te]) => (
<div className="wx-hr" key={t as string}>
<div className="t">{t}</div>
<Cloud size={16} color="var(--faint)" style={{ display: "block", margin: "6px auto 0" }} />
<div className="te">{te}&deg;</div>
</div>
))}
</div>
</div>
</div>
{/* ===== P&L Summary ===== */}
<h2 className="sec-title">P&amp;L Summary</h2>
<div className="grid" style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr", marginBottom: 16 }}>
<PnlCard title="Revenue" value="$626K" labels={days} yk={yk}
series={[{ color: C.orange, values: pnl.revenue }, { color: C.orange2, values: pnl.gross }]} />
<PnlCard title="Gross Profit" value="$114K" labels={days} yk={yk}
series={[{ color: C.cyan, values: pnl.gross }, { color: C.orange, values: pnl.revenue }]} />
<PnlCard title="Commissions" value="$63K" labels={days} yk={yk}
series={[{ color: C.blue, values: pnl.comm }, { color: "#6f9bff", values: pnl.net }]} />
<PnlCard title="Net Profit" value="$51K" labels={days} yk={yk}
series={[{ color: C.orange, values: pnl.net }, { color: C.blue, values: pnl.comm }]} />
</div>
{/* ===== Revenue Potential / Top Reps ===== */}
<div className="grid" style={{ gridTemplateColumns: "1.7fr 1fr" }}>
<div className="dcard">
<div className="card-h">
<div><h3>Revenue Potential</h3><div className="card-sub">By neighborhood rating</div></div>
<span className="pill pill-orange"><ArrowUpRight size={13} /> live</span>
</div>
<BarsChart
labels={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
series={[{ color: C.orange, values: rev }]}
max={18} highlight={6} height={210} barWidth={26}
yTicks={["$240k", "$180k", "$120k", "$60k", "0"]}
/>
</div>
<div className="dcard">
<div className="card-h"><h3>Top Sales Reps</h3><button className="link-btn">View All</button></div>
{[["Hannah Reyes", "hannah@lynkeduppro.com", C.orange], ["Cody Tatum", "cody@lynkeduppro.com", C.cyan], ["Travis Boone", "travis@lynkeduppro.com", C.blue]].map(([nm, em, col], i) => (
<div className="rep-row" key={nm as string}>
<span className="rk">0{i + 1}</span>
<span className="av" style={{ background: col as string, display: "grid", placeItems: "center", color: "#fff", fontWeight: 700, fontSize: 12 }}>{(nm as string).split(" ").map((x) => x[0]).join("")}</span>
<div style={{ flex: 1 }}>
<div className="nm">{nm}</div>
<div className="em">{em}</div>
</div>
<span className="amt">$112,500</span>
</div>
))}
</div>
</div>
<ToastProvider>
{active === "profile" && <Profile />}
{active === "support" && <Support />}
{active === "rules" && <Rules />}
</ToastProvider>
</div>
</div>
</div>
);
}
/* ---- sub components ---- */
function BigStat({ icon, ic, tag, value, label, spark, sc }: { icon: React.ReactNode; ic: string; tag: string; value: string; label: string; spark: number[]; sc: string }) {
return (
<div className="dcard">
<div className="row between" style={{ marginBottom: 14 }}>
<span className="stat-ic" style={{ background: `color-mix(in srgb, ${ic} 18%, transparent)`, color: ic }}>{icon}</span>
<span className="card-sub" style={{ fontWeight: 600, letterSpacing: "0.04em" }}>{tag}</span>
</div>
<div className="row between" style={{ alignItems: "flex-end" }}>
<div>
<div className="big-val">{value}</div>
<div className="lbl">{label}</div>
</div>
<Spark values={spark} color={sc} />
</div>
</div>
);
}
function MiniStat({ icon, ic, value, label, sub, pill }: { icon: React.ReactNode; ic: string; value: string; label: string; sub: string; pill: string }) {
return (
<div className="mini-card">
<div className="top">
<span className="stat-ic" style={{ width: 30, height: 30, background: `color-mix(in srgb, ${ic} 18%, transparent)`, color: ic }}>{icon}</span>
<span className={`pill ${pill}`}>{sub}</span>
</div>
<div className="mid-val" style={{ marginTop: 12 }}>{value}</div>
<div className="lbl">{label}</div>
</div>
);
}
function PnlCard({ title, value, labels, yk, series }: { title: string; value: string; labels: string[]; yk: string[]; series: { color: string; values: number[] }[] }) {
return (
<div className="dcard">
<div className="card-h"><h3>{title}</h3><MoreHorizontal size={16} color="var(--faint)" /></div>
<div className="mid-val" style={{ marginBottom: 14 }}>{value}</div>
<BarsChart labels={labels} series={series} max={9} yTicks={yk} height={120} barWidth={5} />
</div>
);
}
function Legend({ dot, text }: { dot: string; text: string }) {
return <span className="row gap-2" style={{ fontSize: 12, color: "var(--muted)" }}><span className="ldot" style={{ background: dot }} /> {text}</span>;
}
function Readout({ big, small, color }: { big: string; small: string; color?: string }) {
return (
<div>
<div style={{ fontSize: 16, fontWeight: 800, color: color || "var(--text)" }}>{big}</div>
<div className="lbl">{small}</div>
</div>
);
}