From 3ab6768c1cf1949f9d36c2f46b791c29cc813387 Mon Sep 17 00:00:00 2001 From: Goutam Date: Mon, 29 Jun 2026 21:52:34 +0530 Subject: [PATCH] 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) --- .gitignore | 2 + src/app/dashboard/dashboard.css | 452 +++++++++++++++ src/components/dashboard/account-data.ts | 566 ++++++++++++++++++ src/components/dashboard/charts.tsx | 180 ------ src/components/dashboard/dashboard.tsx | 251 +------- src/components/dashboard/figicon.tsx | 17 - src/components/dashboard/figma-icons.ts | 32 -- src/components/dashboard/profile.tsx | 700 +++++++++++++++++++++++ src/components/dashboard/rules.tsx | 44 ++ src/components/dashboard/sidebar.tsx | 65 +-- src/components/dashboard/support.tsx | 545 ++++++++++++++++++ src/components/dashboard/topbar.tsx | 23 +- src/components/dashboard/ui.tsx | 298 ++++++++++ 13 files changed, 2663 insertions(+), 512 deletions(-) create mode 100644 src/components/dashboard/account-data.ts delete mode 100644 src/components/dashboard/charts.tsx delete mode 100644 src/components/dashboard/figicon.tsx delete mode 100644 src/components/dashboard/figma-icons.ts create mode 100644 src/components/dashboard/profile.tsx create mode 100644 src/components/dashboard/rules.tsx create mode 100644 src/components/dashboard/support.tsx create mode 100644 src/components/dashboard/ui.tsx diff --git a/.gitignore b/.gitignore index 5ef6a52..9c8a03f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.vercel diff --git a/src/app/dashboard/dashboard.css b/src/app/dashboard/dashboard.css index 9be87e5..263adac 100644 --- a/src/app/dashboard/dashboard.css +++ b/src/app/dashboard/dashboard.css @@ -210,3 +210,455 @@ /* responsive */ @media (max-width: 1100px) { .dash-sidebar { display: none; } } @media (max-width: 760px) { .dash-content { padding: 16px; } } + +/* ========================================================================= + ACCOUNT MODULE — Profile · Support · Rules design system + All components consume the .dash-root tokens above, so they theme with + [data-theme] automatically. Prefix-free utility names are scoped under + .dash-root to avoid collisions with the rest of the app. + ========================================================================= */ + +.dash-root { + --ring: color-mix(in srgb, var(--orange) 35%, transparent); + --grad-brand: linear-gradient(135deg, #fda913 0%, #fd6d13 100%); + --card-grad: linear-gradient(160deg, color-mix(in srgb, var(--panel-2) 55%, var(--panel)) 0%, var(--panel) 62%); + --card-hi: inset 0 1px 0 color-mix(in srgb, #fff 6%, transparent); + --glow-orange: 0 10px 28px -12px color-mix(in srgb, var(--orange) 60%, transparent); +} +/* ambient depth behind the content */ +.dash-root .dash-content { position: relative; } +.dash-root .dash-content::before { + content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; + background: + radial-gradient(55% 45% at 100% 0%, color-mix(in srgb, var(--orange) 7%, transparent), transparent 70%), + radial-gradient(45% 40% at 0% 100%, color-mix(in srgb, var(--blue) 6%, transparent), transparent 70%); +} +.dash-root .view { position: relative; z-index: 1; } + +/* ---- animations ---- */ +@keyframes ds-fade { from { opacity: 0; } to { opacity: 1; } } +@keyframes ds-pop { from { opacity: 0; transform: translateY(10px) scale(0.985); } to { opacity: 1; transform: none; } } +@keyframes ds-slide-in { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } } +@keyframes ds-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } +@keyframes ds-bounce { 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; } 30% { transform: translateY(-4px); opacity: 1; } } +@keyframes ds-radar { 0% { transform: scale(0.7); opacity: 0.7; } 100% { transform: scale(2.1); opacity: 0; } } +@keyframes ds-bar { from { width: 0; } } + +.dash-root .view { animation: ds-fade 0.3s ease; } +.dash-root .view-body { animation: ds-rise 0.32s ease; } + +/* ---- generic card / layout ---- */ +.dash-root .card { background: var(--card-grad); border: 1px solid var(--border); border-radius: 18px; padding: 22px; box-shadow: var(--card-hi), 0 1px 2px rgba(0,0,0,0.18); } +.dash-root .card-muted { background: var(--panel-2); } +.dash-root .card-pad-0 { padding: 0; overflow: hidden; } +.dash-root .card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; } +.dash-root .card-head.pad { padding: 18px 20px 0; margin-bottom: 14px; } +.dash-root .card-head h3 { font-size: 15px; font-weight: 700; } +.dash-root .card-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; } +.dash-root .card-stack { display: flex; flex-direction: column; gap: 16px; } +.dash-root .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; } +.dash-root .grid-side { display: grid; grid-template-columns: 1.55fr 1fr; gap: 16px; align-items: start; } +.dash-root .consent-cols { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; align-items: start; } +.dash-root .muted-note { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--faint); margin-top: 12px; } +.dash-root .muted-note svg { flex: 0 0 auto; } + +/* ---- PageHead ---- */ +.dash-root .ds-pagehead { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; } +.dash-root .ds-pagehead-l { display: flex; gap: 14px; align-items: center; } +.dash-root .ds-pagehead-ic { width: 52px; height: 52px; border-radius: 16px; flex: 0 0 auto; display: grid; place-items: center; color: #1a1206; background: var(--grad-brand); box-shadow: var(--glow-orange); } +.dash-root .ds-pagehead h1 { font-size: 24px; } +.dash-root .ds-eyebrow { font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--orange); margin-bottom: 3px; } +.dash-root .ds-pagehead h1 { font-size: 23px; font-weight: 800; letter-spacing: -0.02em; } +.dash-root .ds-pagehead p { font-size: 13px; color: var(--muted); margin-top: 3px; } +.dash-root .ds-pagehead-actions { display: flex; gap: 10px; align-items: center; } + +/* ---- Avatar ---- */ +.dash-root .ds-avatar { position: relative; display: inline-grid; place-items: center; flex: 0 0 auto; } +.dash-root .ds-avatar-bg { width: 100%; height: 100%; display: grid; place-items: center; color: #fff; font-weight: 700; letter-spacing: -0.01em; } +.dash-root .ds-avatar-dot { position: absolute; right: -1px; bottom: -1px; border-radius: 50%; border: 2px solid var(--panel); } +.dash-root .status-online { background: var(--green); } +.dash-root .status-away { background: var(--orange); } +.dash-root .status-busy { background: var(--red); } +.dash-root .status-offline { background: var(--faint); } + +/* ---- Pill ---- */ +.dash-root .ds-pill { display: inline-flex; align-items: center; gap: 5px; height: 23px; padding: 0 10px; border-radius: 99px; font-size: 11px; font-weight: 600; white-space: nowrap; } +.dash-root .ds-pill.tone-muted { background: var(--panel-3); color: var(--muted); } +.dash-root .ds-pill.tone-green { background: color-mix(in srgb, var(--green) 16%, transparent); color: var(--green); } +.dash-root .ds-pill.tone-orange { background: color-mix(in srgb, var(--orange) 16%, transparent); color: var(--orange); } +.dash-root .ds-pill.tone-blue { background: color-mix(in srgb, var(--blue) 18%, transparent); color: #6f9bff; } +.dash-root .ds-pill.tone-purple { background: color-mix(in srgb, var(--purple) 18%, transparent); color: #b07bf2; } +.dash-root .ds-pill.tone-cyan { background: color-mix(in srgb, var(--cyan) 16%, transparent); color: var(--cyan); } +.dash-root .ds-pill.tone-red { background: color-mix(in srgb, var(--red) 16%, transparent); color: var(--red); } +.dash-root .ds-pill svg { flex: 0 0 auto; } + +.dash-root .ds-statusdot { width: 8px; height: 8px; border-radius: 99px; display: inline-block; } + +/* ---- Toggle ---- */ +.dash-root .ds-toggle { width: 40px; height: 23px; border-radius: 99px; border: 0; background: var(--track); position: relative; cursor: pointer; transition: background 0.18s; flex: 0 0 auto; padding: 0; } +.dash-root .ds-toggle .knob { position: absolute; top: 3px; left: 3px; width: 17px; height: 17px; border-radius: 50%; background: #fff; transition: transform 0.18s; box-shadow: 0 1px 3px rgba(0,0,0,0.3); } +.dash-root .ds-toggle.on { background: var(--orange); } +.dash-root .ds-toggle.on .knob { transform: translateX(17px); } +.dash-root .ds-toggle.disabled { opacity: 0.45; cursor: not-allowed; } + +/* ---- Field / inputs ---- */ +.dash-root .ds-field { display: block; margin-bottom: 14px; } +.dash-root .ds-field-lbl { display: block; font-size: 12.5px; font-weight: 600; color: var(--text-2); margin-bottom: 7px; } +.dash-root .req { color: var(--red); font-style: normal; margin-left: 3px; } +.dash-root .ds-field-hint { display: block; font-size: 11.5px; color: var(--faint); margin-top: 6px; line-height: 1.4; } +.dash-root .ds-field-err { display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--red); margin-top: 6px; } +.dash-root .ds-input, .dash-root .ds-select, .dash-root .ds-textarea { + width: 100%; height: 42px; padding: 0 13px; border-radius: 11px; border: 1px solid var(--border-2); + background: var(--panel-2); color: var(--text); font-family: inherit; font-size: 13.5px; outline: none; transition: border-color 0.14s, box-shadow 0.14s; +} +.dash-root .ds-textarea { height: auto; padding: 11px 13px; resize: vertical; line-height: 1.5; } +.dash-root .ds-select { appearance: none; cursor: pointer; background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%); background-position: calc(100% - 18px) 18px, calc(100% - 13px) 18px; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 34px; } +.dash-root .ds-input:focus, .dash-root .ds-select:focus, .dash-root .ds-textarea:focus { border-color: var(--orange); box-shadow: 0 0 0 3px var(--ring); } +.dash-root .ds-input.flush { border: 0; background: none; box-shadow: none; padding: 0 4px; height: 38px; } +.dash-root .ds-input.flush:focus { box-shadow: none; } +.dash-root .ds-input::placeholder, .dash-root .ds-textarea::placeholder { color: var(--faint); } + +/* ---- Buttons ---- */ +.dash-root .ds-btn { display: inline-flex; align-items: center; justify-content: center; gap: 7px; height: 40px; padding: 0 16px; border-radius: 11px; border: 1px solid transparent; font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; transition: 0.14s; white-space: nowrap; } +.dash-root .ds-btn.s-sm { height: 32px; padding: 0 12px; font-size: 12px; border-radius: 9px; } +.dash-root .ds-btn.full { width: 100%; } +.dash-root .ds-btn:disabled { opacity: 0.45; cursor: not-allowed; } +.dash-root .ds-btn.v-primary { background: var(--grad-brand); color: #1a1206; box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--orange) 75%, transparent); } +.dash-root .ds-btn.v-primary:not(:disabled):hover { filter: brightness(1.05); transform: translateY(-1px); } +.dash-root .ds-btn.v-ghost { background: transparent; color: var(--muted); } +.dash-root .ds-btn.v-ghost:hover { background: var(--panel-2); color: var(--text); } +.dash-root .ds-btn.v-soft { background: var(--panel-3); color: var(--text); } +.dash-root .ds-btn.v-soft:hover { background: var(--border-2); } +.dash-root .ds-btn.v-outline { background: transparent; border-color: var(--border-2); color: var(--text-2); } +.dash-root .ds-btn.v-outline:hover { border-color: var(--orange); color: var(--orange); } +.dash-root .ds-btn.v-danger { background: color-mix(in srgb, var(--red) 16%, transparent); color: var(--red); } +.dash-root .ds-btn.v-danger:hover { background: var(--red); color: #fff; } + +.dash-root .ds-iconbtn { width: 36px; height: 36px; border-radius: 10px; border: 1px solid var(--border); background: var(--panel-2); color: var(--muted); display: grid; place-items: center; cursor: pointer; transition: 0.14s; flex: 0 0 auto; } +.dash-root .ds-iconbtn:hover { color: var(--text); border-color: var(--border-2); } +.dash-root .ds-iconbtn.sm { width: 28px; height: 28px; border-radius: 8px; } +.dash-root .link-inline { background: none; border: 0; color: var(--orange); font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; } + +/* ---- Segmented / Tabs ---- */ +.dash-root .ds-segmented { display: inline-flex; gap: 3px; padding: 4px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 12px; flex-wrap: wrap; } +.dash-root .ds-segmented .seg { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 14px; border: 0; border-radius: 9px; background: none; color: var(--muted); font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; transition: 0.14s; } +.dash-root .ds-segmented .seg:hover { color: var(--text); } +.dash-root .ds-segmented .seg.active { background: var(--panel); color: var(--text); box-shadow: var(--card-hi), 0 4px 12px -7px rgba(0,0,0,0.5); } + +.dash-root .ds-tabs { display: flex; gap: 4px; padding: 5px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 14px; margin: 18px 0 22px; overflow-x: auto; } +.dash-root .ds-tabs::-webkit-scrollbar { height: 0; } +.dash-root .ds-tab { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border: 0; border-radius: 10px; background: none; color: var(--muted); font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: 0.16s; } +.dash-root .ds-tab:hover { color: var(--text-2); background: color-mix(in srgb, var(--panel) 55%, transparent); } +.dash-root .ds-tab.active { color: var(--orange); background: var(--panel); box-shadow: var(--card-hi), 0 4px 14px -8px rgba(0,0,0,0.5); } +.dash-root .ds-tab-badge { background: var(--orange); color: #1a1206; font-size: 10px; font-weight: 700; min-width: 17px; height: 17px; border-radius: 99px; display: inline-grid; place-items: center; padding: 0 4px; } + +/* ---- OTP ---- */ +.dash-root .ds-otp { display: flex; gap: 9px; } +.dash-root .ds-otp-box { width: 46px; height: 54px; text-align: center; font-size: 22px; font-weight: 700; border-radius: 12px; border: 1px solid var(--border-2); background: var(--panel-2); color: var(--text); outline: none; transition: 0.14s; } +.dash-root .ds-otp-box:focus { border-color: var(--orange); box-shadow: 0 0 0 3px var(--ring); } +.dash-root .otp-wrap { display: flex; flex-direction: column; gap: 12px; align-items: flex-start; } + +/* ---- Modal ---- */ +.dash-root .ds-modal-overlay { position: fixed; inset: 0; z-index: 100; background: rgba(2,2,6,0.6); backdrop-filter: blur(4px); display: grid; place-items: center; padding: 20px; animation: ds-fade 0.18s ease; } +.dash-root .ds-modal { width: 100%; background: var(--panel); border: 1px solid var(--border-2); border-radius: 18px; box-shadow: 0 30px 70px -20px rgba(0,0,0,0.7); animation: ds-pop 0.22s cubic-bezier(0.2,0.7,0.2,1); max-height: 90vh; display: flex; flex-direction: column; } +.dash-root .ds-modal.size-sm { max-width: 400px; } +.dash-root .ds-modal.size-md { max-width: 540px; } +.dash-root .ds-modal.size-lg { max-width: 760px; } +.dash-root .ds-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 20px 20px 14px; border-bottom: 1px solid var(--border); } +.dash-root .ds-modal-head-l { display: flex; gap: 12px; align-items: center; } +.dash-root .ds-modal-ic { width: 38px; height: 38px; border-radius: 11px; flex: 0 0 auto; display: grid; place-items: center; color: var(--orange); background: color-mix(in srgb, var(--orange) 14%, transparent); } +.dash-root .ds-modal-head h3 { font-size: 16px; font-weight: 700; } +.dash-root .ds-modal-head p { font-size: 12.5px; color: var(--muted); margin-top: 3px; } +.dash-root .ds-modal-body { padding: 18px 20px; overflow-y: auto; } +.dash-root .ds-modal-foot { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 20px; border-top: 1px solid var(--border); } + +/* ---- Toasts ---- */ +.dash-root .ds-toasts { position: fixed; right: 20px; bottom: 20px; z-index: 200; display: flex; flex-direction: column; gap: 10px; width: 340px; max-width: calc(100vw - 40px); } +.dash-root .ds-toast { display: flex; gap: 11px; padding: 13px 14px; border-radius: 13px; background: var(--panel); border: 1px solid var(--border-2); box-shadow: var(--shadow); animation: ds-slide-in 0.26s ease; } +.dash-root .ds-toast.tone-success { border-left: 3px solid var(--green); } .dash-root .ds-toast.tone-success > svg { color: var(--green); } +.dash-root .ds-toast.tone-info { border-left: 3px solid var(--blue); } .dash-root .ds-toast.tone-info > svg { color: #6f9bff; } +.dash-root .ds-toast.tone-error { border-left: 3px solid var(--red); } .dash-root .ds-toast.tone-error > svg { color: var(--red); } +.dash-root .ds-toast > svg { flex: 0 0 auto; margin-top: 1px; } +.dash-root .ds-toast-body { flex: 1; min-width: 0; } +.dash-root .ds-toast-title { font-size: 13px; font-weight: 600; } +.dash-root .ds-toast-desc { font-size: 12px; color: var(--muted); margin-top: 2px; } +.dash-root .ds-toast-x { background: none; border: 0; color: var(--faint); cursor: pointer; padding: 0; height: 18px; } + +/* ---- key/value list, callouts ---- */ +.dash-root .kv-list { display: flex; flex-direction: column; } +.dash-root .kv { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 11px 0; border-bottom: 1px solid var(--border); } +.dash-root .kv:last-child { border-bottom: 0; } +.dash-root .kv dt { font-size: 12.5px; color: var(--muted); } +.dash-root .kv dd { font-size: 13.5px; font-weight: 600; text-align: right; } +.dash-root .kv dd.masked { letter-spacing: 0.06em; color: var(--faint); font-weight: 500; } +.dash-root .kv dd.kv-edit { display: inline-flex; align-items: center; gap: 8px; } +.dash-root .kv-edit-btn { width: 26px; height: 26px; border-radius: 8px; border: 1px solid var(--border); background: var(--panel-2); color: var(--muted); display: grid; place-items: center; cursor: pointer; transition: 0.14s; } +.dash-root .kv-edit-btn:hover { color: var(--orange); border-color: var(--orange); } +.dash-root .kv-lock { display: inline-flex; align-items: center; gap: 4px; font-size: 10.5px; color: var(--faint); font-weight: 500; } +.dash-root .edit-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +@media (max-width: 720px) { .dash-root .edit-grid { grid-template-columns: 1fr; } } +.dash-root .callout { display: flex; gap: 11px; padding: 13px 14px; border-radius: 13px; margin-bottom: 16px; } +.dash-root .callout svg { flex: 0 0 auto; margin-top: 1px; } +.dash-root .callout strong { display: block; font-size: 13px; } .dash-root .callout span { font-size: 12px; color: var(--muted); } +.dash-root .callout.tone-green { background: color-mix(in srgb, var(--green) 10%, transparent); } .dash-root .callout.tone-green svg { color: var(--green); } +.dash-root .callout.tone-orange { background: color-mix(in srgb, var(--orange) 10%, transparent); } .dash-root .callout.tone-orange svg { color: var(--orange); } +.dash-root .callout.tone-red { background: color-mix(in srgb, var(--red) 10%, transparent); } .dash-root .callout.tone-red svg { color: var(--red); } + +/* ---- Profile hero ---- */ +.dash-root .profile-hero { position: relative; overflow: hidden; border-radius: 22px; padding: 26px; display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; margin-bottom: 4px; background: linear-gradient(120deg, color-mix(in srgb, var(--orange) 10%, var(--panel)) 0%, var(--panel) 55%); } +.dash-root .profile-hero-bg { position: absolute; inset: 0; background: radial-gradient(90% 140% at 100% -10%, color-mix(in srgb, var(--orange) 22%, transparent), transparent 50%), radial-gradient(70% 120% at -5% 110%, color-mix(in srgb, var(--blue) 14%, transparent), transparent 55%); pointer-events: none; } +.dash-root .profile-hero-main { display: flex; gap: 20px; align-items: center; position: relative; } +.dash-root .profile-hero-avatar { position: relative; border-radius: 24px; box-shadow: 0 14px 34px -12px color-mix(in srgb, var(--orange) 60%, transparent); } +.dash-root .profile-hero-cam { position: absolute; right: -4px; bottom: -4px; width: 28px; height: 28px; border-radius: 9px; border: 2px solid var(--panel); background: var(--orange); color: #1a1206; display: grid; place-items: center; cursor: pointer; } +.dash-root .profile-hero-name { display: flex; align-items: center; gap: 8px; font-size: 21px; font-weight: 800; letter-spacing: -0.02em; } +.dash-root .verified-badge { color: var(--green); } +.dash-root .profile-hero-role { font-size: 13px; color: var(--muted); margin-top: 2px; } +.dash-root .profile-hero-chips { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; } +.dash-root .hero-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; font-weight: 500; color: var(--text-2); padding: 5px 10px; border-radius: 8px; background: var(--panel-2); border: 1px solid var(--border); } +.dash-root .hero-chip svg { color: var(--faint); } +.dash-root .hero-chip.accent { color: var(--orange); border-color: color-mix(in srgb, var(--orange) 30%, transparent); } .dash-root .hero-chip.accent svg { color: var(--orange); } +.dash-root .profile-hero-stats { display: flex; gap: 0; position: relative; } +.dash-root .hero-stat { padding: 0 22px; border-left: 1px solid var(--border); } +.dash-root .hero-stat:first-child { padding-left: 0; border-left: 0; } +.dash-root .hero-stat:last-child { padding-right: 0; } +.dash-root .hero-stat-v { font-size: 20px; font-weight: 800; } +.dash-root .hero-stat-l { font-size: 11.5px; color: var(--muted); margin-top: 2px; } + +/* ---- reveal gate ---- */ +.dash-root .reveal-gate { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; padding: 14px; border-radius: 13px; border: 1px dashed var(--border-2); background: var(--panel-2); margin-bottom: 16px; } +.dash-root .reveal-gate-ic { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; color: var(--orange); background: color-mix(in srgb, var(--orange) 14%, transparent); flex: 0 0 auto; } +.dash-root .reveal-gate-txt { flex: 1; min-width: 140px; } .dash-root .reveal-gate-txt strong { display: block; font-size: 13px; } .dash-root .reveal-gate-txt span { font-size: 12px; color: var(--muted); } +.dash-root .reveal-gate-form { display: flex; gap: 8px; } .dash-root .reveal-gate-form .ds-input { width: 140px; } + +/* ---- KYC ---- */ +.dash-root .kyc-progress { margin-bottom: 18px; } +.dash-root .kyc-progress-bar { height: 9px; border-radius: 99px; background: var(--track); overflow: hidden; } +.dash-root .kyc-progress-bar > span { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--orange), var(--orange-2)); transition: width 0.5s cubic-bezier(0.2,0.7,0.2,1); animation: ds-bar 0.6s ease; } +.dash-root .kyc-progress-legend { display: flex; gap: 16px; margin-top: 10px; flex-wrap: wrap; } +.dash-root .kyc-leg { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--faint); } +.dash-root .kyc-leg.verified { color: var(--green); } .dash-root .kyc-leg.uploaded { color: #6f9bff; } .dash-root .kyc-leg.missing { color: var(--muted); } +.dash-root .kyc-slots { display: flex; flex-direction: column; gap: 12px; } +.dash-root .kyc-slot { display: flex; gap: 12px; padding: 14px; border-radius: 13px; border: 1px solid var(--border); background: var(--panel-2); } +.dash-root .kyc-slot.status-verified { border-color: color-mix(in srgb, var(--green) 40%, transparent); } +.dash-root .kyc-slot.status-rejected { border-color: color-mix(in srgb, var(--red) 40%, transparent); } +.dash-root .kyc-slot-ic { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; background: var(--panel-3); color: var(--muted); flex: 0 0 auto; } +.dash-root .kyc-slot-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 9px; } +.dash-root .kyc-slot-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; } +.dash-root .kyc-slot-title { font-size: 13.5px; font-weight: 600; } +.dash-root .kyc-slot-hint { font-size: 11.5px; color: var(--faint); } +.dash-root .kyc-drop { display: flex; align-items: center; justify-content: center; gap: 8px; height: 42px; border-radius: 11px; border: 1px dashed var(--border-2); background: var(--panel); color: var(--muted); font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; transition: 0.14s; } +.dash-root .kyc-drop:hover { border-color: var(--orange); color: var(--orange); } +.dash-root .kyc-file { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border-radius: 10px; background: var(--panel); border: 1px solid var(--border); } +.dash-root .kyc-file-name { font-size: 12.5px; font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.dash-root .kyc-file-note { font-size: 11px; color: var(--faint); } +.dash-root .rule-mini { list-style: none; padding: 0; margin: 0 0 16px; display: flex; flex-direction: column; gap: 9px; } +.dash-root .rule-mini li { display: flex; align-items: flex-start; gap: 8px; font-size: 12.5px; color: var(--text-2); } +.dash-root .rule-mini svg { color: var(--green); flex: 0 0 auto; margin-top: 2px; } +.dash-root .kyc-doc-pairs { border-top: 1px solid var(--border); padding-top: 14px; } +.dash-root .pair-row { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; font-size: 12.5px; color: var(--muted); } + +/* ---- password meter ---- */ +.dash-root .pw-meter { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } +.dash-root .pw-bars { display: flex; gap: 5px; flex: 1; } +.dash-root .pw-bars .lvl { flex: 1; height: 6px; border-radius: 99px; background: var(--track); transition: background 0.2s; } +.dash-root .pw-bars .lvl-weak { background: var(--red); } .dash-root .pw-bars .lvl-fair { background: var(--orange); } +.dash-root .pw-bars .lvl-good { background: var(--yellow); } .dash-root .pw-bars .lvl-strong { background: var(--green); } +.dash-root .pw-label { font-size: 12px; font-weight: 700; min-width: 48px; text-align: right; } +.dash-root .pw-checks { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } +.dash-root .pw-checks li { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--faint); } +.dash-root .pw-checks li svg { color: var(--faint); } +.dash-root .pw-checks li.ok { color: var(--text-2); } .dash-root .pw-checks li.ok svg { color: var(--green); } + +/* ---- setting rows ---- */ +.dash-root .setting-row { display: flex; align-items: center; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--border); } +.dash-root .setting-row:last-of-type { border-bottom: 0; } +.dash-root .setting-ic { width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center; background: var(--panel-2); color: var(--muted); flex: 0 0 auto; } +.dash-root .setting-txt { flex: 1; min-width: 0; } .dash-root .setting-title { font-size: 13.5px; font-weight: 600; } .dash-root .setting-desc { font-size: 12px; color: var(--muted); margin-top: 2px; } + +/* ---- notifications matrix ---- */ +.dash-root .notif-table { display: flex; flex-direction: column; } +.dash-root .notif-row { display: grid; grid-template-columns: 1.6fr repeat(4, 70px); align-items: center; gap: 8px; padding: 13px 20px; border-top: 1px solid var(--border); } +.dash-root .notif-row.notif-head { border-top: 0; } +.dash-root .notif-head span { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); } +.dash-root .notif-ch { display: inline-flex; align-items: center; gap: 5px; justify-content: center; } +.dash-root .notif-cat-name { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; } +.dash-root .notif-cat-desc { font-size: 11.5px; color: var(--muted); margin-top: 2px; } +.dash-root .lock-ic { color: var(--faint); } + +/* ---- contact-time windows ---- */ +.dash-root .time-windows { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 4px; } +.dash-root .time-win { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 11px 13px; border-radius: 11px; border: 1px solid var(--border-2); background: var(--panel-2); color: var(--muted); cursor: pointer; font-family: inherit; text-align: left; transition: 0.14s; position: relative; } +.dash-root .time-win svg { position: absolute; right: 11px; top: 11px; } +.dash-root .time-win.on { border-color: var(--orange); background: color-mix(in srgb, var(--orange) 9%, transparent); color: var(--text); } .dash-root .time-win.on svg { color: var(--orange); } +.dash-root .tw-label { font-size: 13px; font-weight: 600; } .dash-root .tw-range { font-size: 11.5px; color: var(--faint); } + +/* ---- destinations ---- */ +.dash-root .dest-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; } +.dash-root .dest-row { display: flex; align-items: center; gap: 11px; padding: 11px; border-radius: 11px; background: var(--panel-2); border: 1px solid var(--border); } +.dash-root .dest-ic { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--panel-3); color: var(--muted); flex: 0 0 auto; } +.dash-root .dest-txt { flex: 1; min-width: 0; } .dash-root .dest-label { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; } .dash-root .dest-val { font-size: 12px; color: var(--muted); margin-top: 1px; } + +/* ---- consent ---- */ +.dash-root .consent-list { display: flex; flex-direction: column; gap: 4px; } +.dash-root .consent-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border); } +.dash-root .consent-item:last-child { border-bottom: 0; } +.dash-root .consent-item.locked { opacity: 0.85; } +.dash-root .consent-label { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 600; } .dash-root .consent-desc { font-size: 11.5px; color: var(--muted); margin-top: 2px; } +.dash-root .consent-sub { margin-top: 12px; padding: 12px 14px; border-radius: 12px; background: var(--panel-2); border: 1px solid var(--border); animation: ds-rise 0.2s ease; } +.dash-root .consent-sub-row { display: flex; align-items: center; justify-content: space-between; padding: 7px 0; font-size: 12.5px; } + +/* ---- devices + timeline ---- */ +.dash-root .device-list { display: flex; flex-direction: column; gap: 11px; } +.dash-root .device-row { display: flex; align-items: center; gap: 13px; padding: 13px; border-radius: 13px; background: var(--panel-2); border: 1px solid var(--border); } +.dash-root .device-row.current { border-color: color-mix(in srgb, var(--green) 35%, transparent); } +.dash-root .device-ic { width: 42px; height: 42px; border-radius: 11px; display: grid; place-items: center; background: var(--panel-3); color: var(--muted); flex: 0 0 auto; } +.dash-root .device-txt { flex: 1; min-width: 0; } +.dash-root .device-name { display: flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 600; flex-wrap: wrap; } +.dash-root .device-meta { display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--muted); margin-top: 3px; } +.dash-root .timeline { display: flex; flex-direction: column; } +.dash-root .tl-item { display: flex; gap: 12px; padding-bottom: 16px; position: relative; } +.dash-root .tl-item:not(:last-child)::before { content: ""; position: absolute; left: 11px; top: 24px; bottom: 0; width: 2px; background: var(--border); } +.dash-root .tl-dot { width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center; flex: 0 0 auto; background: var(--panel-3); color: var(--muted); z-index: 1; } +.dash-root .tl-dot.ev-password-change { background: color-mix(in srgb, var(--orange) 18%, transparent); color: var(--orange); } +.dash-root .tl-dot.ev-new-device, .dash-root .tl-dot.ev-awaiting-customer { background: color-mix(in srgb, var(--red) 18%, transparent); color: var(--red); } +.dash-root .tl-dot.ev-resolved, .dash-root .tl-dot.ev-closed { background: color-mix(in srgb, var(--green) 18%, transparent); color: var(--green); } +.dash-root .tl-body { padding-top: 2px; } .dash-root .tl-title { font-size: 13px; font-weight: 600; } .dash-root .tl-meta { font-size: 11.5px; color: var(--muted); margin-top: 1px; } .dash-root .tl-time { font-size: 11px; color: var(--faint); margin-top: 2px; } + +/* ---- Support: channel cards ---- */ +.dash-root .channel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } +.dash-root .channel-card { text-align: left; position: relative; overflow: hidden; padding: 20px; border-radius: 18px; border: 1px solid var(--border); background: var(--card-grad); box-shadow: var(--card-hi); cursor: pointer; font-family: inherit; transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s; display: flex; flex-direction: column; gap: 6px; } +.dash-root .channel-card::before { content: ""; position: absolute; top: -45%; right: -20%; width: 190px; height: 190px; border-radius: 50%; background: radial-gradient(circle, color-mix(in srgb, var(--accent) 26%, transparent), transparent 70%); opacity: 0; transition: opacity 0.25s; pointer-events: none; } +.dash-root .channel-card:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--accent) 50%, transparent); box-shadow: var(--card-hi), 0 18px 42px -22px color-mix(in srgb, var(--accent) 75%, #000); } +.dash-root .channel-card:hover::before { opacity: 1; } +.dash-root .channel-ic { width: 46px; height: 46px; border-radius: 13px; display: grid; place-items: center; color: #fff; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 55%, #000)); box-shadow: 0 8px 18px -9px color-mix(in srgb, var(--accent) 80%, transparent); margin-bottom: 4px; } +.dash-root .channel-status { position: absolute; top: 18px; right: 18px; display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--muted); text-transform: capitalize; } +.dash-root .channel-title { font-size: 15px; font-weight: 700; } +.dash-root .channel-desc { font-size: 12.5px; color: var(--muted); } +.dash-root .channel-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; } +.dash-root .channel-meta { font-size: 11.5px; color: var(--faint); } +.dash-root .channel-cta { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 700; color: var(--accent); } + +/* ---- Support team ---- */ +.dash-root .team-list { display: flex; flex-direction: column; gap: 4px; } +.dash-root .team-row { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--border); } +.dash-root .team-row:last-child { border-bottom: 0; } +.dash-root .team-txt { flex: 1; min-width: 0; } .dash-root .team-name { font-size: 13.5px; font-weight: 600; } .dash-root .team-role { font-size: 12px; color: var(--muted); } .dash-root .team-team { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--faint); margin-top: 2px; } +.dash-root .team-rating { display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; font-weight: 700; color: var(--orange); } + +/* ---- Message Center ---- */ +.dash-root .msgctr { display: grid; grid-template-columns: 300px 1fr; height: 600px; } +.dash-root .msg-inbox { border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; } +.dash-root .msg-inbox-head { display: flex; align-items: center; gap: 8px; padding: 16px 16px 12px; font-size: 14px; font-weight: 700; } +.dash-root .msg-inbox-list { flex: 1; overflow-y: auto; padding: 0 8px 8px; } +.dash-root .msg-thread { width: 100%; display: flex; gap: 10px; padding: 11px; border-radius: 12px; border: 0; background: none; cursor: pointer; font-family: inherit; text-align: left; position: relative; transition: 0.12s; } +.dash-root .msg-thread:hover { background: var(--panel-2); } +.dash-root .msg-thread.active { background: var(--panel-2); } +.dash-root .msg-thread-txt { flex: 1; min-width: 0; } +.dash-root .msg-thread-top { display: flex; justify-content: space-between; gap: 6px; } .dash-root .msg-thread-name { font-size: 13px; font-weight: 600; } .dash-root .msg-thread-time { font-size: 10.5px; color: var(--faint); } +.dash-root .msg-thread-sub { font-size: 12px; color: var(--text-2); margin-top: 1px; } +.dash-root .msg-thread-prev { font-size: 11.5px; color: var(--muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.dash-root .typing-now { color: var(--orange); font-style: italic; } +.dash-root .msg-unread { position: absolute; right: 11px; bottom: 11px; background: var(--orange); color: #1a1206; font-size: 10px; font-weight: 700; min-width: 17px; height: 17px; border-radius: 99px; display: grid; place-items: center; padding: 0 4px; } +.dash-root .msg-pin { position: absolute; right: 12px; top: 12px; color: var(--faint); } +.dash-root .msg-thread-pane { display: flex; flex-direction: column; min-height: 0; } +.dash-root .msg-pane-head { display: flex; align-items: center; gap: 11px; padding: 14px 18px; border-bottom: 1px solid var(--border); } +.dash-root .msg-pane-id { flex: 1; min-width: 0; } .dash-root .msg-pane-name { font-size: 14px; font-weight: 700; } .dash-root .msg-pane-sub { font-size: 12px; color: var(--muted); } +.dash-root .msg-stream { flex: 1; overflow-y: auto; padding: 18px; display: flex; flex-direction: column; gap: 12px; } +.dash-root .msg-stream.sm { max-height: 320px; } +.dash-root .bubble-row { display: flex; gap: 8px; align-items: flex-end; max-width: 78%; animation: ds-rise 0.2s ease; } +.dash-root .bubble-row.me { align-self: flex-end; flex-direction: row-reverse; } +.dash-root .bubble { padding: 9px 13px; border-radius: 14px; background: var(--panel-2); border: 1px solid var(--border); } +.dash-root .bubble p { margin: 0; font-size: 13px; line-height: 1.45; } +.dash-root .bubble-time { display: block; font-size: 10px; color: var(--faint); margin-top: 4px; } +.dash-root .bubble-row.me .bubble { background: var(--orange); border-color: transparent; color: #1a1206; } .dash-root .bubble-row.me .bubble-time { color: color-mix(in srgb, #1a1206 60%, transparent); } +.dash-root .bubble.typing { display: flex; gap: 4px; padding: 13px; } +.dash-root .bubble.typing .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); animation: ds-bounce 1.2s infinite; } +.dash-root .bubble.typing .dot:nth-child(2) { animation-delay: 0.18s; } .dash-root .bubble.typing .dot:nth-child(3) { animation-delay: 0.36s; } +.dash-root .msg-compose { display: flex; align-items: center; gap: 9px; padding: 12px 16px; border-top: 1px solid var(--border); } +.dash-root .msg-compose .ds-input { flex: 1; } + +/* ---- New Ticket routing ---- */ +.dash-root .route-banner { display: flex; align-items: center; gap: 9px; padding: 11px 13px; border-radius: 11px; background: color-mix(in srgb, var(--blue) 9%, transparent); border: 1px solid color-mix(in srgb, var(--blue) 25%, transparent); font-size: 12.5px; margin-bottom: 14px; animation: ds-rise 0.2s ease; } +.dash-root .route-banner svg { color: #6f9bff; } .dash-root .route-banner > span { flex: 1; } +.dash-root .route-list { display: flex; flex-direction: column; gap: 9px; } +.dash-root .route-item { display: flex; align-items: center; gap: 9px; font-size: 12px; } +.dash-root .route-cat { flex: 1; color: var(--text-2); } .dash-root .route-item svg { color: var(--faint); } .dash-root .route-dept { color: var(--muted); font-weight: 600; } + +/* ---- My Tickets table ---- */ +.dash-root .ticket-table { display: flex; flex-direction: column; } +.dash-root .ticket-row { display: grid; grid-template-columns: 2.2fr 1.3fr 0.9fr 1fr 1.1fr; align-items: center; gap: 12px; padding: 14px 20px; border-top: 1px solid var(--border); background: none; border-left: 0; border-right: 0; border-bottom: 0; width: 100%; font-family: inherit; text-align: left; color: var(--text); cursor: pointer; transition: 0.12s; } +.dash-root button.ticket-row:hover { background: var(--panel-2); } +.dash-root .ticket-row.ticket-head { cursor: default; } +.dash-root .ticket-head span { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); } +.dash-root .t-subject { display: flex; flex-direction: column; gap: 2px; font-size: 13px; font-weight: 600; } +.dash-root .t-id { font-size: 11px; color: var(--orange); font-weight: 700; } +.dash-root .t-dept { font-size: 12.5px; color: var(--muted); } +.dash-root .t-updated { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); justify-content: space-between; } +.dash-root .ticket-modal-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; } +.dash-root .lifecycle { display: flex; align-items: center; margin-bottom: 20px; overflow-x: auto; padding-bottom: 4px; } +.dash-root .lc-step { display: inline-flex; align-items: center; gap: 7px; font-size: 11px; color: var(--faint); white-space: nowrap; } +.dash-root .lc-line { width: 26px; height: 2px; background: var(--border); } +.dash-root .lc-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--track); border: 2px solid var(--border-2); } +.dash-root .lc-step.done { color: var(--text-2); } .dash-root .lc-step.done .lc-dot { background: var(--green); border-color: var(--green); } +.dash-root .lc-step.current .lc-dot { background: var(--orange); border-color: var(--orange); box-shadow: 0 0 0 4px color-mix(in srgb, var(--orange) 22%, transparent); } + +/* ---- Help Center ---- */ +.dash-root .help-search { display: flex; align-items: center; gap: 10px; padding: 4px 16px; height: 52px; border-radius: 14px; border: 1px solid var(--border-2); background: var(--panel); margin-bottom: 18px; } +.dash-root .help-search > svg { color: var(--faint); } +.dash-root .help-topics { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; } +.dash-root .help-topic-card { padding: 0; overflow: hidden; --accent: var(--orange); } +.dash-root .help-topic-head { width: 100%; display: flex; align-items: center; gap: 12px; padding: 16px 18px; background: none; border: 0; cursor: pointer; font-family: inherit; text-align: left; } +.dash-root .help-topic-ic { width: 44px; height: 44px; border-radius: 13px; display: grid; place-items: center; color: #fff; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 55%, #000)); box-shadow: 0 8px 18px -10px color-mix(in srgb, var(--accent) 80%, transparent); flex: 0 0 auto; } +.dash-root .help-topic-txt { flex: 1; } .dash-root .help-topic-title { font-size: 14px; font-weight: 700; } .dash-root .help-topic-count { font-size: 11.5px; color: var(--muted); } +.dash-root .acc-chev { color: var(--faint); transition: transform 0.2s; } .dash-root .acc-chev.open { transform: rotate(180deg); } +.dash-root .help-acc-list { padding: 0 18px 10px; } +.dash-root .help-results { display: flex; flex-direction: column; } +.dash-root .help-acc { border-top: 1px solid var(--border); } +.dash-root .help-acc summary { display: flex; align-items: center; gap: 10px; padding: 13px 0; cursor: pointer; list-style: none; } +.dash-root .help-acc summary::-webkit-details-marker { display: none; } +.dash-root .help-q { flex: 1; font-size: 13px; font-weight: 600; } +.dash-root .help-acc summary .acc-chev { transition: transform 0.2s; } +.dash-root .help-acc[open] summary .acc-chev { transform: rotate(180deg); } +.dash-root .help-acc p { margin: 0 0 14px; font-size: 12.5px; color: var(--muted); line-height: 1.55; animation: ds-rise 0.2s ease; } + +/* ---- Live chat handshake ---- */ +.dash-root .chat-connecting { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 30px 0; } +.dash-root .chat-radar { position: relative; display: grid; place-items: center; } +.dash-root .radar-ring { position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--orange); animation: ds-radar 1.8s ease-out infinite; } +.dash-root .radar-ring.d2 { animation-delay: 0.9s; } +.dash-root .chat-connecting-txt { font-size: 14px; font-weight: 600; } .dash-root .chat-connecting-txt .dots span { animation: ds-bounce 1.2s infinite; display: inline-block; } .dash-root .chat-connecting-txt .dots span:nth-child(2){ animation-delay: 0.18s; } .dash-root .chat-connecting-txt .dots span:nth-child(3){ animation-delay: 0.36s; } +.dash-root .livechat-joined { display: flex; align-items: center; gap: 7px; justify-content: center; font-size: 12px; color: var(--muted); padding: 6px 0 10px; } + +/* ---- phone input / callback ---- */ +.dash-root .phone-input { display: flex; gap: 8px; } +.dash-root .phone-input .cc { width: 110px; flex: 0 0 auto; } +.dash-root .phone-input .ds-input { flex: 1; border: 1px solid var(--border-2); background: var(--panel-2); padding: 0 13px; } +.dash-root .cb-slots { display: flex; flex-direction: column; gap: 8px; } +.dash-root .cb-slot { display: flex; justify-content: space-between; padding: 11px 13px; border-radius: 11px; border: 1px solid var(--border-2); background: var(--panel-2); cursor: pointer; font-family: inherit; color: var(--text); transition: 0.14s; } +.dash-root .cb-slot.active { border-color: var(--orange); background: color-mix(in srgb, var(--orange) 9%, transparent); } +.dash-root .cb-slot-l { font-size: 13px; font-weight: 600; } .dash-root .cb-slot-r { font-size: 12px; color: var(--muted); } + +/* ---- Rules grid ---- */ +.dash-root .rules-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; } +.dash-root .rule-card { position: relative; overflow: hidden; --accent: var(--orange); transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s; } +.dash-root .rule-card::before { content: ""; position: absolute; top: -55%; right: -15%; width: 170px; height: 170px; border-radius: 50%; background: radial-gradient(circle, color-mix(in srgb, var(--accent) 18%, transparent), transparent 70%); pointer-events: none; } +.dash-root .rule-card:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--accent) 45%, transparent); box-shadow: 0 18px 42px -24px color-mix(in srgb, var(--accent) 70%, #000); } +.dash-root .rule-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; position: relative; } +.dash-root .rule-n { font-size: 44px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 45%, var(--text))); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } +.dash-root .rule-title { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; margin-bottom: 8px; } .dash-root .rule-title svg { color: var(--accent); flex: 0 0 auto; } +.dash-root .rule-detail { font-size: 12.5px; color: var(--muted); line-height: 1.55; margin: 0; } + +/* ---- responsive ---- */ +@media (max-width: 1180px) { + .dash-root .grid-side, .dash-root .consent-cols, .dash-root .rules-grid, .dash-root .channel-grid, .dash-root .help-topics, .dash-root .grid-2 { grid-template-columns: 1fr; } + .dash-root .msgctr { grid-template-columns: 1fr; height: auto; } + .dash-root .msg-inbox { border-right: 0; border-bottom: 1px solid var(--border); max-height: 280px; } +} +@media (max-width: 720px) { + .dash-root .notif-row { grid-template-columns: 1fr; gap: 10px; } + .dash-root .notif-head { display: none; } + .dash-root .ticket-row { grid-template-columns: 1fr; gap: 6px; } + .dash-root .ticket-head { display: none; } + .dash-root .pw-checks, .dash-root .time-windows { grid-template-columns: 1fr; } + .dash-root .profile-hero-stats { padding-left: 0; border-top: 1px solid var(--border); padding-top: 14px; width: 100%; } +} diff --git a/src/components/dashboard/account-data.ts b/src/components/dashboard/account-data.ts new file mode 100644 index 0000000..b42bf9f --- /dev/null +++ b/src/components/dashboard/account-data.ts @@ -0,0 +1,566 @@ +// ============================================================ +// LynkedUp Pro — Account / Profile / Support mock data (D). +// Everything here is a client-side mock so the screens are +// 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. +// ============================================================ + +/* ---------------------------------------------------------- */ +/* helpers */ +/* ---------------------------------------------------------- */ + +export function maskEmail(email: string): string { + const [user = "", domain = ""] = email.split("@"); + if (!domain) return email; + if (user.length <= 2) return `${user[0] ?? ""}••@${domain}`; + return `${user[0]}${"•".repeat(Math.max(4, user.length - 2))}${user[user.length - 1]}@${domain}`; +} +export function maskPhone(phone: string): string { + const tail = phone.replace(/\D/g, "").slice(-2); + return `+91 ••••• ••${tail}`; +} + +/* ---------------------------------------------------------- */ +/* USER — the signed-in account holder */ +/* ---------------------------------------------------------- */ + +export type RevealField = { label: string; masked: string; full: string }; + +export type User = { + id: string; + name: string; + firstName: string; + lastName: string; + initials: string; + role: string; + avatarGradient: string; + // property / allotment + allotmentNo: string; + plot: string; // verify-to-reveal key + sector: string; + pocket: string; + category: string; + size: string; + // allottee vs account holder + isAllottee: boolean; + relationship: string; // "Self" when account holder IS allottee + allotteeName: string; + // contact (masked until verified) + email: RevealField; + mobile: RevealField; + whatsapp: RevealField; + pan: RevealField; + aadhaar: RevealField; + memberSince: string; + status: "active" | "suspended"; +}; + +export const user: User = { + id: "usr_LUP_00181", + name: "Rajesh Kumar Sharma", + firstName: "Rajesh", + lastName: "Sharma", + initials: "RS", + role: "Property Owner", + avatarGradient: "linear-gradient(135deg,#fda913,#fd6d13)", + allotmentNo: "YEA-654321", + plot: "181", + sector: "Sector 18", + pocket: "Pocket B", + category: "Residential", + 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" }, + 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" }, + aadhaar: { label: "Aadhaar", masked: "•••• •••• 9012", full: "4321 8765 9012" }, + memberSince: "March 2021", + status: "active", +}; + +/* ---------------------------------------------------------- */ +/* COUNTRY CODES */ +/* ---------------------------------------------------------- */ + +export type CountryCode = { code: string; flag: string; name: string; digits: number; example: string }; +export const countryCodes: CountryCode[] = [ + { code: "+91", flag: "🇮🇳", name: "India", digits: 10, example: "98765 43210" }, + { code: "+1", flag: "🇺🇸", name: "United States", digits: 10, example: "201 555 0123" }, + { code: "+44", flag: "🇬🇧", name: "United Kingdom", digits: 10, example: "7400 123456" }, + { code: "+971", flag: "🇦🇪", name: "UAE", digits: 9, example: "50 123 4567" }, + { code: "+65", flag: "🇸🇬", name: "Singapore", digits: 8, example: "8123 4567" }, + { code: "+61", flag: "🇦🇺", name: "Australia", digits: 9, example: "412 345 678" }, +]; + +/* ---------------------------------------------------------- */ +/* KYC */ +/* ---------------------------------------------------------- */ + +export type KycUse = "id" | "address" | "any"; +export type KycDocType = { + id: string; + label: string; + use: KycUse; // what this document can prove + hint: string; + formats: string; // accepted attachment formats + maxMb: number; +}; + +// Note rule: an ID document and an ADDRESS document must be two +// DIFFERENT document types. A passport (use:"any") may satisfy +// either slot but not both at once. +export const kycDocTypes: KycDocType[] = [ + { id: "aadhaar", label: "Aadhaar Card", use: "any", hint: "Front & back, masked first 8 digits", formats: "PDF, JPG, PNG", maxMb: 5 }, + { id: "pan", label: "PAN Card", use: "id", hint: "Clear photo of the front", formats: "PDF, JPG, PNG", maxMb: 5 }, + { id: "passport", label: "Passport", use: "any", hint: "Photo page, must be valid", formats: "PDF, JPG, PNG", maxMb: 5 }, + { id: "voter", label: "Voter ID", use: "id", hint: "Front & back", formats: "PDF, JPG, PNG", maxMb: 5 }, + { id: "dl", label: "Driving Licence", use: "id", hint: "Front & back, not expired", formats: "PDF, JPG, PNG", maxMb: 5 }, + { id: "utility", label: "Utility Bill", use: "address", hint: "Electricity / water, < 3 months old", formats: "PDF, JPG, PNG", maxMb: 8 }, + { id: "bank", label: "Bank Statement", use: "address", hint: "First page with address, < 3 months", formats: "PDF", maxMb: 8 }, + { id: "rent", label: "Registered Rent Agreement", use: "address", hint: "All pages, registered copy", formats: "PDF", maxMb: 10 }, +]; + +export type KycSlot = { + key: "photo" | "id" | "address"; + title: string; + required: boolean; + docId: string | null; // chosen kycDocTypes id + fileName: string | null; + status: "missing" | "uploaded" | "verified" | "rejected"; + note?: string; +}; + +export const kyc: { completion: number; slots: KycSlot[] } = { + completion: 33, + slots: [ + { key: "photo", title: "Live Photo / Selfie", required: true, docId: null, fileName: "selfie_rajesh.jpg", status: "verified", note: "Face match passed" }, + { key: "id", title: "Identity Proof", required: true, docId: "pan", fileName: "pan_front.pdf", status: "uploaded", note: "Under review" }, + { key: "address", title: "Address Proof", required: true, docId: null, fileName: null, status: "missing" }, + ], +}; + +/* ---------------------------------------------------------- */ +/* NOTIFICATIONS — channels + per-destination matrix */ +/* ---------------------------------------------------------- */ + +export type NotifCategory = { + id: string; + label: string; + desc: string; + locked?: boolean; // statutory / security — cannot disable + email: boolean; + sms: boolean; + whatsapp: boolean; + push: boolean; +}; + +export const contactPrefs: NotifCategory[] = [ + { id: "security", label: "Security & sign-in alerts", desc: "New device, password and 2FA changes", locked: true, email: true, sms: true, whatsapp: false, push: true }, + { id: "kyc", label: "KYC & verification", desc: "Document status and re-verification requests", email: true, sms: true, whatsapp: true, push: true }, + { id: "tickets", label: "Support tickets", desc: "Replies, status changes and resolutions", email: true, sms: false, whatsapp: true, push: true }, + { id: "billing", label: "Billing & dues", desc: "Invoices, payment reminders and receipts", email: true, sms: true, whatsapp: false, push: false }, + { id: "inspections", label: "Inspections & schedule", desc: "Upcoming visits and rescheduling", email: true, sms: false, whatsapp: true, push: true }, + { id: "marketing", label: "Offers & product news", desc: "Promotions, surveys and newsletters", email: false, sms: false, whatsapp: false, push: false }, +]; + +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: "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 }, +]; + +export const contactTimezones = [ + "Asia/Kolkata (IST · GMT+5:30)", + "Asia/Dubai (GST · GMT+4:00)", + "Europe/London (GMT/BST)", + "America/New_York (ET)", + "Asia/Singapore (SGT · GMT+8:00)", + "Australia/Sydney (AET)", +]; + +// Contact-time windows the user is willing to be reached in. +export type ContactTime = { id: string; label: string; range: string; enabled: boolean }; +export const contactTimes: ContactTime[] = [ + { id: "morning", label: "Morning", range: "08:00 – 12:00", enabled: true }, + { id: "afternoon", label: "Afternoon", range: "12:00 – 16:00", enabled: true }, + { id: "evening", label: "Evening", range: "16:00 – 20:00", enabled: true }, + { id: "night", label: "Night", range: "20:00 – 22:00", enabled: false }, +]; + +/* ---------------------------------------------------------- */ +/* SECURITY — login activity / devices */ +/* ---------------------------------------------------------- */ + +export type LoginActivity = { + id: string; + device: string; + os: string; + kind: "desktop" | "mobile" | "tablet"; + browser: string; + location: string; + ip: string; + current: boolean; + trusted: boolean; + lastActive: string; + event: "sign-in" | "password-change" | "2fa-enabled" | "new-device" | "sign-out"; +}; + +export const loginActivity: LoginActivity[] = [ + { id: "d1", device: "MacBook Pro 14\"", os: "macOS 15.2", kind: "desktop", browser: "Chrome 132", location: "Noida, IN", ip: "49.36.x.x", current: true, trusted: true, lastActive: "Active now", event: "sign-in" }, + { id: "d2", device: "iPhone 15", os: "iOS 18.3", kind: "mobile", browser: "Safari", location: "Noida, IN", ip: "49.36.x.x", current: false, trusted: true, lastActive: "2 hours ago", event: "sign-in" }, + { id: "d3", device: "Windows PC", os: "Windows 11", kind: "desktop", browser: "Edge 131", location: "Gurugram, IN", ip: "182.71.x.x", current: false, trusted: false, lastActive: "Yesterday, 9:14 PM", event: "new-device" }, + { id: "d4", device: "iPad Air", os: "iPadOS 18", kind: "tablet", browser: "Safari", location: "Dubai, AE", ip: "94.205.x.x", current: false, trusted: false, lastActive: "3 days ago", event: "sign-in" }, + { id: "d5", device: "MacBook Pro 14\"", os: "macOS 15.2", kind: "desktop", browser: "Chrome 132", location: "Noida, IN", ip: "49.36.x.x", current: false, trusted: true, lastActive: "5 days ago", event: "password-change" }, +]; + +/* ---------------------------------------------------------- */ +/* PRIVACY & CONSENT */ +/* ---------------------------------------------------------- */ + +export type ConsentItem = { id: string; label: string; desc: string; granted: boolean; locked?: boolean }; +export type ConsentGroup = { id: string; title: string; items: ConsentItem[] }; + +export const consentGroups: ConsentGroup[] = [ + { + id: "essential", + title: "Essential & statutory", + items: [ + { id: "tos", label: "Terms of Use & service agreement", desc: "Required to operate your account.", granted: true, locked: true }, + { id: "kyc-process", label: "Identity verification processing", desc: "Process KYC documents to meet regulatory obligations.", granted: true, locked: true }, + ], + }, + { + id: "operational", + title: "Operational data sharing", + items: [ + { id: "authority", label: "Share records with the authority", desc: "Sync allotment & inspection records with the development authority.", granted: true }, + { id: "processors", label: "Trusted processors", desc: "Allow vetted vendors to process data on our behalf.", granted: true }, + ], + }, + { + id: "marketing", + title: "Marketing & analytics", + items: [ + { id: "analytics", label: "Product analytics", desc: "Help improve the portal with anonymous usage data.", granted: true }, + { id: "personalize", label: "Personalised recommendations", desc: "Tailor content based on your activity.", granted: false }, + { id: "promos", label: "Promotional messages", desc: "Offers, surveys and partner news.", granted: false }, + ], + }, +]; + +/* ---------------------------------------------------------- */ +/* SUPPORT — channels, team, tickets, threads, help */ +/* ---------------------------------------------------------- */ + +export type SupportChannel = { + id: string; + title: string; + desc: string; + icon: string; // Icon name + accent: string; // token var + meta: string; // SLA / availability + action: string; // CTA label + status: "online" | "busy" | "offline"; +}; + +export const supportChannels: SupportChannel[] = [ + { id: "chat", title: "Live Chat", desc: "Instant help from a support agent", icon: "chat", accent: "var(--orange)", meta: "Avg. wait < 2 min", action: "Start chat", status: "online" }, + { id: "ticket", title: "Raise a Ticket", desc: "Track a request end-to-end", icon: "ticket", accent: "var(--blue)", meta: "First reply in 4 hrs", action: "New ticket", status: "online" }, + { id: "callback", title: "Request Callback", desc: "We'll call you in your time window", icon: "phone", accent: "var(--green)", meta: "Within working hours", action: "Schedule", status: "online" }, + { id: "email", title: "Email Support", desc: "care@lynkeduppro.com", icon: "mail", accent: "var(--purple)", meta: "Reply in 1 business day", action: "Send email", status: "online" }, + { id: "help", title: "Help Center", desc: "Guides, FAQs and how-tos", icon: "book", accent: "var(--cyan)", meta: "120+ articles", action: "Browse", status: "online" }, +]; + +export type SupportAgent = { + id: string; + name: string; + initials: string; + role: string; + team: string; + gradient: string; + status: "online" | "away" | "offline"; + rating: number; +}; + +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 }, +]; + +// Category → department routing + attachment rules for the New Ticket form. +export type TicketCategory = { + id: string; + label: string; + department: string; + sla: string; + requiresAttachment: boolean; + attachmentNote: string; +}; + +export const ticketCategories: TicketCategory[] = [ + { id: "kyc", label: "KYC / Document verification", department: "Accounts & KYC", sla: "4 business hours", requiresAttachment: true, attachmentNote: "Attach the document in question (PDF/JPG/PNG, ≤ 8 MB)." }, + { id: "login", label: "Login / Account access", department: "Portal & Login", sla: "2 business hours", requiresAttachment: false, attachmentNote: "Screenshots optional (PNG/JPG, ≤ 5 MB)." }, + { id: "billing", label: "Billing & payments", department: "Billing", sla: "1 business day", requiresAttachment: true, attachmentNote: "Attach invoice or receipt (PDF, ≤ 8 MB)." }, + { id: "allotment", label: "Allotment / Property records", department: "Accounts & KYC", sla: "1 business day", requiresAttachment: false, attachmentNote: "Allotment letter optional (PDF, ≤ 10 MB)." }, + { id: "inspection", label: "Inspection / Site visit", department: "Inspections", sla: "1 business day", requiresAttachment: false, attachmentNote: "Site photos optional (JPG/PNG, ≤ 10 MB each)." }, + { id: "other", label: "Something else", department: "General Support", sla: "1 business day", requiresAttachment: false, attachmentNote: "Attachments optional (≤ 8 MB)." }, +]; + +export type TicketStatus = "open" | "in-progress" | "awaiting-customer" | "resolved" | "closed"; + +export type TicketEvent = { status: TicketStatus | "created"; label: string; at: string; by: string }; +export type Ticket = { + id: string; + subject: string; + category: string; // TicketCategory label + department: string; + priority: "low" | "medium" | "high" | "urgent"; + status: TicketStatus; + createdAt: string; + updatedAt: string; + agentId: string | null; + timeline: TicketEvent[]; +}; + +export const tickets: Ticket[] = [ + { + id: "LUP-48217", + subject: "Aadhaar re-upload not reflecting", + category: "KYC / Document verification", + department: "Accounts & KYC", + priority: "high", + status: "in-progress", + createdAt: "24 Jun 2026", + updatedAt: "28 Jun 2026", + agentId: "a1", + 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: "You re-uploaded the document", at: "28 Jun, 04:22 PM", by: "You" }, + ], + }, + { + id: "LUP-47980", + subject: "Unable to receive login OTP on mobile", + category: "Login / Account access", + department: "Portal & Login", + priority: "urgent", + status: "resolved", + createdAt: "18 Jun 2026", + updatedAt: "19 Jun 2026", + 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" }, + ], + }, + { + id: "LUP-47512", + subject: "Duplicate maintenance invoice for June", + category: "Billing & payments", + department: "Billing", + priority: "medium", + status: "awaiting-customer", + createdAt: "12 Jun 2026", + updatedAt: "26 Jun 2026", + 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" }, + ], + }, + { + id: "LUP-46330", + subject: "Reschedule roof inspection visit", + category: "Inspection / Site visit", + department: "Inspections", + priority: "low", + status: "closed", + createdAt: "02 Jun 2026", + updatedAt: "06 Jun 2026", + 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: "closed", label: "Inspection completed", at: "06 Jun, 01:30 PM", by: "System" }, + ], + }, +]; + +// Message Center — inbox threads + messages (with a typing flag). +export type ChatMsg = { id: string; from: "me" | "agent"; text: string; at: string }; +export type SupportThread = { + id: string; + agentId: string; + subject: string; + preview: string; + unread: number; + pinned: boolean; + typing: boolean; + updatedAt: string; + messages: ChatMsg[]; +}; + +export const supportThreads: SupportThread[] = [ + { + id: "t1", + agentId: "a1", + subject: "KYC — Aadhaar re-upload", + preview: "Thanks, I can see the new scan now…", + unread: 2, + pinned: true, + 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: "m3", from: "agent", text: "Thanks, I can see the new scan now. Reviewing it with the verification team.", at: "11:43 AM" }, + ], + }, + { + id: "t2", + agentId: "a2", + subject: "Billing — duplicate invoice", + preview: "Could you share the bank reference number?", + unread: 0, + pinned: false, + typing: false, + updatedAt: "Yesterday", + messages: [ + { id: "m1", from: "agent", text: "Hello! I'm reviewing the June billing run for the duplicate charge.", at: "3:30 PM" }, + { id: "m2", from: "agent", text: "Could you share the bank reference number for the second debit?", at: "3:32 PM" }, + ], + }, + { + id: "t3", + agentId: "a3", + subject: "Login — OTP not received", + preview: "Glad it's working now. Closing this out.", + unread: 0, + pinned: false, + typing: false, + updatedAt: "19 Jun", + messages: [ + { id: "m1", from: "me", text: "I stopped getting the OTP on my registered mobile last night.", at: "8:40 PM" }, + { id: "m2", from: "agent", text: "There was a gateway issue in your region — fixed now. Please retry.", at: "10:14 AM" }, + { id: "m3", from: "me", text: "Working now, thank you!", at: "10:20 AM" }, + { id: "m4", from: "agent", text: "Glad it's working now. Closing this out.", at: "10:21 AM" }, + ], + }, +]; + +/* ---------------------------------------------------------- */ +/* HELP CENTER */ +/* ---------------------------------------------------------- */ + +export type HelpArticle = { q: string; a: string }; +export type HelpTopic = { id: string; title: string; icon: string; accent: string; count: number; articles: HelpArticle[] }; + +export const helpTopics: HelpTopic[] = [ + { + id: "getting-started", + title: "Getting started", + icon: "rocket", + 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: "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." }, + ], + }, + { + id: "kyc", + title: "KYC & verification", + icon: "shield", + accent: "var(--blue)", + count: 4, + articles: [ + { q: "Which documents do I need for KYC?", a: "A live photo (mandatory), one identity proof and one address proof. The identity and address documents must be two different document types." }, + { q: "Can I use Aadhaar for both ID and address?", a: "No. ID and address proofs must be different document types. Aadhaar can fill either slot, but not both at the same time." }, + { q: "How long does verification take?", a: "Most documents are reviewed within 4 business hours. You'll be notified on your registered channels." }, + { q: "My document was rejected — what now?", a: "Open the rejected slot, read the reason, and re-upload a clearer copy. A support ticket is created automatically for high-priority cases." }, + ], + }, + { + id: "security", + title: "Login & security", + icon: "lock", + accent: "var(--purple)", + count: 4, + articles: [ + { q: "How do I enable two-factor authentication?", a: "Profile → Security → Two-factor. Enabling 2FA requires both your password and a one-time code for confirmation." }, + { q: "I'm not receiving my OTP", a: "OTPs are sent to your registered mobile. Check signal and spam filters; if it persists, raise a ticket under Login / Account access." }, + { q: "What is the password policy?", a: "At least 8 characters with upper & lower case, a number and a special character — and no common patterns like ‘1234’ or ‘password’." }, + { q: "How do I sign out other devices?", a: "Profile → Devices shows every active session. Use ‘Sign out’ on any device, or ‘Sign out all other devices’." }, + ], + }, + { + id: "billing", + title: "Billing & payments", + icon: "card", + accent: "var(--green)", + count: 3, + articles: [ + { q: "Where can I download invoices?", a: "Invoices are emailed to your primary email and available under Billing. Receipts are issued within minutes of payment." }, + { q: "I was charged twice", a: "Raise a Billing ticket and attach the invoice/receipt. Keep your bank reference handy to speed up the reconciliation." }, + { q: "How do I change my billing email?", a: "Add a destination under Notifications and mark it primary for the Billing category in the per-destination matrix." }, + ], + }, +]; + +/* ---------------------------------------------------------- */ +/* RULES — the 1–13 business-rules checklist */ +/* ---------------------------------------------------------- */ + +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: 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)" }, + { n: 5, title: "Live photo is mandatory", detail: "A live photo / selfie is required for KYC completion in addition to the ID and address proofs. Completion percentage never reaches 100% without it.", tag: "KYC", tagColor: "var(--purple)" }, + { n: 6, title: "Password policy", detail: "Minimum 8 characters with upper & lower case, a number and a special character. Common patterns like ‘1234’, ‘qwerty’ or ‘password’ are rejected.", tag: "Security", tagColor: "var(--blue)" }, + { n: 7, title: "2FA requires password + OTP", detail: "Turning two-factor authentication on or off requires re-entering the password and confirming a one-time code. Authenticator apps and login alerts build on this.", tag: "Security", tagColor: "var(--blue)" }, + { n: 8, title: "Ticket lifecycle", detail: "Tickets move Open → In Progress → Awaiting Customer → Resolved → Closed. Every transition is timestamped and attributed in the status timeline.", tag: "Support", tagColor: "var(--green)" }, + { n: 9, title: "Category → department routing", detail: "Each ticket category auto-routes to the right department (e.g. KYC → Accounts & KYC) with its own SLA shown before submission.", tag: "Support", tagColor: "var(--green)" }, + { n: 10, title: "Attachment rules", detail: "Some categories require an attachment; each defines accepted formats and a maximum file size. The form blocks submission until required attachments are present.", tag: "Support", tagColor: "var(--green)" }, + { n: 11, title: "Contact-time windows + timezone", detail: "Callbacks and proactive messages are only sent inside the contact-time windows the user enables, interpreted in their selected timezone.", tag: "Notifications", tagColor: "var(--cyan)" }, + { n: 12, title: "Statutory consent is locked", detail: "Essential and statutory consents (Terms of Use, KYC processing) cannot be switched off while the account is active. Marketing consents and sub-toggles are fully optional.", tag: "Privacy", tagColor: "var(--red)" }, + { n: 13, title: "New-device login alerts", detail: "Signing in from an untrusted device raises a security alert on the registered channels and lists the device under Devices for review or sign-out.", tag: "Security", tagColor: "var(--blue)" }, +]; + +/* ---------------------------------------------------------- */ +/* password strength (shared with Security tab) */ +/* ---------------------------------------------------------- */ + +export type Strength = { score: number; label: "Weak" | "Fair" | "Good" | "Strong"; checks: { ok: boolean; label: string }[] }; +export function passwordStrength(pw: string): Strength { + const checks = [ + { ok: pw.length >= 8, label: "At least 8 characters" }, + { ok: /[a-z]/.test(pw) && /[A-Z]/.test(pw), label: "Upper & lower case" }, + { ok: /\d/.test(pw), label: "A number" }, + { ok: /[^A-Za-z0-9]/.test(pw), label: "A special character" }, + ]; + const passed = checks.filter((c) => c.ok).length; + const weakList = ["1234", "abcd", "password", "qwerty", "0000"]; + const isWeak = weakList.some((w) => pw.toLowerCase().includes(w)); + const score = isWeak ? Math.min(passed, 1) : passed; + const label = score <= 1 ? "Weak" : score === 2 ? "Fair" : score === 3 ? "Good" : "Strong"; + return { score, label, checks }; +} diff --git a/src/components/dashboard/charts.tsx b/src/components/dashboard/charts.tsx deleted file mode 100644 index 40b2171..0000000 --- a/src/components/dashboard/charts.tsx +++ /dev/null @@ -1,180 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -/* ---- vertical bar chart (HTML/CSS, animated heights) ---- */ -export function BarsChart({ - labels, - series, - max, - yTicks, - height = 132, - highlight, - barWidth = 7, -}: { - labels: string[]; - series: { color: string; values: number[] }[]; - max: number; - yTicks?: string[]; - height?: number; - highlight?: number; // single-series: index to highlight, others muted - barWidth?: number; -}) { - const [on, setOn] = useState(false); - useEffect(() => { - const t = setTimeout(() => setOn(true), 40); - return () => clearTimeout(t); - }, []); - - return ( -
- {yTicks && ( -
- {yTicks.map((t) => {t})} -
- )} -
- {labels.map((lab, i) => ( -
-
- {series.map((s, si) => { - const muted = highlight !== undefined && i !== highlight; - return ( - - ); - })} -
- {lab} -
- ))} -
-
- ); -} - -/* ---- mini sparkline bars (stat cards) ---- */ -export function Spark({ values, color, height = 40, width = 8 }: { values: number[]; color: string; height?: number; width?: number }) { - const [on, setOn] = useState(false); - useEffect(() => { const t = setTimeout(() => setOn(true), 40); return () => clearTimeout(t); }, []); - const max = Math.max(...values, 1); - return ( -
- {values.map((v, i) => ( - - ))} -
- ); -} - -/* ---- donut chart ---- */ -export function Donut({ - segments, total, label, size = 150, thickness = 18, -}: { - segments: { value: number; color: string }[]; - total: string; - label: string; - size?: number; - thickness?: number; -}) { - const [on, setOn] = useState(false); - useEffect(() => { const t = setTimeout(() => setOn(true), 60); return () => clearTimeout(t); }, []); - const r = (size - thickness) / 2; - const c = 2 * Math.PI * r; - const sum = segments.reduce((a, s) => a + s.value, 0) || 1; - let offset = 0; - return ( -
- - - {segments.map((s, i) => { - const frac = s.value / sum; - const len = on ? frac * c : 0; - const dash = `${len} ${c - len}`; - const el = ( - - ); - offset += frac; - return el; - })} - -
-
{total}
-
{label}
-
-
- ); -} - -/* ---- semicircle gauge ---- */ -export function Gauge({ - value, max = 100, color, size = 150, label, sub, -}: { - value: number; max?: number; color: string; size?: number; label?: string; sub?: string; -}) { - const [on, setOn] = useState(false); - useEffect(() => { const t = setTimeout(() => setOn(true), 60); return () => clearTimeout(t); }, []); - const sw = 14; - const r = (size - sw) / 2; - const cx = size / 2, cy = size / 2; - const semi = Math.PI * r; // half circumference - const frac = Math.min(1, value / max); - const len = on ? frac * semi : 0; - return ( -
- - - - - {(label || sub) && ( -
- {label &&
{label}
} - {sub &&
{sub}
} -
- )} -
- ); -} - -/* ---- progress bar ---- */ -export function Progress({ value, color, track }: { value: number; color: string; track?: boolean }) { - const [on, setOn] = useState(false); - useEffect(() => { const t = setTimeout(() => setOn(true), 50); return () => clearTimeout(t); }, []); - return ( -
- -
- ); -} - -/* ---- horizontal segmented bar (pipeline) ---- */ -export function HBar({ value, max, color }: { value: number; max: number; color: string }) { - const [on, setOn] = useState(false); - useEffect(() => { const t = setTimeout(() => setOn(true), 50); return () => clearTimeout(t); }, []); - return ( -
- -
- ); -} diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index e87361d..6aeb789 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -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 = { + 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 (
- +
- - {/* ===== Statistics ===== */} -

Statistics

-
-
- } ic={C.orange} tag="GROSS PROFIT $114K" value="$626K" label="Revenue" spark={sparks.revenue} sc={C.orange} /> - } ic={C.red} tag="REQUIRES ACTION" value="20k" label="Hot Leads" spark={sparks.leads} sc={C.purple} /> - } ic={C.cyan} tag="NEXT 7 DAYS" value="70k" label="Upcoming Meets" spark={sparks.meets} sc={C.cyan} /> -
-
- } ic={C.cyan} value="$311.3k" label="Collected to Date" sub="32 payments" pill="pill-cyan" /> - } ic={C.orange} value="$325.9k" label="Outstanding AR" sub="25 pending" pill="pill-orange" /> - } ic={C.purple} value="$203.7k" label="Pending Payouts" sub="20 pending" pill="pill-blue" /> - } ic={C.blue} value="$432.4k" label="YTD Vendor Spend" sub="57 invoices" pill="pill-blue" /> -
-
- - {/* ===== Pipeline / Win / Storm ===== */} -
-
-

