From d8f99a2e705d286d71af8e80966619090a31a206 Mon Sep 17 00:00:00 2001 From: Bhanu Prakash Sai Potteri Date: Fri, 29 May 2026 17:55:29 +0530 Subject: [PATCH] DV: activity timeline from instance._activities; calendar mode/expert cues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Variant B DV right rail now shows Activity Timeline (LOG); At a Glance + Customer/Vehicle moved to wide lane. Recordings hoisted into left lane. - Timeline derived from instance.data._activities + synthesised INIT from created_at — no /view/audit dependency. - Per-activity context line drawn from instance top-level data (booking_id, feedback_rating, concern, etc.) via lib/activity.ts. - Calendar cells encode td_mode (home/showroom icon) and is_expert_td (gold inset ring); BookingPanel gains Expert badge + Mode/Address rows. - DetailViewPanel gains a TimelineSection mirror for any non-VariantB callers. - fmtAct lifted from AppShell into src/lib/activity.ts so timeline + legacy AppShell stay in sync. - getInstance now sends instance_id as int (Go decoder needs int64). --- src/api/viewService.ts | 5 +- src/components/AppShell.tsx | 17 +--- src/components/DetailViewPanel.tsx | 138 ++++++++++++++++++++++++- src/components/variants/Calendar.tsx | 58 +++++++++-- src/components/variants/VariantB.tsx | 144 ++++++++++++--------------- src/hooks/usePrototypeData.ts | 63 +++++++++++- src/lib/activity.ts | 89 +++++++++++++++++ 7 files changed, 403 insertions(+), 111 deletions(-) create mode 100644 src/lib/activity.ts diff --git a/src/api/viewService.ts b/src/api/viewService.ts index 4ad1a99..0dcf335 100644 --- a/src/api/viewService.ts +++ b/src/api/viewService.ts @@ -364,7 +364,7 @@ export interface InstanceResponse { export function getInstance(workflowUuid: string, instanceId: string): Promise { return request("POST", `/app/${APP_ID}/instance`, { workflow_uuid: workflowUuid, - instance_id: instanceId, + instance_id: Number(instanceId), }); } @@ -380,6 +380,9 @@ export interface AuditEntry { data: Record; execution_state: string; created_at: string; + // Optional human-readable summary line. Populated by useInstanceMeta when + // the audit is synthesised from instance.data._activities. + context?: string; } export function getAuditLog(instanceId: string): Promise { diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 1a94cb7..fb7f2ff 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -12,6 +12,7 @@ import { type LayoutElement, type NavItem, type AuditEntry, } from "../api/viewService"; import { WORKFLOW_ID, ACTIVITY_IDS, STATE_IDS, DETAIL_VIEW_IDS, SCREEN_TO_DV } from "../config"; +import { fmtAct } from "../lib/activity"; // State → contextual action buttons shown above the detail view. const ACTIVITY_META: Record = { @@ -365,19 +366,3 @@ function CurrentAppShell() { ); } -// Friendlier label for our UUID-style activity IDs. -function fmtAct(id: string): string { - switch (id) { - case ACTIVITY_IDS.INIT_ACTIVITY: return "Lead Captured"; - case ACTIVITY_IDS.MARK_TEST_DRIVE_DONE: return "Test Drive Completed"; - case ACTIVITY_IDS.AI_CONFIRM_SCHEDULING_SUCCESS: return "AI · Scheduling Confirmed"; - case ACTIVITY_IDS.AI_CONFIRM_SCHEDULING_FAILED: return "AI · Scheduling Failed"; - case ACTIVITY_IDS.RETRY_SCHEDULING_CALL: return "Retry Scheduling Call"; - case ACTIVITY_IDS.AI_CONFIRM_FEEDBACK_COLLECTED: return "AI · Feedback Collected"; - case ACTIVITY_IDS.AI_CONFIRM_FEEDBACK_FAILED: return "AI · Feedback Failed"; - case ACTIVITY_IDS.RETRY_FEEDBACK_CALL: return "Retry Feedback Call"; - case ACTIVITY_IDS.CONCERN_EXPERT_TD_BOOKED: return "AI · Concern → Expert TD Booked"; - case ACTIVITY_IDS.MARK_EXPERT_TD_DONE: return "Expert TD Completed"; - default: return id.slice(0, 8) + "…"; - } -} diff --git a/src/components/DetailViewPanel.tsx b/src/components/DetailViewPanel.tsx index fb27bcd..c196c76 100644 --- a/src/components/DetailViewPanel.tsx +++ b/src/components/DetailViewPanel.tsx @@ -1,6 +1,8 @@ import { useEffect, useState, useRef } from "react"; -import { Hash, Calendar, Sparkles, TrendingUp, MessageSquare, ChevronDown, ChevronUp } from "lucide-react"; -import { getDVScreen, getDetailView } from "../api/viewService"; +import { Hash, Calendar, Sparkles, TrendingUp, MessageSquare, ChevronDown, ChevronUp, Activity } from "lucide-react"; +import { getDVScreen, getDetailView, getInstance, type InstanceResponse } from "../api/viewService"; +import { WORKFLOW_ID, STATE_IDS, ACTIVITY_IDS } from "../config"; +import { fmtAct } from "../lib/activity"; interface Field { field_key: string; output_label: string; data_type: string } interface Props { viewId: number | string; instanceId: string } @@ -303,6 +305,138 @@ export default function DetailViewPanel({ viewId, instanceId }: Props) { )} )} + + {/* ── Activity timeline ──────────────────────────────────────── */} + {data.instance_id && } + + ); +} + +// --------------------------------------------------------------------------- +// TimelineSection — derived from instance.data._activities (no audit endpoint +// needed). INIT is synthesised from instance.created_at since the platform +// doesn't write INIT into _activities. +// --------------------------------------------------------------------------- + +interface ActivityRecord { + _system?: { created_at?: string; user_id?: string; user_roles?: string[] | null }; +} + +interface TimelineEntry { + activityId: string; + createdAt: string; + userRoles: string[] | null; + synthetic: boolean; +} + +const FAILURE_STATE_IDS = new Set([ + STATE_IDS.SCHEDULING_CALL_FAILED, + STATE_IDS.FEEDBACK_CALL_FAILED, +]); + +function TimelineSection({ instanceId }: { instanceId: string }) { + const [inst, setInst] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (!instanceId) return; + let cancelled = false; + setInst(null); setError(null); + getInstance(WORKFLOW_ID, instanceId) + .then((r) => !cancelled && setInst(r)) + .catch((e: any) => !cancelled && setError(e?.message ?? "failed to load")); + return () => { cancelled = true; }; + }, [instanceId]); + + if (error) return null; + if (inst === null) { + return ( +
+ Loading activity timeline… +
+ ); + } + + const activities = (inst.data as { _activities?: Record } | null)?._activities; + const entries: TimelineEntry[] = []; + + // Synthesise INIT from instance.created_at — _activities never includes it. + entries.push({ + activityId: ACTIVITY_IDS.INIT_ACTIVITY, + createdAt: inst.created_at, + userRoles: null, + synthetic: true, + }); + + if (activities) { + for (const [activityId, record] of Object.entries(activities)) { + entries.push({ + activityId, + createdAt: record._system?.created_at ?? inst.created_at, + userRoles: record._system?.user_roles ?? null, + synthetic: false, + }); + } + } + + entries.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + + const lastIdx = entries.length - 1; + const inFailureState = FAILURE_STATE_IDS.has(inst.current_state_id); + + return ( +
+

