// 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, Inbox, Star, ThumbsUp, ThumbsDown, User, Hash, } 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" }, "Expert TD Scheduled": { 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 ? ( <>

{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}
{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 (
All leads
{total_count} records · 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-lg 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.length > 0 && } {fields.map((f, i) => { const sorted = query.sort_by === f.field_key; return ( ); })} {actionColumns.length > 0 && ( )} {rows.length === 0 ? ( ) : rows.map((row, i) => ( onRowClick(String(row.instance_id))} className="group/row cursor-pointer hover:bg-[#f6f2ea]/60 transition-colors" > {fields.map((f, fi) => ( ))} {actionColumns.length > 0 && ( )} ))}
handleSort(f.field_key)} className={`py-3 text-left text-[11.5px] font-semibold cursor-pointer select-none whitespace-nowrap group/th transition-colors ${ 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" ? : ) : } Action
0 ? 1 : 0)} className="py-20 text-center">
No leads here yet
{query.search ? "Try a different search term" : "New leads will show up here as they come in"}
{query.search && ( )}
{renderCell(row[f.field_key], f.data_type, f.field_key)}
e.stopPropagation()}>
{actionColumns.flatMap((ac) => ac.buttons.filter((b) => evalShowConditions(b.show_conditions, row)) ).map((b: ActionButton, bi) => { const aid = b.params?.activity_uid; if (!aid) return null; return ( ); })}
)} {total_count > 0 && (
Show per page
{from}–{to} of {total_count}
{total_pages > 1 && (
{pageList(page, total_pages).map((p, idx) => p === "…" ? ( ) : ( ) )}
)}
)}
); } const AVATAR_PALETTE: { bg: string; fg: string }[] = [ { bg: "#fde2e8", fg: "#9d0d24" }, // soft rose { bg: "#e3e8f5", fg: "#1e3a8a" }, // soft navy { bg: "#e8f0e3", fg: "#3f6212" }, // soft olive { bg: "#f5ead3", fg: "#854d0e" }, // soft amber { bg: "#e5dff5", fg: "#5b21b6" }, // soft violet { bg: "#d6ecec", fg: "#155e75" }, // soft teal { bg: "#f5e0d3", fg: "#9a3412" }, // soft terracotta { bg: "#e6e8ec", fg: "#334155" }, // soft slate ]; function avatarColor(name: string): { bg: string; fg: string } { let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0; return AVATAR_PALETTE[h % AVATAR_PALETTE.length]; } const MODEL_DOT: Record = { "Thar Roxx": "#9d0d24", "XUV700": "#1e3a8a", "XUV3XO": "#155e75", "Scorpio-N": "#5b21b6", "Bolero Neo": "#854d0e", }; function modelColor(model: string): string { if (MODEL_DOT[model]) return MODEL_DOT[model]; let h = 0; for (let i = 0; i < model.length; i++) h = (h * 31 + model.charCodeAt(i)) >>> 0; const fallback = ["#9d0d24", "#1e3a8a", "#155e75", "#5b21b6", "#854d0e", "#3f6212"]; return fallback[h % fallback.length]; } function pageList(current: number, total: number): (number | "…")[] { if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1); const pages: (number | "…")[] = [1]; const start = Math.max(2, current - 1); const end = Math.min(total - 1, current + 1); if (start > 2) pages.push("…"); for (let i = start; i <= end; i++) pages.push(i); if (end < total - 1) pages.push("…"); pages.push(total); return 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 (fieldKey === "customer_name") { const name = String(value); const initials = name.split(" ").map((n) => n[0]).join("").slice(0, 2).toUpperCase(); const c = avatarColor(name); return ( {initials} {name} ); } if (fieldKey === "customer_phone" || fieldKey === "phone") { const phone = typeof value === "object" && value !== null ? String((value as any).phone_with_dial_code ?? (value as any).phone ?? "") : String(value); if (!phone) return ; return {phone}; } if (fieldKey === "model_interest" || fieldKey === "model") { const dot = modelColor(String(value)); return ( {String(value)} ); } if (fieldKey === "feedback_rating") { const n = Number(value); if (!Number.isFinite(n)) return ; // Render as 5 stars (rating is /10; halve to /5). const filled = Math.round(n / 2); return ( {Array.from({ length: 5 }, (_, i) => ( ))} {n.toFixed(n % 1 === 0 ? 0 : 1)} ); } if (fieldKey === "feedback_would_recommend") { const v = value === true || value === "true" || value === 1 || value === "1"; const no = value === false || value === "false" || value === 0 || value === "0"; if (!v && !no) return ; return v ? Yes : No; } 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: }]; if (instanceState === STATE_IDS.EXPERT_TD_SCHEDULED) return [{ id: ACTIVITY_IDS.MARK_EXPERT_TD_DONE, label: "Mark Expert TD Done", 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. Recording-URL fields are pulled out separately so // they can surface in the narrow lane as a "Play call" quick action instead of // being buried inside the Feedback/AI section. const groups: Record = { customer: [], vehicle: [], booking: [], feedback: [], ai: [], other: [] }; const recordings: typeof dv.fields = []; 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; if (/recording_url$/i.test(f.field_key)) { recordings.push(f); continue; } // Drop plumbing IDs (feedback_call_id, scheduling_call_id, …) from DV sections. if (/_call_id$/i.test(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); } // Lead age — relative-time string from created_at. Empty if no timestamp. const leadAgeLabel = (() => { const ts = dv.data["created_at"]; if (!ts) return ""; const ms = Date.now() - new Date(String(ts)).getTime(); if (!Number.isFinite(ms) || ms < 0) return ""; const mins = Math.floor(ms / 60000); if (mins < 1) return "just now"; if (mins < 60) return `${mins} min${mins === 1 ? "" : "s"} ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs} hour${hrs === 1 ? "" : "s"} ago`; const days = Math.floor(hrs / 24); if (days < 30) return `${days} day${days === 1 ? "" : "s"} ago`; const months = Math.floor(days / 30); if (months < 12) return `${months} month${months === 1 ? "" : "s"} ago`; const years = Math.floor(days / 365); return `${years} year${years === 1 ? "" : "s"} ago`; })(); const playableRecordings = recordings .map((f) => ({ field: f, url: dv.data[f.field_key] })) .filter((r) => r.url != null && r.url !== "" && r.url !== "—") as { field: typeof recordings[number]; url: string }[]; const hasQuickInfo = !!leadAgeLabel || playableRecordings.length > 0; const phone = (() => { const v = dv.data["customer_phone"]; if (typeof v === "object" && v !== null) return String((v as any).phone_with_dial_code ?? (v as any).phone ?? ""); return v ? String(v) : ""; })(); const createdDate = dv.data["created_at"] ? new Date(String(dv.data["created_at"])).toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" }) : ""; return (
{/* Breadcrumb above the card, on the page surface */}
/ {heroValue || `#${instanceId}`}
{aiInFlight && (
Maya is on the call
The AI scheduler is handling this lead. This page refreshes when complete.
)} {/* Unified card — hero on top, hairline divider, then sub-section grid. */}
{/* Hero region — avatar + name + inline state pill on the left, actions on the right. Metadata row uses small inline icons instead of middots for clarity. */}
{/* Avatar circle — first two initials of the customer name */}
{(() => { const src = heroValue || `#${instanceId}`; const parts = src.replace(/[#]/g, "").trim().split(/\s+/).filter(Boolean); const initials = parts.length >= 2 ? (parts[0][0] + parts[1][0]) : (parts[0] || "?").slice(0, 2); return initials.toUpperCase(); })()}

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

{/* Metadata row — small inline icons, less ornament than middots */}
{phone && ( {phone} )} {model && ( {model} )} {createdDate && ( {createdDate} )} {instanceId}
{stateName && ( {stateName} )} {actions.length > 0 && actions.map((a, i) => ( ))}
{/* Sub-section grid — inside the unified card, separated from hero by a hairline. Hide the AI analysis block on the Scheduled Test Drives view; the dealer is the audience there and the AI fields would just be noise. */} {(() => { const visible = Object.entries(groups).filter( ([key, fs]) => fs.length > 0 && !(screenUuid === "98af93e0-0f66-444b-a007-129b322be86b" && key === "ai") ); const wide = visible.filter(([key]) => SECTION_LANE[key] !== "narrow"); const narrow = visible.filter(([key]) => SECTION_LANE[key] === "narrow"); if (visible.length === 0 && !hasQuickInfo) return null; const hasBoth = wide.length > 0 && (narrow.length > 0 || hasQuickInfo); const quickInfo = hasQuickInfo ? (
At a Glance
{leadAgeLabel && (
Lead age
{leadAgeLabel}
)} {playableRecordings.map((r) => (
{fmtLabel(r.field.output_label) || "Recording"}
Play call
))}
) : null; return (
{hasBoth ? (
{wide.map(([key, fs]) => ( ))}
{quickInfo} {narrow.map(([key, fs]) => ( ))}
) : (
{quickInfo} {visible.map(([key, fs]) => ( ))}
)}
); })()}
{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" }, [ACTIVITY_IDS.CONCERN_EXPERT_TD_BOOKED]: { label: "Concern → Expert TD booked", tone: "ai" }, [ACTIVITY_IDS.MARK_EXPERT_TD_DONE]: { label: "Expert TD marked done", tone: "user" }, }; const SECTION_TITLE: Record = { customer: "Customer", vehicle: "Vehicle", booking: "Booking", feedback: "Feedback", ai: "AI · Call Outcome", other: "Other", }; // Lane assignment for the DV sub-section grid. Sections with long-form text or // many fields go in the wide (left, col-span-2) lane; short metadata sections // stack in the narrow (right) lane. const SECTION_LANE: Record = { customer: "narrow", vehicle: "narrow", booking: "wide", feedback: "wide", ai: "wide", other: "wide", }; const SECTION_EMPTY: Record = { customer: { icon: , title: "No customer details yet", body: "Captured when the lead is created.", }, vehicle: { icon: , title: "No vehicle interest recorded", body: "Filled in once the customer picks a model.", }, booking: { icon: , title: "No booking yet", body: "Fills in once a test drive is scheduled.", }, feedback: { icon: , title: "No feedback yet", body: "Captured automatically after the post-test-drive call.", }, ai: { icon: , title: "No AI call activity", body: "Maya's call summary appears here once she's connected.", }, other: { icon: , title: "Nothing recorded here yet", body: "Fields will appear as the workflow progresses.", }, }; function SubSection({ sectionKey, fields, data, lane, }: { sectionKey: string; fields: any[]; data: Record; lane: "wide" | "narrow"; }) { 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 [expanded, setExpanded] = useState>({}); const LONGTEXT_PREVIEW = 220; const gridClass = lane === "narrow" ? "grid grid-cols-1 gap-y-5" : "grid grid-cols-2 gap-x-8 gap-y-5"; return (
{/* Section header — small pink/red uppercase letter-spaced label */}
{title}
{isEmpty ? ( (() => { const e = SECTION_EMPTY[sectionKey] ?? SECTION_EMPTY.other; return (
{e.icon}
{e.title}
{e.body}
); })() ) : (
{fields .filter((f) => { const r = data[f.field_key]; return r != null && r !== "" && r !== "—"; }) .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 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; // Any prose-style value (longtext data_type, or just a long string) reads // as a paragraph — kill the bold weight and let it breathe. const isProse = !empty && (f.data_type === "longtext" || text.length > 80); const isRecommend = !empty && f.field_key === "feedback_would_recommend"; // UUID-ish identifiers (current_state_id, workflow_id, …) read as // plumbing — render compact monospaced chips instead of bold values. const isIdChip = !empty && (/_id$/i.test(f.field_key) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(raw))); // Short enum-ish status fields (scheduling_ended_reason, feedback_failure, // test_drive_outcome, …) read as status chips, tone derived from the value. const isStatusChip = !empty && !isIdChip && /_(reason|failure|outcome|status)$/i.test(f.field_key); const statusTone = isStatusChip ? (() => { const v = String(raw).toLowerCase(); if (/(fail|error|reject|cancel|abort|no_answer|busy|unreach)/.test(v)) return { bg: "#fde2e8", fg: "#9d0d24" }; if (/(success|complete|done|book|confirm|positive|good|happy)/.test(v)) return { bg: "#e7f3eb", fg: "#15803d" }; return { bg: "#f5f5f4", fg: "#57534e" }; })() : null; const isOpen = !!expanded[f.field_key]; const spanFull = lane === "wide" && isProse; return (
{fmtLabel(f.output_label)}
{isStatusChip && statusTone ? ( {text} ) : isIdChip ? ( {text} ) : isRecommend ? ( (raw === true || String(raw).toLowerCase() === "true" || String(raw).toLowerCase() === "yes") ? ( Yes ) : ( No ) ) : isRecording ? ( Play recording ) : isUrl ? ( {text} ) : isLongText ? ( {isOpen ? text : text.slice(0, LONGTEXT_PREVIEW).trimEnd() + "…"} ) : ( text )}
); })}
)}
); }