Open Pipeline

27 leads
-
$396K
- {[["New", 6], ["Contacted", 7], ["Appointment", 7], ["Estimate", 3]].map(([l, n]) => ( -
- {l} - - {n} -
- ))} -
-
-

Win Rate

-
94%
- -
- - - -
-
-
-

Storm-Attributed Revenue

19 Storm Leads
-
24%
- -
$56K of $626K
-
-
- - {/* ===== Roof / Risk / Weather ===== */} -
-
-

Roof Condition

Territory health snapshot
-
- -
-
- - -
-
- -
-

Risk Index

Storm Impact Probability
-
- -
-
- - -
-
- -
-
-
-
80°F
-
Plano, US
-
-
-
Updated: 02:48 PM
-
- 10 mph - 82% -
-
-
-
- {[["12 PM", 94], ["5 PM", 79], ["8 PM", 82], ["10 PM", 90], ["1 AM", 92], ["5 AM", 79]].map(([t, te]) => ( -
-
{t}
- -
{te}°
-
- ))} -
-
-
- - {/* ===== P&L Summary ===== */} -

P&L Summary

-
- - - - -
- - {/* ===== Revenue Potential / Top Reps ===== */} -
-
-
-

Revenue Potential

By neighborhood rating
- live -
- -
-
-

