Hero phase dots, name hierarchy, lighter page title, cleaner sections

This commit is contained in:
Bhanu Prakash Sai Potteri 2026-05-23 14:02:27 +05:30
parent 6148db7902
commit db5d3cb34d

View File

@ -5,11 +5,11 @@
// table below as support. DV: full-page replace with hero card + collapsible // table below as support. DV: full-page replace with hero card + collapsible
// section groups. // section groups.
import { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { import {
ChevronLeft, ChevronRight, Search, ArrowUp, ArrowDown, X, RefreshCw, 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, CalendarCheck2, MessageSquareHeart, Trophy, ArrowLeft, ChevronDown, ChevronUp,
Plus, Plus,
} from "lucide-react"; } from "lucide-react";
@ -66,6 +66,44 @@ const STATE_TONE: Record<string, { bg: string; fg: string; dot: string }> = {
}; };
function tone(name: string) { return STATE_TONE[name] || { bg: "#F5F5F4", fg: "#57534E", dot: "#A8A29E" }; } 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 (
<div className="mt-4 flex items-center gap-2.5 flex-wrap">
{PHASES.map((label, i) => {
const reached = i <= current;
return (
<React.Fragment key={label}>
<div className="flex items-center gap-1.5">
<span
className="w-2.5 h-2.5 rounded-full shrink-0"
style={
reached
? { background: "#e31837" }
: { background: "transparent", border: "2px solid rgba(255,255,255,0.4)" }
}
/>
<span className={`text-[11px] font-semibold uppercase tracking-[0.08em] ${reached ? "text-white" : "text-white/50"}`}>
{label}
</span>
</div>
{i < PHASES.length - 1 && <span className="w-5 h-px" style={{ background: "rgba(255,255,255,0.2)" }} />}
</React.Fragment>
);
})}
</div>
);
}
function iconFor(label: string) { function iconFor(label: string) {
const k = label.toLowerCase(); const k = label.toLowerCase();
if (k.includes("lead")) return <ClipboardList size={15} />; if (k.includes("lead")) return <ClipboardList size={15} />;
@ -123,7 +161,7 @@ export default function VariantB() {
<div className={EYEBROW_CLS} style={EYEBROW_STYLE}> <div className={EYEBROW_CLS} style={EYEBROW_STYLE}>
Test-drive lifecycle Test-drive lifecycle
</div> </div>
<h1 className="text-[40px] font-bold tracking-tight leading-[1.05] mt-2" style={{ color: INK }}> <h1 className="text-[24px] font-semibold tracking-tight leading-tight mt-1.5" style={{ color: INK }}>
{activeNav?.label ?? "Dashboard"} {activeNav?.label ?? "Dashboard"}
</h1> </h1>
</div> </div>
@ -615,44 +653,42 @@ function DetailPage({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Breadcrumb — TM style: red 12px font-weight 700 */} {/* Hero full-bleed navy band. Back button at the top, then the lead block.
<div className="flex items-center gap-2 text-[12px] font-bold leading-6 uppercase tracking-[-0.02em]"> The -mx-10 pulls it out of main's px-10 so it spans the content area
<button onClick={onBack} className="inline-flex items-center gap-1.5 transition-opacity hover:opacity-70" style={{ color: ACCENT }}> edge-to-edge. */}
<ArrowLeft size={13} />{breadcrumbLabel}
</button>
<span className="text-stone-300">/</span>
<span style={{ color: INK }}>Lead detail</span>
</div>
{/* 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). */}
<div <div
className="rounded-none px-7 py-6 text-white relative overflow-hidden" className="rounded-none -mx-10 -mt-10 px-10 pt-7 pb-7 text-white relative overflow-hidden"
style={{ style={{ background: TM_NAVY }}
background: `linear-gradient(120deg, ${ACCENT} 0%, ${ACCENT_DARK} 100%)`,
}}
> >
<span className="absolute top-0 left-0 bottom-0 w-1.5" style={{ background: TM_NAVY }} /> <span className="absolute top-0 left-0 bottom-0 w-1.5" style={{ background: ACCENT }} />
{/* Diagonal navy slice on the far right — TM editorial accent */} {/* Diagonal red slice on the far right — mirrors techmahindra.com footer */}
<span <span
className="absolute top-0 right-0 bottom-0 w-40 pointer-events-none opacity-40" className="absolute top-0 right-0 bottom-0 w-56 pointer-events-none opacity-50"
style={{ style={{
background: `linear-gradient(115deg, transparent 0%, transparent 55%, ${TM_NAVY} 55%, ${TM_NAVY} 58%, transparent 58%, transparent 72%, ${TM_NAVY} 72%, ${TM_NAVY} 74%, transparent 74%)`, background: `linear-gradient(115deg, transparent 0%, transparent 55%, ${ACCENT} 55%, ${ACCENT} 58%, transparent 58%, transparent 72%, ${ACCENT} 72%, ${ACCENT} 74%, transparent 74%)`,
}} }}
/> />
<div className="relative flex items-stretch justify-between gap-8 flex-wrap"> {/* Back link — sits at the hero top, white on navy */}
<div className="flex items-stretch gap-5 min-w-0 flex-1"> <div className="relative mb-5 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] leading-none">
{/* Big number bug — instance ID as editorial signature */} <button
<div className="flex flex-col justify-center pr-5 border-r border-white/15"> onClick={onBack}
<div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-white/60 mb-1">Lead ID</div> className="inline-flex items-center gap-1.5 text-white/75 hover:text-white transition-colors"
<div className="text-[28px] font-bold leading-none tabular-nums">#{instanceId}</div> >
<ArrowLeft size={13} className="shrink-0" />
<span>{breadcrumbLabel}</span>
</button>
<span className="text-white/30">/</span>
<span className="text-white/55">Lead detail</span>
</div> </div>
<div className="min-w-0 flex flex-col justify-center"> <div className="relative flex items-stretch justify-between gap-8 flex-wrap">
<div className="text-[11px] font-semibold uppercase tracking-[0.15em] mb-2" style={{ color: "#ffd9de" }}>Lead</div> <div className="min-w-0 flex-1 flex flex-col justify-center">
<h2 className="text-[30px] font-bold leading-none tracking-tight truncate">{heroValue || `#${instanceId}`}</h2> <div className="text-[11px] font-semibold uppercase tracking-[0.15em] mb-2" style={{ color: ACCENT }}>Lead</div>
<div className="flex items-baseline gap-3 min-w-0 flex-wrap">
<h2 className="text-[36px] font-bold leading-none tracking-tight truncate">{heroValue || `#${instanceId}`}</h2>
<span className="text-[20px] font-semibold leading-none tabular-nums text-white/60">#{instanceId}</span>
</div>
<div className="mt-3 flex items-center gap-3 text-[12.5px] text-white/80 flex-wrap"> <div className="mt-3 flex items-center gap-3 text-[12.5px] text-white/80 flex-wrap">
{model && <span><span className="text-white/55">Model</span> <span className="font-semibold text-white">{model}</span></span>} {model && <span><span className="text-white/55">Model</span> <span className="font-semibold text-white">{model}</span></span>}
{dv.data["created_at"] && ( {dv.data["created_at"] && (
@ -662,7 +698,7 @@ function DetailPage({
</> </>
)} )}
</div> </div>
</div> <PhaseDots state={instanceState} />
</div> </div>
{/* Right column — state badge stacked over action buttons. Fills the dead space. */} {/* 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. */} {/* Stack of independent section cards — each its own border, no shared chrome. */}
<div className="space-y-4"> <div className="space-y-4">
{Object.entries(groups).filter(([, fs]) => fs.length > 0).map(([key, fs]) => ( {Object.entries(groups).filter(([, fs]) => fs.length > 0).map(([key, fs], i) => (
<SectionCard key={key} sectionKey={key} fields={fs} data={dv.data!} /> <SectionCard key={key} index={i} sectionKey={key} fields={fs} data={dv.data!} />
))} ))}
</div> </div>
{audit.length > 0 && ( {audit.length > 0 && (
<div className="bg-white rounded-none p-6" style={{ border: CARD_BORDER }}> <div className="bg-white rounded-none p-6" style={{ border: CARD_BORDER }}>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-3 mb-5">
<Clock size={14} className="text-[#e31837]" /> <span className="text-[11px] font-bold tabular-nums px-1.5 py-0.5 text-white" style={{ background: ACCENT, letterSpacing: "0.04em" }}>LOG</span>
<div className={EYEBROW_CLS} style={EYEBROW_STYLE}>Audit</div> <div className="text-[17px] font-bold tracking-tight leading-none" style={{ color: INK }}>Activity timeline</div>
<div className="text-[11.5px] font-medium tabular-nums text-stone-400">{audit.length} {audit.length === 1 ? "event" : "events"}</div>
</div>
<ol className="relative">
{/* Vertical rail down the timeline */}
<span className="absolute left-[5px] top-2 bottom-2 w-px" style={{ background: TM_BORDER }} />
{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 (
<li key={e.id} className="relative pl-7 pb-4 last:pb-0 text-[13px]">
<span
className="absolute left-0 top-1 w-[11px] h-[11px] rounded-full"
style={{ background: dotColor, boxShadow: `0 0 0 3px white` }}
/>
<div className="flex items-baseline justify-between gap-4">
<div className="min-w-0">
<div className="font-semibold leading-tight" style={{ color: INK }}>{label}</div>
<div className="text-[11.5px] uppercase tracking-[0.04em] text-stone-400 mt-1">
{actor}{e.execution_state && e.execution_state !== "completed" ? ` · ${e.execution_state}` : ""}
</div>
</div>
<span className="text-stone-400 tabular-nums shrink-0 text-[12px]">
{new Date(e.created_at).toLocaleString("en-IN", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })}
</span>
</div> </div>
<div className="text-[18px] font-bold leading-tight -mt-3 mb-4" style={{ color: INK }}>Activity timeline</div>
<ol className="space-y-3">
{audit.map((e, i) => (
<li key={e.id} className="flex items-start gap-3 text-[13px]">
<span className={`mt-1 w-2 h-2 rounded-full shrink-0 ${i === audit.length - 1 ? "" : ""}`} style={{ background: i === audit.length - 1 ? ACCENT : "#D6D3D1" }} />
<span className="text-stone-700 flex-1">{e.activity_id.slice(0, 8)}</span>
<span className="text-stone-400 tabular-nums shrink-0">{new Date(e.created_at).toLocaleString("en-IN", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })}</span>
</li> </li>
))} );
})}
</ol> </ol>
</div> </div>
)} )}
@ -736,6 +794,19 @@ function DetailPage({
} }
// Section titles — single strong word, no subtitle (cleaner editorial feel). // 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<string, { label: string; tone: "user" | "ai" | "system" }> = {
[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<string, string> = { const SECTION_TITLE: Record<string, string> = {
customer: "Customer", customer: "Customer",
vehicle: "Vehicle", vehicle: "Vehicle",
@ -755,8 +826,9 @@ const SECTION_EMPTY: Record<string, string> = {
}; };
function SectionCard({ function SectionCard({
sectionKey, fields, data, index, sectionKey, fields, data,
}: { }: {
index: number;
sectionKey: string; sectionKey: string;
fields: any[]; fields: any[];
data: Record<string, unknown>; data: Record<string, unknown>;
@ -769,18 +841,44 @@ function SectionCard({
const isEmpty = populated === 0; const isEmpty = populated === 0;
const [open, setOpen] = useState(!isEmpty); const [open, setOpen] = useState(!isEmpty);
if (isEmpty) { // White header with a small red number tag — editorial, no navy weight.
return ( const numberTag = (
<div className="bg-white rounded-none px-6 py-5" style={{ border: CARD_BORDER }}> <span
<div className="flex items-baseline gap-3"> className="text-[11px] font-bold tabular-nums px-1.5 py-0.5"
<div className="text-[18px] font-bold tracking-tight leading-none" style={{ color: INK }}>{title}</div> style={{ background: ACCENT, color: "white", letterSpacing: "0.04em" }}
<div className="text-[11.5px] font-medium tabular-nums"> >
<span className="text-stone-400" style={{ fontWeight: 600 }}>0</span> {String(index + 1).padStart(2, "0")}
</span>
);
const headerRow = (interactive: boolean) => (
<div
className={`flex items-center justify-between gap-4 px-6 py-4 ${interactive ? "cursor-pointer group" : ""}`}
style={{ borderBottom: interactive && open ? `1px solid ${TM_BORDER}` : "none" }}
onClick={interactive ? () => setOpen((o) => !o) : undefined}
>
<div className="min-w-0 flex items-center gap-3">
{numberTag}
<div className="text-[17px] font-bold tracking-tight leading-none" style={{ color: INK }}>{title}</div>
<div className="text-[11.5px] font-medium tabular-nums ml-1">
<span style={{ color: populated > 0 ? ACCENT : "#a8a29e", fontWeight: 700 }}>{populated}</span>
<span className="text-stone-300"> / </span> <span className="text-stone-300"> / </span>
<span className="text-stone-400">{fields.length}</span> <span className="text-stone-400">{fields.length}</span>
</div> </div>
</div> </div>
<div className="mt-2 text-[13px] text-stone-400 leading-relaxed"> {interactive && (
<span className="shrink-0 text-stone-400 group-hover:text-stone-700 transition-colors">
{open ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</span>
)}
</div>
);
if (isEmpty) {
return (
<div className="bg-white rounded-none overflow-hidden" style={{ border: CARD_BORDER }}>
{headerRow(false)}
<div className="px-6 pb-5 text-[13px] text-stone-400 leading-relaxed" style={{ marginTop: -8 }}>
{SECTION_EMPTY[sectionKey] ?? "Nothing recorded here yet."} {SECTION_EMPTY[sectionKey] ?? "Nothing recorded here yet."}
</div> </div>
</div> </div>
@ -788,30 +886,12 @@ function SectionCard({
} }
return ( return (
<div className="bg-white rounded-none" style={{ border: CARD_BORDER }}> <div className="bg-white rounded-none overflow-hidden" style={{ border: CARD_BORDER }}>
<button {headerRow(true)}
onClick={() => setOpen((o) => !o)}
className="w-full flex items-center justify-between text-left gap-4 group px-6 py-4"
style={{ borderBottom: open ? `1px solid ${TM_BORDER}` : "none" }}
>
<div className="min-w-0 flex items-baseline gap-3">
<div className="text-[18px] font-bold tracking-tight leading-none" style={{ color: INK }}>
{title}
</div>
<div className="text-[11.5px] font-medium tabular-nums">
<span style={{ color: populated > 0 ? ACCENT : "#a8a29e", fontWeight: 600 }}>{populated}</span>
<span className="text-stone-300"> / </span>
<span className="text-stone-400">{fields.length}</span>
</div>
</div>
<span className="shrink-0 text-stone-400 group-hover:text-stone-600 transition-colors">
{open ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</span>
</button>
{open && ( {open && (
<dl className="px-6 py-2"> <dl className="px-6 py-2">
{fields.map((f, i) => { {fields.map((f) => {
const raw = data[f.field_key]; const raw = data[f.field_key];
const empty = raw == null || raw === "" || raw === "—"; const empty = raw == null || raw === "" || raw === "—";
const text = empty ? "—" : fmtCellText(raw, f.data_type, f.field_key); const text = empty ? "—" : fmtCellText(raw, f.data_type, f.field_key);
@ -819,8 +899,7 @@ function SectionCard({
return ( return (
<div <div
key={f.field_key} key={f.field_key}
className="flex items-baseline gap-6 py-2.5" className="flex items-baseline gap-6 py-2"
style={{ borderTop: i === 0 ? "none" : `1px solid #f1f1f4` }}
> >
<dt className="w-48 shrink-0 text-[11px] font-semibold uppercase tracking-[0.03em] text-stone-500"> <dt className="w-48 shrink-0 text-[11px] font-semibold uppercase tracking-[0.03em] text-stone-500">
{fmtLabel(f.output_label)} {fmtLabel(f.output_label)}