diff --git a/src/app/debug/bookings/BookingDebugPage.module.scss b/src/app/debug/bookings/BookingDebugPage.module.scss
index 46e137b..bb97448 100644
--- a/src/app/debug/bookings/BookingDebugPage.module.scss
+++ b/src/app/debug/bookings/BookingDebugPage.module.scss
@@ -25,3 +25,50 @@
overflow-x: auto;
margin-top: 1rem;
}
+
+.debugOutput {
+ grid-column: 1 / -1;
+ background: #1a1a2e;
+ color: #eee;
+ padding: 1rem;
+ border-radius: 8px;
+ border-left: 4px solid #6366f1;
+
+ h4 {
+ margin: 0 0 0.75rem;
+ font-size: 0.95rem;
+ }
+
+ pre {
+ margin: 0;
+ white-space: pre-wrap;
+ font-size: 0.85rem;
+ }
+}
+
+.errorMessage {
+ color: #ef4444;
+}
+
+.bookedSlotsTable {
+ grid-column: 1 / -1;
+ background: #1a1a2e;
+ color: #eee;
+ padding: 1rem;
+ border-radius: 8px;
+ border-left: 4px solid #22c55e;
+
+ h4 {
+ margin: 0 0 0.75rem;
+ font-size: 0.95rem;
+ }
+}
+
+.bookedSlotsTableBody {
+ th,
+ td {
+ background: #252540 !important;
+ color: #eee !important;
+ border-color: #3f3f5a !important;
+ }
+}
diff --git a/src/app/debug/bookings/page.tsx b/src/app/debug/bookings/page.tsx
index e823f7a..ec9a3a3 100644
--- a/src/app/debug/bookings/page.tsx
+++ b/src/app/debug/bookings/page.tsx
@@ -1,26 +1,155 @@
"use client";
-import { Button, Divider, Group, Select, Textarea, TextInput } from "@mantine/core";
+import {
+ Alert,
+ Button,
+ Divider,
+ Group,
+ Select,
+ Table,
+ Text,
+ Textarea,
+ TextInput,
+} from "@mantine/core";
+import { DateInput, TimeInput } from "@mantine/dates";
import { useForm } from "@mantine/form";
import { notifications } from "@mantine/notifications";
-import { useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { api } from "@/trpc/react";
import { ALL_BOOKING_STATUSES, BookingStatus, type BookingStatusValue } from "@/types/types";
import styles from "./BookingDebugPage.module.scss";
+/** Returns true if end is after start (or invalid); used for form validation. */
+function isEndAfterStart(start: string, end: string) {
+ const a = new Date(start).getTime();
+ const b = new Date(end).getTime();
+ if (Number.isNaN(a) || Number.isNaN(b)) return true; // let backend/zod handle invalid formats
+ return b > a;
+}
+
+/** Formats start and end as a time range string in UTC (e.g. 9:00 AM – 10:30 AM). */
+function formatTimeSlot(startTime: string, endTime: string): string {
+ // Format in UTC to match stored booking times (e.g. "09:00:00+00" -> "9:00 AM")
+ const opts: Intl.DateTimeFormatOptions = {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ timeZone: "UTC",
+ };
+ const start = new Date(startTime).toLocaleTimeString("en-US", opts);
+ const end = new Date(endTime).toLocaleTimeString("en-US", opts);
+ return `${start} – ${end}`;
+}
+
+/** Example pre-filled booking for testing. agencyId must be a valid user.id; set dynamically from getCurrentUser. */
+const EXAMPLE_BOOKING = {
+ title: "Test Example",
+ pickupAddress: "The Inn from the Cold, 110 11 Ave SE, Calgary, AB",
+ destinationAddress: "Sheldon M. Chumir Health Centre, 1213 4 St SW, Calgary, AB",
+ passengerInfo: "John Smith",
+ phoneNumber: "+1 (403) 760-9834",
+ purpose: "Medical appointment",
+ start: "2026-02-12T15:00:00.000Z", // Feb 12, 2026 3:00 PM UTC
+};
+
+/** Returns true if the booking overlaps the given UTC day (day boundaries in UTC). */
+function bookingOverlapsDay(booking: { startTime: string; endTime: string }, day: Date): boolean {
+ const y = day.getUTCFullYear();
+ const m = day.getUTCMonth();
+ const d = day.getUTCDate();
+ const dStart = Date.UTC(y, m, d, 0, 0, 0, 0);
+ const dEnd = Date.UTC(y, m, d, 23, 59, 59, 999);
+
+ const start = new Date(booking.startTime).getTime();
+ const end = new Date(booking.endTime).getTime();
+
+ return start < dEnd && end > dStart;
+}
+
+type BookingForSchedule = {
+ id: number;
+ driverId: string | null;
+ status: string;
+ startTime: string;
+ endTime: string;
+ title: string;
+};
+
+/** Table of a driver's bookings for the selected day; shows time slot and title. */
+function DriverScheduleTable({
+ bookings,
+ day,
+ driverId,
+}: {
+ bookings: BookingForSchedule[];
+ day: Date | null;
+ driverId: string;
+}) {
+ if (!day || !driverId) return null;
+ const bookedSlots = bookings
+ .filter(
+ (b) => b.driverId === driverId && b.status !== "cancelled" && bookingOverlapsDay(b, day),
+ )
+ .sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
+
+ if (bookedSlots.length === 0) {
+ return (
+
+ No bookings this day – driver available all day
+
+ );
+ }
+
+ return (
+
+
+
+ Time slot
+ Booking
+
+
+
+ {bookedSlots.map((b) => (
+
+ {formatTimeSlot(b.startTime, b.endTime)}
+
+ Booking #{b.id} – {b.title}
+
+
+ ))}
+
+
+ );
+}
+
+/** Debug page for creating/editing bookings and viewing driver availability. */
export default function BookingDebugPage() {
const [bookingId, setBookingId] = useState(1);
+ // Day picker for driver availability (date-only)
+ const [selectedDay, setSelectedDay] = useState(() => {
+ const d = new Date(EXAMPLE_BOOKING.start);
+ return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
+ });
+
+ // Start time of day only (HH:mm) - day comes from selectedDay
+ const [startTimeOfDay, setStartTimeOfDay] = useState(() => {
+ const d = new Date(EXAMPLE_BOOKING.start);
+ const h = d.getUTCHours().toString().padStart(2, "0");
+ const m = d.getUTCMinutes().toString().padStart(2, "0");
+ return `${h}:${m}`;
+ });
const form = useForm({
initialValues: {
- title: "",
- pickupAddress: "",
- destinationAddress: "",
- passengerInfo: "",
- start: "",
- end: "",
- agencyId: "",
- purpose: "",
+ title: EXAMPLE_BOOKING.title,
+ pickupAddress: EXAMPLE_BOOKING.pickupAddress,
+ destinationAddress: EXAMPLE_BOOKING.destinationAddress,
+ passengerInfo: EXAMPLE_BOOKING.passengerInfo,
+ phoneNumber: EXAMPLE_BOOKING.phoneNumber ?? "",
+ start: EXAMPLE_BOOKING.start, // will be kept in sync with picker (string)
+ end: "", // will be kept in sync with picker (string), auto-calculated
+ agencyId: "", // set from getCurrentUser (must be valid user.id for FK)
+ purpose: EXAMPLE_BOOKING.purpose,
driverId: "",
status: BookingStatus.INCOMPLETE as BookingStatusValue,
},
@@ -39,11 +168,97 @@ export default function BookingDebugPage() {
const bookingQuery = api.bookings.getById.useQuery({ id: bookingId }, { enabled: false });
+ const canShowDriverAvailability = !!selectedDay && !!form.values.driverId?.trim();
+
const allBookingsQuery = api.bookings.getAll.useQuery(undefined, {
- enabled: false,
+ enabled: true, // fetch on load for both driver availability table and Fetch All
staleTime: 0,
});
+ const listDriversQuery = api.bookings.listDrivers.useQuery();
+ const currentUserQuery = api.bookings.getCurrentUser.useQuery();
+
+ // Set agencyId from current user so it references a valid user (fixes FK constraint)
+ useEffect(() => {
+ if (currentUserQuery.data && !form.values.agencyId) {
+ form.setFieldValue("agencyId", currentUserQuery.data.id);
+ }
+ }, [currentUserQuery.data, form.setFieldValue, form.values.agencyId]);
+
+ const driverOptions = useMemo(
+ () =>
+ (listDriversQuery.data ?? []).map((d) => ({
+ value: d.id,
+ label: "email" in d && d.email != null ? `${d.name} (${d.email})` : d.name,
+ })),
+ [listDriversQuery.data],
+ );
+
+ // Pre-select first driver when options load (for example pre-fill)
+ useEffect(() => {
+ if (driverOptions.length > 0 && !form.values.driverId) {
+ form.setFieldValue("driverId", driverOptions[0]?.value ?? "");
+ }
+ }, [driverOptions, form.setFieldValue, form.values.driverId]);
+
+ // Compute full start ISO from selectedDay + startTimeOfDay (naive UTC to match booking storage)
+ const computedStart = useMemo(() => {
+ const day = selectedDay;
+ const time = startTimeOfDay;
+ if (!day || !time) return "";
+ const parts = time.split(":").map(Number);
+ const [h, m] = parts;
+ if (h == null || m == null || Number.isNaN(h) || Number.isNaN(m)) return "";
+ const y = day.getFullYear();
+ const mo = (day.getMonth() + 1).toString().padStart(2, "0");
+ const d = day.getDate().toString().padStart(2, "0");
+ const hh = h.toString().padStart(2, "0");
+ const mm = m.toString().padStart(2, "0");
+ return `${y}-${mo}-${d}T${hh}:${mm}:00.000Z`;
+ }, [selectedDay, startTimeOfDay]);
+
+ // Keep form.start in sync with computedStart
+ useEffect(() => {
+ form.setFieldValue("start", computedStart);
+ }, [computedStart, form.setFieldValue]);
+
+ const canCalculateEnd =
+ !!form.values.pickupAddress?.trim() &&
+ !!form.values.destinationAddress?.trim() &&
+ !!computedStart;
+
+ const estimatedEndQuery = api.bookings.getEstimatedEndTime.useQuery(
+ {
+ pickupAddress: form.values.pickupAddress,
+ destinationAddress: form.values.destinationAddress,
+ startTime: computedStart,
+ },
+ { enabled: canCalculateEnd, staleTime: 5 * 60 * 1000 },
+ );
+
+ useEffect(() => {
+ if (estimatedEndQuery.data) {
+ form.setFieldValue("end", estimatedEndQuery.data.estimatedEndTime);
+ }
+ }, [estimatedEndQuery.data, form.setFieldValue]);
+
+ const canCheckAvailability =
+ !!form.values.driverId?.trim() &&
+ !!form.values.start &&
+ !!form.values.end &&
+ isEndAfterStart(form.values.start, form.values.end);
+
+ const availabilityQuery = api.bookings.isDriverAvailable.useQuery(
+ {
+ driverId: form.values.driverId,
+ startTime: form.values.start,
+ endTime: form.values.end,
+ pickupAddress: form.values.pickupAddress?.trim() || undefined,
+ destinationAddress: form.values.destinationAddress?.trim() || undefined,
+ },
+ { enabled: canCheckAvailability },
+ );
+
const createMutation = api.bookings.create.useMutation({
onSuccess: async () => {
notifications.show({ color: "green", message: "Booking created!" });
@@ -52,6 +267,17 @@ export default function BookingDebugPage() {
await allBookingsQuery.refetch();
form.reset();
+ const exampleStartDate = new Date(EXAMPLE_BOOKING.start);
+ setSelectedDay(
+ new Date(
+ exampleStartDate.getUTCFullYear(),
+ exampleStartDate.getUTCMonth(),
+ exampleStartDate.getUTCDate(),
+ ),
+ );
+ const h = exampleStartDate.getUTCHours().toString().padStart(2, "0");
+ const m = exampleStartDate.getUTCMinutes().toString().padStart(2, "0");
+ setStartTimeOfDay(`${h}:${m}`);
},
onError: (err) => {
notifications.show({ color: "red", message: err.message });
@@ -89,49 +315,28 @@ export default function BookingDebugPage() {
label: s.replace("-", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
}));
+ const endAfterStart = useMemo(() => {
+ if (!form.values.start || !form.values.end) return true;
+ return isEndAfterStart(form.values.start, form.values.end);
+ }, [form.values.start, form.values.end]);
+
return (
Booking API Debug Panel
-
- setBookingId(Number(e.target.value))}
- />
-
-
-
-
-
- {/* UPDATE STATUS */}
-