Redesign Support as bento hub, restore full sidebar, Western names
- Support Center overview rebuilt as a bento grid: large live-chat hero tile, support team tile, mini channel tiles and a recent-tickets strip. - Restore full workspace sidebar (Workspace/Team/Profile groups) with a premium golden active state; unbuilt modules show a Coming Soon view. - Use Western names across the UI (James Carter; Emma Wilson, Liam Foster, Sophie Turner, Noah Mitchell) incl. tickets, threads, portal demo user. - Remove the Allottee card from Profile; add a clean Account & identity card and live-editable contacts. - Relabel "Plot" -> "House number" everywhere; drop the allotment chip in favour of the property category. - Premium 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:
@@ -4,7 +4,7 @@
|
||||
// fully clickable without a backend. Ready-to-paste shapes.
|
||||
//
|
||||
// Anchored to the same demo identity used by the auth portal:
|
||||
// Rajesh Kumar Sharma · Allotment YEA-654321 · Plot 181.
|
||||
// James Carter · Allotment YEA-654321 · House 181.
|
||||
// ============================================================
|
||||
|
||||
/* ---------------------------------------------------------- */
|
||||
@@ -38,7 +38,7 @@ export type User = {
|
||||
avatarGradient: string;
|
||||
// property / allotment
|
||||
allotmentNo: string;
|
||||
plot: string; // verify-to-reveal key
|
||||
plot: string; // house number — verify-to-reveal key
|
||||
sector: string;
|
||||
pocket: string;
|
||||
category: string;
|
||||
@@ -59,10 +59,10 @@ export type User = {
|
||||
|
||||
export const user: User = {
|
||||
id: "usr_LUP_00181",
|
||||
name: "Rajesh Kumar Sharma",
|
||||
firstName: "Rajesh",
|
||||
lastName: "Sharma",
|
||||
initials: "RS",
|
||||
name: "James Carter",
|
||||
firstName: "James",
|
||||
lastName: "Carter",
|
||||
initials: "JC",
|
||||
role: "Property Owner",
|
||||
avatarGradient: "linear-gradient(135deg,#fda913,#fd6d13)",
|
||||
allotmentNo: "YEA-654321",
|
||||
@@ -73,8 +73,8 @@ export const user: User = {
|
||||
size: "300 sq.m",
|
||||
isAllottee: true,
|
||||
relationship: "Self",
|
||||
allotteeName: "Rajesh Kumar Sharma",
|
||||
email: { label: "Email", masked: maskEmail("rajesh.sharma@example.com"), full: "rajesh.sharma@example.com" },
|
||||
allotteeName: "James Carter",
|
||||
email: { label: "Email", masked: maskEmail("james.carter@example.com"), full: "james.carter@example.com" },
|
||||
mobile: { label: "Mobile", masked: "+91 ••••• ••012", full: "+91 98765 43012" },
|
||||
whatsapp: { label: "WhatsApp", masked: "+91 ••••• ••012", full: "+91 98765 43012" },
|
||||
pan: { label: "PAN", masked: "•••••••• 12F", full: "ABCPS1234F" },
|
||||
@@ -170,8 +170,8 @@ export const contactPrefs: NotifCategory[] = [
|
||||
|
||||
export type ContactDestination = { id: string; channel: "email" | "sms" | "whatsapp"; label: string; value: string; verified: boolean; primary: boolean };
|
||||
export const contactDestinations: ContactDestination[] = [
|
||||
{ id: "e1", channel: "email", label: "Personal email", value: "rajesh.sharma@example.com", verified: true, primary: true },
|
||||
{ id: "e2", channel: "email", label: "Work email", value: "r.sharma@acme.co.in", verified: false, primary: false },
|
||||
{ id: "e1", channel: "email", label: "Personal email", value: "james.carter@example.com", verified: true, primary: true },
|
||||
{ id: "e2", channel: "email", label: "Work email", value: "j.carter@acme.co", verified: false, primary: false },
|
||||
{ id: "s1", channel: "sms", label: "Registered mobile", value: "+91 98765 43012", verified: true, primary: true },
|
||||
{ id: "w1", channel: "whatsapp", label: "WhatsApp", value: "+91 98765 43012", verified: true, primary: true },
|
||||
];
|
||||
@@ -290,10 +290,10 @@ export type SupportAgent = {
|
||||
};
|
||||
|
||||
export const supportTeam: SupportAgent[] = [
|
||||
{ id: "a1", name: "Priya Nair", initials: "PN", role: "Senior Support Lead", team: "Accounts & KYC", gradient: "linear-gradient(135deg,#fda913,#fd6d13)", status: "online", rating: 4.9 },
|
||||
{ id: "a2", name: "Arjun Mehta", initials: "AM", role: "Support Specialist", team: "Billing", gradient: "linear-gradient(135deg,#285ef0,#09b9c6)", status: "online", rating: 4.8 },
|
||||
{ id: "a3", name: "Sara Khan", initials: "SK", role: "Technical Support", team: "Portal & Login", gradient: "linear-gradient(135deg,#9036e9,#285ef0)", status: "away", rating: 4.7 },
|
||||
{ id: "a4", name: "Vikram Rao", initials: "VR", role: "Field Coordinator", team: "Inspections", gradient: "linear-gradient(135deg,#14bc83,#09b9c6)", status: "offline", rating: 4.9 },
|
||||
{ id: "a1", name: "Emma Wilson", initials: "EW", role: "Senior Support Lead", team: "Accounts & KYC", gradient: "linear-gradient(135deg,#fda913,#fd6d13)", status: "online", rating: 4.9 },
|
||||
{ id: "a2", name: "Liam Foster", initials: "LF", role: "Support Specialist", team: "Billing", gradient: "linear-gradient(135deg,#285ef0,#09b9c6)", status: "online", rating: 4.8 },
|
||||
{ id: "a3", name: "Sophie Turner", initials: "ST", role: "Technical Support", team: "Portal & Login", gradient: "linear-gradient(135deg,#9036e9,#285ef0)", status: "away", rating: 4.7 },
|
||||
{ id: "a4", name: "Noah Mitchell", initials: "NM", role: "Field Coordinator", team: "Inspections", gradient: "linear-gradient(135deg,#14bc83,#09b9c6)", status: "offline", rating: 4.9 },
|
||||
];
|
||||
|
||||
// Category → department routing + attachment rules for the New Ticket form.
|
||||
@@ -345,8 +345,8 @@ export const tickets: Ticket[] = [
|
||||
timeline: [
|
||||
{ status: "created", label: "Ticket created", at: "24 Jun, 10:12 AM", by: "You" },
|
||||
{ status: "open", label: "Routed to Accounts & KYC", at: "24 Jun, 10:13 AM", by: "System" },
|
||||
{ status: "in-progress", label: "Priya Nair picked up the ticket", at: "24 Jun, 11:40 AM", by: "Priya Nair" },
|
||||
{ status: "awaiting-customer", label: "Requested a clearer scan", at: "25 Jun, 09:05 AM", by: "Priya Nair" },
|
||||
{ status: "in-progress", label: "Emma Wilson picked up the ticket", at: "24 Jun, 11:40 AM", by: "Emma Wilson" },
|
||||
{ status: "awaiting-customer", label: "Requested a clearer scan", at: "25 Jun, 09:05 AM", by: "Emma Wilson" },
|
||||
{ status: "in-progress", label: "You re-uploaded the document", at: "28 Jun, 04:22 PM", by: "You" },
|
||||
],
|
||||
},
|
||||
@@ -362,8 +362,8 @@ export const tickets: Ticket[] = [
|
||||
agentId: "a3",
|
||||
timeline: [
|
||||
{ status: "created", label: "Ticket created", at: "18 Jun, 08:40 PM", by: "You" },
|
||||
{ status: "in-progress", label: "Sara Khan investigating SMS gateway", at: "18 Jun, 09:02 PM", by: "Sara Khan" },
|
||||
{ status: "resolved", label: "OTP delivery restored", at: "19 Jun, 10:15 AM", by: "Sara Khan" },
|
||||
{ status: "in-progress", label: "Sophie Turner investigating SMS gateway", at: "18 Jun, 09:02 PM", by: "Sophie Turner" },
|
||||
{ status: "resolved", label: "OTP delivery restored", at: "19 Jun, 10:15 AM", by: "Sophie Turner" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -378,8 +378,8 @@ export const tickets: Ticket[] = [
|
||||
agentId: "a2",
|
||||
timeline: [
|
||||
{ status: "created", label: "Ticket created", at: "12 Jun, 02:10 PM", by: "You" },
|
||||
{ status: "in-progress", label: "Arjun Mehta reviewing billing run", at: "12 Jun, 03:30 PM", by: "Arjun Mehta" },
|
||||
{ status: "awaiting-customer", label: "Need bank reference for the second debit", at: "26 Jun, 11:00 AM", by: "Arjun Mehta" },
|
||||
{ status: "in-progress", label: "Liam Foster reviewing billing run", at: "12 Jun, 03:30 PM", by: "Liam Foster" },
|
||||
{ status: "awaiting-customer", label: "Need bank reference for the second debit", at: "26 Jun, 11:00 AM", by: "Liam Foster" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -394,8 +394,8 @@ export const tickets: Ticket[] = [
|
||||
agentId: "a4",
|
||||
timeline: [
|
||||
{ status: "created", label: "Ticket created", at: "02 Jun, 06:20 PM", by: "You" },
|
||||
{ status: "in-progress", label: "Vikram Rao coordinating slot", at: "03 Jun, 10:00 AM", by: "Vikram Rao" },
|
||||
{ status: "resolved", label: "Visit moved to 09 Jun, 11 AM", at: "04 Jun, 04:45 PM", by: "Vikram Rao" },
|
||||
{ status: "in-progress", label: "Noah Mitchell coordinating slot", at: "03 Jun, 10:00 AM", by: "Noah Mitchell" },
|
||||
{ status: "resolved", label: "Visit moved to 09 Jun, 11 AM", at: "04 Jun, 04:45 PM", by: "Noah Mitchell" },
|
||||
{ status: "closed", label: "Inspection completed", at: "06 Jun, 01:30 PM", by: "System" },
|
||||
],
|
||||
},
|
||||
@@ -426,8 +426,8 @@ export const supportThreads: SupportThread[] = [
|
||||
typing: true,
|
||||
updatedAt: "2 min ago",
|
||||
messages: [
|
||||
{ id: "m1", from: "agent", text: "Hi Rajesh, this is Priya from Accounts & KYC. I'm looking into ticket LUP-48217.", at: "11:40 AM" },
|
||||
{ id: "m2", from: "me", text: "Thanks Priya. The earlier scan was blurry so I uploaded a sharper one.", at: "11:42 AM" },
|
||||
{ id: "m1", from: "agent", text: "Hi James, this is Emma from Accounts & KYC. I'm looking into ticket LUP-48217.", at: "11:40 AM" },
|
||||
{ id: "m2", from: "me", text: "Thanks Emma. The earlier scan was blurry so I uploaded a sharper one.", at: "11:42 AM" },
|
||||
{ id: "m3", from: "agent", text: "Thanks, I can see the new scan now. Reviewing it with the verification team.", at: "11:43 AM" },
|
||||
],
|
||||
},
|
||||
@@ -478,7 +478,7 @@ export const helpTopics: HelpTopic[] = [
|
||||
accent: "var(--orange)",
|
||||
count: 3,
|
||||
articles: [
|
||||
{ q: "How do I verify my property details?", a: "Open Profile → Personal Info and click ‘Verify to reveal’. Enter your plot number (e.g. 181) to unmask and confirm your records." },
|
||||
{ q: "How do I verify my property details?", a: "Open Profile → Personal Info and click ‘Verify to reveal’. Enter your house number (e.g. 181) to unmask and confirm your records." },
|
||||
{ q: "What does ‘allottee’ mean if I'm a representative?", a: "If you manage the property on someone's behalf, the account holder and the allottee differ. Your relationship is shown on the Personal Info tab." },
|
||||
{ q: "How do I update my profile photo?", a: "Use the camera button on your profile header card. JPG or PNG up to 5 MB." },
|
||||
],
|
||||
@@ -530,7 +530,7 @@ export const helpTopics: HelpTopic[] = [
|
||||
export type Rule = { n: number; title: string; detail: string; tag: string; tagColor: string };
|
||||
|
||||
export const rules: Rule[] = [
|
||||
{ n: 1, title: "Verify-to-reveal", detail: "Masked personal details (email, mobile, PAN, Aadhaar) stay hidden until the account holder confirms their plot number. For this account the plot is 181.", tag: "Profile", tagColor: "var(--orange)" },
|
||||
{ n: 1, title: "Verify-to-reveal", detail: "Masked personal details (email, mobile, PAN, Aadhaar) stay hidden until the account holder confirms their house number. For this account the house number is 181.", tag: "Profile", tagColor: "var(--orange)" },
|
||||
{ n: 2, title: "OTP goes to the registered mobile", detail: "Any sensitive change — email, mobile or password — sends a one-time code to the registered mobile number, never to the new/unverified destination.", tag: "Security", tagColor: "var(--blue)" },
|
||||
{ n: 3, title: "Allottee ≠ account holder", detail: "When the relationship is not ‘Self’, the account holder is a representative and the allottee's name is shown separately. Actions are logged against the account holder.", tag: "Profile", tagColor: "var(--orange)" },
|
||||
{ n: 4, title: "KYC needs two different documents", detail: "Identity proof and address proof must be two different document types. A single document (e.g. Aadhaar) cannot satisfy both slots at once.", tag: "KYC", tagColor: "var(--purple)" },
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
"use client";
|
||||
|
||||
// ============================================================
|
||||
// Dashboard shell — keeps the sidebar + header and swaps the
|
||||
// inner content between the Profile, Support and Rules views.
|
||||
// Dashboard shell — premium dark theme. Keeps the sidebar +
|
||||
// header and swaps the inner content. Profile / Support / Rules
|
||||
// render real views; other workspace modules show a placeholder.
|
||||
// ============================================================
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { Sidebar, NAV_ITEMS } from "./sidebar";
|
||||
import { Topbar } from "./topbar";
|
||||
import { ToastProvider } from "./ui";
|
||||
import { ToastProvider, PageHead, Btn, Icon } from "./ui";
|
||||
import { Profile } from "./profile";
|
||||
import { Support } from "./support";
|
||||
import { Rules } from "./rules";
|
||||
import "../../app/dashboard/dashboard.css";
|
||||
|
||||
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" },
|
||||
};
|
||||
|
||||
export function Dashboard() {
|
||||
const [theme, setTheme] = useState<"dark" | "light">("dark");
|
||||
const [active, setActive] = useState("profile");
|
||||
const [active, setActive] = useState("dashboard");
|
||||
|
||||
useEffect(() => {
|
||||
// Sync the persisted theme from localStorage (an external system) on mount.
|
||||
@@ -33,21 +28,44 @@ export function Dashboard() {
|
||||
setTheme((t) => { const n = t === "dark" ? "light" : "dark"; try { localStorage.setItem("lup_dash_theme", n); } catch {} return n; });
|
||||
}
|
||||
|
||||
const head = HEADINGS[active] ?? HEADINGS.profile;
|
||||
const item = NAV_ITEMS.find((i) => i.key === active);
|
||||
const title = item?.label ?? "Dashboard";
|
||||
const subtitle = item?.subtitle ?? "Workspace module";
|
||||
|
||||
return (
|
||||
<div className="dash-root" data-theme={theme}>
|
||||
<Sidebar active={active} onSelect={setActive} />
|
||||
<div className="dash-main">
|
||||
<Topbar theme={theme} onToggle={toggle} title={head.title} subtitle={head.subtitle} />
|
||||
<Topbar theme={theme} onToggle={toggle} title={title} subtitle={subtitle} />
|
||||
<div className="dash-content">
|
||||
<ToastProvider>
|
||||
{active === "profile" && <Profile />}
|
||||
{active === "support" && <Support />}
|
||||
{active === "rules" && <Rules />}
|
||||
{active === "profile" ? <Profile />
|
||||
: active === "support" ? <Support />
|
||||
: active === "rules" ? <Rules />
|
||||
: <ComingSoon title={title} icon={item?.icon ?? "dashboard"} onGo={setActive} />}
|
||||
</ToastProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Placeholder for workspace modules that don't have a view yet — keeps the
|
||||
// full navigation coherent and points users to the live sections.
|
||||
function ComingSoon({ title, icon, onGo }: { title: string; icon: string; onGo: (k: string) => void }) {
|
||||
return (
|
||||
<div className="view">
|
||||
<PageHead eyebrow="Workspace" title={title} subtitle="This module is part of the LynkedUp Pro workspace." icon={icon} />
|
||||
<div className="card coming-soon">
|
||||
<span className="coming-soon-ic"><Icon name={icon} size={34} /></span>
|
||||
<h3>{title} is coming soon</h3>
|
||||
<p>This area is reserved for the {title} module. In the meantime, the account experience is fully built out.</p>
|
||||
<div className="coming-soon-actions">
|
||||
<Btn icon="user" onClick={() => onGo("profile")}>Open Profile</Btn>
|
||||
<Btn variant="outline" icon="chat" onClick={() => onGo("support")}>Support Center</Btn>
|
||||
<Btn variant="ghost" icon="check-circle" onClick={() => onGo("rules")}>Rules</Btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,14 +79,14 @@ function ProfileHeaderCard() {
|
||||
<div className="profile-hero-role">{user.role} · Member since {user.memberSince}</div>
|
||||
<div className="profile-hero-chips">
|
||||
<span className="hero-chip"><Icon name="pin" size={13} /> {user.sector} · {user.pocket}</span>
|
||||
<span className="hero-chip"><Icon name="key" size={13} /> Allotment {user.allotmentNo}</span>
|
||||
<span className="hero-chip accent"><Icon name="check" size={13} /> Plot {user.plot}</span>
|
||||
<span className="hero-chip"><Icon name="owners" size={13} /> {user.category}</span>
|
||||
<span className="hero-chip accent"><Icon name="check" size={13} /> House {user.plot}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="profile-hero-stats">
|
||||
<HeroStat value={user.category} label="Category" />
|
||||
<HeroStat value={user.size} label="Plot size" />
|
||||
<HeroStat value={user.size} label="House size" />
|
||||
<HeroStat value="33%" label="KYC complete" accent="var(--orange)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,33 +116,27 @@ function PersonalInfoTab() {
|
||||
|
||||
// editable contact values (live-updating after OTP confirmation)
|
||||
const [contacts, setContacts] = useState({ email: user.email.full, mobile: user.mobile.full, whatsapp: user.whatsapp.full });
|
||||
// editable account-holder fields
|
||||
const [editId, setEditId] = useState(false);
|
||||
const [holder, setHolder] = useState({ name: user.name, relationship: user.relationship });
|
||||
const [holderDraft, setHolderDraft] = useState(holder);
|
||||
|
||||
function verify() {
|
||||
if (plot.trim() === user.plot) { setVerified(true); setErr(""); push({ tone: "success", title: "Verified", desc: "Your full details are now visible and editable." }); }
|
||||
else setErr(`That doesn't match the plot number on file (hint: it's ${user.plot}).`);
|
||||
else setErr(`That doesn't match the house number on file (hint: it's ${user.plot}).`);
|
||||
}
|
||||
|
||||
function mask(kind: ContactKind, full: string) {
|
||||
return kind === "email" ? maskEmail(full) : maskPhone(full);
|
||||
}
|
||||
|
||||
const rows: { key: ContactKind | "pan" | "aadhaar"; label: string; full: string; masked: string; editable: boolean }[] = [
|
||||
{ key: "email", label: "Email", full: contacts.email, masked: mask("email", contacts.email), editable: true },
|
||||
{ key: "mobile", label: "Mobile", full: contacts.mobile, masked: mask("mobile", contacts.mobile), editable: true },
|
||||
{ key: "whatsapp", label: "WhatsApp", full: contacts.whatsapp, masked: mask("whatsapp", contacts.whatsapp), editable: true },
|
||||
{ key: "pan", label: "PAN", full: user.pan.full, masked: user.pan.masked, editable: false },
|
||||
{ key: "aadhaar", label: "Aadhaar", full: user.aadhaar.full, masked: user.aadhaar.masked, editable: false },
|
||||
const contactRows: { key: ContactKind; label: string; full: string; masked: string }[] = [
|
||||
{ key: "email", label: "Email", full: contacts.email, masked: mask("email", contacts.email) },
|
||||
{ key: "mobile", label: "Mobile", full: contacts.mobile, masked: mask("mobile", contacts.mobile) },
|
||||
{ key: "whatsapp", label: "WhatsApp", full: contacts.whatsapp, masked: mask("whatsapp", contacts.whatsapp) },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid-2">
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h3>Personal details</h3>
|
||||
<h3>Contact details</h3>
|
||||
{verified
|
||||
? <Pill tone="green"><Icon name="eye" size={13} /> Revealed</Pill>
|
||||
: <Pill tone="orange"><Icon name="eye-off" size={13} /> Masked</Pill>}
|
||||
@@ -153,10 +147,10 @@ function PersonalInfoTab() {
|
||||
<div className="reveal-gate-ic"><Icon name="lock" size={20} /></div>
|
||||
<div className="reveal-gate-txt">
|
||||
<strong>Verify to reveal & edit</strong>
|
||||
<span>Sensitive details are masked. Enter your plot number to unmask and edit them.</span>
|
||||
<span>Sensitive details are masked. Enter your house number to unmask and edit them.</span>
|
||||
</div>
|
||||
<div className="reveal-gate-form">
|
||||
<input className="ds-input" placeholder="Plot number" value={plot} maxLength={6}
|
||||
<input className="ds-input" placeholder="House number" value={plot} maxLength={6}
|
||||
onChange={(e) => { setPlot(e.target.value.replace(/\D/g, "")); setErr(""); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && verify()} />
|
||||
<Btn icon="check" onClick={verify}>Reveal</Btn>
|
||||
@@ -166,15 +160,14 @@ function PersonalInfoTab() {
|
||||
)}
|
||||
|
||||
<dl className="kv-list">
|
||||
{rows.map((f) => (
|
||||
{contactRows.map((f) => (
|
||||
<div className="kv" key={f.key}>
|
||||
<dt>{f.label}</dt>
|
||||
<dd className={verified ? "kv-edit" : "masked"}>
|
||||
{verified ? f.full : f.masked}
|
||||
{verified && f.editable && (
|
||||
<button className="kv-edit-btn" aria-label={`Edit ${f.label}`} onClick={() => setChange(f.key as ContactKind)}><Icon name="edit" size={13} /></button>
|
||||
{verified && (
|
||||
<button className="kv-edit-btn" aria-label={`Edit ${f.label}`} onClick={() => setChange(f.key)}><Icon name="edit" size={13} /></button>
|
||||
)}
|
||||
{verified && !f.editable && <span className="kv-lock"><Icon name="lock" size={12} /> via KYC</span>}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
@@ -189,40 +182,26 @@ function PersonalInfoTab() {
|
||||
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h3>Allottee & account holder</h3>
|
||||
{!editId
|
||||
? <Btn variant="outline" size="sm" icon="edit" onClick={() => { setHolderDraft(holder); setEditId(true); }}>Edit</Btn>
|
||||
: <div className="row gap-2"><Btn variant="ghost" size="sm" onClick={() => setEditId(false)}>Cancel</Btn><Btn size="sm" icon="check" onClick={() => { setHolder(holderDraft); setEditId(false); push({ tone: "success", title: "Details saved" }); }}>Save</Btn></div>}
|
||||
<h3>Account & identity</h3>
|
||||
{verified
|
||||
? <Pill tone="green"><Icon name="eye" size={13} /> Revealed</Pill>
|
||||
: <Pill tone="muted"><Icon name="lock" size={12} /> Protected</Pill>}
|
||||
</div>
|
||||
{user.isAllottee ? (
|
||||
<div className="callout tone-green">
|
||||
<Icon name="check-circle" size={18} />
|
||||
<div><strong>You are the allottee.</strong><span>The account holder and the allottee are the same person ({holder.relationship}).</span></div>
|
||||
<dl className="kv-list">
|
||||
<div className="kv"><dt>Full name</dt><dd>{user.name}</dd></div>
|
||||
<div className="kv"><dt>Role</dt><dd>{user.role}</dd></div>
|
||||
<div className="kv"><dt>Member since</dt><dd>{user.memberSince}</dd></div>
|
||||
<div className="kv"><dt>Account status</dt><dd><Pill tone="green"><StatusDot status="online" /> Active</Pill></dd></div>
|
||||
<div className="kv">
|
||||
<dt>PAN</dt>
|
||||
<dd className={verified ? "kv-edit" : "masked"}>{verified ? user.pan.full : user.pan.masked}{verified && <span className="kv-lock"><Icon name="lock" size={12} /> via KYC</span>}</dd>
|
||||
</div>
|
||||
) : (
|
||||
<div className="callout tone-orange">
|
||||
<Icon name="info" size={18} />
|
||||
<div><strong>Acting on behalf of the allottee.</strong><span>Account holder differs from the allottee — actions are logged against you.</span></div>
|
||||
<div className="kv">
|
||||
<dt>Aadhaar</dt>
|
||||
<dd className={verified ? "kv-edit" : "masked"}>{verified ? user.aadhaar.full : user.aadhaar.masked}{verified && <span className="kv-lock"><Icon name="lock" size={12} /> via KYC</span>}</dd>
|
||||
</div>
|
||||
)}
|
||||
{editId ? (
|
||||
<div className="edit-grid">
|
||||
<Field label="Account holder name"><input className="ds-input" value={holderDraft.name} onChange={(e) => setHolderDraft((d) => ({ ...d, name: e.target.value }))} /></Field>
|
||||
<Field label="Relationship to allottee">
|
||||
<select className="ds-select" value={holderDraft.relationship} onChange={(e) => setHolderDraft((d) => ({ ...d, relationship: e.target.value }))}>
|
||||
{["Self", "Spouse", "Son / Daughter", "Authorized Representative", "Power of Attorney"].map((r) => <option key={r} value={r}>{r}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
) : (
|
||||
<dl className="kv-list">
|
||||
<div className="kv"><dt>Allottee name</dt><dd>{user.allotteeName}</dd></div>
|
||||
<div className="kv"><dt>Account holder</dt><dd>{holder.name}</dd></div>
|
||||
<div className="kv"><dt>Relationship</dt><dd><Pill tone="blue">{holder.relationship}</Pill></dd></div>
|
||||
<div className="kv"><dt>Allotment no.</dt><dd>{user.allotmentNo}</dd></div>
|
||||
<div className="kv"><dt>Property</dt><dd>{user.sector} · {user.pocket} · Plot {user.plot}</dd></div>
|
||||
</dl>
|
||||
)}
|
||||
</dl>
|
||||
<p className="muted-note"><Icon name="info" size={13} /> PAN & Aadhaar are managed under the KYC tab and can't be edited here.</p>
|
||||
</div>
|
||||
|
||||
<ChangeContactModal
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// ============================================================
|
||||
// Rules Checklist — the 1–13 business rules in one place.
|
||||
// (plot 181 · OTP to registered mobile · KYC two-docs ·
|
||||
// (house 181 · OTP to registered mobile · KYC two-docs ·
|
||||
// password policy · ticket lifecycle · …)
|
||||
// ============================================================
|
||||
|
||||
|
||||
@@ -4,29 +4,61 @@ import { ChevronsUpDown } from "lucide-react";
|
||||
import { Icon } from "./ui";
|
||||
import { user } from "./account-data";
|
||||
|
||||
type Item = { key: string; label: string; icon: string };
|
||||
export type NavItem = { key: string; label: string; icon: string; subtitle?: string };
|
||||
export type NavGroup = { title: string; items: NavItem[] };
|
||||
|
||||
const GROUPS: { title: string; items: Item[] }[] = [
|
||||
// Full workspace navigation. The Account group items (profile/support/rules)
|
||||
// render real views; the rest are workspace modules shown via a placeholder.
|
||||
export const NAV_GROUPS: NavGroup[] = [
|
||||
{
|
||||
title: "Account",
|
||||
title: "Workspace",
|
||||
items: [
|
||||
{ key: "profile", label: "Profile", icon: "user" },
|
||||
{ key: "dashboard", label: "Dashboard", icon: "dashboard", subtitle: "Welcome back — your territory at a glance" },
|
||||
{ key: "owners", label: "Owners Box", icon: "owners" },
|
||||
{ key: "projects", label: "Projects", icon: "projects" },
|
||||
{ key: "leads", label: "Leads", icon: "leads" },
|
||||
{ key: "verify", label: "Lead Verification", icon: "verify" },
|
||||
{ key: "pipeline", label: "Pipeline", icon: "pipeline" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Help & Support",
|
||||
title: "Workspace",
|
||||
items: [
|
||||
{ key: "support", label: "Support Center", icon: "chat" },
|
||||
{ key: "dispatch", label: "LynkDispatch", icon: "dispatch" },
|
||||
{ key: "storm", label: "Storm Intel", icon: "storm" },
|
||||
{ key: "territory", label: "Territory Map", icon: "territory" },
|
||||
{ key: "procanvas", label: "ProCanvas", icon: "procanvas" },
|
||||
{ key: "estimates", label: "Estimates", icon: "estimates" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Reference",
|
||||
title: "Team",
|
||||
items: [
|
||||
{ key: "rules", label: "Rules Checklist", icon: "check-circle" },
|
||||
{ key: "schedule", label: "Team Schedule", icon: "schedule" },
|
||||
{ key: "leaderboard", label: "Leaderboard", icon: "leaderboard" },
|
||||
{ key: "subtasks", label: "Subcontractor Tasks", icon: "subtasks" },
|
||||
{ key: "people", label: "People", icon: "people" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
items: [
|
||||
{ key: "settings", label: "Org Settings", icon: "settings" },
|
||||
{ key: "ai", label: "AI Assistant", icon: "ai" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Profile & Documents",
|
||||
items: [
|
||||
{ key: "profile", label: "Profile", icon: "user", subtitle: "Your account, identity and preferences" },
|
||||
{ key: "support", label: "Support Center", icon: "chat", subtitle: "Get help, track requests and find answers" },
|
||||
{ key: "rules", label: "Rules Checklist", icon: "check-circle", subtitle: "The 13 rules behind Profile & Support" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const NAV_ITEMS: NavItem[] = NAV_GROUPS.flatMap((g) => g.items);
|
||||
|
||||
export function Sidebar({ active, onSelect }: { active: string; onSelect: (k: string) => void }) {
|
||||
return (
|
||||
<aside className="dash-sidebar">
|
||||
@@ -37,13 +69,13 @@ export function Sidebar({ active, onSelect }: { active: string; onSelect: (k: st
|
||||
</div>
|
||||
|
||||
<nav className="dash-nav">
|
||||
{GROUPS.map((g, gi) => (
|
||||
<div key={gi}>
|
||||
{NAV_GROUPS.map((g, gi) => (
|
||||
<div className="nav-section" key={gi}>
|
||||
<div className="nav-group">{g.title}</div>
|
||||
{g.items.map((it) => (
|
||||
<button key={it.key} className={`nav-item ${active === it.key ? "active" : ""}`} onClick={() => onSelect(it.key)}>
|
||||
<Icon name={it.icon} size={18} />
|
||||
{it.label}
|
||||
<span className="nav-item-label">{it.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export function Support() {
|
||||
<Segmented options={SECTIONS} value={section} onChange={setSection} />
|
||||
|
||||
<div className="view-body">
|
||||
{section === "overview" && <Overview onChannel={onChannel} />}
|
||||
{section === "overview" && <Overview onChannel={onChannel} onSection={setSection} />}
|
||||
{section === "messages" && <MessageCenter />}
|
||||
{section === "new" && <NewTicket onDone={() => setSection("tickets")} />}
|
||||
{section === "tickets" && <MyTickets />}
|
||||
@@ -79,32 +79,41 @@ export function Support() {
|
||||
/* Overview — channel cards + support team */
|
||||
/* ============================================================ */
|
||||
|
||||
function Overview({ onChannel }: { onChannel: (id: string) => void }) {
|
||||
return (
|
||||
<div className="grid-side">
|
||||
<div>
|
||||
<div className="channel-grid">
|
||||
{supportChannels.map((c) => (
|
||||
<button key={c.id} className="channel-card" style={{ ["--accent" as string]: c.accent }} onClick={() => onChannel(c.id)}>
|
||||
<span className="channel-ic"><Icon name={c.icon} size={22} /></span>
|
||||
<div className="channel-status"><StatusDot status={c.status} /> {c.status === "online" ? "Available" : c.status}</div>
|
||||
<div className="channel-title">{c.title}</div>
|
||||
<div className="channel-desc">{c.desc}</div>
|
||||
<div className="channel-foot">
|
||||
<span className="channel-meta">{c.meta}</span>
|
||||
<span className="channel-cta">{c.action} <Icon name="arrow" size={14} /></span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
const MINI_AREA: Record<string, string> = { ticket: "ticket", callback: "call", email: "email", help: "help" };
|
||||
|
||||
<div className="card">
|
||||
<div className="card-head"><h3>Your support team</h3><Pill tone="green"><StatusDot status="online" /> 2 online</Pill></div>
|
||||
function Overview({ onChannel, onSection }: { onChannel: (id: string) => void; onSection: (s: string) => void }) {
|
||||
const online = supportTeam.filter((a) => a.status === "online");
|
||||
const mini = supportChannels.filter((c) => ["ticket", "callback", "email", "help"].includes(c.id));
|
||||
const recent = ticketSeed.slice(0, 4);
|
||||
|
||||
return (
|
||||
<div className="support-bento">
|
||||
{/* Hero — live chat */}
|
||||
<button className="bento-tile bento-chat" onClick={() => onChannel("chat")}>
|
||||
<div className="bento-chat-top">
|
||||
<span className="bento-chat-badge"><StatusDot status="online" /> Agents online now</span>
|
||||
<span className="bento-chat-wait"><Icon name="clock" size={13} /> Avg wait < 2 min</span>
|
||||
</div>
|
||||
<div className="bento-chat-mid">
|
||||
<h2>Need a hand?<br />Chat with us live.</h2>
|
||||
<p>Instant help from a real support specialist — no queues, no bots, no waiting on hold.</p>
|
||||
</div>
|
||||
<div className="bento-chat-foot">
|
||||
<div className="bento-avstack">
|
||||
{online.map((a) => <Avatar key={a.id} initials={a.initials} gradient={a.gradient} size={36} />)}
|
||||
<span className="bento-av-more">+{online.length} online</span>
|
||||
</div>
|
||||
<span className="bento-chat-cta">Start chat <Icon name="arrow" size={16} /></span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Support team */}
|
||||
<div className="bento-tile bento-team">
|
||||
<div className="bento-tile-head"><h3>Your support team</h3><Pill tone="green">{online.length} online</Pill></div>
|
||||
<div className="team-list">
|
||||
{supportTeam.map((a) => (
|
||||
<div className="team-row" key={a.id}>
|
||||
<Avatar initials={a.initials} gradient={a.gradient} size={42} status={a.status} />
|
||||
<Avatar initials={a.initials} gradient={a.gradient} size={40} status={a.status} />
|
||||
<div className="team-txt">
|
||||
<div className="team-name">{a.name}</div>
|
||||
<div className="team-role">{a.role}</div>
|
||||
@@ -115,6 +124,30 @@ function Overview({ onChannel }: { onChannel: (id: string) => void }) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mini channel tiles */}
|
||||
{mini.map((c) => (
|
||||
<button key={c.id} className="bento-tile bento-mini" style={{ ["--accent" as string]: c.accent, gridArea: MINI_AREA[c.id] }} onClick={() => onChannel(c.id)}>
|
||||
<span className="bento-mini-ic"><Icon name={c.icon} size={20} /></span>
|
||||
<div className="bento-mini-title">{c.title}</div>
|
||||
<div className="bento-mini-desc">{c.desc}</div>
|
||||
<div className="bento-mini-meta"><span>{c.meta}</span> <Icon name="arrow" size={14} /></div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Recent tickets */}
|
||||
<div className="bento-tile bento-recent">
|
||||
<div className="bento-tile-head"><h3>Recent tickets</h3><button className="link-inline" onClick={() => onSection("tickets")}>View all</button></div>
|
||||
<div className="bento-ticket-row">
|
||||
{recent.map((t) => (
|
||||
<button key={t.id} className="bento-ticket" onClick={() => onSection("tickets")}>
|
||||
<div className="bento-ticket-top"><span className="t-id">{t.id}</span><Pill tone={STATUS_META[t.status].tone}>{STATUS_META[t.status].label}</Pill></div>
|
||||
<div className="bento-ticket-subj">{t.subject}</div>
|
||||
<div className="bento-ticket-meta"><Icon name="clock" size={12} /> {t.updatedAt} · {t.department}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ import {
|
||||
Lock, CreditCard, User, Bell, Eye, EyeOff, Camera, Upload, Plus, Star, Send,
|
||||
Paperclip, Clock, MapPin, Monitor, Smartphone, Tablet, LogOut, ArrowRight,
|
||||
Info, Check, X, Search, ChevronDown, ChevronRight, Trash2, Globe,
|
||||
CheckCircle2, KeyRound, Pencil, Copy, RefreshCw, AlertTriangle, type LucideIcon,
|
||||
CheckCircle2, KeyRound, Pencil, Copy, RefreshCw, AlertTriangle,
|
||||
LayoutDashboard, Building2, FolderKanban, UserPlus, BadgeCheck, Filter,
|
||||
Truck, CloudLightning, Map as MapIcon, PenTool, Calculator, CalendarDays,
|
||||
Trophy, ListChecks, Users, Settings, Sparkles, type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ---------------------------------------------------------- */
|
||||
@@ -38,6 +41,12 @@ const ICONS: Record<string, LucideIcon> = {
|
||||
"check-circle": CheckCircle2, key: KeyRound, edit: Pencil, copy: Copy,
|
||||
refresh: RefreshCw, alert: AlertTriangle, privacy: ShieldCheck, devices: Monitor,
|
||||
desktop: Monitor,
|
||||
// sidebar / workspace nav
|
||||
dashboard: LayoutDashboard, owners: Building2, projects: FolderKanban,
|
||||
leads: UserPlus, verify: BadgeCheck, pipeline: Filter, dispatch: Truck,
|
||||
storm: CloudLightning, territory: MapIcon, procanvas: PenTool,
|
||||
estimates: Calculator, schedule: CalendarDays, leaderboard: Trophy,
|
||||
subtasks: ListChecks, people: Users, settings: Settings, ai: Sparkles,
|
||||
};
|
||||
|
||||
export function Icon({ name, size = 18, className, strokeWidth = 2 }: { name: string; size?: number; className?: string; strokeWidth?: number }) {
|
||||
|
||||
@@ -27,10 +27,10 @@ export const MASKED_WA = "+91 ••••• ••012";
|
||||
|
||||
/* ---- demo account "on file" ---- */
|
||||
export const DEMO_USER = {
|
||||
name: "Rajesh Kumar Sharma",
|
||||
firstName: "Rajesh",
|
||||
lastName: "Sharma",
|
||||
initials: "RS",
|
||||
name: "James Carter",
|
||||
firstName: "James",
|
||||
lastName: "Carter",
|
||||
initials: "JC",
|
||||
allotmentNo: "YEA-654321",
|
||||
sector: "Sector 18",
|
||||
pocket: "Pocket B",
|
||||
@@ -38,7 +38,7 @@ export const DEMO_USER = {
|
||||
category: "Residential",
|
||||
size: "300 sq.m",
|
||||
maskedMobile: MASKED_PHONE,
|
||||
email: "rajesh.sharma@example.com",
|
||||
email: "james.carter@example.com",
|
||||
relationship: "Self",
|
||||
isAllottee: true,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user