diff --git a/.gitignore b/.gitignore index 5b08070..f078cea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules/ dist/ .env *.local + +# Local MCP config (contains DB credentials) +.mcp.json diff --git a/public/cars/be-6.jpg b/public/cars/be-6.jpg new file mode 100644 index 0000000..3c68f98 Binary files /dev/null and b/public/cars/be-6.jpg differ diff --git a/public/cars/xev-9e.jpg b/public/cars/xev-9e.jpg new file mode 100644 index 0000000..73fe92b Binary files /dev/null and b/public/cars/xev-9e.jpg differ diff --git a/public/cars/xuv400-ev.jpg b/public/cars/xuv400-ev.jpg new file mode 100644 index 0000000..48ca4d7 Binary files /dev/null and b/public/cars/xuv400-ev.jpg differ diff --git a/src/components/variants/Calendar.tsx b/src/components/variants/Calendar.tsx index ee7eb61..eefd636 100644 --- a/src/components/variants/Calendar.tsx +++ b/src/components/variants/Calendar.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "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"; +import { CALENDAR_LOOKUPS, WORKFLOW_NUMERIC_ID, ACTIVITY_IDS, DEFAULT_DEALER_ID } from "../../config"; const ACCENT = "#e31837"; const ACCENT_SOFT = "#fde2e8"; @@ -34,12 +34,21 @@ interface TestDrive { interface Car { id: number; + dealer_id: number; model: string; variant?: string | null; color?: string | null; price_inr?: number | string | null; } +interface Dealer { + id: number; + name: string; + city?: string | null; + address?: string | null; + phone?: string | null; +} + function carSlug(model: string): string { return model.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } @@ -83,11 +92,13 @@ function addDaysKey(key: string, days: number): string { export function CalendarView() { const [drives, setDrives] = useState([]); const [cars, setCars] = useState([]); + const [dealers, setDealers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [weekStart, setWeekStart] = useState(null); const [selected, setSelected] = useState(null); const [modelFilter, setModelFilter] = useState(""); + const [dealerId, setDealerId] = useState(DEFAULT_DEALER_ID); useEffect(() => { let cancelled = false; @@ -124,11 +135,18 @@ export function CalendarView() { fieldId: CALENDAR_LOOKUPS.CARS.fieldId, limit: 100, }), + fetchRdbmsLookupRecords(CALENDAR_LOOKUPS.DEALERS.rdbmsTemplateUuid, { + workflowId: WORKFLOW_NUMERIC_ID, + activityId: ACTIVITY_IDS.INIT_ACTIVITY, + fieldId: CALENDAR_LOOKUPS.DEALERS.fieldId, + limit: 100, + }), ]) - .then(([td, c]) => { + .then(([td, c, d]) => { if (cancelled) return; setDrives(td); setCars((c.records as unknown as Car[]) ?? []); + setDealers((d.records as unknown as Dealer[]) ?? []); const todayKey = istDayKey(new Date().toISOString()); const days = td.map((r) => istDayKey(r.slot_start)).sort(); setWeekStart(days.length ? todayKey : null); @@ -144,15 +162,32 @@ export function CalendarView() { return m; }, [cars]); + const dealerById = useMemo(() => { + const m = new Map(); + dealers.forEach((d) => m.set(Number(d.id), d)); + return m; + }, [dealers]); + + // Calendar shows one dealer at a time. Cars + drives carry dealer_id, so the + // whole grid (columns, slot times, bookings) is scoped to the selected dealer. + const dealerCars = useMemo( + () => cars.filter((c) => Number(c.dealer_id) === dealerId), + [cars, dealerId], + ); + const dealerDrives = useMemo( + () => drives.filter((d) => Number(d.dealer_id) === dealerId), + [drives, dealerId], + ); + const models = useMemo(() => { const set = new Set(); - cars.forEach((c) => c.model && set.add(c.model)); + dealerCars.forEach((c) => c.model && set.add(c.model)); return Array.from(set).sort(); - }, [cars]); + }, [dealerCars]); const visibleCars = useMemo( - () => (modelFilter ? cars.filter((c) => c.model === modelFilter) : cars), - [cars, modelFilter], + () => (modelFilter ? dealerCars.filter((c) => c.model === modelFilter) : dealerCars), + [dealerCars, modelFilter], ); const weekDays = useMemo(() => { @@ -163,37 +198,48 @@ export function CalendarView() { // time-slot keys present in the data, sorted const timeSlots = useMemo(() => { const set = new Set(); - drives.forEach((d) => set.add(istTimeKey(d.slot_start))); + dealerDrives.forEach((d) => set.add(istTimeKey(d.slot_start))); return Array.from(set).sort(); - }, [drives]); + }, [dealerDrives]); // index drives by (day, time, car) for O(1) lookup const grid = useMemo(() => { const m = new Map(); - drives.forEach((d) => { + dealerDrives.forEach((d) => { const key = `${istDayKey(d.slot_start)}|${istTimeKey(d.slot_start)}|${d.car_id}`; m.set(key, d); }); return m; - }, [drives]); + }, [dealerDrives]); const visibleCarIds = useMemo(() => new Set(visibleCars.map((c) => Number(c.id))), [visibleCars]); + // Only render rows for cars that actually have ≥1 slot in the displayed week, + // otherwise cars with no slots this week (e.g. seeding gaps) show as dead "—" rows. + const weekCars = useMemo(() => { + const days = new Set(weekDays); + const idsThisWeek = new Set(); + dealerDrives.forEach((d) => { + if (days.has(istDayKey(d.slot_start))) idsThisWeek.add(Number(d.car_id)); + }); + return visibleCars.filter((c) => idsThisWeek.has(Number(c.id))); + }, [visibleCars, dealerDrives, weekDays]); + // For the visible week, count bookings per day for the header summary. const bookingsByDay = useMemo(() => { const m = new Map(); - drives.forEach((d) => { + dealerDrives.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]); + }, [dealerDrives, visibleCarIds]); const totalBookings = useMemo( - () => drives.filter((d) => d.status === "booked" && visibleCarIds.has(Number(d.car_id))).length, - [drives, visibleCarIds], + () => dealerDrives.filter((d) => d.status === "booked" && visibleCarIds.has(Number(d.car_id))).length, + [dealerDrives, visibleCarIds], ); if (loading) { @@ -223,6 +269,19 @@ export function CalendarView() {
+
+ + +