Просмотр исходного кода

Make invoice date fields typeable (#221)

* Make invoice date fields typeable

Invoice date and due date on the work order used calendar-popover-only
pickers, making backdating old invoices slow. Both are now native date
inputs like the record payment dialog, so dates can be typed directly.
The inputs keep the same names, so the save path is unchanged.

* Add calendar picker back to the typeable date fields

New shared DateInput component: a native date input for direct typing
with the shadcn Calendar in a popover behind the icon. Used for the
work order invoice date and due date.

* Use shadcn's typeable date picker pattern for invoice dates

The native date input is segmented rather than free-text, so typing a
date directly did not work. DateInput now follows shadcn's date picker
Input example: a text input parsed with new Date() (accepts formats
like 'March 15 2025', '2025-03-15', '03/15/2025') plus the Calendar
popover. A hidden input carries the ISO value, keeping the form save
path contract unchanged.

* Parse typed dates using the workshop date format

Numeric input like 25.05.2026 now works: dot/slash/dash separators are
accepted, day-vs-month order follows the workshop date format setting
when ambiguous, and values over 12 always read as the day. Display and
placeholder use the same setting, so the field matches dates everywhere
else in the app. Text like 'February 01, 2001' still parses too.

* Make date entry foolproof with year bounds at every layer

Typing compact digits like 25062026 hit the new Date() fallback at an
intermediate keystroke, emitting an absurd year (252620) that autosave
persisted and that crashed the page on read. Three guards now:

- DateInput parses compact digits per the workshop date format and
  rejects years outside 1900-2100 on every parse path
- serviceActions runs all client date strings through toSafeDate(),
  which drops invalid or out-of-range values before persistence
- the work order page falls back instead of crashing on toISOString()
  if bad legacy data is ever encountered

* Keep real toSafeDate in create-workorder test mock

* Use the shared date input everywhere and validate all date parsing

- DateInput (typeable + calendar, workshop date format aware) replaces
  native date inputs across quotes, reminders, payments, service form,
  status reports, recurring invoices, custom fields and report
  schedules; the public portal keeps the browser-native picker
- All server actions parsing client date strings (quote validUntil,
  payment date, reminder dueDate, vehicle purchaseDate, recurring
  invoice and report schedule dates, portal preferredDate, status
  report expiresAt) and the three backup importers now go through
  toSafeDate, rejecting invalid or out-of-range values
- DD.MM.YYYY added to the date format options in localization settings
Bernt Christian Egeland 1 месяц назад
Родитель
Сommit
87809f0b0b
28 измененных файлов с 379 добавлено и 251 удалено
  1. 2 1
      src/__tests__/features/quotes/convert-quote-inventory.test.ts
  2. 2 1
      src/__tests__/features/quotes/create-quote.test.ts
  3. 2 1
      src/__tests__/features/workorders/create-workorder.test.ts
  4. 1 0
      src/app/(authenticated)/settings/localization/localization-settings.tsx
  5. 3 3
      src/app/(authenticated)/settings/report-schedule/report-schedule-settings.tsx
  6. 15 11
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  7. 3 3
      src/app/api/protected/backup/import-invoice-ninja/route.ts
  8. 2 2
      src/app/api/protected/backup/import-lubelog/route.ts
  9. 53 124
      src/app/api/protected/backup/import/route.ts
  10. 198 0
      src/components/ui/date-input.tsx
  11. 5 5
      src/features/billing/Actions/recurringInvoiceActions.ts
  12. 3 2
      src/features/billing/Components/RecurringInvoicesClient.tsx
  13. 3 3
      src/features/custom-fields/Components/CustomFieldsForm.tsx
  14. 2 1
      src/features/payments/Actions/paymentActions.ts
  15. 2 1
      src/features/portal/Actions/portalActions.ts
  16. 5 5
      src/features/quotes/Actions/quoteActions.ts
  17. 9 5
      src/features/quotes/Components/QuoteRightColumn.tsx
  18. 3 2
      src/features/report-schedule/Actions/reportScheduleActions.ts
  19. 2 1
      src/features/status-reports/Actions/createStatusReport.ts
  20. 2 3
      src/features/status-reports/Components/CreateStatusReportDialog.tsx
  21. 3 2
      src/features/vehicles/Actions/reminderActions.ts
  22. 7 7
      src/features/vehicles/Actions/serviceActions.ts
  23. 3 2
      src/features/vehicles/Actions/vehicleActions.ts
  24. 3 3
      src/features/vehicles/Components/ReminderForm.tsx
  25. 5 3
      src/features/vehicles/Components/ServiceForm.tsx
  26. 5 3
      src/features/vehicles/Components/service-detail/PaymentsSection.tsx
  27. 23 57
      src/features/vehicles/Components/service-edit/InvoiceDetailsSection.tsx
  28. 13 0
      src/lib/invoice-utils.ts

+ 2 - 1
src/__tests__/features/quotes/convert-quote-inventory.test.ts

@@ -21,7 +21,8 @@ vi.mock("@/lib/cached-session", () => ({
 
 vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
 
-vi.mock("@/lib/invoice-utils", () => ({
+vi.mock("@/lib/invoice-utils", async (importOriginal) => ({
+  ...(await importOriginal<typeof import("@/lib/invoice-utils")>()),
   resolveInvoicePrefix: vi.fn((p: string) => p),
 }));
 

+ 2 - 1
src/__tests__/features/quotes/create-quote.test.ts

@@ -17,7 +17,8 @@ vi.mock("@/lib/cached-session", () => ({
 
 vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
 
-vi.mock("@/lib/invoice-utils", () => ({
+vi.mock("@/lib/invoice-utils", async (importOriginal) => ({
+  ...(await importOriginal<typeof import("@/lib/invoice-utils")>()),
   resolveInvoicePrefix: vi.fn((p: string) => p),
 }));
 

+ 2 - 1
src/__tests__/features/workorders/create-workorder.test.ts

@@ -23,7 +23,8 @@ vi.mock("@/lib/cached-session", () => ({
 
 vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
 
-vi.mock("@/lib/invoice-utils", () => ({
+vi.mock("@/lib/invoice-utils", async (importOriginal) => ({
+  ...(await importOriginal<typeof import("@/lib/invoice-utils")>()),
   resolveInvoicePrefix: vi.fn((p: string) => {
     const now = new Date();
     return p

+ 1 - 0
src/app/(authenticated)/settings/localization/localization-settings.tsx

@@ -110,6 +110,7 @@ const CURRENCIES = [
 const DATE_FORMAT_OPTIONS = [
   { value: 'MMM d, yyyy', label: 'MMM D, YYYY', example: 'Feb 15, 2026' },
   { value: 'dd/MM/yyyy', label: 'DD/MM/YYYY', example: '15/02/2026' },
+  { value: 'dd.MM.yyyy', label: 'DD.MM.YYYY', example: '15.02.2026' },
   { value: 'MM/dd/yyyy', label: 'MM/DD/YYYY', example: '02/15/2026' },
   { value: 'yyyy-MM-dd', label: 'YYYY-MM-DD', example: '2026-02-15' },
   { value: 'd. MMM yyyy', label: 'D. MMM YYYY', example: '15. Feb 2026' },

+ 3 - 3
src/app/(authenticated)/settings/report-schedule/report-schedule-settings.tsx

@@ -7,6 +7,7 @@ import { toast } from 'sonner'
 import { Button } from '@/components/ui/button'
 import { Card, CardContent } from '@/components/ui/card'
 import { Input } from '@/components/ui/input'
+import { DateInput } from '@/components/ui/date-input'
 import { Label } from '@/components/ui/label'
 import { Switch } from '@/components/ui/switch'
 import { Checkbox } from '@/components/ui/checkbox'
@@ -277,11 +278,10 @@ function ScheduleForm({
           </div>
           <div className="space-y-2">
             <Label htmlFor="end-date">{t('endDate')}</Label>
-            <Input
+            <DateInput
               id="end-date"
-              type="date"
               value={endDate}
-              onChange={(e) => setEndDate(e.target.value)}
+              onChange={setEndDate}
             />
           </div>
         </div>

+ 15 - 11
src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx

@@ -129,6 +129,13 @@ export default async function ServiceDetailPage({
     aiSettingsMap[SETTING_KEYS.AI_ENABLED] === 'true' &&
     !!aiSettingsMap[SETTING_KEYS.AI_API_KEY]
 
+  // A timestamp outside JS date range (bad legacy data) must degrade to a
+  // fallback date, not crash the page on toISOString().
+  const isRenderable = (d: Date | null | undefined): d is Date =>
+    !!d && !isNaN(new Date(d).getTime())
+  const effectiveInvoiceDate =
+    [record.invoiceDate, record.startDateTime, record.serviceDate].find(isRenderable) ?? new Date()
+
   const initialData = {
     id: record.id,
     title: record.title,
@@ -136,23 +143,20 @@ export default async function ServiceDetailPage({
     type: record.type,
     status: record.status,
     mileage: record.mileage,
-    serviceDate: new Date(record.serviceDate).toISOString().split('T')[0],
-    startDateTime: record.startDateTime?.toISOString() ?? null,
-    endDateTime: record.endDateTime?.toISOString() ?? null,
+    serviceDate: (isRenderable(record.serviceDate) ? new Date(record.serviceDate) : new Date())
+      .toISOString()
+      .split('T')[0],
+    startDateTime: isRenderable(record.startDateTime) ? record.startDateTime.toISOString() : null,
+    endDateTime: isRenderable(record.endDateTime) ? record.endDateTime.toISOString() : null,
     techName: record.techName || '',
     diagnosticNotes: record.diagnosticNotes || '',
     invoiceNotes: record.invoiceNotes || '',
     invoiceNumber: record.invoiceNumber || '',
-    invoiceDate: (record.invoiceDate ?? record.startDateTime ?? record.serviceDate)
-      .toISOString()
-      .split('T')[0],
-    invoiceDueDate: record.invoiceDueDate
+    invoiceDate: effectiveInvoiceDate.toISOString().split('T')[0],
+    invoiceDueDate: isRenderable(record.invoiceDueDate)
       ? record.invoiceDueDate.toISOString().split('T')[0]
       : defaultDueDays > 0
-        ? new Date(
-            (record.invoiceDate ?? record.startDateTime ?? record.serviceDate).getTime() +
-              defaultDueDays * 86400000
-          )
+        ? new Date(effectiveInvoiceDate.getTime() + defaultDueDays * 86400000)
             .toISOString()
             .split('T')[0]
         : '',

+ 3 - 3
src/app/api/protected/backup/import-invoice-ninja/route.ts

@@ -1,7 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { db } from "@/lib/db";
-import { resolveInvoicePrefix } from "@/lib/invoice-utils";
+import { resolveInvoicePrefix, toSafeDate } from "@/lib/invoice-utils";
 import { mkdir, writeFile, rm } from "fs/promises";
 import path from "path";
 import os from "os";
@@ -423,7 +423,7 @@ export async function POST(request: NextRequest) {
             type: "repair",
             status: "completed",
             cost: amount,
-            serviceDate: new Date(invoice.date),
+            serviceDate: toSafeDate(invoice.date) ?? new Date(),
             invoiceNumber,
             subtotal: partsTotal + laborTotal,
             discountType:
@@ -541,7 +541,7 @@ export async function POST(request: NextRequest) {
           await tx.payment.create({
             data: {
               amount: parseNum(payable.amount),
-              date: new Date(payment.date),
+              date: toSafeDate(payment.date) ?? new Date(),
               method: getPaymentMethod(payment.type_id),
               note: payment.private_notes || null,
               externalId: payment.transaction_reference || null,

+ 2 - 2
src/app/api/protected/backup/import-lubelog/route.ts

@@ -1,7 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { db } from "@/lib/db";
-import { resolveInvoicePrefix } from "@/lib/invoice-utils";
+import { resolveInvoicePrefix, toSafeDate } from "@/lib/invoice-utils";
 import { mkdir, writeFile, stat, readdir, rm } from "fs/promises";
 import { readFileSync } from "fs";
 import path from "path";
@@ -386,7 +386,7 @@ export async function POST(request: NextRequest) {
             status: "completed",
             cost,
             mileage: sr.Mileage || null,
-            serviceDate: new Date(sr.Date),
+            serviceDate: toSafeDate(sr.Date) ?? new Date(),
             invoiceNumber,
             subtotal: cost,
             totalAmount: cost,

+ 53 - 124
src/app/api/protected/backup/import/route.ts

@@ -1,6 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { db } from "@/lib/db";
+import { toSafeDate } from "@/lib/invoice-utils";
 import { Prisma } from "@/generated/prisma/client";
 import JSZip from "jszip";
 import { mkdir, rm, writeFile } from "fs/promises";
@@ -229,12 +230,8 @@ export async function POST(request: NextRequest) {
               address: (c.address as string) || null,
               company: (c.company as string) || null,
               notes: (c.notes as string) || null,
-              createdAt: c.createdAt
-                ? new Date(c.createdAt as string)
-                : undefined,
-              updatedAt: c.updatedAt
-                ? new Date(c.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(c.createdAt as string),
+              updatedAt: toSafeDate(c.updatedAt as string),
               userId: ctx.userId,
               organizationId: ctx.organizationId,
             })
@@ -254,12 +251,8 @@ export async function POST(request: NextRequest) {
               sortOrder: (t.sortOrder as number) || 0,
               dailyCapacity: (t.dailyCapacity as number) || 480,
               userId: (t.userId as string) || null, // memberId from old backups ignored — no FK to users
-              createdAt: t.createdAt
-                ? new Date(t.createdAt as string)
-                : undefined,
-              updatedAt: t.updatedAt
-                ? new Date(t.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(t.createdAt as string),
+              updatedAt: toSafeDate(t.updatedAt as string),
               organizationId: ctx.organizationId,
             })
           ),
@@ -283,12 +276,8 @@ export async function POST(request: NextRequest) {
               entityType: def.entityType as string,
               sortOrder: (def.sortOrder as number) || 0,
               isActive: def.isActive !== false,
-              createdAt: def.createdAt
-                ? new Date(def.createdAt as string)
-                : undefined,
-              updatedAt: def.updatedAt
-                ? new Date(def.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(def.createdAt as string),
+              updatedAt: toSafeDate(def.updatedAt as string),
               userId: ctx.userId,
               organizationId: ctx.organizationId,
             },
@@ -330,12 +319,8 @@ export async function POST(request: NextRequest) {
               imageUrl: rewriteFileUrl(p.imageUrl as string, ctx.organizationId),
               location: (p.location as string) || null,
               isArchived: (p.isArchived as boolean) || false,
-              createdAt: p.createdAt
-                ? new Date(p.createdAt as string)
-                : undefined,
-              updatedAt: p.updatedAt
-                ? new Date(p.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(p.createdAt as string),
+              updatedAt: toSafeDate(p.updatedAt as string),
               userId: ctx.userId,
               organizationId: ctx.organizationId,
             })
@@ -364,18 +349,12 @@ export async function POST(request: NextRequest) {
               fuelType: (v.fuelType as string) || null,
               transmission: (v.transmission as string) || null,
               engineSize: (v.engineSize as string) || null,
-              purchaseDate: v.purchaseDate
-                ? new Date(v.purchaseDate as string)
-                : null,
+              purchaseDate: toSafeDate(v.purchaseDate as string) ?? null,
               purchasePrice: (v.purchasePrice as number) || null,
               imageUrl: rewriteFileUrl(v.imageUrl as string, ctx.organizationId),
               isArchived: (v.isArchived as boolean) || false,
-              createdAt: v.createdAt
-                ? new Date(v.createdAt as string)
-                : undefined,
-              updatedAt: v.updatedAt
-                ? new Date(v.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(v.createdAt as string),
+              updatedAt: toSafeDate(v.updatedAt as string),
               userId: ctx.userId,
               organizationId: ctx.organizationId,
               customerId: (v.customerId as string) || null,
@@ -391,12 +370,8 @@ export async function POST(request: NextRequest) {
                 title: n.title as string,
                 content: n.content as string,
                 isPinned: (n.isPinned as boolean) || false,
-                createdAt: n.createdAt
-                  ? new Date(n.createdAt as string)
-                  : undefined,
-                updatedAt: n.updatedAt
-                  ? new Date(n.updatedAt as string)
-                  : undefined,
+                createdAt: toSafeDate(n.createdAt as string),
+                updatedAt: toSafeDate(n.updatedAt as string),
                 vehicleId: v.id as string,
               })),
             });
@@ -408,7 +383,7 @@ export async function POST(request: NextRequest) {
             await tx.fuelLog.createMany({
               data: fuelLogs.map((f) => ({
                 id: f.id as string,
-                date: f.date ? new Date(f.date as string) : undefined,
+                date: toSafeDate(f.date as string),
                 mileage: f.mileage as number,
                 gallons: f.gallons as number,
                 pricePerGallon: f.pricePerGallon as number,
@@ -416,12 +391,8 @@ export async function POST(request: NextRequest) {
                 isFillUp: f.isFillUp !== false,
                 station: (f.station as string) || null,
                 notes: (f.notes as string) || null,
-                createdAt: f.createdAt
-                  ? new Date(f.createdAt as string)
-                  : undefined,
-                updatedAt: f.updatedAt
-                  ? new Date(f.updatedAt as string)
-                  : undefined,
+                createdAt: toSafeDate(f.createdAt as string),
+                updatedAt: toSafeDate(f.updatedAt as string),
                 vehicleId: v.id as string,
               })),
             });
@@ -437,15 +408,11 @@ export async function POST(request: NextRequest) {
                 id: r.id as string,
                 title: r.title as string,
                 description: (r.description as string) || null,
-                dueDate: r.dueDate ? new Date(r.dueDate as string) : null,
+                dueDate: toSafeDate(r.dueDate as string) ?? null,
                 dueMileage: (r.dueMileage as number) || null,
                 isCompleted: (r.isCompleted as boolean) || false,
-                createdAt: r.createdAt
-                  ? new Date(r.createdAt as string)
-                  : undefined,
-                updatedAt: r.updatedAt
-                  ? new Date(r.updatedAt as string)
-                  : undefined,
+                createdAt: toSafeDate(r.createdAt as string),
+                updatedAt: toSafeDate(r.updatedAt as string),
                 vehicleId: v.id as string,
               })),
             });
@@ -461,15 +428,19 @@ export async function POST(request: NextRequest) {
               let startDT: Date | undefined;
               let endDT: Date | undefined;
               if (sr.startDateTime) {
-                startDT = new Date(sr.startDateTime as string);
-              } else if (sr.serviceDate) {
-                const sd = new Date(sr.serviceDate as string);
-                const [h, m] = workDayStartTime.split(":").map(Number);
-                startDT = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate(), h, m, 0, 0);
+                startDT = toSafeDate(sr.startDateTime as string);
+              }
+              if (!startDT && sr.serviceDate) {
+                const sd = toSafeDate(sr.serviceDate as string);
+                if (sd) {
+                  const [h, m] = workDayStartTime.split(":").map(Number);
+                  startDT = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate(), h, m, 0, 0);
+                }
               }
               if (sr.endDateTime) {
-                endDT = new Date(sr.endDateTime as string);
-              } else if (startDT) {
+                endDT = toSafeDate(sr.endDateTime as string);
+              }
+              if (!endDT && startDT) {
                 endDT = new Date(startDT.getTime() + 3600000);
               }
 
@@ -482,9 +453,7 @@ export async function POST(request: NextRequest) {
                   status: (sr.status as string) || "completed",
                   cost: (sr.cost as number) || 0,
                   mileage: (sr.mileage as number) || null,
-                  serviceDate: sr.serviceDate
-                    ? new Date(sr.serviceDate as string)
-                    : undefined,
+                  serviceDate: toSafeDate(sr.serviceDate as string),
                   startDateTime: startDT ?? undefined,
                   endDateTime: endDT ?? undefined,
                   shopName: (sr.shopName as string) || null,
@@ -505,12 +474,8 @@ export async function POST(request: NextRequest) {
                   publicToken: (sr.publicToken as string) || null,
                   technicianId: (sr.technicianId as string) || null,
                   sortOrder: (sr.sortOrder as number) || 0,
-                  createdAt: sr.createdAt
-                    ? new Date(sr.createdAt as string)
-                    : undefined,
-                  updatedAt: sr.updatedAt
-                    ? new Date(sr.updatedAt as string)
-                    : undefined,
+                  createdAt: toSafeDate(sr.createdAt as string),
+                  updatedAt: toSafeDate(sr.updatedAt as string),
                   vehicleId: v.id as string,
                 },
               });
@@ -565,9 +530,7 @@ export async function POST(request: NextRequest) {
                     category: (a.category as string) || "diagnostic",
                     description: (a.description as string) || null,
                     includeInInvoice: a.includeInInvoice !== false,
-                    createdAt: a.createdAt
-                      ? new Date(a.createdAt as string)
-                      : undefined,
+                    createdAt: toSafeDate(a.createdAt as string),
                     serviceRecordId: sr.id as string,
                   })),
                 });
@@ -582,15 +545,11 @@ export async function POST(request: NextRequest) {
                   data: payments.map((p) => ({
                     id: p.id as string,
                     amount: p.amount as number,
-                    date: p.date ? new Date(p.date as string) : undefined,
+                    date: toSafeDate(p.date as string),
                     method: (p.method as string) || "other",
                     note: (p.note as string) || null,
-                    createdAt: p.createdAt
-                      ? new Date(p.createdAt as string)
-                      : undefined,
-                    updatedAt: p.updatedAt
-                      ? new Date(p.updatedAt as string)
-                      : undefined,
+                    createdAt: toSafeDate(p.createdAt as string),
+                    updatedAt: toSafeDate(p.updatedAt as string),
                     serviceRecordId: sr.id as string,
                   })),
                 });
@@ -610,9 +569,7 @@ export async function POST(request: NextRequest) {
               title: q.title as string,
               description: (q.description as string) || null,
               status: (q.status as string) || "draft",
-              validUntil: q.validUntil
-                ? new Date(q.validUntil as string)
-                : null,
+              validUntil: toSafeDate(q.validUntil as string) ?? null,
               subtotal: (q.subtotal as number) || 0,
               taxRate: (q.taxRate as number) || 0,
               taxAmount: (q.taxAmount as number) || 0,
@@ -623,12 +580,8 @@ export async function POST(request: NextRequest) {
               totalAmount: (q.totalAmount as number) || 0,
               notes: (q.notes as string) || null,
               convertedToId: (q.convertedToId as string) || null,
-              createdAt: q.createdAt
-                ? new Date(q.createdAt as string)
-                : undefined,
-              updatedAt: q.updatedAt
-                ? new Date(q.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(q.createdAt as string),
+              updatedAt: toSafeDate(q.updatedAt as string),
               userId: ctx.userId,
               organizationId: ctx.organizationId,
               customerId: (q.customerId as string) || null,
@@ -682,12 +635,8 @@ export async function POST(request: NextRequest) {
               name: tmpl.name as string,
               description: (tmpl.description as string) || null,
               isDefault: (tmpl.isDefault as boolean) || false,
-              createdAt: tmpl.createdAt
-                ? new Date(tmpl.createdAt as string)
-                : undefined,
-              updatedAt: tmpl.updatedAt
-                ? new Date(tmpl.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(tmpl.createdAt as string),
+              updatedAt: toSafeDate(tmpl.updatedAt as string),
               organizationId: ctx.organizationId,
             },
           });
@@ -729,23 +678,13 @@ export async function POST(request: NextRequest) {
               status: (insp.status as string) || "in_progress",
               mileage: (insp.mileage as number) || null,
               notes: (insp.notes as string) || null,
-              startDateTime: insp.startDateTime
-                ? new Date(insp.startDateTime as string)
-                : null,
-              endDateTime: insp.endDateTime
-                ? new Date(insp.endDateTime as string)
-                : null,
+              startDateTime: toSafeDate(insp.startDateTime as string) ?? null,
+              endDateTime: toSafeDate(insp.endDateTime as string) ?? null,
               publicToken: (insp.publicToken as string) || null,
-              completedAt: insp.completedAt
-                ? new Date(insp.completedAt as string)
-                : null,
+              completedAt: toSafeDate(insp.completedAt as string) ?? null,
               sortOrder: (insp.sortOrder as number) || 0,
-              createdAt: insp.createdAt
-                ? new Date(insp.createdAt as string)
-                : undefined,
-              updatedAt: insp.updatedAt
-                ? new Date(insp.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(insp.createdAt as string),
+              updatedAt: toSafeDate(insp.updatedAt as string),
               vehicleId: insp.vehicleId as string,
               templateId: insp.templateId as string,
               technicianId: (insp.technicianId as string) || null,
@@ -777,9 +716,7 @@ export async function POST(request: NextRequest) {
                 status: (qr.status as string) || "pending",
                 message: (qr.message as string) || null,
                 selectedItemIds: (qr.selectedItemIds as string[]) || [],
-                createdAt: qr.createdAt
-                  ? new Date(qr.createdAt as string)
-                  : undefined,
+                createdAt: toSafeDate(qr.createdAt as string),
                 inspectionId: insp.id as string,
                 organizationId: ctx.organizationId,
               })),
@@ -794,9 +731,7 @@ export async function POST(request: NextRequest) {
           data: (data.auditLogs as Record<string, unknown>[]).map(
             (log: Record<string, unknown>) => ({
               id: log.id as string,
-              timestamp: log.timestamp
-                ? new Date(log.timestamp as string)
-                : undefined,
+              timestamp: toSafeDate(log.timestamp as string),
               action: log.action as string,
               entity: (log.entity as string) || null,
               entityId: (log.entityId as string) || null,
@@ -826,12 +761,8 @@ export async function POST(request: NextRequest) {
               errorMessage: (msg.errorMessage as string) || null,
               relatedEntityType: (msg.relatedEntityType as string) || null,
               relatedEntityId: (msg.relatedEntityId as string) || null,
-              createdAt: msg.createdAt
-                ? new Date(msg.createdAt as string)
-                : undefined,
-              updatedAt: msg.updatedAt
-                ? new Date(msg.updatedAt as string)
-                : undefined,
+              createdAt: toSafeDate(msg.createdAt as string),
+              updatedAt: toSafeDate(msg.updatedAt as string),
               organizationId: ctx.organizationId,
               customerId: (msg.customerId as string) || null,
             })
@@ -852,9 +783,7 @@ export async function POST(request: NextRequest) {
               entityId: n.entityId as string,
               entityUrl: n.entityUrl as string,
               read: (n.read as boolean) || false,
-              createdAt: n.createdAt
-                ? new Date(n.createdAt as string)
-                : undefined,
+              createdAt: toSafeDate(n.createdAt as string),
               organizationId: ctx.organizationId,
             })
           ),

+ 198 - 0
src/components/ui/date-input.tsx

@@ -0,0 +1,198 @@
+'use client'
+
+import { useRef, useState, useEffect } from 'react'
+import { Button } from '@/components/ui/button'
+import { Calendar } from '@/components/ui/calendar'
+import { Input } from '@/components/ui/input'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import { CalendarIcon } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { useDateSettings } from '@/components/date-settings-context'
+import { formatDate as formatWithOrgPattern } from '@/lib/format'
+
+function toISODate(date: Date): string {
+  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+}
+
+// Sane bounds for any date a workshop would ever type. Everything outside is
+// rejected on every parse path, so garbage can never reach the form value.
+const MIN_YEAR = 1900
+const MAX_YEAR = 2100
+
+/** Build a date from parts; undefined on overflow (32.13.…) or absurd years */
+function buildDate(year: number, month: number, day: number): Date | undefined {
+  if (year < MIN_YEAR || year > MAX_YEAR) return undefined
+  const d = new Date(year, month - 1, day)
+  return d.getFullYear() === year && d.getMonth() === month - 1 && d.getDate() === day
+    ? d
+    : undefined
+}
+
+/**
+ * Parse typed text into a Date. Supports separated numerics (25.05.2026,
+ * 25/05/2026, 2026-05-25) with dot/slash/dash/space, compact digits
+ * (25052026, 250526) and longhand text ("February 01, 2001"). Day-vs-month
+ * order follows the workshop date format when ambiguous; a value over 12 is
+ * always the day. Years outside 1900-2100 never parse.
+ */
+function parseDateText(raw: string, dateFormat: string): Date | undefined {
+  const text = raw.trim()
+  if (!text) return undefined
+
+  const monthFirst = dateFormat.trimStart().startsWith('M')
+  const yearFirst = /^y/i.test(dateFormat.trimStart())
+
+  const m = text.match(/^(\d{1,4})[./\-\s](\d{1,2})[./\-\s](\d{1,4})$/)
+  if (m) {
+    const [a, b, c] = [Number(m[1]), Number(m[2]), Number(m[3])]
+    if (m[1].length === 4) return buildDate(a, b, c) // 2026-05-25
+    const year = c < 100 ? 2000 + c : c
+    if (a > 12 && b <= 12) return buildDate(year, b, a)
+    if (b > 12 && a <= 12) return buildDate(year, a, b)
+    return monthFirst ? buildDate(year, a, b) : buildDate(year, b, a)
+  }
+
+  // Compact digits: 25052026 / 05252026 / 20260525 per format, 250526 short
+  if (/^\d{8}$/.test(text)) {
+    if (yearFirst) return buildDate(+text.slice(0, 4), +text.slice(4, 6), +text.slice(6, 8))
+    const p1 = +text.slice(0, 2)
+    const p2 = +text.slice(2, 4)
+    const year = +text.slice(4, 8)
+    const first = monthFirst ? buildDate(year, p1, p2) : buildDate(year, p2, p1)
+    return (
+      first ??
+      (monthFirst ? buildDate(year, p2, p1) : buildDate(year, p1, p2)) ??
+      buildDate(+text.slice(0, 4), +text.slice(4, 6), +text.slice(6, 8))
+    )
+  }
+  if (/^\d{6}$/.test(text)) {
+    const p1 = +text.slice(0, 2)
+    const p2 = +text.slice(2, 4)
+    const year = 2000 + +text.slice(4, 6)
+    return monthFirst ? buildDate(year, p1, p2) : buildDate(year, p2, p1)
+  }
+  // Any other bare digit run is ambiguous mid-typing input: never guess
+  if (/^[\d./\-\s]*$/.test(text)) return undefined
+
+  // Longhand text ("February 01, 2001"), same year bounds
+  const d = new Date(text)
+  if (isNaN(d.getTime())) return undefined
+  return d.getFullYear() >= MIN_YEAR && d.getFullYear() <= MAX_YEAR ? d : undefined
+}
+
+/**
+ * Typeable date field with a calendar picker (shadcn date picker "Input"
+ * pattern). Displays and parses using the workshop date format setting.
+ * The external value is an ISO YYYY-MM-DD string ('' when empty); when
+ * `name` is set, a hidden input carries the ISO value for form submission.
+ */
+export function DateInput({
+  id,
+  name,
+  value,
+  onChange,
+  className,
+  placeholder,
+}: {
+  id?: string
+  name?: string
+  value: string
+  onChange: (value: string) => void
+  className?: string
+  placeholder?: string
+}) {
+  const { dateFormat } = useDateSettings()
+  const display = (d: Date | undefined) => (d ? formatWithOrgPattern(d, dateFormat) : '')
+
+  const [open, setOpen] = useState(false)
+  const initial = value ? new Date(value + 'T00:00:00') : undefined
+  const [text, setText] = useState(display(initial))
+  const [month, setMonth] = useState<Date | undefined>(initial)
+  // Tracks the last ISO value this component emitted, so external value
+  // changes (reload, reset) update the text without clobbering typing.
+  const lastEmitted = useRef(value)
+
+  useEffect(() => {
+    if (value !== lastEmitted.current) {
+      const d = value ? new Date(value + 'T00:00:00') : undefined
+      setText(display(d))
+      setMonth(d)
+      lastEmitted.current = value
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [value])
+
+  const emit = (iso: string) => {
+    lastEmitted.current = iso
+    onChange(iso)
+  }
+
+  const handleTextChange = (raw: string) => {
+    setText(raw)
+    if (!raw.trim()) {
+      emit('')
+      return
+    }
+    const parsed = parseDateText(raw, dateFormat)
+    if (parsed) {
+      emit(toISODate(parsed))
+      setMonth(parsed)
+    }
+  }
+
+  const handleBlur = () => {
+    // Normalize to the display format, or clear leftover unparseable text
+    const d = value ? new Date(value + 'T00:00:00') : undefined
+    setText(display(d))
+  }
+
+  const selected = value ? new Date(value + 'T00:00:00') : undefined
+
+  return (
+    <div className="relative w-full">
+      {name && <input type="hidden" name={name} value={value} />}
+      <Input
+        id={id}
+        value={text}
+        placeholder={placeholder ?? display(new Date())}
+        onChange={(e) => handleTextChange(e.target.value)}
+        onBlur={handleBlur}
+        onKeyDown={(e) => {
+          if (e.key === 'ArrowDown') {
+            e.preventDefault()
+            setOpen(true)
+          }
+        }}
+        className={cn('pr-9', className)}
+      />
+      <Popover open={open} onOpenChange={setOpen}>
+        <PopoverTrigger asChild>
+          <Button
+            type="button"
+            variant="ghost"
+            size="icon"
+            className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 text-muted-foreground hover:text-foreground"
+          >
+            <CalendarIcon className="h-4 w-4" />
+            <span className="sr-only">Open calendar</span>
+          </Button>
+        </PopoverTrigger>
+        <PopoverContent className="w-auto p-0" align="end">
+          <Calendar
+            mode="single"
+            selected={selected}
+            month={month}
+            onMonthChange={setMonth}
+            onSelect={(d) => {
+              if (d) {
+                emit(toISODate(d))
+                setText(display(d))
+              }
+              setOpen(false)
+            }}
+          />
+        </PopoverContent>
+      </Popover>
+    </div>
+  )
+}

+ 5 - 5
src/features/billing/Actions/recurringInvoiceActions.ts

@@ -10,7 +10,7 @@ import {
   type CreateRecurringInvoiceInput,
   type UpdateRecurringInvoiceInput,
 } from "../Schema/recurringInvoiceSchema";
-import { resolveInvoicePrefix } from "@/lib/invoice-utils";
+import { resolveInvoicePrefix, toSafeDate } from "@/lib/invoice-utils";
 import { calculateTotals } from "@/lib/tax";
 import { lineTotal } from "@/features/inventory/Lib/partPricing";
 
@@ -64,8 +64,8 @@ export async function createRecurringInvoice(input: CreateRecurringInvoiceInput)
         title: parsed.title,
         description: parsed.description,
         frequency: parsed.frequency,
-        nextRunDate: new Date(parsed.nextRunDate),
-        endDate: parsed.endDate ? new Date(parsed.endDate) : null,
+        nextRunDate: toSafeDate(parsed.nextRunDate) ?? new Date(),
+        endDate: toSafeDate(parsed.endDate) ?? null,
         vehicleId: parsed.vehicleId,
         type: parsed.type,
         cost: parsed.cost,
@@ -136,8 +136,8 @@ export async function updateRecurringInvoice(input: UpdateRecurringInvoiceInput)
           ...(parsed.title !== undefined && { title: parsed.title }),
           ...(parsed.description !== undefined && { description: parsed.description }),
           ...(parsed.frequency !== undefined && { frequency: parsed.frequency }),
-          ...(parsed.nextRunDate !== undefined && { nextRunDate: new Date(parsed.nextRunDate) }),
-          ...(parsed.endDate !== undefined && { endDate: parsed.endDate ? new Date(parsed.endDate) : null }),
+          ...(parsed.nextRunDate !== undefined && { nextRunDate: toSafeDate(parsed.nextRunDate) ?? new Date() }),
+          ...(parsed.endDate !== undefined && { endDate: toSafeDate(parsed.endDate) ?? null }),
           ...(parsed.vehicleId !== undefined && { vehicleId: parsed.vehicleId }),
           ...(parsed.type !== undefined && { type: parsed.type }),
           ...(parsed.cost !== undefined && { cost: parsed.cost }),

+ 3 - 2
src/features/billing/Components/RecurringInvoicesClient.tsx

@@ -23,6 +23,7 @@ import {
   DialogTitle,
 } from "@/components/ui/dialog";
 import { Input } from "@/components/ui/input";
+import { DateInput } from "@/components/ui/date-input";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
 import {
@@ -452,11 +453,11 @@ export default function RecurringInvoicesClient({
               </div>
               <div className="space-y-1.5">
                 <Label>{t("recurring.startDate")}</Label>
-                <Input type="date" value={nextRunDate} onChange={(e) => setNextRunDate(e.target.value)} />
+                <DateInput value={nextRunDate} onChange={setNextRunDate} />
               </div>
               <div className="space-y-1.5">
                 <Label>{t("recurring.endDate")}</Label>
-                <Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
+                <DateInput value={endDate} onChange={setEndDate} />
               </div>
             </div>
 

+ 3 - 3
src/features/custom-fields/Components/CustomFieldsForm.tsx

@@ -2,6 +2,7 @@
 
 import { useEffect, useState, useCallback } from "react";
 import { Input } from "@/components/ui/input";
+import { DateInput } from "@/components/ui/date-input";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
 import { Checkbox } from "@/components/ui/checkbox";
@@ -156,10 +157,9 @@ export function CustomFieldsForm({
               )}
 
               {field.fieldType === "date" && (
-                <Input
-                  type="date"
+                <DateInput
                   value={val}
-                  onChange={(e) => updateValue(field.id, e.target.value)}
+                  onChange={(v) => updateValue(field.id, v)}
                   className={cn(hasError && "border-destructive focus-visible:ring-destructive")}
                 />
               )}

+ 2 - 1
src/features/payments/Actions/paymentActions.ts

@@ -1,5 +1,6 @@
 "use server";
 
+import { toSafeDate } from "@/lib/invoice-utils";
 import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
@@ -21,7 +22,7 @@ export async function createPayment(input: unknown) {
       data: {
         serviceRecordId: data.serviceRecordId,
         amount: data.amount,
-        date: new Date(data.date),
+        date: toSafeDate(data.date) ?? new Date(),
         method: data.method,
         note: data.note || null,
       },

+ 2 - 1
src/features/portal/Actions/portalActions.ts

@@ -1,5 +1,6 @@
 'use server'
 
+import { toSafeDate } from "@/lib/invoice-utils";
 import { db } from '@/lib/db'
 import { getCustomerSession, type CustomerSessionData } from '@/lib/customer-session'
 import { notify } from '@/lib/notify'
@@ -336,7 +337,7 @@ export async function createServiceRequest(input: {
     const serviceRequest = await db.serviceRequest.create({
       data: {
         description: input.description,
-        preferredDate: input.preferredDate ? new Date(input.preferredDate) : null,
+        preferredDate: toSafeDate(input.preferredDate) ?? null,
         customerId,
         vehicleId: input.vehicleId,
         organizationId,

+ 5 - 5
src/features/quotes/Actions/quoteActions.ts

@@ -5,7 +5,7 @@ import { withAuth } from "@/lib/with-auth";
 import { createQuoteSchema, updateQuoteSchema } from "../Schema/quoteSchema";
 import { revalidatePath } from "next/cache";
 import { onInventoryChanged } from "@/features/inventory/Lib/onInventoryChanged";
-import { resolveInvoicePrefix } from "@/lib/invoice-utils";
+import { resolveInvoicePrefix, toSafeDate } from "@/lib/invoice-utils";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { reconcileInventoryForParts } from "@/features/inventory/Lib/reconcileStock";
 import { copyFile, mkdir } from "fs/promises";
@@ -196,9 +196,9 @@ export async function createQuote(input: unknown) {
           organizationId,
           taxRate: quoteData.taxRate > 0 ? quoteData.taxRate : defaultTaxRate,
           taxInclusive,
-          validUntil: quoteData.validUntil
-            ? new Date(quoteData.validUntil)
-            : defaultValidUntil(settingsMap["workshop.quoteValidDays"]),
+          validUntil:
+            toSafeDate(quoteData.validUntil) ??
+            defaultValidUntil(settingsMap["workshop.quoteValidDays"]),
           discountType: quoteData.discountType === "none" ? null : quoteData.discountType,
         },
       });
@@ -247,7 +247,7 @@ export async function updateQuote(input: unknown) {
         where: { id },
         data: {
           ...quoteData,
-          validUntil: quoteData.validUntil ? new Date(quoteData.validUntil) : undefined,
+          validUntil: toSafeDate(quoteData.validUntil),
           discountType: quoteData.discountType === "none" ? null : quoteData.discountType,
         },
       });

+ 9 - 5
src/features/quotes/Components/QuoteRightColumn.tsx

@@ -1,9 +1,10 @@
 'use client'
 
-import { memo } from 'react'
+import { memo, useState } from 'react'
 import Link from 'next/link'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
+import { DateInput } from '@/components/ui/date-input'
 import { Label } from '@/components/ui/label'
 import {
   Select,
@@ -49,6 +50,7 @@ export const QuoteRightColumn = memo(function QuoteRightColumn({
   onRevoke,
 }: QuoteRightColumnProps) {
   const formatCurrency = useFormatCurrency()
+  const [validUntil, setValidUntil] = useState(state.defaultValidDate)
   return (
     <div className="space-y-3">
       {/* Convert to Work Order */}
@@ -227,12 +229,14 @@ export const QuoteRightColumn = memo(function QuoteRightColumn({
             <Label htmlFor="validUntil" className="text-xs">
               {t('details.validUntil')}
             </Label>
-            <Input
+            <DateInput
               id="validUntil"
               name="validUntil"
-              type="date"
-              defaultValue={state.defaultValidDate}
-              onChange={state.markDirty}
+              value={validUntil}
+              onChange={(v) => {
+                setValidUntil(v)
+                state.markDirty()
+              }}
             />
           </div>
         </div>

+ 3 - 2
src/features/report-schedule/Actions/reportScheduleActions.ts

@@ -1,5 +1,6 @@
 "use server";
 
+import { toSafeDate } from "@/lib/invoice-utils";
 import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
@@ -97,7 +98,7 @@ export async function createReportSchedule(input: unknown) {
           sections: JSON.stringify(data.sections),
           recipients: JSON.stringify(data.recipients),
           nextRunDate,
-          endDate: data.endDate ? new Date(data.endDate) : null,
+          endDate: toSafeDate(data.endDate) ?? null,
           organizationId,
           createdById: userId,
         },
@@ -141,7 +142,7 @@ export async function updateReportSchedule(input: unknown) {
           sections: JSON.stringify(data.sections),
           recipients: JSON.stringify(data.recipients),
           nextRunDate,
-          endDate: data.endDate ? new Date(data.endDate) : null,
+          endDate: toSafeDate(data.endDate) ?? null,
           isActive: data.isActive ?? existing.isActive,
         },
       });

+ 2 - 1
src/features/status-reports/Actions/createStatusReport.ts

@@ -1,5 +1,6 @@
 "use server";
 
+import { toSafeDate } from "@/lib/invoice-utils";
 import { randomBytes } from "crypto";
 import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
@@ -35,7 +36,7 @@ export async function createStatusReport(input: unknown) {
           organizationId,
           technicianId: technician?.id || serviceRecord.technicianId,
           status: data.videoUrl ? "published" : "draft",
-          expiresAt: data.expiresAt ? new Date(data.expiresAt) : new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
+          expiresAt: toSafeDate(data.expiresAt) ?? new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
         },
       });
 

+ 2 - 3
src/features/status-reports/Components/CreateStatusReportDialog.tsx

@@ -12,6 +12,7 @@ import {
 } from "@/components/ui/drawer";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
+import { DateInput } from "@/components/ui/date-input";
 import { Textarea } from "@/components/ui/textarea";
 import { Label } from "@/components/ui/label";
 import { Camera, Loader2, Upload, Video, Send, X } from "lucide-react";
@@ -184,9 +185,7 @@ export function CreateStatusReportDialog({
 
       <div className="space-y-1.5">
         <Label htmlFor="sr-expires">{t("expiresLabel")}</Label>
-        <Input id="sr-expires" type="date" value={expiresAt}
-          onChange={(e) => setExpiresAt(e.target.value)}
-          min={new Date().toISOString().split("T")[0]} />
+        <DateInput id="sr-expires" value={expiresAt} onChange={setExpiresAt} />
       </div>
     </div>
   );

+ 3 - 2
src/features/vehicles/Actions/reminderActions.ts

@@ -1,5 +1,6 @@
 "use server";
 
+import { toSafeDate } from "@/lib/invoice-utils";
 import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
@@ -40,7 +41,7 @@ export async function createReminder(input: unknown) {
     const reminder = await db.reminder.create({
       data: {
         ...data,
-        dueDate: data.dueDate ? new Date(data.dueDate) : null,
+        dueDate: toSafeDate(data.dueDate) ?? null,
       },
     });
     revalidatePath(`/vehicles/${data.vehicleId}`);
@@ -72,7 +73,7 @@ export async function updateReminder(input: unknown) {
       data: {
         ...data,
         description: data.description !== undefined ? (data.description || null) : undefined,
-        dueDate: data.dueDate !== undefined ? (data.dueDate ? new Date(data.dueDate) : null) : undefined,
+        dueDate: data.dueDate !== undefined ? (toSafeDate(data.dueDate) ?? null) : undefined,
         dueMileage: data.dueMileage !== undefined ? (data.dueMileage ?? null) : undefined,
       },
     });

+ 7 - 7
src/features/vehicles/Actions/serviceActions.ts

@@ -8,7 +8,7 @@ import { onInventoryChanged } from "@/features/inventory/Lib/onInventoryChanged"
 import { unlink } from "fs/promises";
 import { randomUUID } from "crypto";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
-import { resolveInvoicePrefix } from "@/lib/invoice-utils";
+import { resolveInvoicePrefix, toSafeDate } from "@/lib/invoice-utils";
 import { notificationBus } from "@/lib/notification-bus";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { reconcileInventoryForParts } from "@/features/inventory/Lib/reconcileStock";
@@ -289,9 +289,9 @@ export async function createServiceRecord(input: unknown) {
           taxInclusive,
           shopName,
           invoiceNumber,
-          serviceDate: new Date(serviceDate),
-          invoiceDate: invoiceDate ? new Date(invoiceDate) : new Date(serviceDate),
-          invoiceDueDate: invoiceDueDate ? new Date(invoiceDueDate) : undefined,
+          serviceDate: toSafeDate(serviceDate) ?? new Date(),
+          invoiceDate: toSafeDate(invoiceDate) ?? toSafeDate(serviceDate) ?? new Date(),
+          invoiceDueDate: toSafeDate(invoiceDueDate),
           warrantyMonths: warrantyMonths || null,
           warrantyMileage: warrantyMileage || null,
           warrantyNotes: warrantyNotes || null,
@@ -438,9 +438,9 @@ export async function updateServiceRecord(input: unknown) {
           invoiceNotes: recordData.invoiceNotes !== undefined ? (recordData.invoiceNotes || null) : undefined,
           invoiceNumber: recordData.invoiceNumber !== undefined ? (recordData.invoiceNumber || null) : undefined,
           mileage: recordData.mileage !== undefined ? (recordData.mileage ?? null) : undefined,
-          serviceDate: data.serviceDate ? new Date(data.serviceDate) : undefined,
-          invoiceDate: data.invoiceDate ? new Date(data.invoiceDate) : undefined,
-          invoiceDueDate: data.invoiceDueDate ? new Date(data.invoiceDueDate) : undefined,
+          serviceDate: toSafeDate(data.serviceDate),
+          invoiceDate: toSafeDate(data.invoiceDate),
+          invoiceDueDate: toSafeDate(data.invoiceDueDate),
           warrantyMonths: data.warrantyMonths !== undefined ? (data.warrantyMonths || null) : undefined,
           warrantyMileage: data.warrantyMileage !== undefined ? (data.warrantyMileage || null) : undefined,
           warrantyNotes: data.warrantyNotes !== undefined ? (data.warrantyNotes || null) : undefined,

+ 3 - 2
src/features/vehicles/Actions/vehicleActions.ts

@@ -1,5 +1,6 @@
 "use server";
 
+import { toSafeDate } from "@/lib/invoice-utils";
 import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
@@ -139,7 +140,7 @@ export async function createVehicle(input: unknown) {
     const vehicle = await db.vehicle.create({
       data: {
         ...data,
-        purchaseDate: data.purchaseDate ? new Date(data.purchaseDate) : null,
+        purchaseDate: toSafeDate(data.purchaseDate) ?? null,
         customerId: data.customerId || null,
         userId,
         organizationId,
@@ -197,7 +198,7 @@ export async function updateVehicle(input: unknown) {
         transmission: data.transmission !== undefined ? (data.transmission || null) : undefined,
         engineSize: data.engineSize !== undefined ? (data.engineSize || null) : undefined,
         engineCode: data.engineCode !== undefined ? (data.engineCode || null) : undefined,
-        purchaseDate: data.purchaseDate ? new Date(data.purchaseDate) : undefined,
+        purchaseDate: toSafeDate(data.purchaseDate),
         customerId: data.customerId !== undefined ? (data.customerId || null) : undefined,
       },
     });

+ 3 - 3
src/features/vehicles/Components/ReminderForm.tsx

@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
 import { useRouter } from "next/navigation";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
+import { DateInput } from "@/components/ui/date-input";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
 import {
@@ -120,11 +121,10 @@ export function ReminderForm({ vehicleId, open, onOpenChange, reminder }: Remind
           <div className="grid grid-cols-2 gap-4">
             <div className="space-y-2">
               <Label htmlFor="reminder-dueDate">{t("dueDateLabel")}</Label>
-              <Input
+              <DateInput
                 id="reminder-dueDate"
-                type="date"
                 value={dueDate}
-                onChange={(e) => setDueDate(e.target.value)}
+                onChange={setDueDate}
               />
             </div>
             <div className="space-y-2">

+ 5 - 3
src/features/vehicles/Components/ServiceForm.tsx

@@ -4,6 +4,7 @@ import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
+import { DateInput } from "@/components/ui/date-input";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
 import {
@@ -38,6 +39,7 @@ export function ServiceForm({
   const modal = useGlassModal();
   const [loading, setLoading] = useState(false);
   const [type, setType] = useState("maintenance");
+  const [serviceDate, setServiceDate] = useState(new Date().toISOString().split("T")[0]);
 
   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
     e.preventDefault();
@@ -104,11 +106,11 @@ export function ServiceForm({
             </div>
             <div className="space-y-2">
               <Label htmlFor="serviceDate">Date</Label>
-              <Input
+              <DateInput
                 id="serviceDate"
                 name="serviceDate"
-                type="date"
-                defaultValue={new Date().toISOString().split("T")[0]}
+                value={serviceDate}
+                onChange={setServiceDate}
               />
             </div>
           </div>

+ 5 - 3
src/features/vehicles/Components/service-detail/PaymentsSection.tsx

@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
+import { DateInput } from "@/components/ui/date-input";
 import { Label } from "@/components/ui/label";
 import {
   Select,
@@ -54,6 +55,7 @@ export function PaymentsSection({
   const tc = useTranslations("common.buttons");
   const [showForm, setShowForm] = useState(false);
   const [paymentMethod, setPaymentMethod] = useState("other");
+  const [paymentDate, setPaymentDate] = useState(new Date().toISOString().split("T")[0]);
   const { formatDate } = useFormatDate();
   const formRef = useRef<HTMLDivElement>(null);
 
@@ -133,9 +135,9 @@ export function PaymentsSection({
               </div>
               <div className="space-y-1">
                 <Label htmlFor="paymentDate" className="text-xs">{t("date")}</Label>
-                <Input
-                  id="paymentDate" name="paymentDate" type="date"
-                  defaultValue={new Date().toISOString().split("T")[0]}
+                <DateInput
+                  id="paymentDate" name="paymentDate"
+                  value={paymentDate} onChange={setPaymentDate}
                 />
               </div>
               <div className="space-y-1">

+ 23 - 57
src/features/vehicles/Components/service-edit/InvoiceDetailsSection.tsx

@@ -5,8 +5,7 @@ import { useTranslations } from 'next-intl'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Button } from '@/components/ui/button'
-import { Calendar } from '@/components/ui/calendar'
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import { DateInput } from '@/components/ui/date-input'
 import {
   Select,
   SelectContent,
@@ -14,7 +13,7 @@ import {
   SelectTrigger,
   SelectValue,
 } from '@/components/ui/select'
-import { CalendarIcon, Check, Loader2 } from 'lucide-react'
+import { Check, Loader2 } from 'lucide-react'
 import { cn } from '@/lib/utils'
 import type { InitialData } from './form-types'
 
@@ -42,35 +41,18 @@ export function InvoiceDetailsSection({
   paymentLoading,
 }: InvoiceDetailsSectionProps) {
   const t = useTranslations('service.basicInfo')
-  const [invoiceDate, setInvoiceDate] = useState<Date | undefined>(
-    initialData.invoiceDate ? new Date(initialData.invoiceDate + 'T00:00:00') : undefined
-  )
-  const [invoiceDueDate, setInvoiceDueDate] = useState<Date | undefined>(
-    initialData.invoiceDueDate ? new Date(initialData.invoiceDueDate + 'T00:00:00') : undefined
-  )
+  // ISO YYYY-MM-DD strings, matching the native date input's value format
+  const [invoiceDate, setInvoiceDate] = useState(initialData.invoiceDate || '')
+  const [invoiceDueDate, setInvoiceDueDate] = useState(initialData.invoiceDueDate || '')
 
   useEffect(() => {
-    setInvoiceDate(
-      initialData.invoiceDate ? new Date(initialData.invoiceDate + 'T00:00:00') : undefined
-    )
+    setInvoiceDate(initialData.invoiceDate || '')
   }, [initialData.invoiceDate])
 
   useEffect(() => {
-    setInvoiceDueDate(
-      initialData.invoiceDueDate ? new Date(initialData.invoiceDueDate + 'T00:00:00') : undefined
-    )
+    setInvoiceDueDate(initialData.invoiceDueDate || '')
   }, [initialData.invoiceDueDate])
 
-  const formatDate = (date: Date | undefined) => {
-    if (!date) return ''
-    return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
-  }
-
-  const toISODate = (date: Date | undefined) => {
-    if (!date) return ''
-    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
-  }
-
   return (
     <div className="rounded-lg border p-3 space-y-3">
       <div className="flex items-center justify-between">
@@ -154,40 +136,24 @@ export function InvoiceDetailsSection({
 
       <div className="grid grid-cols-2 gap-2">
         <div className="space-y-1">
-          <Label className="text-xs">{t('invoiceDate')}</Label>
-          <input type="hidden" name="invoiceDate" value={toISODate(invoiceDate)} />
-          <Popover>
-            <PopoverTrigger asChild>
-              <Button
-                variant="outline"
-                className={cn('w-full justify-start text-left font-normal h-9 text-sm', !invoiceDate && 'text-muted-foreground')}
-              >
-                <CalendarIcon className="mr-2 h-3.5 w-3.5" />
-                <span suppressHydrationWarning>{invoiceDate ? formatDate(invoiceDate) : t('invoiceDate')}</span>
-              </Button>
-            </PopoverTrigger>
-            <PopoverContent className="w-auto p-0" align="start">
-              <Calendar mode="single" selected={invoiceDate} onSelect={(d) => { setInvoiceDate(d); onDirty?.() }} />
-            </PopoverContent>
-          </Popover>
+          <Label htmlFor="invoiceDate" className="text-xs">{t('invoiceDate')}</Label>
+          <DateInput
+            id="invoiceDate"
+            name="invoiceDate"
+            value={invoiceDate}
+            onChange={(v) => { setInvoiceDate(v); onDirty?.() }}
+            className="h-9 text-sm"
+          />
         </div>
         <div className="space-y-1">
-          <Label className="text-xs">{t('invoiceDueDate')}</Label>
-          <input type="hidden" name="invoiceDueDate" value={toISODate(invoiceDueDate)} />
-          <Popover>
-            <PopoverTrigger asChild>
-              <Button
-                variant="outline"
-                className={cn('w-full justify-start text-left font-normal h-9 text-sm', !invoiceDueDate && 'text-muted-foreground')}
-              >
-                <CalendarIcon className="mr-2 h-3.5 w-3.5" />
-                <span suppressHydrationWarning>{invoiceDueDate ? formatDate(invoiceDueDate) : t('invoiceDueDate')}</span>
-              </Button>
-            </PopoverTrigger>
-            <PopoverContent className="w-auto p-0" align="start">
-              <Calendar mode="single" selected={invoiceDueDate} onSelect={(d) => { setInvoiceDueDate(d); onDirty?.() }} />
-            </PopoverContent>
-          </Popover>
+          <Label htmlFor="invoiceDueDate" className="text-xs">{t('invoiceDueDate')}</Label>
+          <DateInput
+            id="invoiceDueDate"
+            name="invoiceDueDate"
+            value={invoiceDueDate}
+            onChange={(v) => { setInvoiceDueDate(v); onDirty?.() }}
+            className="h-9 text-sm"
+          />
         </div>
       </div>
     </div>

+ 13 - 0
src/lib/invoice-utils.ts

@@ -6,6 +6,19 @@ export function resolveInvoicePrefix(prefix: string): string {
   return prefix.replace(/\{year\}/g, String(new Date().getFullYear()));
 }
 
+/**
+ * Parse a client-supplied date string for persistence. Returns undefined when
+ * missing, unparseable, or outside 1900-2100, so malformed input can never
+ * reach the database (out-of-range timestamps break rendering later).
+ */
+export function toSafeDate(value: string | undefined | null): Date | undefined {
+  if (!value) return undefined;
+  const d = new Date(value);
+  if (isNaN(d.getTime())) return undefined;
+  const year = d.getFullYear();
+  return year >= 1900 && year <= 2100 ? d : undefined;
+}
+
 /**
  * The date an invoice is presented with: an explicitly set invoice date
  * wins over the scheduled start, which wins over the service date.