+ Activity Timeline +

+
    + + {entries.map((e, i) => { + const isLast = i === lastIdx; + const isFirst = i === 0; + const failed = isLast && inFailureState; + const dotBg = failed ? "#dc2626" : isLast ? "#C8102E" : isFirst ? "#10b981" : "#d1d5db"; + const actor = (e.userRoles && e.userRoles.length > 0) ? e.userRoles.join(", ") : "system"; + return ( +
  1. + +
    + + {fmtAct(e.activityId)} + + {failed && ( + + Failed + + )} + {isLast && !failed && ( + + Latest + + )} +
    +
    + {new Date(e.createdAt).toLocaleString("en-IN", { + day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", + })} + · + {actor} +
    +
  2. + ); + })} +
); } diff --git a/src/components/variants/Calendar.tsx b/src/components/variants/Calendar.tsx index b6b09dc..370acfd 100644 --- a/src/components/variants/Calendar.tsx +++ b/src/components/variants/Calendar.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from "react"; -import { ChevronLeft, ChevronRight, Phone, X, Calendar as CalendarIcon } from "lucide-react"; +import { ChevronLeft, ChevronRight, Phone, X, Calendar as CalendarIcon, Home, Building2 } from "lucide-react"; import { fetchRdbmsLookupRecords } from "../../api/viewService"; import { CALENDAR_LOOKUPS, WORKFLOW_NUMERIC_ID, ACTIVITY_IDS } from "../../config"; @@ -27,6 +27,9 @@ interface TestDrive { feedback_would_recommend?: boolean | null; feedback_collected_at?: string | null; notes?: string | null; + td_mode?: "home" | "showroom" | null; + is_expert_td?: boolean | null; + home_address?: string | null; } interface Car { @@ -254,7 +257,7 @@ export function CalendarView() { {/* Legend */} -
+
Booked @@ -263,6 +266,16 @@ export function CalendarView() { Available
+
+ Showroom +
+
+ Home +
+
+ + Expert TD +
{totalBookings} bookings across {weekDays.length} days
@@ -321,20 +334,34 @@ export function CalendarView() {
); } - const booked = cell.status === "booked"; + const booked = cell.status === "booked"; + const isHome = cell.td_mode === "home"; + const isExpert = !!cell.is_expert_td; + const ModeIcon = isHome ? Home : Building2; return (