Bernt Christian Egeland 7 месяцев назад
Родитель
Сommit
ed6e29b689

+ 0 - 0
plans/aithauth_example.ts


+ 35 - 5
src/app/(authenticated)/calendar/page.tsx

@@ -1,18 +1,44 @@
 import { getCalendarEvents } from "@/features/calendar/Actions/calendarActions";
+import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
+import { getSettings } from "@/features/settings/Actions/settingsActions";
+import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { PageHeader } from "@/components/page-header";
 import CalendarClient from "@/features/calendar/Components/CalendarClient";
 
+function toLocalDateStr(d: Date): string {
+  const y = d.getFullYear();
+  const m = String(d.getMonth() + 1).padStart(2, "0");
+  const day = String(d.getDate()).padStart(2, "0");
+  return `${y}-${m}-${day}`;
+}
+
 export default async function CalendarPage() {
   const now = new Date();
   const start = new Date(now.getFullYear(), now.getMonth(), 1);
   const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
 
-  const result = await getCalendarEvents({
-    start: start.toISOString().split("T")[0],
-    end: end.toISOString().split("T")[0],
-  });
+  const [eventsResult, settingsResult, vehiclesResult] = await Promise.all([
+    getCalendarEvents({
+      start: toLocalDateStr(start),
+      end: toLocalDateStr(end),
+    }),
+    getSettings([SETTING_KEYS.CURRENCY_CODE]),
+    getVehicles(),
+  ]);
 
-  const events = result.success && result.data ? result.data : [];
+  const events = eventsResult.success && eventsResult.data ? eventsResult.data : [];
+  const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
+  const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
+  const vehicles = vehiclesResult.success && vehiclesResult.data
+    ? vehiclesResult.data.map((v) => ({
+        id: v.id,
+        make: v.make,
+        model: v.model,
+        year: v.year,
+        licensePlate: v.licensePlate,
+        customer: v.customer,
+      }))
+    : [];
 
   return (
     <>
@@ -22,6 +48,10 @@ export default async function CalendarPage() {
           initialEvents={events}
           initialMonth={now.getMonth()}
           initialYear={now.getFullYear()}
+          initialDay={now.getDate()}
+          todayStr={toLocalDateStr(now)}
+          vehicles={vehicles}
+          currencyCode={currencyCode}
         />
       </div>
     </>

+ 70 - 5
src/features/calendar/Actions/calendarActions.ts

@@ -7,14 +7,29 @@ import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 export type CalendarEvent = {
   id: string;
   title: string;
-  date: string;
-  type: "service" | "reminder";
+  date: string; // YYYY-MM-DD local date
+  time: string | null; // HH:MM or null
+  type: "service" | "reminder" | "quote";
   status: string;
   vehicleId: string;
   vehicleLabel: string;
   customerName: string | null;
+  invoiceNumber: string | null;
+  amount: number | null;
 };
 
+/** Format a Date as YYYY-MM-DD in local time (avoids UTC shift from toISOString) */
+function toLocalDateStr(d: Date): string {
+  const y = d.getFullYear();
+  const m = String(d.getMonth() + 1).padStart(2, "0");
+  const day = String(d.getDate()).padStart(2, "0");
+  return `${y}-${m}-${day}`;
+}
+
+function toTimeStr(d: Date): string {
+  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
+}
+
 export async function getCalendarEvents(params: {
   start: string;
   end: string;
@@ -24,7 +39,7 @@ export async function getCalendarEvents(params: {
     const end = new Date(params.end);
     end.setHours(23, 59, 59, 999);
 
-    const [services, reminders] = await Promise.all([
+    const [services, reminders, quotes] = await Promise.all([
       db.serviceRecord.findMany({
         where: {
           vehicle: { organizationId },
@@ -35,6 +50,9 @@ export async function getCalendarEvents(params: {
           title: true,
           serviceDate: true,
           status: true,
+          invoiceNumber: true,
+          totalAmount: true,
+          cost: true,
           vehicleId: true,
           vehicle: {
             select: {
@@ -69,30 +87,77 @@ export async function getCalendarEvents(params: {
         },
         orderBy: { dueDate: "asc" },
       }),
+      db.quote.findMany({
+        where: {
+          organizationId,
+          validUntil: { gte: start, lte: end },
+          status: { notIn: ["converted", "rejected"] },
+        },
+        select: {
+          id: true,
+          title: true,
+          quoteNumber: true,
+          validUntil: true,
+          status: true,
+          totalAmount: true,
+          vehicleId: true,
+          vehicle: {
+            select: {
+              make: true,
+              model: true,
+              year: true,
+              customer: { select: { name: true } },
+            },
+          },
+          customer: { select: { name: true } },
+        },
+        orderBy: { validUntil: "asc" },
+      }),
     ]);
 
     const events: CalendarEvent[] = [
       ...services.map((s) => ({
         id: s.id,
         title: s.title,
-        date: s.serviceDate.toISOString(),
+        date: toLocalDateStr(s.serviceDate),
+        time: toTimeStr(s.serviceDate),
         type: "service" as const,
         status: s.status,
         vehicleId: s.vehicleId,
         vehicleLabel: `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}`,
         customerName: s.vehicle.customer?.name ?? null,
+        invoiceNumber: s.invoiceNumber,
+        amount: s.totalAmount > 0 ? s.totalAmount : s.cost > 0 ? s.cost : null,
       })),
       ...reminders
         .filter((r) => r.dueDate !== null)
         .map((r) => ({
           id: r.id,
           title: r.title,
-          date: r.dueDate!.toISOString(),
+          date: toLocalDateStr(r.dueDate!),
+          time: null,
           type: "reminder" as const,
           status: r.isCompleted ? "completed" : new Date(r.dueDate!) < new Date() ? "overdue" : "upcoming",
           vehicleId: r.vehicleId,
           vehicleLabel: `${r.vehicle.year} ${r.vehicle.make} ${r.vehicle.model}`,
           customerName: r.vehicle.customer?.name ?? null,
+          invoiceNumber: null,
+          amount: null,
+        })),
+      ...quotes
+        .filter((q) => q.validUntil !== null && q.vehicleId !== null)
+        .map((q) => ({
+          id: q.id,
+          title: q.title,
+          date: toLocalDateStr(q.validUntil!),
+          time: null,
+          type: "quote" as const,
+          status: q.status,
+          vehicleId: q.vehicleId!,
+          vehicleLabel: q.vehicle ? `${q.vehicle.year} ${q.vehicle.make} ${q.vehicle.model}` : "",
+          customerName: q.customer?.name ?? q.vehicle?.customer?.name ?? null,
+          invoiceNumber: q.quoteNumber,
+          amount: q.totalAmount > 0 ? q.totalAmount : null,
         })),
     ];
 

+ 310 - 67
src/features/calendar/Components/CalendarClient.tsx

@@ -1,18 +1,51 @@
 "use client";
 
-import { useState, useCallback, useEffect } from "react";
+import { useState, useCallback, useMemo } from "react";
+import { useRouter } from "next/navigation";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent } from "@/components/ui/card";
-import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+  ChevronLeft,
+  ChevronRight,
+  Loader2,
+  Plus,
+  Car,
+  Search,
+  Wrench,
+  FileText,
+  AlertTriangle,
+} from "lucide-react";
 import { CalendarDayCell } from "./CalendarDayCell";
 import { CalendarEventList } from "./CalendarEventList";
+import { toLocalDateStr } from "./calendar-utils";
 import { getCalendarEvents } from "../Actions/calendarActions";
 import type { CalendarEvent } from "../Actions/calendarActions";
 
+interface Vehicle {
+  id: string;
+  make: string;
+  model: string;
+  year: number;
+  licensePlate: string | null;
+  customer: { id: string; name: string; company: string | null } | null;
+}
+
 interface CalendarClientProps {
   initialEvents: CalendarEvent[];
   initialMonth: number;
   initialYear: number;
+  initialDay: number;
+  todayStr: string; // YYYY-MM-DD computed on server to avoid hydration mismatch
+  vehicles: Vehicle[];
+  currencyCode: string;
 }
 
 function getMonthDays(year: number, month: number) {
@@ -32,7 +65,7 @@ function getMonthDays(year: number, month: number) {
     days.push(new Date(year, month, d));
   }
 
-  // Next month padding (fill to 42 or at least complete the last week)
+  // Next month padding (fill to complete the last week)
   const remaining = 7 - (days.length % 7);
   if (remaining < 7) {
     for (let i = 1; i <= remaining; i++) {
@@ -49,25 +82,80 @@ const MONTH_NAMES = [
   "July", "August", "September", "October", "November", "December",
 ];
 
-export default function CalendarClient({ initialEvents, initialMonth, initialYear }: CalendarClientProps) {
+export default function CalendarClient({
+  initialEvents,
+  initialMonth,
+  initialYear,
+  initialDay,
+  todayStr,
+  vehicles,
+  currencyCode,
+}: CalendarClientProps) {
+  const router = useRouter();
+
   const [month, setMonth] = useState(initialMonth);
   const [year, setYear] = useState(initialYear);
   const [events, setEvents] = useState<CalendarEvent[]>(initialEvents);
   const [loading, setLoading] = useState(false);
-  const [selectedDate, setSelectedDate] = useState<Date>(new Date());
+  const [selectedDate, setSelectedDate] = useState<Date>(
+    () => new Date(initialYear, initialMonth, initialDay)
+  );
+
+  // Filters
+  const [showServices, setShowServices] = useState(true);
+  const [showReminders, setShowReminders] = useState(true);
+  const [showQuotes, setShowQuotes] = useState(true);
+
+  // Vehicle picker
+  const [showPicker, setShowPicker] = useState(false);
+  const [vehicleSearch, setVehicleSearch] = useState("");
 
-  const today = new Date();
-  const todayStr = today.toISOString().split("T")[0];
+  const selectedDateStr = toLocalDateStr(selectedDate);
+
+  const filteredEvents = useMemo(() => {
+    return events.filter((e) => {
+      if (e.type === "service" && !showServices) return false;
+      if (e.type === "reminder" && !showReminders) return false;
+      if (e.type === "quote" && !showQuotes) return false;
+      return true;
+    });
+  }, [events, showServices, showReminders, showQuotes]);
+
+  // Month summary stats
+  const stats = useMemo(() => {
+    const serviceCount = events.filter((e) => e.type === "service").length;
+    const overdueReminders = events.filter(
+      (e) => e.type === "reminder" && e.status === "overdue"
+    ).length;
+    const pendingQuotes = events.filter(
+      (e) => e.type === "quote" && e.status !== "approved"
+    ).length;
+    const totalRevenue = events
+      .filter((e) => e.type === "service" && e.amount != null)
+      .reduce((sum, e) => sum + (e.amount ?? 0), 0);
+    return { serviceCount, overdueReminders, pendingQuotes, totalRevenue };
+  }, [events]);
 
   const days = getMonthDays(year, month);
 
+  const filteredVehicles = useMemo(() => {
+    if (!vehicleSearch.trim()) return vehicles;
+    const q = vehicleSearch.toLowerCase();
+    return vehicles.filter(
+      (v) =>
+        `${v.year} ${v.make} ${v.model}`.toLowerCase().includes(q) ||
+        v.licensePlate?.toLowerCase().includes(q) ||
+        v.customer?.name.toLowerCase().includes(q)
+    );
+  }, [vehicles, vehicleSearch]);
+
   const fetchEvents = useCallback(async (y: number, m: number) => {
     setLoading(true);
     const start = new Date(y, m, 1);
     const end = new Date(y, m + 1, 0);
     const result = await getCalendarEvents({
-      start: start.toISOString().split("T")[0],
-      end: end.toISOString().split("T")[0],
+      start: toLocalDateStr(start),
+      end: toLocalDateStr(end),
     });
     if (result.success && result.data) {
       setEvents(result.data);
@@ -80,6 +168,7 @@ export default function CalendarClient({ initialEvents, initialMonth, initialYea
     const newYear = month === 0 ? year - 1 : year;
     setMonth(newMonth);
     setYear(newYear);
+    setSelectedDate(new Date(newYear, newMonth, 1));
     fetchEvents(newYear, newMonth);
   };
 
@@ -88,88 +177,242 @@ export default function CalendarClient({ initialEvents, initialMonth, initialYea
     const newYear = month === 11 ? year + 1 : year;
     setMonth(newMonth);
     setYear(newYear);
+    setSelectedDate(new Date(newYear, newMonth, 1));
     fetchEvents(newYear, newMonth);
   };
 
   const goToToday = () => {
     const t = new Date();
+    const needsFetch = t.getMonth() !== month || t.getFullYear() !== year;
     setMonth(t.getMonth());
     setYear(t.getFullYear());
     setSelectedDate(t);
-    if (t.getMonth() !== month || t.getFullYear() !== year) {
+    if (needsFetch) {
       fetchEvents(t.getFullYear(), t.getMonth());
     }
   };
 
-  // Sync selected date when month changes
-  useEffect(() => {
-    setSelectedDate(new Date(year, month, 1));
-  }, [month, year]);
+  const handleCreateWorkOrder = (vehicleId: string) => {
+    setShowPicker(false);
+    setVehicleSearch("");
+    router.push(`/vehicles/${vehicleId}/service/new`);
+  };
 
   return (
-    <div className="grid gap-4 lg:grid-cols-3">
-      {/* Calendar grid */}
-      <div className="lg:col-span-2">
+    <div className="space-y-4">
+      {/* Summary stats */}
+      <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
         <Card className="border-0 shadow-sm">
-          <CardContent className="p-4">
-            {/* Header */}
-            <div className="flex items-center justify-between mb-4">
-              <div className="flex items-center gap-2">
-                <Button size="icon" variant="outline" className="h-8 w-8" onClick={goToPrev}>
-                  <ChevronLeft className="h-4 w-4" />
-                </Button>
-                <Button size="icon" variant="outline" className="h-8 w-8" onClick={goToNext}>
-                  <ChevronRight className="h-4 w-4" />
-                </Button>
-                <h2 className="text-lg font-semibold ml-2">
-                  {MONTH_NAMES[month]} {year}
-                </h2>
-                {loading && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground ml-2" />}
-              </div>
-              <Button size="sm" variant="outline" onClick={goToToday}>
-                Today
-              </Button>
+          <CardContent className="p-3 flex items-center gap-3">
+            <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-blue-500/10">
+              <Wrench className="h-4 w-4 text-blue-600" />
             </div>
-
-            {/* Weekday headers */}
-            <div className="grid grid-cols-7 gap-px mb-1">
-              {WEEKDAYS.map((day) => (
-                <div key={day} className="text-center text-xs font-medium text-muted-foreground py-1">
-                  {day}
-                </div>
-              ))}
+            <div>
+              <p className="text-2xl font-bold leading-none">{stats.serviceCount}</p>
+              <p className="text-xs text-muted-foreground mt-0.5">Services</p>
             </div>
-
-            {/* Day grid */}
-            <div className="grid grid-cols-7 gap-px">
-              {days.map((date) => {
-                const dateStr = date.toISOString().split("T")[0];
-                const selStr = selectedDate.toISOString().split("T")[0];
-                return (
-                  <CalendarDayCell
-                    key={dateStr}
-                    date={date}
-                    events={events}
-                    isCurrentMonth={date.getMonth() === month}
-                    isToday={dateStr === todayStr}
-                    isSelected={dateStr === selStr}
-                    onClick={() => setSelectedDate(date)}
-                  />
-                );
-              })}
+          </CardContent>
+        </Card>
+        <Card className="border-0 shadow-sm">
+          <CardContent className="p-3 flex items-center gap-3">
+            <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-red-500/10">
+              <AlertTriangle className="h-4 w-4 text-red-600" />
+            </div>
+            <div>
+              <p className="text-2xl font-bold leading-none">{stats.overdueReminders}</p>
+              <p className="text-xs text-muted-foreground mt-0.5">Overdue</p>
+            </div>
+          </CardContent>
+        </Card>
+        <Card className="border-0 shadow-sm">
+          <CardContent className="p-3 flex items-center gap-3">
+            <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-violet-500/10">
+              <FileText className="h-4 w-4 text-violet-600" />
+            </div>
+            <div>
+              <p className="text-2xl font-bold leading-none">{stats.pendingQuotes}</p>
+              <p className="text-xs text-muted-foreground mt-0.5">Pending Quotes</p>
             </div>
           </CardContent>
         </Card>
-      </div>
-
-      {/* Event list sidebar */}
-      <div>
         <Card className="border-0 shadow-sm">
-          <CardContent className="p-4">
-            <CalendarEventList events={events} selectedDate={selectedDate} />
+          <CardContent className="p-3 flex items-center gap-3">
+            <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-emerald-500/10">
+              <span className="text-sm font-bold text-emerald-600">$</span>
+            </div>
+            <div>
+              <p className="text-2xl font-bold leading-none tabular-nums">
+                {stats.totalRevenue > 0
+                  ? new Intl.NumberFormat(undefined, {
+                      notation: "compact",
+                      maximumFractionDigits: 1,
+                    }).format(stats.totalRevenue)
+                  : "0"}
+              </p>
+              <p className="text-xs text-muted-foreground mt-0.5">Revenue</p>
+            </div>
           </CardContent>
         </Card>
       </div>
+
+      <div className="grid gap-4 lg:grid-cols-3">
+        {/* Calendar grid */}
+        <div className="lg:col-span-2">
+          <Card className="border-0 shadow-sm">
+            <CardContent className="p-4">
+              {/* Header */}
+              <div className="flex items-center justify-between mb-4">
+                <div className="flex items-center gap-2">
+                  <Button size="icon" variant="outline" className="h-8 w-8" onClick={goToPrev}>
+                    <ChevronLeft className="h-4 w-4" />
+                  </Button>
+                  <Button size="icon" variant="outline" className="h-8 w-8" onClick={goToNext}>
+                    <ChevronRight className="h-4 w-4" />
+                  </Button>
+                  <h2 className="text-lg font-semibold ml-2">
+                    {MONTH_NAMES[month]} {year}
+                  </h2>
+                  {loading && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground ml-2" />}
+                </div>
+                <div className="flex items-center gap-2">
+                  <Button size="sm" variant="outline" onClick={goToToday}>
+                    Today
+                  </Button>
+                  <Button
+                    size="sm"
+                    onClick={() => setShowPicker(true)}
+                  >
+                    <Plus className="h-4 w-4 mr-1" />
+                    Work Order
+                  </Button>
+                </div>
+              </div>
+
+              {/* Filters */}
+              <div className="flex items-center gap-4 mb-3 text-sm">
+                <label className="flex items-center gap-1.5 cursor-pointer">
+                  <Checkbox
+                    checked={showServices}
+                    onCheckedChange={(v) => setShowServices(!!v)}
+                  />
+                  <div className="h-2 w-2 rounded-full bg-blue-500" />
+                  <span className="text-muted-foreground">Services</span>
+                </label>
+                <label className="flex items-center gap-1.5 cursor-pointer">
+                  <Checkbox
+                    checked={showReminders}
+                    onCheckedChange={(v) => setShowReminders(!!v)}
+                  />
+                  <div className="h-2 w-2 rounded-full bg-amber-500" />
+                  <span className="text-muted-foreground">Reminders</span>
+                </label>
+                <label className="flex items-center gap-1.5 cursor-pointer">
+                  <Checkbox
+                    checked={showQuotes}
+                    onCheckedChange={(v) => setShowQuotes(!!v)}
+                  />
+                  <div className="h-2 w-2 rounded-full bg-violet-500" />
+                  <span className="text-muted-foreground">Quotes</span>
+                </label>
+              </div>
+
+              {/* Weekday headers */}
+              <div className="grid grid-cols-7 gap-px mb-1">
+                {WEEKDAYS.map((day) => (
+                  <div key={day} className="text-center text-xs font-medium text-muted-foreground py-1">
+                    {day}
+                  </div>
+                ))}
+              </div>
+
+              {/* Day grid */}
+              <div className="grid grid-cols-7 gap-px">
+                {days.map((date) => {
+                  const dateStr = toLocalDateStr(date);
+                  return (
+                    <CalendarDayCell
+                      key={dateStr}
+                      date={date}
+                      events={filteredEvents}
+                      isCurrentMonth={date.getMonth() === month}
+                      isToday={dateStr === todayStr}
+                      isSelected={dateStr === selectedDateStr}
+                      onClick={() => setSelectedDate(date)}
+                    />
+                  );
+                })}
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+
+        {/* Event list sidebar */}
+        <div>
+          <Card className="border-0 shadow-sm">
+            <CardContent className="p-4">
+              <CalendarEventList
+                events={filteredEvents}
+                dateStr={selectedDateStr}
+                selectedDate={selectedDate}
+                currencyCode={currencyCode}
+              />
+            </CardContent>
+          </Card>
+        </div>
+      </div>
+
+      {/* Vehicle picker dialog */}
+      <Dialog open={showPicker} onOpenChange={setShowPicker}>
+        <DialogContent className="max-w-md">
+          <DialogHeader>
+            <DialogTitle>Select Vehicle for Work Order</DialogTitle>
+          </DialogHeader>
+          <div className="space-y-3">
+            <div className="relative">
+              <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
+              <Input
+                placeholder="Search vehicles..."
+                value={vehicleSearch}
+                onChange={(e) => setVehicleSearch(e.target.value)}
+                className="pl-9"
+                autoFocus
+              />
+            </div>
+            <div className="max-h-[300px] overflow-y-auto">
+              {filteredVehicles.length === 0 ? (
+                <p className="py-6 text-center text-sm text-muted-foreground">
+                  {vehicles.length === 0
+                    ? "No vehicles found. Add a vehicle first."
+                    : "No vehicles match your search."}
+                </p>
+              ) : (
+                <div className="space-y-1">
+                  {filteredVehicles.map((v) => (
+                    <button
+                      key={v.id}
+                      type="button"
+                      className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition-colors hover:bg-muted"
+                      onClick={() => handleCreateWorkOrder(v.id)}
+                    >
+                      <Car className="h-4 w-4 shrink-0 text-muted-foreground" />
+                      <div className="min-w-0 flex-1">
+                        <p className="text-sm font-medium">
+                          {v.year} {v.make} {v.model}
+                        </p>
+                        <p className="text-xs text-muted-foreground">
+                          {[v.licensePlate, v.customer?.name]
+                            .filter(Boolean)
+                            .join(" · ") || "No plate"}
+                        </p>
+                      </div>
+                    </button>
+                  ))}
+                </div>
+              )}
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
     </div>
   );
 }

+ 55 - 13
src/features/calendar/Components/CalendarDayCell.tsx

@@ -1,19 +1,25 @@
 "use client";
 
+import { toLocalDateStr } from "./calendar-utils";
 import type { CalendarEvent } from "../Actions/calendarActions";
 
 function getEventDotColor(event: CalendarEvent) {
   if (event.type === "service") {
     switch (event.status) {
       case "completed": return "bg-emerald-500";
-      case "in_progress": return "bg-blue-500";
+      case "in_progress": case "in-progress": return "bg-blue-500";
+      case "waiting-parts": return "bg-orange-500";
       default: return "bg-amber-500";
     }
   }
+  if (event.type === "quote") {
+    return event.status === "sent" ? "bg-violet-500" : "bg-violet-300";
+  }
+  // reminder
   switch (event.status) {
     case "completed": return "bg-emerald-500";
     case "overdue": return "bg-red-500";
-    default: return "bg-muted-foreground";
+    default: return "bg-slate-400";
   }
 }
 
@@ -27,36 +33,72 @@ interface CalendarDayCellProps {
 }
 
 export function CalendarDayCell({ date, events, isCurrentMonth, isToday, isSelected, onClick }: CalendarDayCellProps) {
-  const dateStr = date.toISOString().split("T")[0];
-  const dayEvents = events.filter((e) => e.date.split("T")[0] === dateStr);
+  const dateStr = toLocalDateStr(date);
+  const dayEvents = events.filter((e) => e.date === dateStr);
+  const serviceCount = dayEvents.filter((e) => e.type === "service").length;
+  const reminderCount = dayEvents.filter((e) => e.type === "reminder").length;
+  const quoteCount = dayEvents.filter((e) => e.type === "quote").length;
 
   return (
     <button
       type="button"
       onClick={onClick}
       className={`
-        relative flex flex-col items-center justify-start p-1.5 min-h-[72px] w-full rounded-md text-sm transition-colors
+        relative flex flex-col items-center justify-start p-1 min-h-[80px] w-full rounded-md text-sm transition-colors
         ${isCurrentMonth ? "" : "text-muted-foreground/40"}
-        ${isToday ? "bg-primary/10 font-semibold" : ""}
+        ${isToday && !isSelected ? "bg-primary/10" : ""}
         ${isSelected ? "ring-2 ring-primary bg-primary/5" : "hover:bg-muted/50"}
       `}
     >
-      <span className={`text-xs ${isToday ? "text-primary font-bold" : ""}`}>
+      <span
+        className={`
+          flex h-6 w-6 items-center justify-center rounded-full text-xs
+          ${isToday ? "bg-primary text-primary-foreground font-bold" : ""}
+        `}
+      >
         {date.getDate()}
       </span>
       {dayEvents.length > 0 && (
-        <div className="flex gap-0.5 mt-1 flex-wrap justify-center max-w-full">
-          {dayEvents.slice(0, 4).map((event) => (
+        <div className="flex flex-col gap-0.5 mt-0.5 w-full px-0.5">
+          {/* Show up to 3 mini event labels on larger screens */}
+          {dayEvents.slice(0, 2).map((event) => (
             <div
               key={`${event.type}-${event.id}`}
-              className={`h-1.5 w-1.5 rounded-full ${getEventDotColor(event)}`}
-            />
+              className={`hidden sm:flex items-center gap-1 rounded px-1 py-0 text-[10px] leading-tight truncate ${getEventBgColor(event)}`}
+            >
+              <div className={`h-1 w-1 shrink-0 rounded-full ${getEventDotColor(event)}`} />
+              <span className="truncate">{event.title}</span>
+            </div>
           ))}
-          {dayEvents.length > 4 && (
-            <span className="text-[9px] text-muted-foreground">+{dayEvents.length - 4}</span>
+          {dayEvents.length > 2 && (
+            <span className="hidden sm:block text-[9px] text-muted-foreground px-1">+{dayEvents.length - 2} more</span>
           )}
+          {/* Mobile: show dots */}
+          <div className="flex sm:hidden gap-0.5 justify-center flex-wrap">
+            {serviceCount > 0 && <div className="flex items-center gap-0.5"><div className="h-1.5 w-1.5 rounded-full bg-blue-500" />{serviceCount > 1 && <span className="text-[8px] text-muted-foreground">{serviceCount}</span>}</div>}
+            {reminderCount > 0 && <div className="flex items-center gap-0.5"><div className="h-1.5 w-1.5 rounded-full bg-amber-500" />{reminderCount > 1 && <span className="text-[8px] text-muted-foreground">{reminderCount}</span>}</div>}
+            {quoteCount > 0 && <div className="flex items-center gap-0.5"><div className="h-1.5 w-1.5 rounded-full bg-violet-500" />{quoteCount > 1 && <span className="text-[8px] text-muted-foreground">{quoteCount}</span>}</div>}
+          </div>
         </div>
       )}
     </button>
   );
 }
+
+function getEventBgColor(event: CalendarEvent) {
+  if (event.type === "service") {
+    switch (event.status) {
+      case "completed": return "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400";
+      case "in_progress": case "in-progress": return "bg-blue-500/10 text-blue-700 dark:text-blue-400";
+      case "waiting-parts": return "bg-orange-500/10 text-orange-700 dark:text-orange-400";
+      default: return "bg-amber-500/10 text-amber-700 dark:text-amber-400";
+    }
+  }
+  if (event.type === "quote") {
+    return "bg-violet-500/10 text-violet-700 dark:text-violet-400";
+  }
+  switch (event.status) {
+    case "overdue": return "bg-red-500/10 text-red-700 dark:text-red-400";
+    default: return "bg-slate-500/10 text-slate-600 dark:text-slate-400";
+  }
+}

+ 90 - 45
src/features/calendar/Components/CalendarEventList.tsx

@@ -2,83 +2,128 @@
 
 import Link from "next/link";
 import { Badge } from "@/components/ui/badge";
+import { formatDateHeader } from "./calendar-utils";
+import { formatCurrency } from "@/lib/format";
 import type { CalendarEvent } from "../Actions/calendarActions";
 
 function getStatusColor(event: CalendarEvent) {
   if (event.type === "service") {
     switch (event.status) {
       case "completed": return "bg-emerald-500";
-      case "in_progress": return "bg-blue-500";
+      case "in_progress": case "in-progress": return "bg-blue-500";
+      case "waiting-parts": return "bg-orange-500";
       default: return "bg-amber-500";
     }
   }
-  // reminder
+  if (event.type === "quote") {
+    return event.status === "sent" ? "bg-violet-500" : "bg-violet-300";
+  }
   switch (event.status) {
     case "completed": return "bg-emerald-500";
     case "overdue": return "bg-red-500";
-    default: return "bg-muted-foreground";
+    default: return "bg-slate-400";
   }
 }
 
 function getStatusBadge(event: CalendarEvent) {
   if (event.type === "service") {
     switch (event.status) {
-      case "completed": return <Badge variant="outline" className="text-emerald-600 border-emerald-300">Completed</Badge>;
-      case "in_progress": return <Badge variant="outline" className="text-blue-600 border-blue-300">In Progress</Badge>;
-      default: return <Badge variant="outline" className="text-amber-600 border-amber-300">Pending</Badge>;
+      case "completed": return <Badge variant="outline" className="text-emerald-600 border-emerald-300 text-[10px] px-1.5 py-0">Completed</Badge>;
+      case "in_progress": case "in-progress": return <Badge variant="outline" className="text-blue-600 border-blue-300 text-[10px] px-1.5 py-0">In Progress</Badge>;
+      case "waiting-parts": return <Badge variant="outline" className="text-orange-600 border-orange-300 text-[10px] px-1.5 py-0">Waiting Parts</Badge>;
+      case "ready": return <Badge variant="outline" className="text-cyan-600 border-cyan-300 text-[10px] px-1.5 py-0">Ready</Badge>;
+      default: return <Badge variant="outline" className="text-amber-600 border-amber-300 text-[10px] px-1.5 py-0">Pending</Badge>;
+    }
+  }
+  if (event.type === "quote") {
+    switch (event.status) {
+      case "sent": return <Badge variant="outline" className="text-violet-600 border-violet-300 text-[10px] px-1.5 py-0">Sent</Badge>;
+      case "approved": return <Badge variant="outline" className="text-emerald-600 border-emerald-300 text-[10px] px-1.5 py-0">Approved</Badge>;
+      default: return <Badge variant="outline" className="text-violet-600 border-violet-300 text-[10px] px-1.5 py-0">Draft</Badge>;
     }
   }
   switch (event.status) {
-    case "completed": return <Badge variant="outline" className="text-emerald-600 border-emerald-300">Done</Badge>;
-    case "overdue": return <Badge variant="destructive">Overdue</Badge>;
-    default: return <Badge variant="outline">Upcoming</Badge>;
+    case "completed": return <Badge variant="outline" className="text-emerald-600 border-emerald-300 text-[10px] px-1.5 py-0">Done</Badge>;
+    case "overdue": return <Badge variant="destructive" className="text-[10px] px-1.5 py-0">Overdue</Badge>;
+    default: return <Badge variant="outline" className="text-[10px] px-1.5 py-0">Upcoming</Badge>;
+  }
+}
+
+function getTypeBadge(type: CalendarEvent["type"]) {
+  switch (type) {
+    case "service": return <Badge variant="secondary" className="shrink-0 text-[10px] px-1.5 py-0 bg-blue-500/10 text-blue-700 dark:text-blue-400">Service</Badge>;
+    case "reminder": return <Badge variant="secondary" className="shrink-0 text-[10px] px-1.5 py-0 bg-amber-500/10 text-amber-700 dark:text-amber-400">Reminder</Badge>;
+    case "quote": return <Badge variant="secondary" className="shrink-0 text-[10px] px-1.5 py-0 bg-violet-500/10 text-violet-700 dark:text-violet-400">Quote</Badge>;
   }
 }
 
+function getEventLink(event: CalendarEvent) {
+  if (event.type === "quote") return `/quotes/${event.id}`;
+  return `/vehicles/${event.vehicleId}`;
+}
+
 interface CalendarEventListProps {
   events: CalendarEvent[];
+  dateStr: string; // YYYY-MM-DD
   selectedDate: Date;
+  currencyCode: string;
 }
 
-export function CalendarEventList({ events, selectedDate }: CalendarEventListProps) {
-  const dateStr = selectedDate.toISOString().split("T")[0];
-  const dayEvents = events.filter((e) => e.date.split("T")[0] === dateStr);
-
-  if (dayEvents.length === 0) {
-    return (
-      <div className="text-center py-8 text-sm text-muted-foreground">
-        No events on {selectedDate.toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" })}
-      </div>
-    );
-  }
+export function CalendarEventList({ events, dateStr, selectedDate, currencyCode }: CalendarEventListProps) {
+  const dayEvents = events.filter((e) => e.date === dateStr);
 
   return (
-    <div className="space-y-2">
-      <p className="text-sm font-medium text-muted-foreground mb-3">
-        {selectedDate.toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" })}
+    <div className="space-y-3">
+      <p className="text-sm font-semibold">
+        {formatDateHeader(selectedDate)}
       </p>
-      {dayEvents.map((event) => (
-        <Link
-          key={`${event.type}-${event.id}`}
-          href={`/vehicles/${event.vehicleId}`}
-          className="flex items-start gap-3 rounded-lg border p-3 transition-colors hover:bg-muted/50"
-        >
-          <div className={`mt-1.5 h-2 w-2 shrink-0 rounded-full ${getStatusColor(event)}`} />
-          <div className="min-w-0 flex-1">
-            <div className="flex items-center gap-2">
-              <p className="text-sm font-medium truncate">{event.title}</p>
-              {getStatusBadge(event)}
-            </div>
-            <p className="text-xs text-muted-foreground mt-0.5">{event.vehicleLabel}</p>
-            {event.customerName && (
-              <p className="text-xs text-muted-foreground">{event.customerName}</p>
-            )}
-          </div>
-          <Badge variant="secondary" className="shrink-0 text-xs">
-            {event.type === "service" ? "Service" : "Reminder"}
-          </Badge>
-        </Link>
-      ))}
+
+      {dayEvents.length === 0 ? (
+        <div className="text-center py-8">
+          <p className="text-sm text-muted-foreground mb-1">No events</p>
+          <p className="text-xs text-muted-foreground">
+            Select a day with events or create a new work order.
+          </p>
+        </div>
+      ) : (
+        <div className="space-y-2">
+          {dayEvents.map((event) => (
+            <Link
+              key={`${event.type}-${event.id}`}
+              href={getEventLink(event)}
+              className="flex items-start gap-2.5 rounded-lg border p-2.5 transition-colors hover:bg-muted/50"
+            >
+              <div className={`mt-1.5 h-2 w-2 shrink-0 rounded-full ${getStatusColor(event)}`} />
+              <div className="min-w-0 flex-1">
+                <div className="flex items-center gap-1.5 flex-wrap">
+                  <p className="text-sm font-medium truncate">{event.title}</p>
+                  {getStatusBadge(event)}
+                </div>
+                <div className="flex items-center gap-2 mt-0.5">
+                  <p className="text-xs text-muted-foreground truncate">{event.vehicleLabel}</p>
+                  {event.time && (
+                    <span className="text-xs text-muted-foreground shrink-0">{event.time}</span>
+                  )}
+                </div>
+                {event.customerName && (
+                  <p className="text-xs text-muted-foreground">{event.customerName}</p>
+                )}
+                <div className="flex items-center gap-2 mt-1">
+                  {getTypeBadge(event.type)}
+                  {event.invoiceNumber && (
+                    <span className="text-[10px] text-muted-foreground">#{event.invoiceNumber}</span>
+                  )}
+                  {event.amount != null && (
+                    <span className="text-[10px] font-medium ml-auto">
+                      {formatCurrency(event.amount, currencyCode)}
+                    </span>
+                  )}
+                </div>
+              </div>
+            </Link>
+          ))}
+        </div>
+      )}
     </div>
   );
 }

+ 16 - 0
src/features/calendar/Components/calendar-utils.ts

@@ -0,0 +1,16 @@
+/** Format a Date as YYYY-MM-DD in local time (avoids UTC shift from toISOString) */
+export function toLocalDateStr(d: Date): string {
+  const y = d.getFullYear();
+  const m = String(d.getMonth() + 1).padStart(2, "0");
+  const day = String(d.getDate()).padStart(2, "0");
+  return `${y}-${m}-${day}`;
+}
+
+export function formatDateHeader(d: Date): string {
+  return d.toLocaleDateString(undefined, {
+    weekday: "long",
+    month: "long",
+    day: "numeric",
+    year: "numeric",
+  });
+}

+ 3 - 3
src/features/vehicles/Actions/createDraftServiceRecord.ts

@@ -4,7 +4,7 @@ import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 
-export async function createDraftServiceRecord(vehicleId: string) {
+export async function createDraftServiceRecord(vehicleId: string, serviceDate?: Date) {
   return withAuth(
     async ({ organizationId }) => {
       const vehicle = await db.vehicle.findFirst({
@@ -70,11 +70,11 @@ export async function createDraftServiceRecord(vehicleId: string) {
         data: {
           title: "New Service Record",
           type: "maintenance",
-          status: "in_progress",
+          status: "pending",
           vehicleId,
           shopName,
           invoiceNumber,
-          serviceDate: now,
+          serviceDate: serviceDate ?? now,
         },
       });
 

+ 10 - 0
src/lib/with-auth.ts

@@ -1,3 +1,4 @@
+import { ZodError } from "zod";
 import { getCachedSession, getCachedMembership } from "./cached-session";
 import { db } from "./db";
 import type { PermissionInput } from "./permissions";
@@ -66,6 +67,15 @@ export async function withAuth<T>(
     });
     return { success: true, data };
   } catch (error) {
+    if (error instanceof ZodError) {
+      const messages = error.issues.map((e) => {
+        const field = e.path.join(".");
+        return field ? `${field}: ${e.message}` : e.message;
+      });
+      const message = messages.join(". ");
+      console.error("[withAuth] Validation error:", message);
+      return { success: false, error: message };
+    }
     const message =
       error instanceof Error ? error.message : "An unexpected error occurred";
     console.error("[withAuth] Error:", message);