Top Sales Reps

- {[["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) => ( -
- 0{i + 1} - {(nm as string).split(" ").map((x) => x[0]).join("")} -
-
{nm}
-
{em}
-
- $112,500 -
- ))} -
-
- + + {active === "profile" && } + {active === "support" && } + {active === "rules" && } +
); } - -/* ---- 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 ( -
-
- {icon} - {tag} -
-
-
-
{value}
-
{label}
-
- -
-
- ); -} - -function MiniStat({ icon, ic, value, label, sub, pill }: { icon: React.ReactNode; ic: string; value: string; label: string; sub: string; pill: string }) { - return ( -
-
- {icon} - {sub} -
-
{value}
-
{label}
-
- ); -} - -function PnlCard({ title, value, labels, yk, series }: { title: string; value: string; labels: string[]; yk: string[]; series: { color: string; values: number[] }[] }) { - return ( -
-

{title}

-
{value}
- -
- ); -} - -function Legend({ dot, text }: { dot: string; text: string }) { - return {text}; -} - -function Readout({ big, small, color }: { big: string; small: string; color?: string }) { - return ( -
-
{big}
-
{small}
-
- ); -} diff --git a/src/components/dashboard/figicon.tsx b/src/components/dashboard/figicon.tsx deleted file mode 100644 index ef9b5c6..0000000 --- a/src/components/dashboard/figicon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -import { Sparkles } from "lucide-react"; -import { figmaIcons } from "./figma-icons"; - -/** Renders a Figma-exported icon (recolored to currentColor) or a fallback. */ -export function FigIcon({ name, size = 18 }: { name: string; size?: number }) { - const svg = figmaIcons[name]; - if (!svg) return ; - return ( - - ); -} diff --git a/src/components/dashboard/figma-icons.ts b/src/components/dashboard/figma-icons.ts deleted file mode 100644 index 705de77..0000000 --- a/src/components/dashboard/figma-icons.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Auto-generated from Figma (Linkedup-Web). Colors normalized to currentColor. -export const figmaIcons: Record = { - "dashboard": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "dispatch": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "estimates": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "home": "\n\n\n", - "i_ar": "\n\n", - "i_bell": "\n\n\n\n\n\n\n\n\n\n\n\n\n", - "i_collected": "\n\n\n\n\n\n\n\n\n", - "i_hotleads": "\n\n", - "i_humidity": "\n\n\n", - "i_meets": "\n\n", - "i_payouts": "\n\n", - "i_revenue": "\n\n\n", - "i_search": "\n\n", - "i_theme": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "i_vendor": "\n\n", - "i_wind": "\n\n\n", - "leaderboard": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "leads": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "owners": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "people": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "pipeline": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "procanvas": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "projects": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "schedule": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "settings": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "storm": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "subtasks": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "territory": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "verify": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", -}; diff --git a/src/components/dashboard/profile.tsx b/src/components/dashboard/profile.tsx new file mode 100644 index 0000000..7f9fa8d --- /dev/null +++ b/src/components/dashboard/profile.tsx @@ -0,0 +1,700 @@ +"use client"; + +// ============================================================ +// Profile — header card + 6 tabs: +// Personal Info · KYC · Security · Notifications · Privacy · Devices +// ============================================================ + +import { useEffect, useMemo, useState } from "react"; +import { + user, kycDocTypes, kyc as kycSeed, contactPrefs, contactDestinations, + contactTimezones, contactTimes, loginActivity, consentGroups, + passwordStrength, maskEmail, maskPhone, + type KycSlot, type NotifCategory, type ConsentGroup, +} from "./account-data"; +import { + Avatar, Btn, Field, Icon, Modal, OtpField, PageHead, Pill, SegTabs, + StatusDot, Toggle, useToast, +} from "./ui"; + +const TABS = [ + { value: "personal", label: "Personal Info", icon: "user" }, + { value: "kyc", label: "KYC", icon: "shield-check" }, + { value: "security", label: "Security", icon: "lock" }, + { value: "notifications", label: "Notifications", icon: "bell" }, + { value: "privacy", label: "Privacy & Consent", icon: "privacy" }, + { value: "devices", label: "Devices", icon: "devices" }, +]; + +export function Profile() { + const [tab, setTab] = useState("personal"); + + return ( +
+ Account active} + /> + + + + + +
+ {tab === "personal" && } + {tab === "kyc" && } + {tab === "security" && } + {tab === "notifications" && } + {tab === "privacy" && } + {tab === "devices" && } +
+
+ ); +} + +/* ============================================================ */ +/* Header card */ +/* ============================================================ */ + +function ProfileHeaderCard() { + const { push } = useToast(); + return ( +
+
+
+
+ + +
+
+
+ {user.name} + +
+
{user.role} · Member since {user.memberSince}
+
+ {user.sector} · {user.pocket} + Allotment {user.allotmentNo} + Plot {user.plot} +
+
+
+
+ + + +
+
+ ); +} +function HeroStat({ value, label, accent }: { value: string; label: string; accent?: string }) { + return ( +
+
{value}
+
{label}
+
+ ); +} + +/* ============================================================ */ +/* Tab: Personal Info — verify-to-reveal + change email/mobile */ +/* ============================================================ */ + +type ContactKind = "email" | "mobile" | "whatsapp"; + +function PersonalInfoTab() { + const { push } = useToast(); + const [verified, setVerified] = useState(false); + const [plot, setPlot] = useState(""); + const [err, setErr] = useState(""); + const [change, setChange] = useState(null); + + // 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}).`); + } + + 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 }, + ]; + + return ( +
+
+
+

Personal details

+ {verified + ? Revealed + : Masked} +
+ + {!verified && ( +
+
+
+ Verify to reveal & edit + Sensitive details are masked. Enter your plot number to unmask and edit them. +
+
+ { setPlot(e.target.value.replace(/\D/g, "")); setErr(""); }} + onKeyDown={(e) => e.key === "Enter" && verify()} /> + Reveal +
+ {err &&
{err}
} +
+ )} + +
+ {rows.map((f) => ( +
+
{f.label}
+
+ {verified ? f.full : f.masked} + {verified && f.editable && ( + + )} + {verified && !f.editable && via KYC} +
+
+ ))} +
+ +
+ setChange("email")} disabled={!verified}>Change email + setChange("mobile")} disabled={!verified}>Change mobile +
+ {!verified &&

Reveal your details first to edit contact information.

} +
+ +
+
+

