Bladeren bron

Right-click menu for calendar days (#238)

* add right click context menu

* locale

* clean up calendar page
Bernt Christian Egeland 1 maand geleden
bovenliggende
commit
478c3c1fd1

+ 8 - 0
messages/de/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} weitere",
+  "monthTotal": "{count} Termine in diesem Monat",
+  "contextMenu": {
+    "newWorkOrder": "Neuer Arbeitsauftrag",
+    "newReminder": "Neue Erinnerung",
+    "newQuote": "Neues Angebot",
+    "openEvent": "Öffnen ({count})",
+    "hint": "Tipp: Rechtsklick auf einen Tag zeigt Aktionen"
+  },
   "events": {
     "status": {
       "completed": "Abgeschlossen",

+ 8 - 0
messages/en/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} more",
+  "monthTotal": "{count} events this month",
+  "contextMenu": {
+    "newWorkOrder": "New work order",
+    "newReminder": "New reminder",
+    "newQuote": "New quote",
+    "openEvent": "Open ({count})",
+    "hint": "Tip: right-click a day for actions"
+  },
   "events": {
     "status": {
       "completed": "Completed",

+ 8 - 0
messages/es/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} más",
+  "monthTotal": "{count} eventos este mes",
+  "contextMenu": {
+    "newWorkOrder": "Nueva orden de trabajo",
+    "newReminder": "Nuevo recordatorio",
+    "newQuote": "Nuevo presupuesto",
+    "openEvent": "Abrir ({count})",
+    "hint": "Consejo: haz clic derecho en un día para ver las acciones"
+  },
   "events": {
     "status": {
       "completed": "Completado",

+ 8 - 0
messages/fr/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} de plus",
+  "monthTotal": "{count} événements ce mois-ci",
+  "contextMenu": {
+    "newWorkOrder": "Nouvel ordre de travail",
+    "newReminder": "Nouveau rappel",
+    "newQuote": "Nouveau devis",
+    "openEvent": "Ouvrir ({count})",
+    "hint": "Astuce : faites un clic droit sur un jour pour voir les actions"
+  },
   "events": {
     "status": {
       "completed": "Terminé",

+ 8 - 0
messages/it/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} altri",
+  "monthTotal": "{count} eventi questo mese",
+  "contextMenu": {
+    "newWorkOrder": "Nuovo ordine di lavoro",
+    "newReminder": "Nuovo promemoria",
+    "newQuote": "Nuovo preventivo",
+    "openEvent": "Apri ({count})",
+    "hint": "Suggerimento: fai clic con il tasto destro su un giorno per le azioni"
+  },
   "events": {
     "status": {
       "completed": "Completato",

+ 8 - 0
messages/lt/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Naudoti šiandienos datą"
   },
   "more": "+ {count} daugiau",
+  "monthTotal": "{count} įvykių šį mėnesį",
+  "contextMenu": {
+    "newWorkOrder": "Naujas darbo užsakymas",
+    "newReminder": "Naujas priminimas",
+    "newQuote": "Naujas pasiūlymas",
+    "openEvent": "Atidaryti ({count})",
+    "hint": "Patarimas: spustelėkite dieną dešiniuoju pelės mygtuku, kad pamatytumėte veiksmus"
+  },
   "events": {
     "status": {
       "completed": "Užbaigta",

+ 8 - 0
messages/nb/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} flere",
+  "monthTotal": "{count} hendelser denne måneden",
+  "contextMenu": {
+    "newWorkOrder": "Ny arbeidsordre",
+    "newReminder": "Ny påminnelse",
+    "newQuote": "Nytt tilbud",
+    "openEvent": "Åpne ({count})",
+    "hint": "Tips: høyreklikk en dag for handlinger"
+  },
   "events": {
     "status": {
       "completed": "Fullført",

+ 8 - 0
messages/nl/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} meer",
+  "monthTotal": "{count} items deze maand",
+  "contextMenu": {
+    "newWorkOrder": "Nieuwe werkorder",
+    "newReminder": "Nieuwe herinnering",
+    "newQuote": "Nieuwe offerte",
+    "openEvent": "Openen ({count})",
+    "hint": "Tip: klik met de rechtermuisknop op een dag voor acties"
+  },
   "events": {
     "status": {
       "completed": "Voltooid",

+ 8 - 0
messages/pl/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} więcej",
+  "monthTotal": "{count} wydarzeń w tym miesiącu",
+  "contextMenu": {
+    "newWorkOrder": "Nowe zlecenie",
+    "newReminder": "Nowe przypomnienie",
+    "newQuote": "Nowa wycena",
+    "openEvent": "Otwórz ({count})",
+    "hint": "Wskazówka: kliknij dzień prawym przyciskiem myszy, aby zobaczyć akcje"
+  },
   "events": {
     "status": {
       "completed": "Ukończono",

+ 8 - 0
messages/pt-BR/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} mais",
+  "monthTotal": "{count} eventos neste mês",
+  "contextMenu": {
+    "newWorkOrder": "Nova ordem de serviço",
+    "newReminder": "Novo lembrete",
+    "newQuote": "Novo orçamento",
+    "openEvent": "Abrir ({count})",
+    "hint": "Dica: clique com o botão direito em um dia para ver as ações"
+  },
   "events": {
     "status": {
       "completed": "Concluído",

+ 8 - 0
messages/ru/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Сегодня"
   },
   "more": "+ ещё {count}",
+  "monthTotal": "{count} событий в этом месяце",
+  "contextMenu": {
+    "newWorkOrder": "Новый заказ-наряд",
+    "newReminder": "Новое напоминание",
+    "newQuote": "Новое предложение",
+    "openEvent": "Открыть ({count})",
+    "hint": "Совет: щёлкните по дню правой кнопкой мыши, чтобы увидеть действия"
+  },
   "events": {
     "status": {
       "completed": "Завершено",

+ 8 - 0
messages/tr/calendar.json

@@ -43,6 +43,14 @@
     "useToday": "Use today"
   },
   "more": "+ {count} daha",
+  "monthTotal": "bu ay {count} kayıt",
+  "contextMenu": {
+    "newWorkOrder": "Yeni iş emri",
+    "newReminder": "Yeni hatırlatıcı",
+    "newQuote": "Yeni teklif",
+    "openEvent": "Aç ({count})",
+    "hint": "İpucu: işlemler için bir güne sağ tıklayın"
+  },
   "events": {
     "status": {
       "completed": "Tamamlandı",

+ 90 - 74
src/features/calendar/Components/CalendarClient.tsx

@@ -11,12 +11,12 @@ import {
   ChevronRight,
   Loader2,
   Plus,
-  Wrench,
-  FileText,
-  AlertTriangle,
+  MousePointerClick,
 } from "lucide-react";
 import { CalendarDayCell } from "./CalendarDayCell";
 import { CalendarEventList } from "./CalendarEventList";
+import { ReminderFormDialog } from "@/features/vehicles/Components/ReminderFormDialog";
+import { NewQuoteDialog } from "@/features/quotes/Components/NewQuoteDialog";
 import { toLocalDateStr } from "./calendar-utils";
 import { getCalendarEvents } from "../Actions/calendarActions";
 import type { CalendarEvent } from "../Actions/calendarActions";
@@ -117,6 +117,11 @@ export default function CalendarClient({
   const [showDateChoice, setShowDateChoice] = useState(false);
   const [workOrderDate, setWorkOrderDate] = useState<string | undefined>(undefined);
 
+  // Right-click menu targets: the day the menu was opened on
+  const [showReminderDialog, setShowReminderDialog] = useState(false);
+  const [showQuoteDialog, setShowQuoteDialog] = useState(false);
+  const [menuDateStr, setMenuDateStr] = useState<string | undefined>(undefined);
+
   const selectedDateStr = toLocalDateStr(selectedDate);
 
   const handleNewWorkOrder = useCallback(() => {
@@ -143,20 +148,16 @@ export default function CalendarClient({
     });
   }, [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]);
+  // What the displayed month holds, counted before the filters hide anything
+  const counts = useMemo(
+    () => ({
+      services: events.filter((e) => e.type === "service").length,
+      reminders: events.filter((e) => e.type === "reminder").length,
+      quotes: events.filter((e) => e.type === "quote").length,
+      total: events.length,
+    }),
+    [events]
+  );
 
   const days = getMonthDays(year, month, weekStartDay);
 
@@ -174,6 +175,48 @@ export default function CalendarClient({
     setLoading(false);
   }, []);
 
+  // Noon parse, so the seeded day survives any timezone the workshop sits in.
+  // Memoised because the reminder dialog re-seeds whenever this value changes.
+  const menuDate = useMemo(
+    () => (menuDateStr ? new Date(`${menuDateStr}T12:00:00`) : undefined),
+    [menuDateStr]
+  );
+
+  // The reminder dialog picks vehicles from a flat list with the customer inlined
+  const reminderVehicles = useMemo(
+    () =>
+      vehicles.map((v) => ({
+        id: v.id,
+        make: v.make,
+        model: v.model,
+        year: v.year,
+        licensePlate: v.licensePlate,
+        customerName: v.customer?.name ?? null,
+        customerId: v.customer?.id ?? null,
+      })),
+    [vehicles]
+  );
+
+  // Right-click actions carry their day explicitly, so no date-choice prompt
+  const handleMenuWorkOrder = useCallback((dateStr: string) => {
+    setWorkOrderDate(dateStr);
+    setShowPicker(true);
+  }, []);
+
+  const handleMenuReminder = useCallback((dateStr: string) => {
+    setMenuDateStr(dateStr);
+    setShowReminderDialog(true);
+  }, []);
+
+  const handleMenuQuote = useCallback((dateStr: string) => {
+    setMenuDateStr(dateStr);
+    setShowQuoteDialog(true);
+  }, []);
+
+  const refreshMonth = useCallback(() => {
+    fetchEvents(year, month);
+  }, [fetchEvents, year, month]);
+
   const goToPrev = () => {
     const newMonth = month === 0 ? 11 : month - 1;
     const newYear = month === 0 ? year - 1 : year;
@@ -205,61 +248,6 @@ export default function CalendarClient({
 
   return (
     <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-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>
-            <div>
-              <p className="text-2xl font-bold leading-none">{stats.serviceCount}</p>
-              <p className="text-xs text-muted-foreground mt-0.5">{t('stats.services')}</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-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">{t('stats.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">{t('stats.pendingQuotes')}</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-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">{t('stats.revenue')}</p>
-            </div>
-          </CardContent>
-        </Card>
-      </div>
-
       <div className="grid gap-4 lg:grid-cols-3">
         {/* Calendar grid */}
         <div className="lg:col-span-2">
@@ -293,8 +281,8 @@ export default function CalendarClient({
                 </div>
               </div>
 
-              {/* Filters */}
-              <div className="flex items-center gap-4 mb-3 text-sm">
+              {/* Filters, each carrying what the month holds of that type */}
+              <div className="flex flex-wrap items-center gap-x-4 gap-y-2 mb-3 text-sm">
                 <label className="flex items-center gap-1.5 cursor-pointer">
                   <Checkbox
                     checked={showServices}
@@ -302,6 +290,7 @@ export default function CalendarClient({
                   />
                   <div className="h-2 w-2 rounded-full bg-blue-500" />
                   <span className="text-muted-foreground">{t('filters.services')}</span>
+                  <span className="font-medium tabular-nums">{counts.services}</span>
                 </label>
                 <label className="flex items-center gap-1.5 cursor-pointer">
                   <Checkbox
@@ -310,6 +299,7 @@ export default function CalendarClient({
                   />
                   <div className="h-2 w-2 rounded-full bg-amber-500" />
                   <span className="text-muted-foreground">{t('filters.reminders')}</span>
+                  <span className="font-medium tabular-nums">{counts.reminders}</span>
                 </label>
                 <label className="flex items-center gap-1.5 cursor-pointer">
                   <Checkbox
@@ -318,7 +308,11 @@ export default function CalendarClient({
                   />
                   <div className="h-2 w-2 rounded-full bg-violet-500" />
                   <span className="text-muted-foreground">{t('filters.quotes')}</span>
+                  <span className="font-medium tabular-nums">{counts.quotes}</span>
                 </label>
+                <span className="text-xs text-muted-foreground ml-auto">
+                  {t('monthTotal', { count: counts.total })}
+                </span>
               </div>
 
               {/* Weekday headers */}
@@ -343,10 +337,18 @@ export default function CalendarClient({
                       isToday={dateStr === todayStr}
                       isSelected={dateStr === selectedDateStr}
                       onClick={() => setSelectedDate(date)}
+                      onNewWorkOrder={() => handleMenuWorkOrder(dateStr)}
+                      onNewReminder={() => handleMenuReminder(dateStr)}
+                      onNewQuote={() => handleMenuQuote(dateStr)}
                     />
                   );
                 })}
               </div>
+
+              <div className="hidden md:flex items-center gap-1.5 pt-3 text-xs text-muted-foreground">
+                <MousePointerClick className="h-3.5 w-3.5" />
+                {t('contextMenu.hint')}
+              </div>
             </CardContent>
           </Card>
         </div>
@@ -390,6 +392,20 @@ export default function CalendarClient({
         </DialogContent>
       </Dialog>
 
+      <ReminderFormDialog
+        open={showReminderDialog}
+        onOpenChange={setShowReminderDialog}
+        vehicles={reminderVehicles}
+        defaultDueDate={menuDate}
+        onSaved={refreshMonth}
+      />
+
+      <NewQuoteDialog
+        open={showQuoteDialog}
+        onOpenChange={setShowQuoteDialog}
+        defaultValidUntil={menuDateStr}
+      />
+
       <VehiclePickerDialog
         open={showPicker}
         onOpenChange={setShowPicker}

+ 129 - 41
src/features/calendar/Components/CalendarDayCell.tsx

@@ -1,7 +1,21 @@
 "use client";
 
+import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { toLocalDateStr } from "./calendar-utils";
+import { Bell, ExternalLink, FileText, Wrench } from "lucide-react";
+import {
+  ContextMenu,
+  ContextMenuContent,
+  ContextMenuItem,
+  ContextMenuLabel,
+  ContextMenuSeparator,
+  ContextMenuSub,
+  ContextMenuSubContent,
+  ContextMenuSubTrigger,
+  ContextMenuTrigger,
+} from "@/components/ui/context-menu";
+import { useFormatDate } from "@/lib/use-format-date";
+import { getEventLink, toLocalDateStr } from "./calendar-utils";
 import type { CalendarEvent } from "../Actions/calendarActions";
 
 function getEventDotColor(event: CalendarEvent) {
@@ -24,6 +38,9 @@ function getEventDotColor(event: CalendarEvent) {
   }
 }
 
+/** How many of the day's events the right-click menu lists before it stops */
+const MAX_MENU_EVENTS = 8;
+
 interface CalendarDayCellProps {
   date: Date;
   events: CalendarEvent[];
@@ -31,10 +48,25 @@ interface CalendarDayCellProps {
   isToday: boolean;
   isSelected: boolean;
   onClick: () => void;
+  onNewWorkOrder: () => void;
+  onNewReminder: () => void;
+  onNewQuote: () => void;
 }
 
-export function CalendarDayCell({ date, events, isCurrentMonth, isToday, isSelected, onClick }: CalendarDayCellProps) {
+export function CalendarDayCell({
+  date,
+  events,
+  isCurrentMonth,
+  isToday,
+  isSelected,
+  onClick,
+  onNewWorkOrder,
+  onNewReminder,
+  onNewQuote,
+}: CalendarDayCellProps) {
   const t = useTranslations('calendar');
+  const router = useRouter();
+  const { formatDate } = useFormatDate();
   const dateStr = toLocalDateStr(date);
   const dayEvents = events.filter((e) => e.date === dateStr);
   const serviceCount = dayEvents.filter((e) => e.type === "service").length;
@@ -42,48 +74,104 @@ export function CalendarDayCell({ date, events, isCurrentMonth, isToday, isSelec
   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 min-h-[80px] w-full rounded-md text-sm transition-colors
-        ${isCurrentMonth ? "" : "text-muted-foreground/40"}
-        ${isToday && !isSelected ? "bg-primary/10" : ""}
-        ${isSelected ? "ring-2 ring-primary bg-primary/5" : "hover:bg-muted/50"}
-      `}
+    <ContextMenu
+      // Right-clicking a day also selects it, so the sidebar and any dialog
+      // the menu opens are talking about the same date.
+      onOpenChange={(open) => {
+        if (open && !isSelected) onClick();
+      }}
     >
-      <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 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={`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>
+      <ContextMenuTrigger asChild>
+        <button
+          type="button"
+          onClick={onClick}
+          className={`
+            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 && !isSelected ? "bg-primary/10" : ""}
+            ${isSelected ? "ring-2 ring-primary bg-primary/5" : "hover:bg-muted/50"}
+          `}
+        >
+          <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 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={`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 > 2 && (
+                <span className="hidden sm:block text-[9px] text-muted-foreground px-1">{t('more', { count: dayEvents.length - 2 })}</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>
-          ))}
-          {dayEvents.length > 2 && (
-            <span className="hidden sm:block text-[9px] text-muted-foreground px-1">{t('more', { count: dayEvents.length - 2 })}</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>
+        </button>
+      </ContextMenuTrigger>
+
+      <ContextMenuContent className="min-w-56">
+        <ContextMenuLabel className="text-xs font-normal text-muted-foreground">
+          {formatDate(date)}
+        </ContextMenuLabel>
+        <ContextMenuSeparator />
+        <ContextMenuItem onClick={onNewWorkOrder}>
+          <Wrench className="mr-2 h-4 w-4" />
+          {t('contextMenu.newWorkOrder')}
+        </ContextMenuItem>
+        <ContextMenuItem onClick={onNewReminder}>
+          <Bell className="mr-2 h-4 w-4" />
+          {t('contextMenu.newReminder')}
+        </ContextMenuItem>
+        <ContextMenuItem onClick={onNewQuote}>
+          <FileText className="mr-2 h-4 w-4" />
+          {t('contextMenu.newQuote')}
+        </ContextMenuItem>
+        {dayEvents.length > 0 && (
+          <>
+            <ContextMenuSeparator />
+            <ContextMenuSub>
+              <ContextMenuSubTrigger>
+                <ExternalLink className="mr-2 h-4 w-4" />
+                {t('contextMenu.openEvent', { count: dayEvents.length })}
+              </ContextMenuSubTrigger>
+              <ContextMenuSubContent className="max-w-72">
+                {dayEvents.slice(0, MAX_MENU_EVENTS).map((event) => (
+                  <ContextMenuItem
+                    key={`${event.type}-${event.id}`}
+                    onClick={() => router.push(getEventLink(event))}
+                  >
+                    <div className={`mr-2 h-1.5 w-1.5 shrink-0 rounded-full ${getEventDotColor(event)}`} />
+                    <span className="truncate">{event.title}</span>
+                  </ContextMenuItem>
+                ))}
+                {dayEvents.length > MAX_MENU_EVENTS && (
+                  <ContextMenuItem disabled>
+                    {t('more', { count: dayEvents.length - MAX_MENU_EVENTS })}
+                  </ContextMenuItem>
+                )}
+              </ContextMenuSubContent>
+            </ContextMenuSub>
+          </>
+        )}
+      </ContextMenuContent>
+    </ContextMenu>
   );
 }
 

+ 1 - 7
src/features/calendar/Components/CalendarEventList.tsx

@@ -3,7 +3,7 @@
 import Link from "next/link";
 import { useTranslations } from "next-intl";
 import { Badge } from "@/components/ui/badge";
-import { formatDateHeader } from "./calendar-utils";
+import { formatDateHeader, getEventLink } from "./calendar-utils";
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import type { CalendarEvent } from "../Actions/calendarActions";
 
@@ -57,12 +57,6 @@ function getTypeBadge(type: CalendarEvent["type"], t: (key: string) => string) {
   }
 }
 
-function getEventLink(event: CalendarEvent) {
-  if (event.type === "quote") return `/quotes/${event.id}`;
-  if (!event.vehicleId) return event.type === "reminder" ? "/reminders" : `/sales/${event.id}`;
-  return `/vehicles/${event.vehicleId}`;
-}
-
 interface CalendarEventListProps {
   events: CalendarEvent[];
   dateStr: string; // YYYY-MM-DD

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

@@ -6,6 +6,17 @@ export function toLocalDateStr(d: Date): string {
   return `${y}-${m}-${day}`;
 }
 
+/** Where an event points: the quote itself, otherwise the vehicle it belongs to */
+export function getEventLink(event: {
+  id: string;
+  type: "service" | "reminder" | "quote";
+  vehicleId: string | null;
+}): string {
+  if (event.type === "quote") return `/quotes/${event.id}`;
+  if (!event.vehicleId) return event.type === "reminder" ? "/reminders" : `/sales/${event.id}`;
+  return `/vehicles/${event.vehicleId}`;
+}
+
 export function formatDateHeader(d: Date): string {
   return d.toLocaleDateString("en-US", {
     weekday: "long",

+ 7 - 0
src/features/quotes/Components/NewQuoteDialog.tsx

@@ -34,12 +34,16 @@ export function NewQuoteDialog({
   onOpenChange,
   defaultVehicle = null,
   defaultCustomer = null,
+  defaultValidUntil,
 }: {
   open: boolean
   onOpenChange: (open: boolean) => void
   /** Preselects the vehicle (and its label) when opening, e.g. from a vehicle page */
   defaultVehicle?: VehicleOption | null
   defaultCustomer?: CustomerOption | null
+  /** YYYY-MM-DD the quote expires on, e.g. the calendar day it was created from.
+   *  Left unset, the quote falls back to the workshop's default validity period. */
+  defaultValidUntil?: string
 }) {
   const router = useRouter()
   const t = useTranslations('quotes')
@@ -68,6 +72,9 @@ export function NewQuoteDialog({
       title: title.trim(),
       vehicleId: vehicleId || undefined,
       customerId: customerId || undefined,
+      // Midday keeps the quote on the day that was picked no matter which side
+      // of UTC the workshop sits on
+      validUntil: defaultValidUntil ? `${defaultValidUntil}T12:00:00` : undefined,
       status: 'draft',
       subtotal: 0,
       taxRate: 0,

+ 360 - 0
src/features/vehicles/Components/ReminderFormDialog.tsx

@@ -0,0 +1,360 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useTranslations } from "next-intl";
+import { format } from "date-fns";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Calendar } from "@/components/ui/calendar";
+import { CustomerCombobox } from "@/features/quotes/Components/CustomerCombobox";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+} from "@/components/ui/command";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { CalendarIcon, Check, ChevronsUpDown, Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+import { useGlassModal } from "@/components/glass-modal";
+import { createReminder, updateReminder } from "../Actions/reminderActions";
+
+export interface ReminderFormVehicle {
+  id: string;
+  make: string;
+  model: string;
+  year: number;
+  licensePlate: string | null;
+  customerName: string | null;
+  customerId: string | null;
+}
+
+export interface ReminderFormValues {
+  id: string;
+  title: string;
+  description: string | null;
+  dueDate: Date | null;
+  dueMileage: number | null;
+  notifyInApp: boolean;
+  notifyEmail: boolean;
+  customer: { id: string; name: string } | null;
+  vehicle: { id: string } | null;
+}
+
+interface ReminderFormDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  vehicles: ReminderFormVehicle[];
+  /** Set to edit an existing reminder; left out, the dialog creates a new one */
+  reminder?: ReminderFormValues;
+  /** Seeds the due date of a new reminder, e.g. the calendar day that was right-clicked */
+  defaultDueDate?: Date;
+  onSaved?: () => void;
+}
+
+/** YYYY-MM-DD from the local calendar day, never the UTC one */
+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 ReminderFormDialog({
+  open,
+  onOpenChange,
+  vehicles,
+  reminder,
+  defaultDueDate,
+  onSaved,
+}: ReminderFormDialogProps) {
+  const t = useTranslations("reminders");
+  const tv = useTranslations("vehicles.reminders");
+  const tc = useTranslations("common.buttons");
+  const modal = useGlassModal();
+
+  const [formVehicleId, setFormVehicleId] = useState("");
+  const [formTitle, setFormTitle] = useState("");
+  const [formDescription, setFormDescription] = useState("");
+  const [formDueDate, setFormDueDate] = useState<Date | undefined>();
+  const [formDueMileage, setFormDueMileage] = useState("");
+  const [formNotifyInApp, setFormNotifyInApp] = useState(true);
+  const [formNotifyEmail, setFormNotifyEmail] = useState(false);
+  const [formCustomerId, setFormCustomerId] = useState("");
+  const [formCustomer, setFormCustomer] = useState<{ id: string; name: string; company: string | null } | null>(null);
+  const [formLoading, setFormLoading] = useState(false);
+  const [vehicleOpen, setVehicleOpen] = useState(false);
+  const [calendarOpen, setCalendarOpen] = useState(false);
+
+  const isEdit = !!reminder;
+
+  // Re-seed on every open so the last session's values never leak in
+  useEffect(() => {
+    if (!open) return;
+    if (reminder) {
+      setFormVehicleId(reminder.vehicle?.id ?? "");
+      setFormTitle(reminder.title);
+      setFormDescription(reminder.description || "");
+      setFormDueDate(reminder.dueDate ? new Date(reminder.dueDate) : undefined);
+      setFormDueMileage(reminder.dueMileage ? String(reminder.dueMileage) : "");
+      setFormNotifyInApp(reminder.notifyInApp ?? true);
+      setFormNotifyEmail(reminder.notifyEmail ?? false);
+      setFormCustomerId(reminder.customer?.id ?? "");
+      setFormCustomer(reminder.customer ? { ...reminder.customer, company: null } : null);
+    } else {
+      setFormVehicleId("");
+      setFormTitle("");
+      setFormDescription("");
+      setFormDueDate(defaultDueDate);
+      setFormDueMileage("");
+      setFormNotifyInApp(true);
+      setFormNotifyEmail(false);
+      setFormCustomerId("");
+      setFormCustomer(null);
+    }
+  }, [open, reminder, defaultDueDate]);
+
+  const selectedVehicle = vehicles.find((v) => v.id === formVehicleId);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setFormLoading(true);
+
+    const payload = {
+      vehicleId: formVehicleId || null,
+      customerId: formCustomerId || null,
+      title: formTitle,
+      description: formDescription || undefined,
+      // Midday on the local day, so the reminder stays on the day that was
+      // picked whichever side of UTC the workshop sits on
+      dueDate: formDueDate ? `${toLocalDateStr(formDueDate)}T12:00:00` : undefined,
+      dueMileage: formDueMileage ? Number(formDueMileage) : undefined,
+      notifyInApp: formNotifyInApp,
+      notifyEmail: formNotifyEmail,
+    };
+
+    const result = reminder
+      ? await updateReminder({ ...payload, id: reminder.id })
+      : await createReminder(payload);
+
+    if (result.success) {
+      toast.success(isEdit ? tv("reminderUpdated") : tv("reminderCreated"));
+      onOpenChange(false);
+      onSaved?.();
+    } else {
+      modal.open("error", "Error", result.error || tv("saveError", { action: isEdit ? "update" : "create" }));
+    }
+    setFormLoading(false);
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-md">
+        <DialogHeader>
+          <DialogTitle>{isEdit ? tv("editTitle") : tv("addTitle")}</DialogTitle>
+        </DialogHeader>
+        <form onSubmit={handleSubmit} className="space-y-4">
+          {/* Target: workshop (nothing), customer, or vehicle */}
+          <div className="space-y-2">
+            <Label>{t("relatesTo")}</Label>
+            <CustomerCombobox
+              value={formCustomerId}
+              initialCustomer={formCustomer}
+              placeholder={t("selectCustomer")}
+              noneLabel={t("noneOption")}
+              onChange={(id) => {
+                setFormCustomerId(id);
+                // A vehicle belonging to another customer no longer fits
+                const current = vehicles.find((v) => v.id === formVehicleId);
+                if (id && current && current.customerId !== id) {
+                  setFormVehicleId("");
+                }
+              }}
+            />
+            <Popover open={vehicleOpen} onOpenChange={setVehicleOpen}>
+              <PopoverTrigger asChild>
+                <Button
+                  variant="outline"
+                  role="combobox"
+                  aria-expanded={vehicleOpen}
+                  className="w-full justify-between font-normal"
+                >
+                  {selectedVehicle
+                    ? `${selectedVehicle.year} ${selectedVehicle.make} ${selectedVehicle.model}${selectedVehicle.licensePlate ? ` · ${selectedVehicle.licensePlate}` : ""}`
+                    : t("selectVehicle")}
+                  <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                </Button>
+              </PopoverTrigger>
+              <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
+                <Command>
+                  <CommandInput placeholder={t("searchVehicle")} />
+                  <CommandList>
+                    <CommandEmpty>{t("noVehicle")}</CommandEmpty>
+                    <CommandGroup>
+                      <CommandItem
+                        value="__none__"
+                        onSelect={() => {
+                          setFormVehicleId("");
+                          setVehicleOpen(false);
+                        }}
+                      >
+                        <Check
+                          className={cn("mr-2 h-4 w-4", !formVehicleId ? "opacity-100" : "opacity-0")}
+                        />
+                        {t("noneOption")}
+                      </CommandItem>
+                      {vehicles
+                        .filter((v) => !formCustomerId || v.customerId === formCustomerId)
+                        .map((v) => (
+                        <CommandItem
+                          key={v.id}
+                          value={`${v.year} ${v.make} ${v.model} ${v.licensePlate || ""} ${v.customerName || ""}`}
+                          onSelect={() => {
+                            setFormVehicleId(v.id);
+                            // The vehicle's customer follows automatically
+                            if (v.customerId) {
+                              setFormCustomerId(v.customerId);
+                              setFormCustomer({ id: v.customerId, name: v.customerName || "", company: null });
+                            }
+                            setVehicleOpen(false);
+                          }}
+                        >
+                          <Check
+                            className={cn(
+                              "mr-2 h-4 w-4",
+                              formVehicleId === v.id ? "opacity-100" : "opacity-0"
+                            )}
+                          />
+                          <div>
+                            <p className="text-sm">
+                              {v.year} {v.make} {v.model}
+                              {v.licensePlate && <span className="ml-1.5 text-muted-foreground">· {v.licensePlate}</span>}
+                            </p>
+                            {v.customerName && (
+                              <p className="text-xs text-muted-foreground">{v.customerName}</p>
+                            )}
+                          </div>
+                        </CommandItem>
+                      ))}
+                    </CommandGroup>
+                  </CommandList>
+                </Command>
+              </PopoverContent>
+            </Popover>
+            {!formVehicleId && !formCustomerId && (
+              <p className="text-xs text-muted-foreground">{t("workshopHint")}</p>
+            )}
+          </div>
+
+          {/* Title */}
+          <div className="space-y-2">
+            <Label htmlFor="reminder-title">{tv("titleLabel")}</Label>
+            <Input
+              id="reminder-title"
+              value={formTitle}
+              onChange={(e) => setFormTitle(e.target.value)}
+              placeholder={tv("titlePlaceholder")}
+              required
+            />
+          </div>
+
+          {/* Description */}
+          <div className="space-y-2">
+            <Label htmlFor="reminder-desc">{tv("descriptionLabel")}</Label>
+            <Textarea
+              id="reminder-desc"
+              value={formDescription}
+              onChange={(e) => setFormDescription(e.target.value)}
+              placeholder={tv("descriptionPlaceholder")}
+              rows={2}
+            />
+          </div>
+
+          {/* Due date + mileage */}
+          <div className="grid grid-cols-2 gap-4">
+            <div className="space-y-2">
+              <Label>{tv("dueDateLabel")}</Label>
+              <Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
+                <PopoverTrigger asChild>
+                  <Button
+                    variant="outline"
+                    className={cn(
+                      "w-full justify-start text-left font-normal",
+                      !formDueDate && "text-muted-foreground"
+                    )}
+                  >
+                    <CalendarIcon className="mr-2 h-4 w-4" />
+                    {formDueDate ? format(formDueDate, "PPP") : t("pickDate")}
+                  </Button>
+                </PopoverTrigger>
+                <PopoverContent className="w-auto p-0" align="start">
+                  <Calendar
+                    mode="single"
+                    selected={formDueDate}
+                    onSelect={(date) => {
+                      setFormDueDate(date);
+                      setCalendarOpen(false);
+                    }}
+                    initialFocus
+                  />
+                </PopoverContent>
+              </Popover>
+            </div>
+            <div className="space-y-2">
+              <Label htmlFor="reminder-mileage">{tv("dueMileageLabel")}</Label>
+              <Input
+                id="reminder-mileage"
+                type="number"
+                value={formDueMileage}
+                onChange={(e) => setFormDueMileage(e.target.value)}
+                placeholder={tv("dueMileagePlaceholder")}
+              />
+            </div>
+          </div>
+
+          <div className="space-y-2">
+            <Label>{tv("notifyLabel")}</Label>
+            <div className="flex gap-6">
+              <label className="flex items-center gap-2 text-sm font-normal">
+                <Checkbox
+                  checked={formNotifyInApp}
+                  onCheckedChange={(v) => setFormNotifyInApp(v === true)}
+                />
+                {tv("notifyInApp")}
+              </label>
+              <label className="flex items-center gap-2 text-sm font-normal">
+                <Checkbox
+                  checked={formNotifyEmail}
+                  onCheckedChange={(v) => setFormNotifyEmail(v === true)}
+                />
+                {tv("notifyEmail")}
+              </label>
+            </div>
+          </div>
+
+          <div className="flex justify-end gap-3 pt-2">
+            <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
+              {tc("cancel")}
+            </Button>
+            <Button type="submit" disabled={formLoading}>
+              {formLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              {isEdit ? tc("saveChanges") : tv("addTitle")}
+            </Button>
+          </div>
+        </form>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 9 - 295
src/features/vehicles/Components/RemindersPageClient.tsx

@@ -4,31 +4,9 @@ import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
 import { useFormatDate } from "@/lib/use-format-date";
-import { format } from "date-fns";
 import { Card, CardContent } from "@/components/ui/card";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import { Checkbox } from "@/components/ui/checkbox";
-import { CustomerCombobox } from "@/features/quotes/Components/CustomerCombobox";
-import { Calendar } from "@/components/ui/calendar";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import {
-  Command,
-  CommandEmpty,
-  CommandGroup,
-  CommandInput,
-  CommandItem,
-  CommandList,
-} from "@/components/ui/command";
-import {
-  Dialog,
-  DialogContent,
-  DialogHeader,
-  DialogTitle,
-} from "@/components/ui/dialog";
 import {
   DropdownMenu,
   DropdownMenuContent,
@@ -47,11 +25,8 @@ import { TableCellLink } from "@/components/table-cell-link";
 import {
   AlertTriangle,
   Bell,
-  CalendarIcon,
   Car,
-  Check,
   CheckCircle2,
-  ChevronsUpDown,
   Clock,
   MoreVertical,
   Pencil,
@@ -60,15 +35,8 @@ import {
   User,
 } from "lucide-react";
 import { toast } from "sonner";
-import { cn } from "@/lib/utils";
-import { useGlassModal } from "@/components/glass-modal";
-import { Loader2 } from "lucide-react";
-import {
-  toggleReminder,
-  deleteReminder,
-  createReminder,
-  updateReminder,
-} from "../Actions/reminderActions";
+import { toggleReminder, deleteReminder } from "../Actions/reminderActions";
+import { ReminderFormDialog } from "./ReminderFormDialog";
 
 interface Reminder {
   id: string;
@@ -127,7 +95,6 @@ export function RemindersPageClient({ reminders, vehicles, unitSystem }: Reminde
   const tc = useTranslations("common.buttons");
   const tcm = useTranslations("common.contextMenu");
   const router = useRouter();
-  const modal = useGlassModal();
   const { formatDate } = useFormatDate();
   const distUnit = unitSystem === "metric" ? "km" : "mi";
 
@@ -135,20 +102,6 @@ export function RemindersPageClient({ reminders, vehicles, unitSystem }: Reminde
   const [showForm, setShowForm] = useState(false);
   const [editingReminder, setEditingReminder] = useState<Reminder | undefined>();
 
-  // Form state
-  const [formVehicleId, setFormVehicleId] = useState("");
-  const [formTitle, setFormTitle] = useState("");
-  const [formDescription, setFormDescription] = useState("");
-  const [formDueDate, setFormDueDate] = useState<Date | undefined>();
-  const [formDueMileage, setFormDueMileage] = useState("");
-  const [formNotifyInApp, setFormNotifyInApp] = useState(true);
-  const [formNotifyEmail, setFormNotifyEmail] = useState(false);
-  const [formCustomerId, setFormCustomerId] = useState("");
-  const [formCustomer, setFormCustomer] = useState<{ id: string; name: string; company: string | null } | null>(null);
-  const [formLoading, setFormLoading] = useState(false);
-  const [vehicleOpen, setVehicleOpen] = useState(false);
-  const [calendarOpen, setCalendarOpen] = useState(false);
-
   const filtered = reminders.filter((r) => {
     if (filter === "active") return !r.isCompleted;
     if (filter === "completed") return r.isCompleted;
@@ -174,63 +127,14 @@ export function RemindersPageClient({ reminders, vehicles, unitSystem }: Reminde
 
   const openAddForm = () => {
     setEditingReminder(undefined);
-    setFormVehicleId("");
-    setFormTitle("");
-    setFormDescription("");
-    setFormDueDate(undefined);
-    setFormDueMileage("");
-    setFormNotifyInApp(true);
-    setFormNotifyEmail(false);
-    setFormCustomerId("");
-    setFormCustomer(null);
     setShowForm(true);
   };
 
   const openEditForm = (r: Reminder) => {
     setEditingReminder(r);
-    setFormVehicleId(r.vehicle?.id ?? "");
-    setFormTitle(r.title);
-    setFormDescription(r.description || "");
-    setFormDueDate(r.dueDate ? new Date(r.dueDate) : undefined);
-    setFormDueMileage(r.dueMileage ? String(r.dueMileage) : "");
-    setFormNotifyInApp(r.notifyInApp ?? true);
-    setFormNotifyEmail(r.notifyEmail ?? false);
-    setFormCustomerId(r.customer?.id ?? "");
-    setFormCustomer(r.customer ? { ...r.customer, company: null } : null);
     setShowForm(true);
   };
 
-  const handleFormSubmit = async (e: React.FormEvent) => {
-    e.preventDefault();
-    setFormLoading(true);
-
-    const payload = {
-      vehicleId: formVehicleId || null,
-      customerId: formCustomerId || null,
-      title: formTitle,
-      description: formDescription || undefined,
-      dueDate: formDueDate ? formDueDate.toISOString().split("T")[0] : undefined,
-      dueMileage: formDueMileage ? Number(formDueMileage) : undefined,
-      notifyInApp: formNotifyInApp,
-      notifyEmail: formNotifyEmail,
-    };
-
-    const result = editingReminder
-      ? await updateReminder({ ...payload, id: editingReminder.id })
-      : await createReminder(payload);
-
-    if (result.success) {
-      toast.success(editingReminder ? tv("reminderUpdated") : tv("reminderCreated"));
-      setShowForm(false);
-      router.refresh();
-    } else {
-      modal.open("error", "Error", result.error || tv("saveError", { action: editingReminder ? "update" : "create" }));
-    }
-    setFormLoading(false);
-  };
-
-  const selectedVehicle = vehicles.find((v) => v.id === formVehicleId);
-
   return (
     <>
       <div className="flex flex-wrap items-center justify-between gap-3">
@@ -404,203 +308,13 @@ export function RemindersPageClient({ reminders, vehicles, unitSystem }: Reminde
         </Card>
       )}
 
-      {/* Add/Edit Reminder Dialog */}
-      <Dialog open={showForm} onOpenChange={setShowForm}>
-        <DialogContent className="sm:max-w-md">
-          <DialogHeader>
-            <DialogTitle>{editingReminder ? tv("editTitle") : tv("addTitle")}</DialogTitle>
-          </DialogHeader>
-          <form onSubmit={handleFormSubmit} className="space-y-4">
-            {/* Target: workshop (nothing), customer, or vehicle */}
-            <div className="space-y-2">
-              <Label>{t("relatesTo")}</Label>
-              <CustomerCombobox
-                value={formCustomerId}
-                initialCustomer={formCustomer}
-                placeholder={t("selectCustomer")}
-                noneLabel={t("noneOption")}
-                onChange={(id) => {
-                  setFormCustomerId(id);
-                  // A vehicle belonging to another customer no longer fits
-                  const current = vehicles.find((v) => v.id === formVehicleId);
-                  if (id && current && current.customerId !== id) {
-                    setFormVehicleId("");
-                  }
-                }}
-              />
-              <Popover open={vehicleOpen} onOpenChange={setVehicleOpen}>
-                <PopoverTrigger asChild>
-                  <Button
-                    variant="outline"
-                    role="combobox"
-                    aria-expanded={vehicleOpen}
-                    className="w-full justify-between font-normal"
-                  >
-                    {selectedVehicle
-                      ? `${selectedVehicle.year} ${selectedVehicle.make} ${selectedVehicle.model}${selectedVehicle.licensePlate ? ` · ${selectedVehicle.licensePlate}` : ""}`
-                      : t("selectVehicle")}
-                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                  </Button>
-                </PopoverTrigger>
-                <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
-                  <Command>
-                    <CommandInput placeholder={t("searchVehicle")} />
-                    <CommandList>
-                      <CommandEmpty>{t("noVehicle")}</CommandEmpty>
-                      <CommandGroup>
-                        <CommandItem
-                          value="__none__"
-                          onSelect={() => {
-                            setFormVehicleId("");
-                            setVehicleOpen(false);
-                          }}
-                        >
-                          <Check
-                            className={cn("mr-2 h-4 w-4", !formVehicleId ? "opacity-100" : "opacity-0")}
-                          />
-                          {t("noneOption")}
-                        </CommandItem>
-                        {vehicles
-                          .filter((v) => !formCustomerId || v.customerId === formCustomerId)
-                          .map((v) => (
-                          <CommandItem
-                            key={v.id}
-                            value={`${v.year} ${v.make} ${v.model} ${v.licensePlate || ""} ${v.customerName || ""}`}
-                            onSelect={() => {
-                              setFormVehicleId(v.id);
-                              // The vehicle's customer follows automatically
-                              if (v.customerId) {
-                                setFormCustomerId(v.customerId);
-                                setFormCustomer({ id: v.customerId, name: v.customerName || "", company: null });
-                              }
-                              setVehicleOpen(false);
-                            }}
-                          >
-                            <Check
-                              className={cn(
-                                "mr-2 h-4 w-4",
-                                formVehicleId === v.id ? "opacity-100" : "opacity-0"
-                              )}
-                            />
-                            <div>
-                              <p className="text-sm">
-                                {v.year} {v.make} {v.model}
-                                {v.licensePlate && <span className="ml-1.5 text-muted-foreground">· {v.licensePlate}</span>}
-                              </p>
-                              {v.customerName && (
-                                <p className="text-xs text-muted-foreground">{v.customerName}</p>
-                              )}
-                            </div>
-                          </CommandItem>
-                        ))}
-                      </CommandGroup>
-                    </CommandList>
-                  </Command>
-                </PopoverContent>
-              </Popover>
-              {!formVehicleId && !formCustomerId && (
-                <p className="text-xs text-muted-foreground">{t("workshopHint")}</p>
-              )}
-            </div>
-
-            {/* Title */}
-            <div className="space-y-2">
-              <Label htmlFor="reminder-title">{tv("titleLabel")}</Label>
-              <Input
-                id="reminder-title"
-                value={formTitle}
-                onChange={(e) => setFormTitle(e.target.value)}
-                placeholder={tv("titlePlaceholder")}
-                required
-              />
-            </div>
-
-            {/* Description */}
-            <div className="space-y-2">
-              <Label htmlFor="reminder-desc">{tv("descriptionLabel")}</Label>
-              <Textarea
-                id="reminder-desc"
-                value={formDescription}
-                onChange={(e) => setFormDescription(e.target.value)}
-                placeholder={tv("descriptionPlaceholder")}
-                rows={2}
-              />
-            </div>
-
-            {/* Due date + mileage */}
-            <div className="grid grid-cols-2 gap-4">
-              <div className="space-y-2">
-                <Label>{tv("dueDateLabel")}</Label>
-                <Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
-                  <PopoverTrigger asChild>
-                    <Button
-                      variant="outline"
-                      className={cn(
-                        "w-full justify-start text-left font-normal",
-                        !formDueDate && "text-muted-foreground"
-                      )}
-                    >
-                      <CalendarIcon className="mr-2 h-4 w-4" />
-                      {formDueDate ? format(formDueDate, "PPP") : t("pickDate")}
-                    </Button>
-                  </PopoverTrigger>
-                  <PopoverContent className="w-auto p-0" align="start">
-                    <Calendar
-                      mode="single"
-                      selected={formDueDate}
-                      onSelect={(date) => {
-                        setFormDueDate(date);
-                        setCalendarOpen(false);
-                      }}
-                      initialFocus
-                    />
-                  </PopoverContent>
-                </Popover>
-              </div>
-              <div className="space-y-2">
-                <Label htmlFor="reminder-mileage">{tv("dueMileageLabel")}</Label>
-                <Input
-                  id="reminder-mileage"
-                  type="number"
-                  value={formDueMileage}
-                  onChange={(e) => setFormDueMileage(e.target.value)}
-                  placeholder={tv("dueMileagePlaceholder")}
-                />
-              </div>
-            </div>
-
-            <div className="space-y-2">
-              <Label>{tv("notifyLabel")}</Label>
-              <div className="flex gap-6">
-                <label className="flex items-center gap-2 text-sm font-normal">
-                  <Checkbox
-                    checked={formNotifyInApp}
-                    onCheckedChange={(v) => setFormNotifyInApp(v === true)}
-                  />
-                  {tv("notifyInApp")}
-                </label>
-                <label className="flex items-center gap-2 text-sm font-normal">
-                  <Checkbox
-                    checked={formNotifyEmail}
-                    onCheckedChange={(v) => setFormNotifyEmail(v === true)}
-                  />
-                  {tv("notifyEmail")}
-                </label>
-              </div>
-            </div>
-
-            <div className="flex justify-end gap-3 pt-2">
-              <Button type="button" variant="outline" onClick={() => setShowForm(false)}>
-                {tc("cancel")}
-              </Button>
-              <Button type="submit" disabled={formLoading}>
-                {formLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-                {editingReminder ? tc("saveChanges") : tv("addTitle")}
-              </Button>
-            </div>
-          </form>
-        </DialogContent>
-      </Dialog>
+      <ReminderFormDialog
+        open={showForm}
+        onOpenChange={setShowForm}
+        vehicles={vehicles}
+        reminder={editingReminder}
+        onSaved={() => router.refresh()}
+      />
     </>
   );
 }