Demo Slots calendar: week grid, model filter, right-panel booking details with car images

This commit is contained in:
Bhanu Prakash Sai Potteri 2026-05-23 14:50:01 +05:30
parent db5d3cb34d
commit 19e1ddc0f8
9 changed files with 613 additions and 1 deletions

BIN
public/cars/bolero-neo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/cars/scorpio-n.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
public/cars/thar-roxx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/cars/xuv3xo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/cars/xuv700.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -381,3 +381,35 @@ export interface AuditEntry {
export function getAuditLog(instanceId: string): Promise<AuditEntry[]> { export function getAuditLog(instanceId: string): Promise<AuditEntry[]> {
return request("GET", `/app/${APP_ID}/view/audit?instance_id=${encodeURIComponent(instanceId)}`); return request("GET", `/app/${APP_ID}/view/audit?instance_id=${encodeURIComponent(instanceId)}`);
} }
// ---------------------------------------------------------------------------
// RDBMS lookup records — used by the demo calendar to pull rows from the
// `mahindra_demo` schema via the lookup-field config on the INIT activity.
// ---------------------------------------------------------------------------
export interface RdbmsLookupResponse {
records: Record<string, unknown>[];
total: number;
limit: number;
offset: number;
}
export interface RdbmsLookupQuery {
workflowId: number;
activityId: string;
fieldId: string;
limit?: number;
offset?: number;
search?: string;
}
export function fetchRdbmsLookupRecords(templateUuid: string, q: RdbmsLookupQuery): Promise<RdbmsLookupResponse> {
return request("POST", `/app/${APP_ID}/rdbms-templates/${encodeURIComponent(templateUuid)}/records`, {
workflow_id: q.workflowId,
activity_id: q.activityId,
field_id: q.fieldId,
limit: q.limit ?? 1000,
offset: q.offset ?? 0,
search: q.search ?? "",
});
}

View File

@ -0,0 +1,562 @@
import { useEffect, useMemo, useState } from "react";
import { ChevronLeft, ChevronRight, Phone, X, Calendar as CalendarIcon } from "lucide-react";
import { fetchRdbmsLookupRecords } from "../../api/viewService";
import { CALENDAR_LOOKUPS, WORKFLOW_NUMERIC_ID, ACTIVITY_IDS } from "../../config";
const ACCENT = "#e31837";
const ACCENT_SOFT = "#fde2e8";
const TM_NAVY = "#061f5c";
const TM_BORDER = "#e4e4ed";
const INK = "#17181a";
const IST_OFFSET_MIN = 330;
interface TestDrive {
id: number;
dealer_id: number;
car_id: number;
slot_start: string;
slot_end: string;
status: "available" | "booked";
customer_name?: string | null;
customer_phone?: string | null;
booked_at?: string | null;
completed_at?: string | null;
feedback_rating?: number | null;
feedback_liked_most?: string | null;
feedback_concern?: string | null;
feedback_would_recommend?: boolean | null;
feedback_collected_at?: string | null;
notes?: string | null;
}
interface Car {
id: number;
model: string;
variant?: string | null;
color?: string | null;
price_inr?: number | string | null;
}
function carSlug(model: string): string {
return model.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
function carImageUrl(model: string): string {
return `${import.meta.env.BASE_URL}cars/${carSlug(model)}.jpg`;
}
function fmtPriceInr(v: number | string | null | undefined): string {
if (v == null || v === "") return "—";
const n = typeof v === "string" ? Number(v) : v;
if (!Number.isFinite(n)) return String(v);
return n.toLocaleString("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 });
}
function toIST(iso: string): Date {
// Shift UTC to IST by adding 5:30. Returns a Date whose UTC fields equal IST wall-clock.
return new Date(new Date(iso).getTime() + IST_OFFSET_MIN * 60_000);
}
function istDayKey(iso: string): string {
const d = toIST(iso);
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
}
function istTimeKey(iso: string): string {
const d = toIST(iso);
return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
}
function fmtDayLabel(key: string): { weekday: string; day: string; month: string } {
const [y, m, d] = key.split("-").map(Number);
const dt = new Date(Date.UTC(y, m - 1, d));
return {
weekday: dt.toLocaleDateString("en-US", { weekday: "short", timeZone: "UTC" }),
day: String(d).padStart(2, "0"),
month: dt.toLocaleDateString("en-US", { month: "short", timeZone: "UTC" }),
};
}
function addDaysKey(key: string, days: number): string {
const [y, m, d] = key.split("-").map(Number);
const dt = new Date(Date.UTC(y, m - 1, d + days));
return `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, "0")}-${String(dt.getUTCDate()).padStart(2, "0")}`;
}
export function CalendarView() {
const [drives, setDrives] = useState<TestDrive[]>([]);
const [cars, setCars] = useState<Car[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [weekStart, setWeekStart] = useState<string | null>(null);
const [selected, setSelected] = useState<TestDrive | null>(null);
const [modelFilter, setModelFilter] = useState<string>("");
useEffect(() => {
let cancelled = false;
setLoading(true);
Promise.all([
fetchRdbmsLookupRecords(CALENDAR_LOOKUPS.TEST_DRIVES.rdbmsTemplateUuid, {
workflowId: WORKFLOW_NUMERIC_ID,
activityId: ACTIVITY_IDS.INIT_ACTIVITY,
fieldId: CALENDAR_LOOKUPS.TEST_DRIVES.fieldId,
limit: 1000,
}),
fetchRdbmsLookupRecords(CALENDAR_LOOKUPS.CARS.rdbmsTemplateUuid, {
workflowId: WORKFLOW_NUMERIC_ID,
activityId: ACTIVITY_IDS.INIT_ACTIVITY,
fieldId: CALENDAR_LOOKUPS.CARS.fieldId,
limit: 100,
}),
])
.then(([td, c]) => {
if (cancelled) return;
setDrives((td.records as unknown as TestDrive[]) ?? []);
setCars((c.records as unknown as Car[]) ?? []);
const days = (td.records as unknown as TestDrive[])
.map((r) => istDayKey(r.slot_start))
.sort();
setWeekStart(days[0] ?? null);
})
.catch((e: any) => !cancelled && setError(e?.message ?? "Failed to load"))
.finally(() => !cancelled && setLoading(false));
return () => { cancelled = true; };
}, []);
const carById = useMemo(() => {
const m = new Map<number, Car>();
cars.forEach((c) => m.set(Number(c.id), c));
return m;
}, [cars]);
const models = useMemo(() => {
const set = new Set<string>();
cars.forEach((c) => c.model && set.add(c.model));
return Array.from(set).sort();
}, [cars]);
const visibleCars = useMemo(
() => (modelFilter ? cars.filter((c) => c.model === modelFilter) : cars),
[cars, modelFilter],
);
const weekDays = useMemo(() => {
if (!weekStart) return [];
return Array.from({ length: 7 }, (_, i) => addDaysKey(weekStart, i));
}, [weekStart]);
// time-slot keys present in the data, sorted
const timeSlots = useMemo(() => {
const set = new Set<string>();
drives.forEach((d) => set.add(istTimeKey(d.slot_start)));
return Array.from(set).sort();
}, [drives]);
// index drives by (day, time, car) for O(1) lookup
const grid = useMemo(() => {
const m = new Map<string, TestDrive>();
drives.forEach((d) => {
const key = `${istDayKey(d.slot_start)}|${istTimeKey(d.slot_start)}|${d.car_id}`;
m.set(key, d);
});
return m;
}, [drives]);
const visibleCarIds = useMemo(() => new Set(visibleCars.map((c) => Number(c.id))), [visibleCars]);
// For the visible week, count bookings per day for the header summary.
const bookingsByDay = useMemo(() => {
const m = new Map<string, number>();
drives.forEach((d) => {
if (d.status !== "booked") return;
if (!visibleCarIds.has(Number(d.car_id))) return;
const k = istDayKey(d.slot_start);
m.set(k, (m.get(k) ?? 0) + 1);
});
return m;
}, [drives, visibleCarIds]);
const totalBookings = useMemo(
() => drives.filter((d) => d.status === "booked" && visibleCarIds.has(Number(d.car_id))).length,
[drives, visibleCarIds],
);
if (loading) {
return <div className="text-stone-400 text-sm">Loading calendar</div>;
}
if (error) {
return (
<div className="px-4 py-3 text-sm" style={{ background: ACCENT_SOFT, color: "#5f0229", border: `1px solid ${ACCENT}` }}>
Couldn't load calendar: {error}
</div>
);
}
if (!weekStart || timeSlots.length === 0) {
return <div className="text-stone-400 text-sm">No slots configured.</div>;
}
return (
<>
{/* Header — page title + week nav */}
<div className="mb-6 flex items-end justify-between gap-4 flex-wrap">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.15em]" style={{ color: ACCENT }}>
Test-drive lifecycle
</div>
<h1 className="text-[24px] font-semibold tracking-tight leading-tight mt-1.5" style={{ color: INK }}>
Slots calendar
</h1>
</div>
<div className="flex items-center gap-3 flex-wrap">
<div className="relative">
<select
value={modelFilter}
onChange={(e) => setModelFilter(e.target.value)}
className="h-9 pl-3 pr-8 text-[12.5px] font-semibold bg-white appearance-none cursor-pointer focus:outline-none"
style={{ border: `1px solid ${TM_BORDER}`, color: INK, borderRadius: 4 }}
>
<option value="">All models</option>
{models.map((m) => <option key={m} value={m}>{m}</option>)}
</select>
<span className="absolute right-2.5 top-1/2 -translate-y-1/2 text-stone-400 pointer-events-none text-[10px]"></span>
</div>
<button
onClick={() => setWeekStart((k) => k && addDaysKey(k, -7))}
className="h-9 w-9 inline-flex items-center justify-center bg-white hover:bg-stone-50 transition-colors"
style={{ border: `1px solid ${TM_BORDER}`, color: INK }}
aria-label="Previous week"
>
<ChevronLeft size={16} />
</button>
<div className="text-[13px] font-semibold tabular-nums px-3" style={{ color: INK }}>
{fmtDayLabel(weekDays[0]).day} {fmtDayLabel(weekDays[0]).month} {fmtDayLabel(weekDays[6]).day} {fmtDayLabel(weekDays[6]).month}
</div>
<button
onClick={() => setWeekStart((k) => k && addDaysKey(k, 7))}
className="h-9 w-9 inline-flex items-center justify-center bg-white hover:bg-stone-50 transition-colors"
style={{ border: `1px solid ${TM_BORDER}`, color: INK }}
aria-label="Next week"
>
<ChevronRight size={16} />
</button>
</div>
</div>
{/* Legend */}
<div className="mb-4 flex items-center gap-5 text-[11.5px] text-stone-500">
<div className="flex items-center gap-2">
<span className="w-3 h-3" style={{ background: ACCENT }} />
Booked
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-white" style={{ border: `1px solid ${TM_BORDER}` }} />
Available
</div>
<div className="ml-auto text-stone-400">
{totalBookings} bookings across {weekDays.length} days
</div>
</div>
{/* Grid */}
<div className="bg-white overflow-x-auto" style={{ border: `1px solid ${TM_BORDER}` }}>
<table className="w-full" style={{ tableLayout: "fixed", borderCollapse: "collapse" }}>
<colgroup>
<col style={{ width: 80 }} />
{weekDays.map((d) => <col key={d} />)}
</colgroup>
<thead>
<tr>
<th className="text-left px-3 py-3 text-[10px] font-semibold uppercase tracking-[0.12em] text-stone-400" style={{ borderBottom: `1px solid ${TM_BORDER}` }}>
IST
</th>
{weekDays.map((dk) => {
const lbl = fmtDayLabel(dk);
const count = bookingsByDay.get(dk) ?? 0;
return (
<th key={dk} className="text-left px-3 py-3" style={{ borderBottom: `1px solid ${TM_BORDER}`, borderLeft: `1px solid ${TM_BORDER}` }}>
<div className="flex items-baseline justify-between gap-2">
<div>
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-stone-400">{lbl.weekday}</div>
<div className="text-[15px] font-bold leading-none mt-1 tabular-nums" style={{ color: INK }}>
{lbl.day} <span className="text-[11px] font-medium text-stone-400 ml-0.5">{lbl.month}</span>
</div>
</div>
{count > 0 && (
<span className="text-[10px] font-bold tabular-nums text-white px-1.5 py-0.5" style={{ background: ACCENT }}>
{count}
</span>
)}
</div>
</th>
);
})}
</tr>
</thead>
<tbody>
{timeSlots.map((tk) => (
<tr key={tk}>
<td className="px-3 py-3 align-top text-[12px] font-semibold tabular-nums text-stone-500" style={{ borderTop: `1px solid ${TM_BORDER}` }}>
{tk}
</td>
{weekDays.map((dk) => (
<td key={dk + tk} className="px-2 py-2 align-top" style={{ borderTop: `1px solid ${TM_BORDER}`, borderLeft: `1px solid ${TM_BORDER}` }}>
<div className="grid grid-cols-1 gap-1">
{visibleCars.map((c) => {
const cell = grid.get(`${dk}|${tk}|${Number(c.id)}`);
if (!cell) {
return (
<div key={c.id} className="h-7 px-2 flex items-center text-[10.5px] text-stone-300" style={{ border: `1px dashed ${TM_BORDER}` }}>
</div>
);
}
const booked = cell.status === "booked";
return (
<button
key={c.id}
onClick={() => booked && setSelected(cell)}
className={`h-7 px-2 flex items-center justify-between gap-2 text-[10.5px] font-medium transition-colors ${booked ? "cursor-pointer" : "cursor-default"}`}
style={
booked
? { background: ACCENT, color: "white" }
: { background: "white", color: "#a8a29e", border: `1px solid ${TM_BORDER}` }
}
title={booked ? `${cell.customer_name ?? "Booked"} · ${c.model}` : `Available · ${c.model}`}
>
<span className="truncate">{c.model}</span>
{booked && cell.customer_name && (
<span className="truncate font-semibold text-white/90">
{cell.customer_name.split(" ")[0]}
</span>
)}
</button>
);
})}
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<BookingPanel
drive={selected}
car={selected ? carById.get(Number(selected.car_id)) : undefined}
onClose={() => setSelected(null)}
/>
</>
);
}
function BookingPanel({ drive, car, onClose }: { drive: TestDrive | null; car: Car | undefined; onClose: () => void }) {
const open = !!drive;
return (
<>
<div
className={`fixed inset-0 z-40 transition-opacity duration-200 ${open ? "opacity-100" : "opacity-0 pointer-events-none"}`}
style={{ background: "rgba(6,31,92,0.35)" }}
onClick={onClose}
/>
<aside
className={`fixed top-0 right-0 bottom-0 z-50 w-full max-w-md bg-white shadow-2xl flex flex-col transition-transform duration-200 ease-out ${open ? "translate-x-0" : "translate-x-full"}`}
style={{ borderLeft: `1px solid ${TM_BORDER}` }}
role="dialog"
aria-hidden={!open}
>
{drive && <BookingPanelBody drive={drive} car={car} onClose={onClose} />}
</aside>
</>
);
}
function BookingPanelBody({ drive, car, onClose }: { drive: TestDrive; car: Car | undefined; onClose: () => void }) {
const day = fmtDayLabel(istDayKey(drive.slot_start));
const time = `${istTimeKey(drive.slot_start)} ${istTimeKey(drive.slot_end)} IST`;
const hasFeedback = drive.feedback_rating != null || !!drive.feedback_liked_most || !!drive.feedback_concern || drive.feedback_would_recommend != null;
const completed = !!drive.completed_at;
return (
<>
<div className="relative px-6 py-5 text-white shrink-0" style={{ background: TM_NAVY }}>
<span className="absolute top-0 left-0 bottom-0 w-1.5" style={{ background: ACCENT }} />
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.15em]" style={{ color: ACCENT_SOFT }}>
Booking
</div>
<div className="text-[22px] font-bold leading-tight mt-1">
{drive.customer_name ?? "—"}
</div>
<div className="mt-2 text-[12.5px] text-white/80 flex items-center gap-2 flex-wrap">
<CalendarIcon size={13} />
<span className="font-semibold">{day.weekday} {day.day} {day.month}</span>
<span className="text-white/40">·</span>
<span className="tabular-nums">{time}</span>
</div>
</div>
<button
onClick={onClose}
className="text-white/70 hover:text-white transition-colors shrink-0"
aria-label="Close"
>
<X size={18} />
</button>
</div>
</div>
<div className="overflow-y-auto flex-1">
{/* Car image hero */}
{car && (
<div className="relative w-full bg-stone-100" style={{ aspectRatio: "16 / 9" }}>
<img
src={carImageUrl(car.model)}
alt={car.model}
className="absolute inset-0 w-full h-full object-cover"
onError={(e) => {
const img = e.currentTarget as HTMLImageElement;
const fallback = `${import.meta.env.BASE_URL}cars/xuv700.jpg`;
if (img.src.endsWith(fallback)) { img.style.display = "none"; return; }
img.src = fallback;
}}
/>
<div className="absolute inset-x-0 bottom-0 px-5 py-3" style={{ background: "linear-gradient(to top, rgba(6,31,92,0.85), transparent)" }}>
<div className="text-white text-[18px] font-bold leading-none">{car.model}</div>
{car.variant && <div className="text-white/80 text-[12px] mt-1">{car.variant}</div>}
</div>
</div>
)}
<PanelSection label="Vehicle">
<Row label="Model">
<span className="font-semibold" style={{ color: INK }}>{car?.model ?? "—"}</span>
</Row>
<Row label="Variant">{car?.variant || "—"}</Row>
<Row label="Color">
{car?.color ? (
<span className="inline-flex items-center gap-2">
<span className="w-3 h-3 rounded-full" style={{ background: colorSwatch(car.color), border: "1px solid rgba(0,0,0,0.1)" }} />
{car.color}
</span>
) : "—"}
</Row>
<Row label="Ex-showroom">
<span className="font-semibold tabular-nums" style={{ color: INK }}>{fmtPriceInr(car?.price_inr)}</span>
</Row>
</PanelSection>
<PanelSection label="Customer">
<Row label="Name">
<span className="font-semibold" style={{ color: INK }}>{drive.customer_name ?? "—"}</span>
</Row>
<Row label="Phone">
{drive.customer_phone ? (
<a href={`tel:${drive.customer_phone}`} className="inline-flex items-center gap-1.5 font-semibold" style={{ color: ACCENT }}>
<Phone size={12} />
{drive.customer_phone}
</a>
) : "—"}
</Row>
</PanelSection>
<PanelSection label="Booking">
<Row label="Status">
<StatusPill status={completed ? "Completed" : "Booked"} tone={completed ? "completed" : "booked"} />
</Row>
<Row label="Slot">
<span className="font-semibold tabular-nums" style={{ color: INK }}>{day.weekday} {day.day} {day.month}, {time}</span>
</Row>
<Row label="Booked at">
{drive.booked_at ? new Date(drive.booked_at).toLocaleString("en-IN", { dateStyle: "medium", timeStyle: "short" }) : "—"}
</Row>
{completed && (
<Row label="Completed at">
{new Date(drive.completed_at!).toLocaleString("en-IN", { dateStyle: "medium", timeStyle: "short" })}
</Row>
)}
{drive.notes && <Row label="Notes">{drive.notes}</Row>}
</PanelSection>
<PanelSection label="Feedback">
{hasFeedback ? (
<>
{drive.feedback_rating != null && (
<Row label="Rating">
<RatingStars value={Number(drive.feedback_rating)} />
</Row>
)}
{drive.feedback_would_recommend != null && (
<Row label="Recommends">
<StatusPill
status={drive.feedback_would_recommend ? "Yes" : "No"}
tone={drive.feedback_would_recommend ? "completed" : "failed"}
/>
</Row>
)}
{drive.feedback_liked_most && <Row label="Liked most">{drive.feedback_liked_most}</Row>}
{drive.feedback_concern && <Row label="Concern">{drive.feedback_concern}</Row>}
{drive.feedback_collected_at && (
<Row label="Collected at">
{new Date(drive.feedback_collected_at).toLocaleString("en-IN", { dateStyle: "medium", timeStyle: "short" })}
</Row>
)}
</>
) : (
<div className="text-[12.5px] text-stone-400 italic px-1">
{completed ? "No feedback captured yet." : "Feedback will appear here after the drive is completed."}
</div>
)}
</PanelSection>
</div>
</>
);
}
function PanelSection({ label, children }: { label: string; children: React.ReactNode }) {
return (
<section className="px-6 py-5" style={{ borderBottom: `1px solid ${TM_BORDER}` }}>
<div className="text-[10px] font-bold uppercase tracking-[0.15em] text-stone-400 mb-3">{label}</div>
<div className="space-y-3">{children}</div>
</section>
);
}
function StatusPill({ status, tone }: { status: string; tone: "booked" | "completed" | "failed" }) {
const palette: Record<string, { bg: string; fg: string; dot: string }> = {
booked: { bg: ACCENT_SOFT, fg: "#5f0229", dot: ACCENT },
completed: { bg: "#e8f5ec", fg: "#0f5132", dot: "#2f855a" },
failed: { bg: "#fde8e8", fg: "#7f1d1d", dot: "#dc2626" },
};
const p = palette[tone];
return (
<span className="inline-flex items-center gap-1.5 px-2 py-1 text-[11px] font-bold uppercase tracking-[0.04em]" style={{ background: p.bg, color: p.fg, borderRadius: 4 }}>
<span className="w-1.5 h-1.5 rounded-full" style={{ background: p.dot }} />
{status}
</span>
);
}
function RatingStars({ value }: { value: number }) {
const v = Math.max(0, Math.min(5, Math.round(value)));
return (
<span className="inline-flex items-center gap-1.5">
<span className="text-[14px] tabular-nums" style={{ color: ACCENT, letterSpacing: 1 }}>
{"★".repeat(v)}<span className="text-stone-300">{"★".repeat(5 - v)}</span>
</span>
<span className="text-[11px] font-semibold tabular-nums text-stone-500">{v}/5</span>
</span>
);
}
function colorSwatch(color: string): string {
const c = color.toLowerCase();
if (c.includes("red") || c.includes("tango")) return "#c62828";
if (c.includes("white")) return "#f5f5f5";
if (c.includes("black") || c.includes("napoli") || c.includes("stealth")) return "#1a1a1a";
if (c.includes("silver") || c.includes("grey") || c.includes("gray")) return "#9ca3af";
if (c.includes("blue")) return "#1e3a8a";
return "#9ca3af";
}
function Row({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex items-baseline gap-4 text-[13px]">
<div className="w-24 shrink-0 text-[11px] font-semibold uppercase tracking-[0.05em] text-stone-500">{label}</div>
<div className="flex-1 min-w-0 break-words" style={{ color: INK }}>{children}</div>
</div>
);
}

View File

@ -25,7 +25,8 @@ import {
type ActionButton, type AnalyticsSpec, type ScreenButton, type ActionButton, type AnalyticsSpec, type ScreenButton,
} from "../../hooks/usePrototypeData"; } from "../../hooks/usePrototypeData";
import FormModal from "../FormModal"; import FormModal from "../FormModal";
import { SCREEN_TO_DV, DETAIL_VIEW_IDS, ACTIVITY_IDS, STATE_IDS } from "../../config"; 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 // Palette — Tech Mahindra brand. Red is the identity color (the "Tech" in
@ -155,6 +156,8 @@ export default function VariantB() {
onAction={(a) => setFormModal(a)} onAction={(a) => setFormModal(a)}
breadcrumbLabel={activeNav?.label ?? "Pipeline"} breadcrumbLabel={activeNav?.label ?? "Pipeline"}
/> />
) : activeScreenId === DEMO_SLOTS_SCREEN_UUID ? (
<CalendarView />
) : activeScreenId ? ( ) : activeScreenId ? (
<> <>
<div className="mb-8"> <div className="mb-8">

View File

@ -51,6 +51,21 @@ export const STATE_IDS = {
END_STATE: "33cce5a0-f493-4fac-a1b7-7b68fbcbffce", END_STATE: "33cce5a0-f493-4fac-a1b7-7b68fbcbffce",
} as const; } as const;
// Demo calendar — backed by mahindra_demo schema via rdbms-template lookups on
// the INIT activity (storage_columns=id, display_columns=everything we need).
export const WORKFLOW_NUMERIC_ID = 29347;
export const DEMO_SLOTS_SCREEN_UUID = "98b1b946-f67d-4faa-b50f-4566f7e194d0";
export const CALENDAR_LOOKUPS = {
TEST_DRIVES: {
rdbmsTemplateUuid: "c71a16b0-8fc6-4ef2-b52d-2672e4dad8db",
fieldId: "test_drives_lookup",
},
CARS: {
rdbmsTemplateUuid: "3efdfe0f-8ed9-4fbc-95ce-0f3788622c9c",
fieldId: "cars_lookup",
},
} as const;
// Roles // Roles
export const ROLES = { export const ROLES = {
SALES_AGENT: "Sales Agent", SALES_AGENT: "Sales Agent",