Allottee & account holder

+ {!editId + ? { setHolderDraft(holder); setEditId(true); }}>Edit + :
setEditId(false)}>Cancel { setHolder(holderDraft); setEditId(false); push({ tone: "success", title: "Details saved" }); }}>Save
} +
+ {user.isAllottee ? ( +
+ +
You are the allottee.The account holder and the allottee are the same person ({holder.relationship}).
+
+ ) : ( +
+ +
Acting on behalf of the allottee.Account holder differs from the allottee — actions are logged against you.
+
+ )} + {editId ? ( +
+ setHolderDraft((d) => ({ ...d, name: e.target.value }))} /> + + + +
+ ) : ( +
+
Allottee name
{user.allotteeName}
+
Account holder
{holder.name}
+
Relationship
{holder.relationship}
+
Allotment no.
{user.allotmentNo}
+
Property
{user.sector} · {user.pocket} · Plot {user.plot}
+
+ )} +
+ + setChange(null)} + onSave={(kind, value) => setContacts((c) => ({ ...c, [kind]: value }))} + /> +
+ ); +} + +const CONTACT_LABEL: Record = { email: "email", mobile: "mobile", whatsapp: "WhatsApp number" }; + +// OTP-to-registered-mobile flow for changing a contact field. On confirm the +// new value is saved back so the Personal Info card updates live. +function ChangeContactModal({ kind, current, onClose, onSave }: { + kind: null | ContactKind; current: string; onClose: () => void; onSave: (kind: ContactKind, value: string) => void; +}) { + const { push } = useToast(); + const [step, setStep] = useState<"enter" | "otp">("enter"); + const [val, setVal] = useState(""); + const [otp, setOtp] = useState(""); + + // prefill with the current value whenever a new field is opened + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + if (kind) { setStep("enter"); setVal(current); setOtp(""); } + }, [kind, current]); + + function close() { setStep("enter"); setVal(""); setOtp(""); onClose(); } + + const isEmail = kind === "email"; + const label = kind ? CONTACT_LABEL[kind] : ""; + return ( + Cancel { setStep("otp"); push({ tone: "info", title: "Code sent", desc: `OTP sent to your registered mobile ${user.mobile.masked}.` }); }}>Send code + : <> setStep("enter")}>Back { if (kind) onSave(kind, val.trim()); push({ tone: "success", title: `${label[0].toUpperCase()}${label.slice(1)} updated`, desc: "Your change has been confirmed." }); close(); }}>Confirm + } + > + {step === "enter" ? ( + + setVal(e.target.value)} /> + + ) : ( +
+ + +
+ )} +
+ ); +} + +/* ============================================================ */ +/* Tab: KYC — inline upload, ID≠address, photo mandatory, % */ +/* ============================================================ */ + +function KycTab() { + const { push } = useToast(); + const [slots, setSlots] = useState(kycSeed.slots); + + const idDoc = slots.find((s) => s.key === "id")?.docId ?? null; + const addrDoc = slots.find((s) => s.key === "address")?.docId ?? null; + + const completion = useMemo(() => { + const done = slots.filter((s) => s.status === "verified" || s.status === "uploaded").length; + return Math.round((done / slots.length) * 100); + }, [slots]); + + function options(key: KycSlot["key"]) { + const want = key === "id" ? ["id", "any"] : key === "address" ? ["address", "any"] : []; + return kycDocTypes.filter((d) => want.includes(d.use)); + } + + function pickDoc(key: KycSlot["key"], docId: string) { + // enforce two-different-documents rule between ID and address + if (key === "id" && docId === addrDoc) { push({ tone: "error", title: "Pick a different document", desc: "Identity and address proofs must be two different documents." }); return; } + if (key === "address" && docId === idDoc) { push({ tone: "error", title: "Pick a different document", desc: "Address and identity proofs must be two different documents." }); return; } + setSlots((s) => s.map((x) => x.key === key ? { ...x, docId } : x)); + } + + function upload(key: KycSlot["key"]) { + const slot = slots.find((s) => s.key === key)!; + if (key !== "photo" && !slot.docId) { push({ tone: "error", title: "Select a document type first" }); return; } + const name = key === "photo" ? "selfie.jpg" : `${slot.docId}_upload.pdf`; + setSlots((s) => s.map((x) => x.key === key ? { ...x, fileName: name, status: "uploaded", note: "Under review" } : x)); + push({ tone: "success", title: "Uploaded", desc: "Your document is queued for review." }); + } + + function remove(key: KycSlot["key"]) { + setSlots((s) => s.map((x) => x.key === key ? { ...x, fileName: null, docId: key === "photo" ? null : x.docId, status: "missing", note: undefined } : x)); + } + + const photoDone = slots.find((s) => s.key === "photo")!.status !== "missing"; + + return ( +
+
+
+

Verification documents

+ {completion}% complete +
+ +
+
+
+ {slots.map((s) => ( + + {s.title} + + ))} +
+
+ + {!photoDone &&
Live photo is mandatory.KYC cannot be completed without a verified photo.
} + +
+ {slots.map((slot) => ( + pickDoc(slot.key, d)} onUpload={() => upload(slot.key)} onRemove={() => remove(slot.key)} /> + ))} +
+
+ +
+

Rules

+
    +
  • A live photo / selfie is mandatory.
  • +
  • Provide one ID and one address proof.
  • +
  • ID and address must be two different documents.
  • +
  • Accepted: PDF, JPG, PNG · size limits per document.
  • +
  • Review completes within ~4 business hours.
  • +
+
+
Current selection
+
Identity{kycDocTypes.find((d) => d.id === idDoc)?.label ?? "Not chosen"}
+
Address{kycDocTypes.find((d) => d.id === addrDoc)?.label ?? "Not chosen"}
+
+
+
+ ); +} + +function KycSlotRow({ slot, options, onPick, onUpload, onRemove }: { + slot: KycSlot; options: typeof kycDocTypes; onPick: (d: string) => void; onUpload: () => void; onRemove: () => void; +}) { + const tone = slot.status === "verified" ? "green" : slot.status === "uploaded" ? "blue" : slot.status === "rejected" ? "red" : "muted"; + const docMeta = kycDocTypes.find((d) => d.id === slot.docId); + return ( +
+
+
+
+ {slot.title}{slot.required && *} + {slot.status === "missing" ? "Missing" : slot.status === "uploaded" ? "Under review" : slot.status === "verified" ? "Verified" : "Rejected"} +
+ + {slot.key !== "photo" && ( + + )} + + {docMeta &&
{docMeta.hint} · {docMeta.formats} · ≤ {docMeta.maxMb} MB
} + + {slot.fileName ? ( +
+ + {slot.fileName} + {slot.note && {slot.note}} + +
+ ) : ( + + )} +
+
+ ); +} + +/* ============================================================ */ +/* Tab: Security */ +/* ============================================================ */ + +function SecurityTab() { + const { push } = useToast(); + const [pw, setPw] = useState(""); + const [twoFA, setTwoFA] = useState(false); + const [authApp, setAuthApp] = useState(false); + const [alerts, setAlerts] = useState(true); + const [twoFaModal, setTwoFaModal] = useState(false); + const strength = passwordStrength(pw); + + return ( +
+
+

Password

Updated 5 days ago
+ + setPw(e.target.value)} /> + + {pw && ( +
+
{[0, 1, 2, 3].map((i) => )}
+
{strength.label}
+
+ )} +
    + {strength.checks.map((c) =>
  • {c.label}
  • )} +
+
{ push({ tone: "success", title: "Password updated" }); setPw(""); }}>Update password
+
+ +
+

