From db5d3cb34d10dee0cb4b717c34101b866304eb95 Mon Sep 17 00:00:00 2001 From: Bhanu Prakash Sai Potteri Date: Sat, 23 May 2026 14:02:27 +0530 Subject: [PATCH] Hero phase dots, name hierarchy, lighter page title, cleaner sections --- src/components/variants/VariantB.tsx | 259 +++++++++++++++++---------- 1 file changed, 169 insertions(+), 90 deletions(-) diff --git a/src/components/variants/VariantB.tsx b/src/components/variants/VariantB.tsx index e84a29b..a9981d6 100644 --- a/src/components/variants/VariantB.tsx +++ b/src/components/variants/VariantB.tsx @@ -5,11 +5,11 @@ // table below as support. DV: full-page replace with hero card + collapsible // section groups. -import { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ChevronLeft, ChevronRight, Search, ArrowUp, ArrowDown, X, RefreshCw, - CheckCircle2, LogOut, Sparkles, Clock, Car, ClipboardList, PhoneOutgoing, + CheckCircle2, LogOut, Sparkles, Car, ClipboardList, PhoneOutgoing, CalendarCheck2, MessageSquareHeart, Trophy, ArrowLeft, ChevronDown, ChevronUp, Plus, } from "lucide-react"; @@ -66,6 +66,44 @@ const STATE_TONE: Record = { }; function tone(name: string) { return STATE_TONE[name] || { bg: "#F5F5F4", fg: "#57534E", dot: "#A8A29E" }; } +const PHASES = ["Captured", "Scheduling", "Test drive", "Feedback"] as const; +function phaseIndexFor(state: string | null | undefined): number { + if (!state) return 0; + if (state === STATE_IDS.AWAITING_FEEDBACK_CALL || state === STATE_IDS.FEEDBACK_CALL_FAILED) return 3; + if (state === STATE_IDS.FEEDBACK_COLLECTED || state === STATE_IDS.END_STATE) return 3; + if (state === STATE_IDS.SCHEDULED) return 2; + if (state === STATE_IDS.AWAITING_SCHEDULER_CALL || state === STATE_IDS.SCHEDULING_CALL_FAILED) return 1; + return 0; +} +function PhaseDots({ state }: { state: string | null | undefined }) { + const current = phaseIndexFor(state); + return ( +
+ {PHASES.map((label, i) => { + const reached = i <= current; + return ( + +
+ + + {label} + +
+ {i < PHASES.length - 1 && } +
+ ); + })} +
+ ); +} + function iconFor(label: string) { const k = label.toLowerCase(); if (k.includes("lead")) return ; @@ -123,7 +161,7 @@ export default function VariantB() {
Test-drive lifecycle
-

+

{activeNav?.label ?? "Dashboard"}

@@ -615,54 +653,52 @@ function DetailPage({ return (
- {/* Breadcrumb — TM style: red 12px font-weight 700 */} -
- - / - Lead detail -
- - {/* Hero — editorial TM band. Big red number bug (instance id), name + meta on - the left, state badge as a solid white tablet on the right (fills the - previously-empty space and carries the strongest signal: status). */} + {/* Hero — full-bleed navy band. Back button at the top, then the lead block. + The -mx-10 pulls it out of main's px-10 so it spans the content area + edge-to-edge. */}
- - {/* Diagonal navy slice on the far right — TM editorial accent */} + + {/* Diagonal red slice on the far right — mirrors techmahindra.com footer */} -
-
- {/* Big number bug — instance ID as editorial signature */} -
-
Lead ID
-
#{instanceId}
-
+ {/* Back link — sits at the hero top, white on navy */} +
+ + / + Lead detail +
-
-
Lead
-

{heroValue || `#${instanceId}`}

-
- {model && Model {model}} - {dv.data["created_at"] && ( - <> - {model && ·} - Created {new Date(String(dv.data["created_at"])).toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" })} - - )} -
+
+
+
Lead
+
+

{heroValue || `#${instanceId}`}

+ #{instanceId}
+
+ {model && Model {model}} + {dv.data["created_at"] && ( + <> + {model && ·} + Created {new Date(String(dv.data["created_at"])).toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" })} + + )} +
+
{/* Right column — state badge stacked over action buttons. Fills the dead space. */} @@ -708,26 +744,48 @@ function DetailPage({ {/* Stack of independent section cards — each its own border, no shared chrome. */}
- {Object.entries(groups).filter(([, fs]) => fs.length > 0).map(([key, fs]) => ( - + {Object.entries(groups).filter(([, fs]) => fs.length > 0).map(([key, fs], i) => ( + ))}
{audit.length > 0 && (
-
- -
Audit
+
+ LOG +
Activity timeline
+
{audit.length} {audit.length === 1 ? "event" : "events"}
-
Activity timeline
-
    - {audit.map((e, i) => ( -
  1. - - {e.activity_id.slice(0, 8)}… - {new Date(e.created_at).toLocaleString("en-IN", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })} -
  2. - ))} +
      + {/* Vertical rail down the timeline */} + + {audit.map((e, i) => { + const meta = ACTIVITY_LABELS[e.activity_id]; + const label = meta?.label ?? "Workflow step"; + const tone = meta?.tone ?? "system"; + const isLast = i === audit.length - 1; + const dotColor = isLast ? ACCENT : tone === "ai" ? TM_NAVY : tone === "user" ? "#57534e" : "#a8a29e"; + const actor = tone === "ai" ? "AI · Maya" : (e.user_roles?.[0] ?? "System"); + return ( +
    1. + +
      +
      +
      {label}
      +
      + {actor}{e.execution_state && e.execution_state !== "completed" ? ` · ${e.execution_state}` : ""} +
      +
      + + {new Date(e.created_at).toLocaleString("en-IN", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })} + +
      +
    2. + ); + })}
)} @@ -736,6 +794,19 @@ function DetailPage({ } // Section titles — single strong word, no subtitle (cleaner editorial feel). +// Human labels for activity UUIDs that appear in the audit log. Anything not +// in this map falls back to "Workflow step". +const ACTIVITY_LABELS: Record = { + [ACTIVITY_IDS.INIT_ACTIVITY]: { label: "Lead captured", tone: "user" }, + [ACTIVITY_IDS.MARK_TEST_DRIVE_DONE]: { label: "Test drive marked done", tone: "user" }, + [ACTIVITY_IDS.AI_CONFIRM_SCHEDULING_SUCCESS]: { label: "Scheduling call succeeded", tone: "ai" }, + [ACTIVITY_IDS.AI_CONFIRM_SCHEDULING_FAILED]: { label: "Scheduling call failed", tone: "ai" }, + [ACTIVITY_IDS.RETRY_SCHEDULING_CALL]: { label: "Retried scheduling call", tone: "user" }, + [ACTIVITY_IDS.AI_CONFIRM_FEEDBACK_COLLECTED]: { label: "Feedback collected", tone: "ai" }, + [ACTIVITY_IDS.AI_CONFIRM_FEEDBACK_FAILED]: { label: "Feedback call failed", tone: "ai" }, + [ACTIVITY_IDS.RETRY_FEEDBACK_CALL]: { label: "Retried feedback call", tone: "user" }, +}; + const SECTION_TITLE: Record = { customer: "Customer", vehicle: "Vehicle", @@ -755,8 +826,9 @@ const SECTION_EMPTY: Record = { }; function SectionCard({ - sectionKey, fields, data, + index, sectionKey, fields, data, }: { + index: number; sectionKey: string; fields: any[]; data: Record; @@ -769,18 +841,44 @@ function SectionCard({ const isEmpty = populated === 0; const [open, setOpen] = useState(!isEmpty); + // White header with a small red number tag — editorial, no navy weight. + const numberTag = ( + + {String(index + 1).padStart(2, "0")} + + ); + + const headerRow = (interactive: boolean) => ( +
setOpen((o) => !o) : undefined} + > +
+ {numberTag} +
{title}
+
+ 0 ? ACCENT : "#a8a29e", fontWeight: 700 }}>{populated} + / + {fields.length} +
+
+ {interactive && ( + + {open ? : } + + )} +
+ ); + if (isEmpty) { return ( -
-
-
{title}
-
- 0 - / - {fields.length} -
-
-
+
+ {headerRow(false)} +
{SECTION_EMPTY[sectionKey] ?? "Nothing recorded here yet."}
@@ -788,30 +886,12 @@ function SectionCard({ } return ( -
- +
+ {headerRow(true)} {open && (
- {fields.map((f, i) => { + {fields.map((f) => { const raw = data[f.field_key]; const empty = raw == null || raw === "" || raw === "—"; const text = empty ? "—" : fmtCellText(raw, f.data_type, f.field_key); @@ -819,8 +899,7 @@ function SectionCard({ return (
{fmtLabel(f.output_label)}