// PROTOTYPE — Variant B: "Insights Studio" // Visual language: Tech Mahindra brand — red #e31837 (their identity color) // with navy #061f5c as structural supporting. Poppins typography on a cream // #f6f2ea canvas. Left sidebar, hero KPI cards, big charts as centerpiece, // table below as support. DV: full-page replace with hero card + collapsible // section groups. 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, Car, ClipboardList, PhoneOutgoing, CalendarCheck2, MessageSquareHeart, Trophy, ArrowLeft, ChevronDown, ChevronUp, Plus, PlayCircle, ExternalLink, } from "lucide-react"; import { BarChart as RBarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, Cell, PieChart as RPieChart, Pie, Legend, } from "recharts"; import { useAuthContext } from "../../hooks/AuthContext"; import { useHeaderConfig, useActiveScreenRecordView, useRecordView, useDetailView, useInstanceMeta, evalShowConditions, fmtCellText, fmtLabel, type ActionButton, type AnalyticsSpec, type ScreenButton, } from "../../hooks/usePrototypeData"; import FormModal from "../FormModal"; import { SCREEN_TO_DV, DETAIL_VIEW_IDS, ACTIVITY_IDS, STATE_IDS, DEMO_SLOTS_SCREEN_UUID } from "../../config"; import { CalendarView } from "./Calendar"; // --------------------------------------------------------------------------- // Palette — Tech Mahindra brand. Red is the identity color (the "Tech" in // their logo, their CTAs). Navy is structural/supporting. Wine deepens the // red. Cream is the canvas. // --------------------------------------------------------------------------- const ACCENT = "#e31837"; // TM red — primary identity const ACCENT_DARK = "#5f0229"; // TM wine — gradient end / depth const ACCENT_SOFT = "#fde2e8"; // TM red soft tint const TM_NAVY = "#061f5c"; // TM navy — structural supporting const SURFACE = "#ffffff"; // sidebar: pure white, separated by TM border const SURFACE_ALT = "#fafafa"; // hover/subtle wash on white surfaces const TM_BORDER = "#e4e4ed"; // TM neutral border (from techmahindra.com CSS) const INK = "#17181a"; // TM body text // Chart palette — TM red leads, navy + slate support, wine + bright red round it out. const PALETTE = ["#e31837", "#061f5c", "#5f0229", "#3b4f61", "#bc2130", "#5b7a95"]; // TM signature visual treatments — pulled from techmahindra.com CSS. // They use sharp 0px corners, hard offset shadows (4px 4px 0 #color, no blur), // and red eyebrow labels with -0.02em tracking. // Flat card chrome — 1px TM border instead of the brutalist hard offset shadow. // In a dense dashboard the offset shadows competed with the data; a hairline // border gives definition without the visual noise. const CARD_BORDER = `1px solid ${TM_BORDER}`; const EYEBROW_CLS = "text-[14px] font-semibold tracking-[-0.02em] leading-[110%]"; const EYEBROW_STYLE = { color: ACCENT }; // TM red eyebrow const STATE_TONE: Record = { "Awaiting Scheduler Call": { bg: "#FEF3C7", fg: "#92400E", dot: "#e6ab2e" }, "Scheduled": { bg: "#FEF3C7", fg: "#92400E", dot: "#e6ab2e" }, "Awaiting Feedback Call": { bg: "#FEF3C7", fg: "#92400E", dot: "#e6ab2e" }, "Feedback Collected": { bg: "#DCFCE7", fg: "#166534", dot: "#27a846" }, "Scheduling Call Failed": { bg: ACCENT_SOFT, fg: ACCENT_DARK, dot: ACCENT }, "Feedback Call Failed": { bg: ACCENT_SOFT, fg: ACCENT_DARK, dot: ACCENT }, "End State": { bg: "#F5F5F4", fg: "#57534E", dot: "#81818c" }, }; function tone(name: string) { return STATE_TONE[name] || { bg: "#F5F5F4", fg: "#57534E", dot: "#A8A29E" }; } function iconFor(label: string) { const k = label.toLowerCase(); if (k.includes("lead")) return ; if (k.includes("active") || k.includes("scheduling")) return ; if (k.includes("scheduled")) return ; if (k.includes("feedback")) return ; if (k.includes("completed") || k.includes("done")) return ; return ; } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- export default function VariantB() { const navigate = useNavigate(); const params = useParams(); const activeScreenId = params.screenId || null; const detailInstanceId = params.instanceId || null; const { navItems } = useHeaderConfig(); const [refreshKey, setRefreshKey] = useState(0); const [formModal, setFormModal] = useState<{ activityId: string; instanceId?: string; title: string } | null>(null); useEffect(() => { if (!activeScreenId && !detailInstanceId && navItems.length > 0) { navigate(`/screen/${navItems[0].screen_uuid}?variant=B`, { replace: true }); } }, [activeScreenId, detailInstanceId, navItems, navigate]); const goTo = (uuid: string) => navigate(`/screen/${uuid}?variant=B`); const openDetail = (id: string) => navigate(`/screen/${activeScreenId}/detail/${id}?variant=B`); const back = () => navigate(`/screen/${activeScreenId}?variant=B`); const activeNav = navItems.find((n) => n.screen_uuid === activeScreenId); return (
{/* Sidebar — cream, gives the TM warmth a permanent home */} {/* Main — clean white canvas for data */}
{detailInstanceId && activeScreenId ? ( setFormModal(a)} breadcrumbLabel={activeNav?.label ?? "Pipeline"} /> ) : activeScreenId === DEMO_SLOTS_SCREEN_UUID ? ( ) : activeScreenId ? ( <>
Test-drive lifecycle

{activeNav?.label ?? "Dashboard"}

setFormModal(a)} refreshKey={refreshKey} /> ) : null}
{formModal && ( setFormModal(null)} onSuccess={() => { setFormModal(null); setRefreshKey((k) => k + 1); }} /> )}
); } // --------------------------------------------------------------------------- // Sidebar // --------------------------------------------------------------------------- function Sidebar({ navItems, activeScreenId, onNavigate, }: { navItems: any[]; activeScreenId: string | null; onNavigate: (u: string) => void }) { const { user, logout } = useAuthContext(); const navigate = useNavigate(); const initials = (user?.name || "U").split(" ").map((n) => n[0]).join("").slice(0, 2).toUpperCase(); const role = user?.roles?.[0] ?? ""; const handleLogout = () => { logout(); navigate("/login"); }; return ( ); } // --------------------------------------------------------------------------- // Dashboard content // --------------------------------------------------------------------------- function DashboardContent({ activeScreenId, onRowClick, onOpenForm, refreshKey, }: { activeScreenId: string; onRowClick: (id: string) => void; onOpenForm: (a: { activityId: string; instanceId?: string; title: string }) => void; refreshKey: number; }) { const { viewId, screenButtons } = useActiveScreenRecordView(activeScreenId); const rv = useRecordView(viewId); useEffect(() => { if (refreshKey > 0) rv.refetch(); /* eslint-disable-next-line */ }, [refreshKey]); if (!viewId) return
Loading…
; const tileSpecs = rv.analytics.filter((a): a is Extract => a.kind === "tile"); const chartSpecs = rv.analytics.filter((a): a is Extract => a.kind === "chart"); return (
{/* Hero KPI cards */} {tileSpecs.length > 0 && (
{tileSpecs.map((t, i) => { const v = rv.tileValues.find((tv) => tv.tile_uid === t.uid); const display = v?.value == null ? "—" : typeof v.value === "number" ? v.value.toLocaleString("en-IN") : String(v.value); const isPrimary = i === 0; return (
{/* Navy structural accent in the corner of the primary red tile */} {isPrimary && ( )}
{t.title}
{rv.loading || rv.dataLoading ? : display}
total this week
); })}
)} {/* Big charts as centerpiece */} {chartSpecs.length > 0 && (
{chartSpecs.map((c) => { const cd = rv.chartData.find((d) => d.chart_uid === c.uid); const data = (cd?.rows ?? []) .map((r) => ({ name: String(r.dimension ?? "—"), value: Number(r.value ?? 0) })) .filter((d) => d.name !== "—" && d.value > 0); return (
Insight
{c.title}
Distribution across the pipeline
{rv.loading || rv.dataLoading ? (
) : data.length === 0 ? (
No data yet
) : (
{c.chartType === "pie" ? ( {data.map((_, i) => )} ) : ( {data.map((_, i) => )} )}
)}
); })}
)}
); } // Per-column max width (px). Long-text columns get the widest cap; the rest // stay tight so a single overflowing summary can't push the whole table wide. function colMaxWidth(dataType: string, fieldKey: string): number { const k = fieldKey.toLowerCase(); if (dataType === "longtext") return 360; if (k.includes("summary") || k.includes("reason") || k.includes("transcript") || k.includes("recording")) return 320; if (k === "instance_id" || k.endsWith("_id")) return 110; if (dataType === "number") return 130; if (dataType === "date" || dataType === "datetime") return 140; if (k.includes("email")) return 220; if (k.includes("phone")) return 160; return 200; } function RecordTable({ rv, onRowClick, onOpenForm, screenButtons, }: { rv: ReturnType; onRowClick: (id: string) => void; onOpenForm: (a: { activityId: string; instanceId?: string; title: string }) => void; screenButtons: ScreenButton[]; }) { const [searchInput, setSearchInput] = useState(""); const { fields, rows, columnLabels, actionColumns, pagination, query, dataLoading, error } = rv; const { page, total_count, total_pages, limit } = pagination; const from = total_count > 0 ? (page - 1) * limit + 1 : 0; const to = Math.min(page * limit, total_count); const handleSort = (k: string) => rv.setQuery((q) => ({ ...q, page: 1, sort_by: k, sort_dir: q.sort_by === k && q.sort_dir === "asc" ? "desc" : "asc" })); const submitSearch = (e: React.FormEvent) => { e.preventDefault(); rv.setQuery((q) => ({ ...q, page: 1, search: searchInput })); }; return (
Data
All records
{total_count} leads · showing {from}–{to}
{screenButtons.map((b) => ( ))}
setSearchInput(e.target.value)} placeholder="Search leads…" className="w-56 h-9 pl-9 pr-3 text-[13px] border border-stone-200 rounded-none bg-stone-50 text-stone-800 focus:outline-none focus:bg-white" style={{ borderColor: searchInput ? ACCENT : undefined }} /> {searchInput && ( )}
{error ? (
{error}
) : (
{dataLoading && (
)} {fields.map((f) => ( ))} {actionColumns.map((ac) => ( ))} {fields.map((f, i) => { const sorted = query.sort_by === f.field_key; return ( ); })} {actionColumns.map((ac) => ( ))} {rows.length === 0 ? ( ) : rows.map((row, i) => ( onRowClick(String(row.instance_id))} className="cursor-pointer hover:bg-[#f6f2ea]/60 transition-colors" > {fields.map((f, fi) => ( ))} {actionColumns.map((ac) => { const visible = ac.buttons.filter((b) => evalShowConditions(b.show_conditions, row)); return ( ); })} ))}
handleSort(f.field_key)} className={`py-4 text-left text-[11.5px] font-semibold cursor-pointer select-none whitespace-nowrap ${ i === 0 ? "pl-7 pr-4" : "px-4" } ${sorted ? "text-[#e31837]" : "text-stone-500 hover:text-stone-800"}`} > {columnLabels[f.field_key] || f.output_label || f.field_key} {sorted && (query.sort_dir === "asc" ? : )} {ac.display}
No records yet
{renderCell(row[f.field_key], f.data_type, f.field_key)}
e.stopPropagation()}> {visible.length === 0 ? : (
{visible.map((b: ActionButton, bi) => { const aid = b.params?.activity_uid; if (!aid) return null; return ( ); })}
)}
)} {total_count > 0 && total_pages > 1 && (
Page {page} of {total_pages}
)}
); } function renderCell(value: unknown, dataType: string, fieldKey: string): React.ReactNode { if (value == null || value === "") return ; if (fieldKey === "current_state_name") { const t = tone(String(value)); return ( {String(value)} ); } if (fieldKey === "instance_id") { return #{String(value).slice(0, 8)}; } if (dataType === "number") return {fmtCellText(value, dataType, fieldKey)}; return fmtCellText(value, dataType, fieldKey); } // --------------------------------------------------------------------------- // Detail page (full-width replace) // --------------------------------------------------------------------------- function DetailPage({ screenUuid, instanceId, refreshKey, onBack, onAction, breadcrumbLabel, }: { screenUuid: string; instanceId: string; refreshKey: number; onBack: () => void; onAction: (a: { activityId: string; instanceId: string; title: string }) => void; breadcrumbLabel: string; }) { const dvViewId = SCREEN_TO_DV[screenUuid] || DETAIL_VIEW_IDS.INSTANCE_DETAIL; const dv = useDetailView(dvViewId, instanceId); const { instanceState, audit } = useInstanceMeta(instanceId, refreshKey); const actions = useMemo(() => { if (!instanceState) return [] as Array<{ id: string; label: string; icon: React.ReactNode }>; if (instanceState === STATE_IDS.SCHEDULED) return [{ id: ACTIVITY_IDS.MARK_TEST_DRIVE_DONE, label: "Mark Test Drive Done", icon: }]; if (instanceState === STATE_IDS.SCHEDULING_CALL_FAILED) return [{ id: ACTIVITY_IDS.RETRY_SCHEDULING_CALL, label: "Retry Scheduling Call", icon: }]; if (instanceState === STATE_IDS.FEEDBACK_CALL_FAILED) return [{ id: ACTIVITY_IDS.RETRY_FEEDBACK_CALL, label: "Retry Feedback Call", icon: }]; return []; }, [instanceState]); const aiInFlight = instanceState === STATE_IDS.AWAITING_SCHEDULER_CALL || instanceState === STATE_IDS.AWAITING_FEEDBACK_CALL; if (dv.loading) return
Loading lead…
; if (!dv.data) return null; const heroValue = String(dv.data["customer_name"] ?? ""); const stateName = String(dv.data["current_state_name"] ?? ""); const t = tone(stateName); const model = String(dv.data["model_interest"] ?? ""); // Group fields by prefix. const groups: Record = { customer: [], vehicle: [], booking: [], feedback: [], ai: [], other: [] }; const sys = new Set(["instance_id", "current_state_name", "created_at", "updated_at", "customer_name", "model_interest", "analysis_so_far"]); for (const f of dv.fields) { if (sys.has(f.field_key)) continue; const k = f.field_key.toLowerCase(); if (k.startsWith("customer_") || k === "phone" || k === "email" || k === "lead_source") groups.customer.push(f); else if (k.startsWith("vehicle_") || k === "variant" || k === "color" || k === "fuel_type") groups.vehicle.push(f); else if (k.startsWith("booking_") || k === "dealer_name" || k === "dealer_id" || k.includes("slot")) groups.booking.push(f); else if (k.startsWith("feedback_") || k === "rating" || k === "would_recommend" || k === "test_drive_outcome") groups.feedback.push(f); else if (k.startsWith("ai_") || k.startsWith("call_") || k.includes("summary") || k.includes("confidence") || k.includes("reasoning") || k.includes("failure_reason") || k.includes("escalation")) groups.ai.push(f); else groups.other.push(f); } return (
{/* 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 red slice on the far right — mirrors techmahindra.com footer */} {/* Back link — sits at the hero top, white on navy */}
/ Lead detail
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. */}
{stateName && (
{stateName}
)} {actions.length > 0 && (
{actions.map((a) => ( ))}
)}
{aiInFlight && (
Maya is on the call
The AI scheduler is handling this lead. This page refreshes when complete.
)} {/* Stack of independent section cards — each its own border, no shared chrome. */}
{Object.entries(groups).filter(([, fs]) => fs.length > 0).map(([key, fs], i) => ( ))}
{audit.length > 0 && (
LOG
Activity timeline
{audit.length} {audit.length === 1 ? "event" : "events"}
    {/* 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. ); })}
)}
); } // 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", booking: "Booking", feedback: "Feedback", ai: "AI · Call Outcome", other: "Other", }; const SECTION_EMPTY: Record = { customer: "No customer details captured yet.", vehicle: "No vehicle interest recorded yet.", booking: "No booking yet — fills in once a test drive is scheduled.", feedback: "No feedback yet — captured automatically after the post-test-drive call.", ai: "No AI call activity yet.", other: "Nothing recorded here yet.", }; function SectionCard({ index, sectionKey, fields, data, }: { index: number; sectionKey: string; fields: any[]; data: Record; }) { const title = SECTION_TITLE[sectionKey] ?? "Other"; const populated = fields.filter((f) => { const r = data[f.field_key]; return r != null && r !== "" && r !== "—"; }).length; const isEmpty = populated === 0; const [open, setOpen] = useState(!isEmpty); const [expanded, setExpanded] = useState>({}); const LONGTEXT_PREVIEW = 220; // 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 (
{headerRow(false)}
{SECTION_EMPTY[sectionKey] ?? "Nothing recorded here yet."}
); } return (
{headerRow(true)} {open && (
{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); const isProse = !empty && text.length > 60; const isRecording = !empty && /recording_url$/i.test(f.field_key); const isUrl = !empty && !isRecording && typeof raw === "string" && /^https?:\/\//i.test(raw); const isLongText = !empty && f.data_type === "longtext" && text.length > LONGTEXT_PREVIEW; const isOpen = !!expanded[f.field_key]; return (
{fmtLabel(f.output_label)}
{isRecording ? ( Play recording ) : isUrl ? ( {text} ) : isLongText ? ( {isOpen ? text : text.slice(0, LONGTEXT_PREVIEW).trimEnd() + "…"} ) : ( text )}
); })}
)}
); }