Two-factor & sign-in

+ + { if (v) setTwoFaModal(true); else { setTwoFA(false); setAuthApp(false); push({ tone: "info", title: "Two-factor disabled" }); } }} label="2FA" /> + + + { setAuthApp(v); push({ tone: v ? "success" : "info", title: v ? "Authenticator linked" : "Authenticator removed" }); }} label="Authenticator" /> + + + { setAlerts(v); push({ tone: "info", title: v ? "Login alerts on" : "Login alerts off" }); }} label="Login alerts" /> + + {!twoFA &&

Enable two-factor authentication to link an authenticator app.

} +
+ + setTwoFaModal(false)} onDone={() => { setTwoFA(true); setTwoFaModal(false); push({ tone: "success", title: "Two-factor enabled", desc: "Your account is now protected with 2FA." }); }} /> +
+ ); +} + +function TwoFaModal({ open, onClose, onDone }: { open: boolean; onClose: () => void; onDone: () => void }) { + const { push } = useToast(); + const [step, setStep] = useState<"password" | "otp">("password"); + const [pw, setPw] = useState(""); + const [otp, setOtp] = useState(""); + function close() { setStep("password"); setPw(""); setOtp(""); onClose(); } + return ( + Cancel { setStep("otp"); push({ tone: "info", title: "Code sent", desc: "OTP sent to your registered mobile." }); }}>Continue + : <> setStep("password")}>BackEnable 2FA} + > + {step === "password" ? ( + setPw(e.target.value)} /> + ) : ( +
Both password and OTP are required to turn 2FA on.
+ )} +
+ ); +} + +function SettingRow({ icon, title, desc, children }: { icon: string; title: string; desc: string; children: React.ReactNode }) { + return ( +
+ +
{title}
{desc}
+
{children}
+
+ ); +} + +/* ============================================================ */ +/* Tab: Notifications */ +/* ============================================================ */ + +function NotificationsTab() { + const { push } = useToast(); + const [prefs, setPrefs] = useState(contactPrefs); + const [tz, setTz] = useState(contactTimezones[0]); + const [times, setTimes] = useState(contactTimes); + const channels: { key: keyof NotifCategory; label: string; icon: string }[] = [ + { key: "email", label: "Email", icon: "mail" }, + { key: "sms", label: "SMS", icon: "phone" }, + { key: "whatsapp", label: "WhatsApp", icon: "chat" }, + { key: "push", label: "Push", icon: "bell" }, + ]; + + function toggle(catId: string, ch: keyof NotifCategory) { + setPrefs((p) => p.map((c) => { + if (c.id !== catId) return c; + if (c.locked) { push({ tone: "info", title: "Required notifications", desc: "Security alerts can't be turned off." }); return c; } + return { ...c, [ch]: !c[ch] } as NotifCategory; + })); + } + + return ( +
+
+

Per-channel matrix

Choose how each category reaches you
+
+
+ Category + {channels.map((c) => {c.label})} +
+ {prefs.map((cat) => ( +
+ + {cat.label}{cat.locked && } + {cat.desc} + + {channels.map((c) => ( + + toggle(cat.id, c.key)} label={`${cat.label} ${c.label}`} /> + + ))} +
+ ))} +
+
+ +
+
+

Contact-time window

+ + + +
+ {times.map((t) => ( + + ))} +
+

Callbacks & proactive messages only arrive inside enabled windows, in your timezone.

+
+ +
+

Destinations

+
+ {contactDestinations.map((d) => ( +
+ +
{d.label}{d.primary && Primary}
{d.value}
+ {d.verified ? Verified : push({ tone: "info", title: "Verification sent" })}>Verify} +
+ ))} +
+ push({ tone: "info", title: "Add destination" })}>Add destination +
+
+
+ ); +} + +/* ============================================================ */ +/* Tab: Privacy & Consent */ +/* ============================================================ */ + +function PrivacyTab() { + const { push } = useToast(); + const [groups, setGroups] = useState(consentGroups); + + function toggle(gid: string, iid: string) { + setGroups((gs) => gs.map((g) => g.id !== gid ? g : { + ...g, + items: g.items.map((it) => { + if (it.id !== iid) return it; + if (it.locked) { push({ tone: "info", title: "Required consent", desc: "Statutory consents can't be withdrawn while active." }); return it; } + return { ...it, granted: !it.granted }; + }), + })); + } + + return ( +
+ {groups.map((g) => ( +
+
+

{g.title}

+ {g.id === "marketing" && Optional} + {g.id === "essential" && Required} +
+
+ {g.items.map((it) => ( +
+
+
{it.label}{it.locked && }
+
{it.desc}
+
+ toggle(g.id, it.id)} label={it.label} /> +
+ ))} +
+ {g.id === "marketing" && groups.find((x) => x.id === "marketing")!.items.find((i) => i.id === "promos")!.granted && ( +
+
Promotional sub-preferences
+ {["Product offers", "Surveys & feedback", "Partner news"].map((s, i) => ( + + ))} +
+ )} +
+ ))} +
+ ); +} + +/* ============================================================ */ +/* Tab: Devices */ +/* ============================================================ */ + +function DevicesTab() { + const { push } = useToast(); + const [devices, setDevices] = useState(loginActivity); + + function signOut(id: string) { + setDevices((d) => d.filter((x) => x.id !== id)); + push({ tone: "success", title: "Signed out", desc: "That device no longer has access." }); + } + + return ( +
+
+
+

Connected devices

+ { setDevices((d) => d.filter((x) => x.current)); push({ tone: "success", title: "Other devices signed out" }); }}>Sign out all others +
+
+ {devices.filter((d) => d.event !== "password-change").map((d) => ( +
+ +
+
{d.device}{d.current && This device}{!d.trusted && New}
+
{d.browser} · {d.os}
+
{d.location} · {d.ip} · {d.lastActive}
+
+ {!d.current && } +
+ ))} +
+
+ +
+

Recent activity

+
+ {loginActivity.map((a) => ( +
+ +
+
{labelForEvent(a.event)}
+
{a.device} · {a.location}
+
{a.lastActive}
+
+
+ ))} +
+
+
+ ); +} + +function labelForEvent(ev: string) { + switch (ev) { + case "password-change": return "Password changed"; + case "new-device": return "New device signed in"; + case "2fa-enabled": return "Two-factor enabled"; + case "sign-out": return "Signed out"; + default: return "Successful sign-in"; + } +} diff --git a/src/components/dashboard/rules.tsx b/src/components/dashboard/rules.tsx new file mode 100644 index 0000000..88a66e5 --- /dev/null +++ b/src/components/dashboard/rules.tsx @@ -0,0 +1,44 @@ +"use client"; + +// ============================================================ +// Rules Checklist — the 1–13 business rules in one place. +// (plot 181 · OTP to registered mobile · KYC two-docs · +// password policy · ticket lifecycle · …) +// ============================================================ + +import { useMemo, useState } from "react"; +import { rules } from "./account-data"; +import { Icon, PageHead, Pill, Segmented } from "./ui"; + +export function Rules() { + const tags = useMemo(() => ["All", ...Array.from(new Set(rules.map((r) => r.tag)))], []); + const [tag, setTag] = useState("All"); + const list = tag === "All" ? rules : rules.filter((r) => r.tag === tag); + + return ( +
+ {rules.length} rules} + /> + + ({ value: t, label: t }))} /> + +
+ {list.map((r) => ( +
+
+ {String(r.n).padStart(2, "0")} + {r.tag} +
+
{r.title}
+

{r.detail}

+
+ ))} +
+
+ ); +} diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index abe56c8..6402781 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -1,49 +1,32 @@ "use client"; import { ChevronsUpDown } from "lucide-react"; -import { FigIcon } from "./figicon"; +import { Icon } from "./ui"; +import { user } from "./account-data"; -type Item = { key: string; label: string }; +type Item = { key: string; label: string; icon: string }; const GROUPS: { title: string; items: Item[] }[] = [ { - title: "Workspace", + title: "Account", items: [ - { key: "dashboard", label: "Dashboard" }, - { key: "owners", label: "Owners Box" }, - { key: "projects", label: "Projects" }, - { key: "leads", label: "Leads" }, - { key: "verify", label: "Lead Verification" }, - { key: "pipeline", label: "Pipeline" }, + { key: "profile", label: "Profile", icon: "user" }, ], }, { - title: "Workspace", + title: "Help & Support", items: [ - { key: "dispatch", label: "LynkDispatch" }, - { key: "storm", label: "Storm Intel" }, - { key: "territory", label: "Territory Map" }, - { key: "procanvas", label: "ProCanvas" }, - { key: "estimates", label: "Estimates" }, + { key: "support", label: "Support Center", icon: "chat" }, ], }, { - title: "Team", + title: "Reference", items: [ - { key: "schedule", label: "Team Schedule" }, - { key: "leaderboard", label: "Leaderboard" }, - { key: "subtasks", label: "Subcontractor Tasks" }, - { key: "people", label: "People" }, + { key: "rules", label: "Rules Checklist", icon: "check-circle" }, ], }, ]; -const EXTRA: Item[] = [ - { key: "settings", label: "Org Settings" }, - { key: "ai", label: "AI Assistant" }, - { key: "home", label: "Home" }, -]; - export function Sidebar({ active, onSelect }: { active: string; onSelect: (k: string) => void }) { return ( ); } - -function NavBtn({ it, active, onSelect }: { it: Item; active: string; onSelect: (k: string) => void }) { - return ( - - ); -} diff --git a/src/components/dashboard/support.tsx b/src/components/dashboard/support.tsx new file mode 100644 index 0000000..34680f2 --- /dev/null +++ b/src/components/dashboard/support.tsx @@ -0,0 +1,545 @@ +"use client"; + +// ============================================================ +// Support Center +// Overview (5 channel cards + team) · Message Center · +// New Ticket · My Tickets (+ timeline modal) · Help Center +// plus Live Chat handshake & Callback/Email modals. +// ============================================================ + +import { useEffect, useMemo, useRef, useState } from "react"; +import { + supportChannels, supportTeam, ticketCategories, tickets as ticketSeed, + supportThreads as threadSeed, helpTopics, contactTimes, countryCodes, + type Ticket, type TicketStatus, type SupportThread, type ChatMsg, +} from "./account-data"; +import { + Avatar, Btn, Field, Icon, Modal, PageHead, Pill, Segmented, StatusDot, useToast, +} from "./ui"; + +const SECTIONS = [ + { value: "overview", label: "Overview", icon: "chat" }, + { value: "messages", label: "Messages", icon: "mail" }, + { value: "new", label: "New Ticket", icon: "ticket" }, + { value: "tickets", label: "My Tickets", icon: "book" }, + { value: "help", label: "Help Center", icon: "info" }, +]; + +const STATUS_META: Record = { + created: { label: "Created", tone: "muted" }, + open: { label: "Open", tone: "blue" }, + "in-progress": { label: "In Progress", tone: "orange" }, + "awaiting-customer": { label: "Awaiting You", tone: "purple" }, + resolved: { label: "Resolved", tone: "green" }, + closed: { label: "Closed", tone: "muted" }, +}; + +export function Support() { + const [section, setSection] = useState("overview"); + const [chatOpen, setChatOpen] = useState(false); + const [callbackOpen, setCallbackOpen] = useState(false); + const [emailOpen, setEmailOpen] = useState(false); + + function onChannel(id: string) { + if (id === "chat") setChatOpen(true); + else if (id === "ticket") setSection("new"); + else if (id === "callback") setCallbackOpen(true); + else if (id === "email") setEmailOpen(true); + else if (id === "help") setSection("help"); + } + + return ( +
+ setChatOpen(true)}>Start live chat} + /> + + + +
+ {section === "overview" && } + {section === "messages" && } + {section === "new" && setSection("tickets")} />} + {section === "tickets" && } + {section === "help" && } +
+ + setChatOpen(false)} /> + setCallbackOpen(false)} /> + setEmailOpen(false)} /> +
+ ); +} + +/* ============================================================ */ +/* Overview — channel cards + support team */ +/* ============================================================ */ + +function Overview({ onChannel }: { onChannel: (id: string) => void }) { + return ( +
+
+
+ {supportChannels.map((c) => ( + + ))} +
+
+ +
+

Your support team

2 online
+
+ {supportTeam.map((a) => ( +
+ +
+
{a.name}
+
{a.role}
+
{a.team}
+
+
{a.rating}
+
+ ))} +
+
+
+ ); +} + +/* ============================================================ */ +/* Message Center — inbox + live thread + typing */ +/* ============================================================ */ + +function MessageCenter() { + const [threads, setThreads] = useState(threadSeed); + const [activeId, setActiveId] = useState(threadSeed[0].id); + const [draft, setDraft] = useState(""); + const endRef = useRef(null); + const active = threads.find((t) => t.id === activeId)!; + + function send() { + const text = draft.trim(); + if (!text) return; + const msg: ChatMsg = { id: `m${Date.now()}`, from: "me", text, at: "Now" }; + setThreads((ts) => ts.map((t) => t.id === activeId ? { ...t, messages: [...t.messages, msg], typing: true, preview: text } : t)); + setDraft(""); + // simulate agent typing → reply + setTimeout(() => { + const reply: ChatMsg = { id: `m${Date.now() + 1}`, from: "agent", text: "Thanks — noted. I'll update the ticket and get back to you shortly.", at: "Now" }; + setThreads((ts) => ts.map((t) => t.id === activeId ? { ...t, messages: [...t.messages, reply], typing: false } : t)); + endRef.current?.scrollIntoView({ behavior: "smooth" }); + }, 1800); + setTimeout(() => endRef.current?.scrollIntoView({ behavior: "smooth" }), 50); + } + + function openThread(id: string) { + setActiveId(id); + setThreads((ts) => ts.map((t) => t.id === id ? { ...t, unread: 0 } : t)); + } + + const agent = supportTeam.find((a) => a.id === active.agentId)!; + + return ( +
+ + +
+
+ +
+
{agent.name}
+
{active.typing ? typing… : `${agent.role} · ${agent.team}`}
+
+ {active.subject} +
+ +
+ {active.messages.map((m) => ( +
+ {m.from === "agent" && } +

{m.text}

{m.at}
+
+ ))} + {active.typing && ( +
+ +
+
+ )} +
+
+ +
+ + setDraft(e.target.value)} onKeyDown={(e) => e.key === "Enter" && send()} /> + Send +
+
+
+ ); +} + +/* ============================================================ */ +/* New Ticket — category → department routing + attachment rules */ +/* ============================================================ */ + +function NewTicket({ onDone }: { onDone: () => void }) { + const { push } = useToast(); + const [catId, setCatId] = useState(""); + const [subject, setSubject] = useState(""); + const [desc, setDesc] = useState(""); + const [priority, setPriority] = useState("medium"); + const [file, setFile] = useState(null); + + const cat = ticketCategories.find((c) => c.id === catId) ?? null; + const needAttachment = cat?.requiresAttachment && !file; + const canSubmit = catId && subject.trim() && desc.trim() && !needAttachment; + + function submit() { + push({ tone: "success", title: "Ticket created", desc: `Routed to ${cat?.department}. We'll reply within ${cat?.sla}.` }); + onDone(); + } + + return ( +
+
+

Raise a new ticket

+ + + + + + {cat && ( +
+ + Routes to {cat.department} + SLA {cat.sla} +
+ )} + + setSubject(e.target.value)} /> +