Explorar el Código

Add parts-only counter sales (invoices without a vehicle) (#227)

* chnaged to customer id instead of org

* Add right-click context menu on work orders, audit fixes for counter sales

* Fix work board crash on counter sales in unassigned panel
Bernt Christian Egeland hace 1 mes
padre
commit
2f66d718bb
Se han modificado 100 ficheros con 1973 adiciones y 972 borrados
  1. 1 0
      messages/de/audit.json
  2. 12 2
      messages/de/workOrders.json
  3. 1 0
      messages/en/audit.json
  4. 12 2
      messages/en/workOrders.json
  5. 1 0
      messages/es/audit.json
  6. 12 2
      messages/es/workOrders.json
  7. 1 0
      messages/fr/audit.json
  8. 12 2
      messages/fr/workOrders.json
  9. 1 0
      messages/it/audit.json
  10. 12 2
      messages/it/workOrders.json
  11. 1 0
      messages/lt/audit.json
  12. 12 2
      messages/lt/workOrders.json
  13. 1 0
      messages/nb/audit.json
  14. 12 2
      messages/nb/workOrders.json
  15. 1 0
      messages/nl/audit.json
  16. 12 2
      messages/nl/workOrders.json
  17. 1 0
      messages/pl/audit.json
  18. 12 2
      messages/pl/workOrders.json
  19. 1 0
      messages/pt-BR/audit.json
  20. 12 2
      messages/pt-BR/workOrders.json
  21. 1 0
      messages/ru/audit.json
  22. 12 2
      messages/ru/workOrders.json
  23. 1 0
      messages/tr/audit.json
  24. 12 2
      messages/tr/workOrders.json
  25. 28 0
      prisma/migrations/20260811200000_service_record_org_scope/migration.sql
  26. 41 23
      prisma/schema.prisma
  27. 257 0
      src/__tests__/features/counter-sales.test.ts
  28. 6 6
      src/__tests__/multitenancy/service-record-isolation.test.ts
  29. 6 8
      src/__tests__/multitenancy/vehicle-service-isolation.test.ts
  30. 12 9
      src/app/(authenticated)/billing/billing-client.tsx
  31. 10 9
      src/app/(authenticated)/dashboard-client.tsx
  32. 2 2
      src/app/(authenticated)/inventory/[id]/part-detail-client.tsx
  33. 18 0
      src/app/(authenticated)/sales/[serviceId]/page.tsx
  34. 3 272
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  35. 74 45
      src/app/(authenticated)/work-orders/work-orders-client.tsx
  36. 6 3
      src/app/(public)/portal/[orgId]/invoices/[invoiceId]/pdf/route.ts
  37. 17 7
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  38. 14 4
      src/app/(public)/share/invoice/[orgId]/[token]/page.tsx
  39. 13 11
      src/app/(public)/share/status-report/[orgId]/[token]/status-report-view.tsx
  40. 32 1
      src/app/api/protected/backup/export/route.ts
  41. 2 1
      src/app/api/protected/backup/import-invoice-ninja/route.ts
  42. 2 1
      src/app/api/protected/backup/import-lubelog/route.ts
  43. 159 129
      src/app/api/protected/backup/import/route.ts
  44. 11 1
      src/app/api/protected/services/[id]/pdf/route.ts
  45. 3 3
      src/app/api/public/files/[token]/[...path]/route.ts
  46. 1 1
      src/app/api/public/share/invoice/[orgId]/[token]/checkout/route.ts
  47. 2 2
      src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts
  48. 7 4
      src/app/api/public/share/invoice/[orgId]/[token]/verify/route.ts
  49. 2 2
      src/app/api/webhooks/paypal/route.ts
  50. 2 2
      src/app/api/webhooks/stripe/route.ts
  51. 4 4
      src/app/api/webhooks/vipps/route.ts
  52. 154 5
      src/components/vehicle-picker-dialog.tsx
  53. 2 2
      src/features/admin/Actions/getOrganizationDetails.ts
  54. 5 5
      src/features/ai/Actions/aiActions.ts
  55. 6 3
      src/features/ai/tools/workshop-tools.ts
  56. 17 16
      src/features/billing/Actions/billingActions.ts
  57. 2 1
      src/features/billing/Actions/recurringInvoiceActions.ts
  58. 5 4
      src/features/calendar/Actions/calendarActions.ts
  59. 1 0
      src/features/calendar/Components/CalendarEventList.tsx
  60. 6 4
      src/features/dashboard/custom-cards/server-registry.ts
  61. 2 1
      src/features/email/Actions/emailActions.ts
  62. 11 1
      src/features/invoices/Pdf/buildInvoicePdfBuffer.ts
  63. 10 4
      src/features/payments/Actions/paymentActions.ts
  64. 7 4
      src/features/portal/Actions/portalActions.ts
  65. 2 1
      src/features/quotes/Actions/quoteActions.ts
  66. 13 11
      src/features/reports/Actions/reportActions.ts
  67. 2 2
      src/features/search/Actions/searchActions.ts
  68. 5 5
      src/features/search/Components/SearchCommand.tsx
  69. 6 6
      src/features/settings/Actions/applyTaxRateToExisting.ts
  70. 5 1
      src/features/settings/Actions/deleteContent.ts
  71. 1 1
      src/features/status-reports/Actions/createStatusReport.ts
  72. 8 6
      src/features/status-reports/Actions/getPublicStatusReport.ts
  73. 1 1
      src/features/status-reports/Actions/getStatusReportsForService.ts
  74. 11 2
      src/features/status-reports/Actions/sendStatusReport.ts
  75. 8 3
      src/features/status-reports/Actions/submitFeedback.ts
  76. 6 2
      src/features/vehicles/Actions/addPartToServiceRecord.ts
  77. 1 1
      src/features/vehicles/Actions/addServiceAttachment.ts
  78. 201 140
      src/features/vehicles/Actions/createDraftServiceRecord.ts
  79. 6 4
      src/features/vehicles/Actions/dashboardActions.ts
  80. 2 0
      src/features/vehicles/Actions/findingActions.ts
  81. 19 5
      src/features/vehicles/Actions/getMyActiveJobs.ts
  82. 1 1
      src/features/vehicles/Actions/predictedMaintenanceActions.ts
  83. 108 40
      src/features/vehicles/Actions/serviceActions.ts
  84. 1 1
      src/features/vehicles/Actions/updateServiceAttachment.ts
  85. 31 26
      src/features/vehicles/Components/MyActiveJobs.tsx
  86. 14 7
      src/features/vehicles/Components/invoice-pdf/InfoSection.tsx
  87. 1 1
      src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx
  88. 9 1
      src/features/vehicles/Components/invoice-pdf/types.ts
  89. 3 1
      src/features/vehicles/Components/service-detail/types.ts
  90. 24 20
      src/features/vehicles/Components/service-edit/BasicInfoSection.tsx
  91. 20 15
      src/features/vehicles/Components/service-edit/InvoiceDetailsSection.tsx
  92. 10 8
      src/features/vehicles/Components/service-page/DetailsLeftColumn.tsx
  93. 4 3
      src/features/vehicles/Components/service-page/DetailsRightColumn.tsx
  94. 25 19
      src/features/vehicles/Components/service-page/ServicePageClient.tsx
  95. 291 0
      src/features/vehicles/Components/service-page/ServiceRecordPage.tsx
  96. 2 2
      src/features/vehicles/Components/service-page/UnifiedServiceHeader.tsx
  97. 2 2
      src/features/vehicles/Components/service-page/service-page-types.ts
  98. 6 6
      src/features/vehicles/Components/service-page/useServiceActions.ts
  99. 3 3
      src/features/vehicles/Components/service-page/useServiceFormState.ts
  100. 3 1
      src/features/vehicles/Schema/serviceSchema.ts

+ 1 - 0
messages/de/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Kunde erstellt",
     "customer_create": "Kunde erstellt",
     "customer_update": "Kunde aktualisiert",
     "customer_update": "Kunde aktualisiert",
     "customer_delete": "Kunde gelöscht",
     "customer_delete": "Kunde gelöscht",
+    "customer_backfillNumbers": "Kundennummern nachgetragen",
     "inventory_create": "Lagerteil erstellt",
     "inventory_create": "Lagerteil erstellt",
     "inventory_update": "Lagerteil aktualisiert",
     "inventory_update": "Lagerteil aktualisiert",
     "inventory_delete": "Lagerteil gelöscht",
     "inventory_delete": "Lagerteil gelöscht",

+ 12 - 2
messages/de/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Gesamt"
       "total": "Gesamt"
     },
     },
     "emailSubject": "Fahrzeugstatusupdate: {status}",
     "emailSubject": "Fahrzeugstatusupdate: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Arbeitsauftrag öffnen",
+      "openVehicle": "Fahrzeug öffnen",
+      "openCustomer": "Kunde öffnen",
+      "hint": "Tipp: Rechtsklick auf eine Zeile für Aktionen"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Fahrzeug für Arbeitsauftrag auswählen",
     "title": "Fahrzeug für Arbeitsauftrag auswählen",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Kundenname ist erforderlich",
     "customerNameRequired": "Kundenname ist erforderlich",
     "failedCreateCustomer": "Kunde konnte nicht erstellt werden",
     "failedCreateCustomer": "Kunde konnte nicht erstellt werden",
     "failedCreateVehicle": "Fahrzeug konnte nicht erstellt werden",
     "failedCreateVehicle": "Fahrzeug konnte nicht erstellt werden",
-    "somethingWentWrong": "Etwas ist schief gelaufen"
+    "somethingWentWrong": "Etwas ist schief gelaufen",
+    "partsSale": "Reiner Teileverkauf",
+    "partsSaleDescription": "Rechnung ohne Fahrzeug erstellen, für Teileverkauf über die Theke.",
+    "createSale": "Verkauf erstellen",
+    "none": "Keiner"
   },
   },
   "notify": {
   "notify": {
     "title": "{name} benachrichtigen",
     "title": "{name} benachrichtigen",

+ 1 - 0
messages/en/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Created Customer",
     "customer_create": "Created Customer",
     "customer_update": "Updated Customer",
     "customer_update": "Updated Customer",
     "customer_delete": "Deleted Customer",
     "customer_delete": "Deleted Customer",
+    "customer_backfillNumbers": "Backfilled Customer Numbers",
     "inventory_create": "Created Inventory Part",
     "inventory_create": "Created Inventory Part",
     "inventory_update": "Updated Inventory Part",
     "inventory_update": "Updated Inventory Part",
     "inventory_delete": "Deleted Inventory Part",
     "inventory_delete": "Deleted Inventory Part",

+ 12 - 2
messages/en/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
       "total": "Total"
     },
     },
     "emailSubject": "Vehicle Status Update: {status}",
     "emailSubject": "Vehicle Status Update: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Open work order",
+      "openVehicle": "Open vehicle",
+      "openCustomer": "Open customer",
+      "hint": "Tip: right-click a row for actions"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Select Vehicle for Work Order",
     "title": "Select Vehicle for Work Order",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Customer name is required",
     "customerNameRequired": "Customer name is required",
     "failedCreateCustomer": "Failed to create customer",
     "failedCreateCustomer": "Failed to create customer",
     "failedCreateVehicle": "Failed to create vehicle",
     "failedCreateVehicle": "Failed to create vehicle",
-    "somethingWentWrong": "Something went wrong"
+    "somethingWentWrong": "Something went wrong",
+    "partsSale": "Parts-Only Sale",
+    "partsSaleDescription": "Create an invoice without a vehicle, for over-the-counter parts sales.",
+    "createSale": "Create Sale",
+    "none": "None"
   },
   },
   "notify": {
   "notify": {
     "title": "Notify {name}",
     "title": "Notify {name}",

+ 1 - 0
messages/es/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Cliente creado",
     "customer_create": "Cliente creado",
     "customer_update": "Cliente actualizado",
     "customer_update": "Cliente actualizado",
     "customer_delete": "Cliente eliminado",
     "customer_delete": "Cliente eliminado",
+    "customer_backfillNumbers": "Números de cliente completados",
     "inventory_create": "Pieza de inventario creada",
     "inventory_create": "Pieza de inventario creada",
     "inventory_update": "Pieza de inventario actualizada",
     "inventory_update": "Pieza de inventario actualizada",
     "inventory_delete": "Pieza de inventario eliminada",
     "inventory_delete": "Pieza de inventario eliminada",

+ 12 - 2
messages/es/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
       "total": "Total"
     },
     },
     "emailSubject": "Actualización de Estado del Vehículo: {status}",
     "emailSubject": "Actualización de Estado del Vehículo: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Abrir orden de trabajo",
+      "openVehicle": "Abrir vehículo",
+      "openCustomer": "Abrir cliente",
+      "hint": "Consejo: clic derecho en una fila para ver acciones"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Seleccionar Vehículo para Orden de Trabajo",
     "title": "Seleccionar Vehículo para Orden de Trabajo",
@@ -61,7 +67,11 @@
     "customerNameRequired": "El nombre del cliente es requerido",
     "customerNameRequired": "El nombre del cliente es requerido",
     "failedCreateCustomer": "Error al crear cliente",
     "failedCreateCustomer": "Error al crear cliente",
     "failedCreateVehicle": "Error al crear vehículo",
     "failedCreateVehicle": "Error al crear vehículo",
-    "somethingWentWrong": "Algo salió mal"
+    "somethingWentWrong": "Algo salió mal",
+    "partsSale": "Venta solo de piezas",
+    "partsSaleDescription": "Crea una factura sin vehículo, para ventas de piezas en mostrador.",
+    "createSale": "Crear venta",
+    "none": "Ninguno"
   },
   },
   "notify": {
   "notify": {
     "title": "Notificar a {name}",
     "title": "Notificar a {name}",

+ 1 - 0
messages/fr/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Client créé",
     "customer_create": "Client créé",
     "customer_update": "Client mis à jour",
     "customer_update": "Client mis à jour",
     "customer_delete": "Client supprimé",
     "customer_delete": "Client supprimé",
+    "customer_backfillNumbers": "Numéros de client complétés",
     "inventory_create": "Pièce d'inventaire créée",
     "inventory_create": "Pièce d'inventaire créée",
     "inventory_update": "Pièce d'inventaire mise à jour",
     "inventory_update": "Pièce d'inventaire mise à jour",
     "inventory_delete": "Pièce d'inventaire supprimée",
     "inventory_delete": "Pièce d'inventaire supprimée",

+ 12 - 2
messages/fr/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
       "total": "Total"
     },
     },
     "emailSubject": "Mise à jour du Statut du Véhicule: {status}",
     "emailSubject": "Mise à jour du Statut du Véhicule: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Ouvrir l'ordre de travail",
+      "openVehicle": "Ouvrir le véhicule",
+      "openCustomer": "Ouvrir le client",
+      "hint": "Astuce : clic droit sur une ligne pour les actions"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Sélectionner un Véhicule pour l'Ordre de Travail",
     "title": "Sélectionner un Véhicule pour l'Ordre de Travail",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Le nom du client est requis",
     "customerNameRequired": "Le nom du client est requis",
     "failedCreateCustomer": "Impossible de créer le client",
     "failedCreateCustomer": "Impossible de créer le client",
     "failedCreateVehicle": "Impossible de créer le véhicule",
     "failedCreateVehicle": "Impossible de créer le véhicule",
-    "somethingWentWrong": "Une erreur s'est produite"
+    "somethingWentWrong": "Une erreur s'est produite",
+    "partsSale": "Vente de pièces seule",
+    "partsSaleDescription": "Créez une facture sans véhicule, pour la vente de pièces au comptoir.",
+    "createSale": "Créer la vente",
+    "none": "Aucun"
   },
   },
   "notify": {
   "notify": {
     "title": "Notifier {name}",
     "title": "Notifier {name}",

+ 1 - 0
messages/it/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Cliente creato",
     "customer_create": "Cliente creato",
     "customer_update": "Cliente aggiornato",
     "customer_update": "Cliente aggiornato",
     "customer_delete": "Cliente eliminato",
     "customer_delete": "Cliente eliminato",
+    "customer_backfillNumbers": "Numeri cliente completati",
     "inventory_create": "Pezzo di inventario creato",
     "inventory_create": "Pezzo di inventario creato",
     "inventory_update": "Pezzo di inventario aggiornato",
     "inventory_update": "Pezzo di inventario aggiornato",
     "inventory_delete": "Pezzo di inventario eliminato",
     "inventory_delete": "Pezzo di inventario eliminato",

+ 12 - 2
messages/it/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Totale"
       "total": "Totale"
     },
     },
     "emailSubject": "Aggiornamento Stato Veicolo: {status}",
     "emailSubject": "Aggiornamento Stato Veicolo: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Apri ordine di lavoro",
+      "openVehicle": "Apri veicolo",
+      "openCustomer": "Apri cliente",
+      "hint": "Suggerimento: clic destro su una riga per le azioni"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Seleziona Veicolo per Ordine di Lavoro",
     "title": "Seleziona Veicolo per Ordine di Lavoro",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Il nome del cliente è obbligatorio",
     "customerNameRequired": "Il nome del cliente è obbligatorio",
     "failedCreateCustomer": "Impossibile creare il cliente",
     "failedCreateCustomer": "Impossibile creare il cliente",
     "failedCreateVehicle": "Impossibile creare il veicolo",
     "failedCreateVehicle": "Impossibile creare il veicolo",
-    "somethingWentWrong": "Qualcosa è andato storto"
+    "somethingWentWrong": "Qualcosa è andato storto",
+    "partsSale": "Vendita solo ricambi",
+    "partsSaleDescription": "Crea una fattura senza veicolo, per la vendita di ricambi al banco.",
+    "createSale": "Crea vendita",
+    "none": "Nessuno"
   },
   },
   "notify": {
   "notify": {
     "title": "Notifica {name}",
     "title": "Notifica {name}",

+ 1 - 0
messages/lt/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Sukurtas klientas",
     "customer_create": "Sukurtas klientas",
     "customer_update": "Atnaujintas klientas",
     "customer_update": "Atnaujintas klientas",
     "customer_delete": "Ištrintas klientas",
     "customer_delete": "Ištrintas klientas",
+    "customer_backfillNumbers": "Užpildyti klientų numeriai",
     "inventory_create": "Sukurta inventoriaus dalis",
     "inventory_create": "Sukurta inventoriaus dalis",
     "inventory_update": "Atnaujinta inventoriaus dalis",
     "inventory_update": "Atnaujinta inventoriaus dalis",
     "inventory_delete": "Ištrinta inventoriaus dalis",
     "inventory_delete": "Ištrinta inventoriaus dalis",

+ 12 - 2
messages/lt/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Suma"
       "total": "Suma"
     },
     },
     "emailSubject": "Transporto priemonės būsenos atnaujinimas: {status}",
     "emailSubject": "Transporto priemonės būsenos atnaujinimas: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Atidaryti darbo užsakymą",
+      "openVehicle": "Atidaryti transporto priemonę",
+      "openCustomer": "Atidaryti klientą",
+      "hint": "Patarimas: spustelėkite eilutę dešiniuoju mygtuku veiksmams"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Pasirinkite transporto priemonę darbo užsakymui",
     "title": "Pasirinkite transporto priemonę darbo užsakymui",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Kliento vardas yra privalomas",
     "customerNameRequired": "Kliento vardas yra privalomas",
     "failedCreateCustomer": "Nepavyko sukurti kliento",
     "failedCreateCustomer": "Nepavyko sukurti kliento",
     "failedCreateVehicle": "Nepavyko sukurti transporto priemonės",
     "failedCreateVehicle": "Nepavyko sukurti transporto priemonės",
-    "somethingWentWrong": "Kažkas nutiko"
+    "somethingWentWrong": "Kažkas nutiko",
+    "partsSale": "Tik dalių pardavimas",
+    "partsSaleDescription": "Sukurkite sąskaitą be transporto priemonės, dalių pardavimui prie prekystalio.",
+    "createSale": "Sukurti pardavimą",
+    "none": "Nėra"
   },
   },
   "notify": {
   "notify": {
     "title": "Pranešti {name}",
     "title": "Pranešti {name}",

+ 1 - 0
messages/nb/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Opprettet kunde",
     "customer_create": "Opprettet kunde",
     "customer_update": "Oppdaterte kunde",
     "customer_update": "Oppdaterte kunde",
     "customer_delete": "Slettet kunde",
     "customer_delete": "Slettet kunde",
+    "customer_backfillNumbers": "Etterfylte kundenumre",
     "inventory_create": "Opprettet lagerdel",
     "inventory_create": "Opprettet lagerdel",
     "inventory_update": "Oppdaterte lagerdel",
     "inventory_update": "Oppdaterte lagerdel",
     "inventory_delete": "Slettet lagerdel",
     "inventory_delete": "Slettet lagerdel",

+ 12 - 2
messages/nb/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
       "total": "Total"
     },
     },
     "emailSubject": "Kjøretøystatus oppdatert: {status}",
     "emailSubject": "Kjøretøystatus oppdatert: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Åpne arbeidsordre",
+      "openVehicle": "Åpne kjøretøy",
+      "openCustomer": "Åpne kunde",
+      "hint": "Tips: høyreklikk på en rad for handlinger"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Velg kjøretøy for arbeidsordre",
     "title": "Velg kjøretøy for arbeidsordre",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Kundenavn er påkrevd",
     "customerNameRequired": "Kundenavn er påkrevd",
     "failedCreateCustomer": "Kunne ikke opprette kunde",
     "failedCreateCustomer": "Kunne ikke opprette kunde",
     "failedCreateVehicle": "Kunne ikke opprette kjøretøy",
     "failedCreateVehicle": "Kunne ikke opprette kjøretøy",
-    "somethingWentWrong": "Noe gikk galt"
+    "somethingWentWrong": "Noe gikk galt",
+    "partsSale": "Kun delesalg",
+    "partsSaleDescription": "Opprett en faktura uten kjøretøy, for salg av deler over disk.",
+    "createSale": "Opprett salg",
+    "none": "Ingen"
   },
   },
   "notify": {
   "notify": {
     "title": "Varsle {name}",
     "title": "Varsle {name}",

+ 1 - 0
messages/nl/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Klant aangemaakt",
     "customer_create": "Klant aangemaakt",
     "customer_update": "Klant bijgewerkt",
     "customer_update": "Klant bijgewerkt",
     "customer_delete": "Klant verwijderd",
     "customer_delete": "Klant verwijderd",
+    "customer_backfillNumbers": "Klantnummers aangevuld",
     "inventory_create": "Inventarisonderdeel aangemaakt",
     "inventory_create": "Inventarisonderdeel aangemaakt",
     "inventory_update": "Inventarisonderdeel bijgewerkt",
     "inventory_update": "Inventarisonderdeel bijgewerkt",
     "inventory_delete": "Inventarisonderdeel verwijderd",
     "inventory_delete": "Inventarisonderdeel verwijderd",

+ 12 - 2
messages/nl/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Totaal"
       "total": "Totaal"
     },
     },
     "emailSubject": "Voertuigstatus bijgewerkt: {status}",
     "emailSubject": "Voertuigstatus bijgewerkt: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Werkorder openen",
+      "openVehicle": "Voertuig openen",
+      "openCustomer": "Klant openen",
+      "hint": "Tip: rechtsklik op een rij voor acties"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Voertuig selecteren voor werkorder",
     "title": "Voertuig selecteren voor werkorder",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Klantnaam is verplicht",
     "customerNameRequired": "Klantnaam is verplicht",
     "failedCreateCustomer": "Klant maken mislukt",
     "failedCreateCustomer": "Klant maken mislukt",
     "failedCreateVehicle": "Voertuig maken mislukt",
     "failedCreateVehicle": "Voertuig maken mislukt",
-    "somethingWentWrong": "Er is iets mis gegaan"
+    "somethingWentWrong": "Er is iets mis gegaan",
+    "partsSale": "Alleen onderdelenverkoop",
+    "partsSaleDescription": "Maak een factuur zonder voertuig, voor verkoop van onderdelen aan de balie.",
+    "createSale": "Verkoop aanmaken",
+    "none": "Geen"
   },
   },
   "notify": {
   "notify": {
     "title": "Stuur bericht naar {name}",
     "title": "Stuur bericht naar {name}",

+ 1 - 0
messages/pl/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Utworzono klienta",
     "customer_create": "Utworzono klienta",
     "customer_update": "Zaktualizowano klienta",
     "customer_update": "Zaktualizowano klienta",
     "customer_delete": "Usunięto klienta",
     "customer_delete": "Usunięto klienta",
+    "customer_backfillNumbers": "Uzupełniono numery klientów",
     "inventory_create": "Utworzono część magazynową",
     "inventory_create": "Utworzono część magazynową",
     "inventory_update": "Zaktualizowano część magazynową",
     "inventory_update": "Zaktualizowano część magazynową",
     "inventory_delete": "Usunięto część magazynową",
     "inventory_delete": "Usunięto część magazynową",

+ 12 - 2
messages/pl/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Razem"
       "total": "Razem"
     },
     },
     "emailSubject": "Aktualizacja Statusu Pojazdu: {status}",
     "emailSubject": "Aktualizacja Statusu Pojazdu: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Otwórz zlecenie",
+      "openVehicle": "Otwórz pojazd",
+      "openCustomer": "Otwórz klienta",
+      "hint": "Wskazówka: kliknij wiersz prawym przyciskiem, aby zobaczyć akcje"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Wybierz Pojazd do Zlecenia Pracy",
     "title": "Wybierz Pojazd do Zlecenia Pracy",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Nazwa klienta jest wymagana",
     "customerNameRequired": "Nazwa klienta jest wymagana",
     "failedCreateCustomer": "Nie udało się utworzyć klienta",
     "failedCreateCustomer": "Nie udało się utworzyć klienta",
     "failedCreateVehicle": "Nie udało się utworzyć pojazdu",
     "failedCreateVehicle": "Nie udało się utworzyć pojazdu",
-    "somethingWentWrong": "Coś poszło nie tak"
+    "somethingWentWrong": "Coś poszło nie tak",
+    "partsSale": "Sprzedaż samych części",
+    "partsSaleDescription": "Utwórz fakturę bez pojazdu, do sprzedaży części przy ladzie.",
+    "createSale": "Utwórz sprzedaż",
+    "none": "Brak"
   },
   },
   "notify": {
   "notify": {
     "title": "Powiadom {name}",
     "title": "Powiadom {name}",

+ 1 - 0
messages/pt-BR/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Cliente criado",
     "customer_create": "Cliente criado",
     "customer_update": "Cliente atualizado",
     "customer_update": "Cliente atualizado",
     "customer_delete": "Cliente excluído",
     "customer_delete": "Cliente excluído",
+    "customer_backfillNumbers": "Números de cliente preenchidos",
     "inventory_create": "Peça de inventário criada",
     "inventory_create": "Peça de inventário criada",
     "inventory_update": "Peça de inventário atualizada",
     "inventory_update": "Peça de inventário atualizada",
     "inventory_delete": "Peça de inventário excluída",
     "inventory_delete": "Peça de inventário excluída",

+ 12 - 2
messages/pt-BR/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
       "total": "Total"
     },
     },
     "emailSubject": "Atualização de Status do Veículo: {status}",
     "emailSubject": "Atualização de Status do Veículo: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Abrir ordem de serviço",
+      "openVehicle": "Abrir veículo",
+      "openCustomer": "Abrir cliente",
+      "hint": "Dica: clique com o botão direito em uma linha para ver ações"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Selecionar Veículo para Ordem de Serviço",
     "title": "Selecionar Veículo para Ordem de Serviço",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Nome do cliente é obrigatório",
     "customerNameRequired": "Nome do cliente é obrigatório",
     "failedCreateCustomer": "Falha ao criar cliente",
     "failedCreateCustomer": "Falha ao criar cliente",
     "failedCreateVehicle": "Falha ao criar veículo",
     "failedCreateVehicle": "Falha ao criar veículo",
-    "somethingWentWrong": "Algo deu errado"
+    "somethingWentWrong": "Algo deu errado",
+    "partsSale": "Venda apenas de peças",
+    "partsSaleDescription": "Crie uma fatura sem veículo, para vendas de peças no balcão.",
+    "createSale": "Criar venda",
+    "none": "Nenhum"
   },
   },
   "notify": {
   "notify": {
     "title": "Notificar {name}",
     "title": "Notificar {name}",

+ 1 - 0
messages/ru/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Создан клиент",
     "customer_create": "Создан клиент",
     "customer_update": "Обновлён клиент",
     "customer_update": "Обновлён клиент",
     "customer_delete": "Удалён клиент",
     "customer_delete": "Удалён клиент",
+    "customer_backfillNumbers": "Заполнены номера клиентов",
     "inventory_create": "Создана складская запчасть",
     "inventory_create": "Создана складская запчасть",
     "inventory_update": "Обновлена складская запчасть",
     "inventory_update": "Обновлена складская запчасть",
     "inventory_delete": "Удалена складская запчасть",
     "inventory_delete": "Удалена складская запчасть",

+ 12 - 2
messages/ru/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Итого"
       "total": "Итого"
     },
     },
     "emailSubject": "Обновление статуса транспортного средства: {status}",
     "emailSubject": "Обновление статуса транспортного средства: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "Открыть заказ-наряд",
+      "openVehicle": "Открыть автомобиль",
+      "openCustomer": "Открыть клиента",
+      "hint": "Подсказка: правый клик по строке — действия"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "Выберите транспортное средство для заказ-наряда",
     "title": "Выберите транспортное средство для заказ-наряда",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Имя клиента обязательно",
     "customerNameRequired": "Имя клиента обязательно",
     "failedCreateCustomer": "Не удалось создать клиента",
     "failedCreateCustomer": "Не удалось создать клиента",
     "failedCreateVehicle": "Не удалось создать транспортное средство",
     "failedCreateVehicle": "Не удалось создать транспортное средство",
-    "somethingWentWrong": "Что-то пошло не так"
+    "somethingWentWrong": "Что-то пошло не так",
+    "partsSale": "Продажа только запчастей",
+    "partsSaleDescription": "Создайте счёт без автомобиля для продажи запчастей у прилавка.",
+    "createSale": "Создать продажу",
+    "none": "Нет"
   },
   },
   "notify": {
   "notify": {
     "title": "Уведомить {name}",
     "title": "Уведомить {name}",

+ 1 - 0
messages/tr/audit.json

@@ -38,6 +38,7 @@
     "customer_create": "Müşteri oluşturuldu",
     "customer_create": "Müşteri oluşturuldu",
     "customer_update": "Müşteri güncellendi",
     "customer_update": "Müşteri güncellendi",
     "customer_delete": "Müşteri silindi",
     "customer_delete": "Müşteri silindi",
+    "customer_backfillNumbers": "Müşteri numaraları dolduruldu",
     "inventory_create": "Envanter parçası oluşturuldu",
     "inventory_create": "Envanter parçası oluşturuldu",
     "inventory_update": "Envanter parçası güncellendi",
     "inventory_update": "Envanter parçası güncellendi",
     "inventory_delete": "Envanter parçası silindi",
     "inventory_delete": "Envanter parçası silindi",

+ 12 - 2
messages/tr/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Toplam"
       "total": "Toplam"
     },
     },
     "emailSubject": "Araç Durum Güncellemesi: {status}",
     "emailSubject": "Araç Durum Güncellemesi: {status}",
-    "changeStatus": "Change status"
+    "changeStatus": "Change status",
+    "contextMenu": {
+      "open": "İş emrini aç",
+      "openVehicle": "Aracı aç",
+      "openCustomer": "Müşteriyi aç",
+      "hint": "İpucu: işlemler için bir satıra sağ tıklayın"
+    }
   },
   },
   "vehiclePicker": {
   "vehiclePicker": {
     "title": "İş Emri için Araç Seç",
     "title": "İş Emri için Araç Seç",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Müşteri adı gereklidir",
     "customerNameRequired": "Müşteri adı gereklidir",
     "failedCreateCustomer": "Müşteri oluşturulamadı",
     "failedCreateCustomer": "Müşteri oluşturulamadı",
     "failedCreateVehicle": "Araç oluşturulamadı",
     "failedCreateVehicle": "Araç oluşturulamadı",
-    "somethingWentWrong": "Bir sorun oluştu"
+    "somethingWentWrong": "Bir sorun oluştu",
+    "partsSale": "Yalnızca parça satışı",
+    "partsSaleDescription": "Tezgahtan parça satışları için araçsız bir fatura oluşturun.",
+    "createSale": "Satış oluştur",
+    "none": "Yok"
   },
   },
   "notify": {
   "notify": {
     "title": "{name} Bilgilendir",
     "title": "{name} Bilgilendir",

+ 28 - 0
prisma/migrations/20260811200000_service_record_org_scope/migration.sql

@@ -0,0 +1,28 @@
+-- ServiceRecord gains its own organization scope and an optional direct
+-- customer; vehicle becomes optional (parts-only / counter sales).
+-- Idempotent and backfilling: safe on databases of any size and safe to
+-- re-run. Existing records keep working unchanged.
+
+ALTER TABLE "service_records" ADD COLUMN IF NOT EXISTS "organizationId" TEXT;
+ALTER TABLE "service_records" ADD COLUMN IF NOT EXISTS "customerId" TEXT;
+
+-- Backfill organization from the owning vehicle (every existing record has one)
+UPDATE "service_records" sr
+SET "organizationId" = v."organizationId"
+FROM "vehicles" v
+WHERE sr."vehicleId" = v."id" AND sr."organizationId" IS NULL;
+
+ALTER TABLE "service_records" ALTER COLUMN "vehicleId" DROP NOT NULL;
+
+DO $$ BEGIN
+  ALTER TABLE "service_records" ADD CONSTRAINT "service_records_organizationId_fkey"
+    FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "service_records" ADD CONSTRAINT "service_records_customerId_fkey"
+    FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+CREATE INDEX IF NOT EXISTS "service_records_organizationId_idx" ON "service_records"("organizationId");
+CREATE INDEX IF NOT EXISTS "service_records_customerId_idx" ON "service_records"("customerId");

+ 41 - 23
prisma/schema.prisma

@@ -228,8 +228,21 @@ model ServiceRecord {
   createdAt    DateTime  @default(now())
   createdAt    DateTime  @default(now())
   updatedAt    DateTime  @updatedAt
   updatedAt    DateTime  @updatedAt
 
 
-  vehicleId String
-  vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
+  /// Null for parts-only / counter sales; such records use customerId
+  /// (or neither, for anonymous walk-in sales)
+  vehicleId String?
+  vehicle   Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
+
+  /// Direct customer for records without a vehicle. Vehicle-linked records
+  /// leave this null and resolve the customer through the vehicle, so
+  /// reassigning a vehicle keeps invoices following the vehicle's customer.
+  customerId String?
+  customer   Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
+
+  /// Denormalized from the vehicle historically; now the canonical scope so
+  /// records exist without a vehicle. Nullable to mirror Vehicle.organizationId.
+  organizationId String?
+  organization   Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
 
 
   technicianId String?
   technicianId String?
   technician   Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
   technician   Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
@@ -247,6 +260,8 @@ model ServiceRecord {
 
 
   @@index([vehicleId])
   @@index([vehicleId])
   @@index([technicianId])
   @@index([technicianId])
+  @@index([organizationId])
+  @@index([customerId])
   @@map("service_records")
   @@map("service_records")
 }
 }
 
 
@@ -357,8 +372,10 @@ model Customer {
   customerSessions CustomerSession[]
   customerSessions CustomerSession[]
   serviceRequests  ServiceRequest[]
   serviceRequests  ServiceRequest[]
 
 
-  @@index([organizationId])
+  serviceRecords ServiceRecord[]
+
   @@unique([organizationId, customerNumber])
   @@unique([organizationId, customerNumber])
+  @@index([organizationId])
   @@map("customers")
   @@map("customers")
 }
 }
 
 
@@ -479,23 +496,23 @@ model LaborPresetPart {
 }
 }
 
 
 model InventoryPart {
 model InventoryPart {
-  id            String   @id @default(cuid())
+  id            String  @id @default(cuid())
   partNumber    String?
   partNumber    String?
   barcode       String?
   barcode       String?
   name          String
   name          String
   description   String?
   description   String?
   category      String?
   category      String?
-  quantity      Int      @default(0)
-  minQuantity   Int      @default(0)
-  unitCost      Float    @default(0)
-  sellPrice     Float    @default(0)
+  quantity      Int     @default(0)
+  minQuantity   Int     @default(0)
+  unitCost      Float   @default(0)
+  sellPrice     Float   @default(0)
   supplier      String?
   supplier      String?
   supplierPhone String?
   supplierPhone String?
   supplierEmail String?
   supplierEmail String?
   supplierUrl   String?
   supplierUrl   String?
   imageUrl      String?
   imageUrl      String?
   location      String?
   location      String?
-  isArchived    Boolean  @default(false)
+  isArchived    Boolean @default(false)
 
 
   /// Set when a low-stock alert has been sent for the current dip below
   /// Set when a low-stock alert has been sent for the current dip below
   /// `minQuantity`, and cleared once stock climbs back above it. This
   /// `minQuantity`, and cleared once stock climbs back above it. This
@@ -503,8 +520,8 @@ model InventoryPart {
   /// every check — it alerts once per descent, not once per run.
   /// every check — it alerts once per descent, not once per run.
   lowStockAlertedAt DateTime?
   lowStockAlertedAt DateTime?
 
 
-  createdAt     DateTime @default(now())
-  updatedAt     DateTime @updatedAt
+  createdAt DateTime @default(now())
+  updatedAt DateTime @updatedAt
 
 
   userId String
   userId String
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -530,12 +547,12 @@ model StockMovement {
   id String @id @default(cuid())
   id String @id @default(cuid())
 
 
   /// Signed change in whole units. Negative = consumed, positive = restocked.
   /// Signed change in whole units. Negative = consumed, positive = restocked.
-  delta Int
+  delta         Int
   /// Part balance immediately after this movement was applied.
   /// Part balance immediately after this movement was applied.
   quantityAfter Int
   quantityAfter Int
   /// Why the stock moved — see StockMovementReason in src/features/inventory/Lib/stockMovement.ts
   /// Why the stock moved — see StockMovementReason in src/features/inventory/Lib/stockMovement.ts
-  reason String
-  note   String?
+  reason        String
+  note          String?
 
 
   inventoryPartId String
   inventoryPartId String
   inventoryPart   InventoryPart @relation(fields: [inventoryPartId], references: [id], onDelete: Cascade)
   inventoryPart   InventoryPart @relation(fields: [inventoryPartId], references: [id], onDelete: Cascade)
@@ -645,17 +662,17 @@ model QuoteAttachment {
 }
 }
 
 
 model QuotePart {
 model QuotePart {
-  id         String  @id @default(cuid())
-  partNumber String?
-  name       String
-  quantity   Float   @default(1)
+  id            String  @id @default(cuid())
+  partNumber    String?
+  name          String
+  quantity      Float   @default(1)
   /// Internal purchase cost — never shown to customers (quote PDF/share
   /// Internal purchase cost — never shown to customers (quote PDF/share
   /// render unitPrice only)
   /// render unitPrice only)
-  unitCost      Float @default(0)
-  markupPercent Float @default(0)
-  unitPrice  Float   @default(0)
-  total      Float   @default(0)
-  excluded   Boolean @default(false)
+  unitCost      Float   @default(0)
+  markupPercent Float   @default(0)
+  unitPrice     Float   @default(0)
+  total         Float   @default(0)
+  excluded      Boolean @default(false)
 
 
   /// Optional link to the stocked part this line was picked from. Carried into
   /// Optional link to the stocked part this line was picked from. Carried into
   /// ServicePart on conversion so the job deducts the right inventory item.
   /// ServicePart on conversion so the job deducts the right inventory item.
@@ -751,6 +768,7 @@ model Organization {
   members                OrganizationMember[]
   members                OrganizationMember[]
   roles                  Role[]
   roles                  Role[]
   vehicles               Vehicle[]
   vehicles               Vehicle[]
+  serviceRecords         ServiceRecord[]
   customers              Customer[]
   customers              Customer[]
   quotes                 Quote[]
   quotes                 Quote[]
   inventoryParts         InventoryPart[]
   inventoryParts         InventoryPart[]

+ 257 - 0
src/__tests__/features/counter-sales.test.ts

@@ -0,0 +1,257 @@
+/**
+ * Counter sales (service records without a vehicle)
+ *
+ * Verifies that vehicle-less records require a direct customer, that the
+ * customer must belong to the caller's org, that created records carry the
+ * org id themselves, and that attaching a vehicle later clears the direct
+ * customer link.
+ */
+import { describe, it, expect, vi, beforeEach } from "vitest";
+
+vi.mock("@/lib/cached-session", () => ({
+  getCachedSession: vi.fn(),
+  getCachedMembership: vi.fn(),
+}));
+vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
+vi.mock("@/lib/resolve-upload-path", () => ({
+  resolveUploadPath: vi.fn((url: string) => `/uploads/${url}`),
+}));
+vi.mock("@/lib/notification-bus", () => ({
+  notificationBus: { emit: vi.fn() },
+}));
+vi.mock("@/features/inventory/Lib/onInventoryChanged", () => ({
+  onInventoryChanged: vi.fn(),
+}));
+vi.mock("@/lib/db", () => ({
+  db: {
+    user: { findUnique: vi.fn() },
+    vehicle: { findFirst: vi.fn(), update: vi.fn() },
+    customer: { findFirst: vi.fn() },
+    organization: { findUnique: vi.fn() },
+    appSetting: { findMany: vi.fn(), updateMany: vi.fn() },
+    technician: { findFirst: vi.fn() },
+    serviceRecord: { findFirst: vi.fn(), findMany: vi.fn(), create: vi.fn(), update: vi.fn() },
+    $transaction: vi.fn(),
+  },
+}));
+
+import { getCachedSession, getCachedMembership } from "@/lib/cached-session";
+import { db } from "@/lib/db";
+import { createServiceRecord, updateServiceRecord } from "@/features/vehicles/Actions/serviceActions";
+import { createDraftCounterSale } from "@/features/vehicles/Actions/createDraftServiceRecord";
+
+const mockSession = vi.mocked(getCachedSession);
+const mockMembership = vi.mocked(getCachedMembership);
+const ORG_A = "org-a";
+
+function setupOrgAOwner() {
+  mockSession.mockResolvedValue({ user: { id: "user-a", email: "a@example.com" } } as any);
+  mockMembership.mockResolvedValue({
+    organizationId: ORG_A, role: "owner", roleId: null, customRole: null,
+  } as any);
+  vi.mocked(db.user.findUnique).mockResolvedValue({ isSuperAdmin: false } as any);
+}
+
+function setupCreateEnvironment() {
+  vi.mocked(db.appSetting.findMany).mockResolvedValue([] as any);
+  vi.mocked(db.appSetting.updateMany).mockResolvedValue({ count: 0 } as any);
+  vi.mocked(db.organization.findUnique).mockResolvedValue({ name: "Workshop A" } as any);
+  vi.mocked(db.technician.findFirst).mockResolvedValue(null);
+  vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
+}
+
+beforeEach(() => { vi.resetAllMocks(); });
+
+describe("createServiceRecord — counter sale (no vehicle)", () => {
+  it("rejects a vehicle-less record without a customer", async () => {
+    setupOrgAOwner();
+    setupCreateEnvironment();
+
+    const result = await createServiceRecord({ vehicleId: null, title: "Counter sale" });
+
+    expect(result.success).toBe(false);
+    expect(result.error).toMatch(/customer is required/i);
+  });
+
+  it("rejects when the customer belongs to another org", async () => {
+    setupOrgAOwner();
+    setupCreateEnvironment();
+    vi.mocked(db.customer.findFirst).mockResolvedValue(null);
+
+    const result = await createServiceRecord({
+      vehicleId: null,
+      customerId: "cust-other-org",
+      title: "Counter sale",
+    });
+
+    expect(result.success).toBe(false);
+    expect(result.error).toBe("Customer not found");
+    expect(vi.mocked(db.customer.findFirst)).toHaveBeenCalledWith(
+      expect.objectContaining({
+        where: expect.objectContaining({ id: "cust-other-org", organizationId: ORG_A }),
+      })
+    );
+  });
+
+  it("creates the record with organizationId and the direct customer link", async () => {
+    setupOrgAOwner();
+    setupCreateEnvironment();
+    vi.mocked(db.customer.findFirst).mockResolvedValue({ id: "cust-a", taxExempt: false } as any);
+
+    const txCreate = vi.fn().mockResolvedValue({ id: "sr-new", invoiceNumber: "2026-1001", vehicleId: null });
+    vi.mocked(db.$transaction).mockImplementation(async (fn: any) =>
+      fn({ serviceRecord: { create: txCreate } })
+    );
+
+    const result = await createServiceRecord({
+      vehicleId: null,
+      customerId: "cust-a",
+      title: "Counter sale",
+    });
+
+    expect(result.success).toBe(true);
+    expect(txCreate).toHaveBeenCalledWith(
+      expect.objectContaining({
+        data: expect.objectContaining({
+          organizationId: ORG_A,
+          vehicleId: null,
+          customerId: "cust-a",
+        }),
+      })
+    );
+    // No vehicle to update mileage on
+    expect(vi.mocked(db.vehicle.update)).not.toHaveBeenCalled();
+  });
+
+  it("keeps customerId null for vehicle-linked records", async () => {
+    setupOrgAOwner();
+    setupCreateEnvironment();
+    vi.mocked(db.vehicle.findFirst).mockResolvedValue({
+      id: "veh-a", mileage: 10000, customer: { taxExempt: false },
+    } as any);
+    vi.mocked(db.vehicle.update).mockResolvedValue({} as any);
+
+    const txCreate = vi.fn().mockResolvedValue({ id: "sr-new", invoiceNumber: "2026-1001", vehicleId: "veh-a" });
+    vi.mocked(db.$transaction).mockImplementation(async (fn: any) =>
+      fn({ serviceRecord: { create: txCreate } })
+    );
+
+    const result = await createServiceRecord({
+      vehicleId: "veh-a",
+      // A stray customerId on a vehicle-linked record must be ignored: the
+      // invoice follows the vehicle's current customer.
+      customerId: "cust-a",
+      title: "Oil change",
+    });
+
+    expect(result.success).toBe(true);
+    expect(txCreate).toHaveBeenCalledWith(
+      expect.objectContaining({
+        data: expect.objectContaining({
+          organizationId: ORG_A,
+          vehicleId: "veh-a",
+          customerId: null,
+        }),
+      })
+    );
+  });
+});
+
+describe("updateServiceRecord — counter sale semantics", () => {
+  it("treats a null vehicleId as no-change (does not detach the vehicle)", async () => {
+    setupOrgAOwner();
+    vi.mocked(db.serviceRecord.findFirst).mockResolvedValue({
+      id: "sr-a", vehicleId: "veh-a", customerId: null, status: "pending",
+      attachments: [], serviceDate: new Date(),
+      vehicle: { id: "veh-a", mileage: 10000, make: "Toyota", model: "Camry", year: 2020, licensePlate: null },
+    } as any);
+    const txUpdate = vi.fn().mockResolvedValue({ id: "sr-a", status: "pending", title: "x" });
+    vi.mocked(db.$transaction).mockImplementation(async (fn: any) =>
+      fn({ serviceRecord: { update: txUpdate } })
+    );
+
+    const result = await updateServiceRecord({ id: "sr-a", vehicleId: null, title: "x" });
+
+    expect(result.success).toBe(true);
+    expect(txUpdate).toHaveBeenCalledWith(
+      expect.objectContaining({
+        data: expect.not.objectContaining({ vehicleId: expect.anything() }),
+      })
+    );
+  });
+
+  it("verifies org ownership of the target vehicle when attaching one", async () => {
+    setupOrgAOwner();
+    vi.mocked(db.serviceRecord.findFirst).mockResolvedValue({
+      id: "sr-a", vehicleId: null, customerId: "cust-a", status: "pending",
+      attachments: [], serviceDate: new Date(), vehicle: null,
+    } as any);
+    vi.mocked(db.vehicle.findFirst).mockResolvedValue(null);
+
+    const result = await updateServiceRecord({ id: "sr-a", vehicleId: "veh-other-org" });
+
+    expect(result.success).toBe(false);
+    expect(result.error).toBe("Vehicle not found");
+    expect(vi.mocked(db.vehicle.findFirst)).toHaveBeenCalledWith(
+      expect.objectContaining({
+        where: expect.objectContaining({ id: "veh-other-org", organizationId: ORG_A }),
+      })
+    );
+  });
+
+  it("clears the direct customer link when a vehicle is attached", async () => {
+    setupOrgAOwner();
+    vi.mocked(db.serviceRecord.findFirst).mockResolvedValue({
+      id: "sr-a", vehicleId: null, customerId: "cust-a", status: "pending",
+      attachments: [], serviceDate: new Date(), vehicle: null,
+    } as any);
+    vi.mocked(db.vehicle.findFirst).mockResolvedValue({ id: "veh-a" } as any);
+    const txUpdate = vi.fn().mockResolvedValue({ id: "sr-a", status: "pending", title: "x" });
+    vi.mocked(db.$transaction).mockImplementation(async (fn: any) =>
+      fn({ serviceRecord: { update: txUpdate } })
+    );
+
+    const result = await updateServiceRecord({ id: "sr-a", vehicleId: "veh-a" });
+
+    expect(result.success).toBe(true);
+    expect(txUpdate).toHaveBeenCalledWith(
+      expect.objectContaining({
+        data: expect.objectContaining({ vehicleId: "veh-a", customerId: null }),
+      })
+    );
+  });
+});
+
+describe("createDraftCounterSale — cross-org isolation", () => {
+  it("rejects a customer from another org", async () => {
+    setupOrgAOwner();
+    vi.mocked(db.customer.findFirst).mockResolvedValue(null);
+
+    const result = await createDraftCounterSale("cust-other-org");
+
+    expect(result.success).toBe(false);
+    expect(result.error).toBe("Customer not found");
+  });
+
+  it("creates a draft scoped to the caller's org with no vehicle", async () => {
+    setupOrgAOwner();
+    setupCreateEnvironment();
+    vi.mocked(db.customer.findFirst).mockResolvedValue({ id: "cust-a", taxExempt: false } as any);
+    vi.mocked(db.serviceRecord.create).mockResolvedValue({
+      id: "sr-draft", invoiceNumber: "2026-1001", customerId: "cust-a",
+    } as any);
+
+    const result = await createDraftCounterSale("cust-a");
+
+    expect(result.success).toBe(true);
+    expect(vi.mocked(db.serviceRecord.create)).toHaveBeenCalledWith(
+      expect.objectContaining({
+        data: expect.objectContaining({
+          organizationId: ORG_A,
+          vehicleId: null,
+          customerId: "cust-a",
+        }),
+      })
+    );
+  });
+});

+ 6 - 6
src/__tests__/multitenancy/service-record-isolation.test.ts

@@ -61,10 +61,10 @@ const ORG_A_RECORD = {
   vehicle: { id: "veh-a", mileage: 50000, make: "Toyota", model: "Camry", year: 2020, licensePlate: "ABC123" },
   vehicle: { id: "veh-a", mileage: 50000, make: "Toyota", model: "Camry", year: 2020, licensePlate: "ABC123" },
 };
 };
 
 
+// Service records carry their own organizationId (counter sales have no
+// vehicle), so every ownership check scopes on the record directly.
 const orgWhereClause = expect.objectContaining({
 const orgWhereClause = expect.objectContaining({
-  where: expect.objectContaining({
-    vehicle: expect.objectContaining({ organizationId: ORG_A }),
-  }),
+  where: expect.objectContaining({ organizationId: ORG_A }),
 });
 });
 
 
 beforeEach(() => { vi.resetAllMocks(); });
 beforeEach(() => { vi.resetAllMocks(); });
@@ -78,7 +78,7 @@ describe("getServiceRecord — cross-org isolation", () => {
     expect(result.data).toBeNull();
     expect(result.data).toBeNull();
   });
   });
 
 
-  it("scopes the query to the caller's organizationId via vehicle relation", async () => {
+  it("scopes the query to the caller's organizationId", async () => {
     setupOrgAOwner();
     setupOrgAOwner();
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     await getServiceRecord("sr-a");
     await getServiceRecord("sr-a");
@@ -103,7 +103,7 @@ describe("updateServiceRecord — cross-org isolation", () => {
     expect(result.error).toBe("Service record not found");
     expect(result.error).toBe("Service record not found");
   });
   });
 
 
-  it("ownership check always includes organizationId via vehicle relation", async () => {
+  it("ownership check always includes organizationId", async () => {
     setupOrgAOwner();
     setupOrgAOwner();
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     await updateServiceRecord({ id: "sr-x" });
     await updateServiceRecord({ id: "sr-x" });
@@ -132,7 +132,7 @@ describe("deleteServiceRecord — cross-org isolation", () => {
     expect(result.error).toBe("Record not found");
     expect(result.error).toBe("Record not found");
   });
   });
 
 
-  it("ownership check always includes organizationId via vehicle relation", async () => {
+  it("ownership check always includes organizationId", async () => {
     setupOrgAOwner();
     setupOrgAOwner();
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     await deleteServiceRecord("sr-x");
     await deleteServiceRecord("sr-x");

+ 6 - 8
src/__tests__/multitenancy/vehicle-service-isolation.test.ts

@@ -259,17 +259,17 @@ describe("getServiceRecord — cross-org isolation", () => {
     expect(result.data).toBeNull();
     expect(result.data).toBeNull();
   });
   });
 
 
-  it("db query scopes service records to the caller's org via vehicle relation", async () => {
+  it("db query scopes service records to the caller's org", async () => {
     setupOrgAOwner();
     setupOrgAOwner();
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
 
 
     await getServiceRecord("sr-a");
     await getServiceRecord("sr-a");
 
 
+    // Records carry their own organizationId now (counter sales have no
+    // vehicle), so scoping is on the record itself.
     expect(vi.mocked(db.serviceRecord.findFirst)).toHaveBeenCalledWith(
     expect(vi.mocked(db.serviceRecord.findFirst)).toHaveBeenCalledWith(
       expect.objectContaining({
       expect.objectContaining({
-        where: expect.objectContaining({
-          vehicle: expect.objectContaining({ organizationId: ORG_A }),
-        }),
+        where: expect.objectContaining({ organizationId: ORG_A }),
       })
       })
     );
     );
   });
   });
@@ -323,7 +323,7 @@ describe("deleteServiceAttachment — cross-org isolation", () => {
     expect(result.error).toBe("Attachment not found");
     expect(result.error).toBe("Attachment not found");
   });
   });
 
 
-  it("attachment lookup always scopes through vehicle's organizationId", async () => {
+  it("attachment lookup always scopes through the record's organizationId", async () => {
     setupOrgAOwner();
     setupOrgAOwner();
     vi.mocked(db.serviceAttachment.findFirst).mockResolvedValue(null);
     vi.mocked(db.serviceAttachment.findFirst).mockResolvedValue(null);
 
 
@@ -332,9 +332,7 @@ describe("deleteServiceAttachment — cross-org isolation", () => {
     expect(vi.mocked(db.serviceAttachment.findFirst)).toHaveBeenCalledWith(
     expect(vi.mocked(db.serviceAttachment.findFirst)).toHaveBeenCalledWith(
       expect.objectContaining({
       expect.objectContaining({
         where: expect.objectContaining({
         where: expect.objectContaining({
-          serviceRecord: expect.objectContaining({
-            vehicle: expect.objectContaining({ organizationId: ORG_A }),
-          }),
+          serviceRecord: expect.objectContaining({ organizationId: ORG_A }),
         }),
         }),
       })
       })
     );
     );

+ 12 - 9
src/app/(authenticated)/billing/billing-client.tsx

@@ -48,11 +48,11 @@ interface BillingRecord {
     model: string;
     model: string;
     year: number;
     year: number;
     licensePlate: string | null;
     licensePlate: string | null;
-    customer: {
-      id: string;
-      name: string;
-    } | null;
-  };
+  } | null;
+  customer: {
+    id: string;
+    name: string;
+  } | null;
 }
 }
 
 
 interface PaginatedBillingData {
 interface PaginatedBillingData {
@@ -179,7 +179,9 @@ export default function BillingClient({
 
 
   const handleRowClick = (record: BillingRecord) => {
   const handleRowClick = (record: BillingRecord) => {
     router.push(
     router.push(
-      `/vehicles/${record.vehicle.id}/service/${record.id}`
+      record.vehicle
+        ? `/vehicles/${record.vehicle.id}/service/${record.id}`
+        : `/sales/${record.id}`
     );
     );
   };
   };
 
 
@@ -376,11 +378,12 @@ export default function BillingClient({
                     </TableCell>
                     </TableCell>
                     <TableCell className="truncate">{record.title}</TableCell>
                     <TableCell className="truncate">{record.title}</TableCell>
                     <TableCell className="truncate">
                     <TableCell className="truncate">
-                      {record.vehicle.year} {record.vehicle.make}{" "}
-                      {record.vehicle.model}
+                      {record.vehicle
+                        ? `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`
+                        : "\u2014"}
                     </TableCell>
                     </TableCell>
                     <TableCell className="truncate">
                     <TableCell className="truncate">
-                      {record.vehicle.customer?.name || "\u2014"}
+                      {record.customer?.name || "\u2014"}
                     </TableCell>
                     </TableCell>
                     <TableCell>
                     <TableCell>
                       {formatDate(new Date(record.serviceDate))}
                       {formatDate(new Date(record.serviceDate))}

+ 10 - 9
src/app/(authenticated)/dashboard-client.tsx

@@ -78,6 +78,7 @@ interface ServiceItem {
   cost: number;
   cost: number;
   serviceDate: Date;
   serviceDate: Date;
   startDateTime: Date | null;
   startDateTime: Date | null;
+  customer: { id: string; name: string } | null;
   vehicle: {
   vehicle: {
     id: string;
     id: string;
     make: string;
     make: string;
@@ -85,7 +86,7 @@ interface ServiceItem {
     year: number;
     year: number;
     licensePlate: string | null;
     licensePlate: string | null;
     customer: { id: string; name: string } | null;
     customer: { id: string; name: string } | null;
-  };
+  } | null;
 }
 }
 
 
 interface DashboardStats {
 interface DashboardStats {
@@ -1269,7 +1270,7 @@ export function DashboardClient({
                           className={`cursor-pointer transition-opacity ${navigatingId === s.id ? "opacity-50" : ""}`}
                           className={`cursor-pointer transition-opacity ${navigatingId === s.id ? "opacity-50" : ""}`}
                           onClick={() => {
                           onClick={() => {
                             setNavigatingId(s.id);
                             setNavigatingId(s.id);
-                            router.push(`/vehicles/${s.vehicle.id}/service/${s.id}`);
+                            router.push(s.vehicle ? `/vehicles/${s.vehicle.id}/service/${s.id}` : `/sales/${s.id}`);
                           }}
                           }}
                         >
                         >
                           <TableCell className="font-mono text-xs">
                           <TableCell className="font-mono text-xs">
@@ -1277,16 +1278,16 @@ export function DashboardClient({
                           </TableCell>
                           </TableCell>
                           <TableCell>
                           <TableCell>
                             <div>
                             <div>
-                              {s.vehicle.licensePlate && (
+                              {s.vehicle?.licensePlate && (
                                 <span className="font-mono text-sm">{s.vehicle.licensePlate}</span>
                                 <span className="font-mono text-sm">{s.vehicle.licensePlate}</span>
                               )}
                               )}
                               <p className="text-xs text-muted-foreground">
                               <p className="text-xs text-muted-foreground">
-                                {s.vehicle.year} {s.vehicle.make} {s.vehicle.model}
+                                {s.vehicle ? `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}` : "-"}
                               </p>
                               </p>
                             </div>
                             </div>
                           </TableCell>
                           </TableCell>
                           <TableCell className="hidden sm:table-cell text-muted-foreground">
                           <TableCell className="hidden sm:table-cell text-muted-foreground">
-                            {s.vehicle.customer?.name || "-"}
+                            {(s.customer ?? s.vehicle?.customer)?.name || "-"}
                           </TableCell>
                           </TableCell>
                           <TableCell className="font-medium">{s.title}</TableCell>
                           <TableCell className="font-medium">{s.title}</TableCell>
                           {stats.isAdmin && (
                           {stats.isAdmin && (
@@ -1340,21 +1341,21 @@ export function DashboardClient({
                         className={`cursor-pointer transition-opacity ${navigatingId === s.id ? "opacity-50" : ""}`}
                         className={`cursor-pointer transition-opacity ${navigatingId === s.id ? "opacity-50" : ""}`}
                         onClick={() => {
                         onClick={() => {
                           setNavigatingId(s.id);
                           setNavigatingId(s.id);
-                          router.push(`/vehicles/${s.vehicle.id}/service/${s.id}`);
+                          router.push(s.vehicle ? `/vehicles/${s.vehicle.id}/service/${s.id}` : `/sales/${s.id}`);
                         }}
                         }}
                       >
                       >
                         <TableCell>
                         <TableCell>
                           <div>
                           <div>
-                            {s.vehicle.licensePlate && (
+                            {s.vehicle?.licensePlate && (
                               <span className="font-mono text-sm font-medium">{s.vehicle.licensePlate}</span>
                               <span className="font-mono text-sm font-medium">{s.vehicle.licensePlate}</span>
                             )}
                             )}
                             <p className="text-xs text-muted-foreground">
                             <p className="text-xs text-muted-foreground">
-                              {s.vehicle.year} {s.vehicle.make} {s.vehicle.model}
+                              {s.vehicle ? `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}` : "-"}
                             </p>
                             </p>
                           </div>
                           </div>
                         </TableCell>
                         </TableCell>
                         <TableCell className="hidden sm:table-cell text-muted-foreground">
                         <TableCell className="hidden sm:table-cell text-muted-foreground">
-                          {s.vehicle.customer?.name || "-"}
+                          {(s.customer ?? s.vehicle?.customer)?.name || "-"}
                         </TableCell>
                         </TableCell>
                         <TableCell className="font-medium">{s.title}</TableCell>
                         <TableCell className="font-medium">{s.title}</TableCell>
                         <TableCell>
                         <TableCell>

+ 2 - 2
src/app/(authenticated)/inventory/[id]/part-detail-client.tsx

@@ -231,9 +231,9 @@ export function PartDetailClient({
                     </TableCell>
                     </TableCell>
                     <TableCell className="font-medium">{m.quantityAfter}</TableCell>
                     <TableCell className="font-medium">{m.quantityAfter}</TableCell>
                     <TableCell>
                     <TableCell>
-                      {m.serviceRecordId && m.vehicleId ? (
+                      {m.serviceRecordId ? (
                         <Link
                         <Link
-                          href={`/vehicles/${m.vehicleId}/service/${m.serviceRecordId}`}
+                          href={m.vehicleId ? `/vehicles/${m.vehicleId}/service/${m.serviceRecordId}` : `/sales/${m.serviceRecordId}`}
                           className="inline-flex items-center gap-1 text-primary hover:underline"
                           className="inline-flex items-center gap-1 text-primary hover:underline"
                         >
                         >
                           {m.label}
                           {m.label}

+ 18 - 0
src/app/(authenticated)/sales/[serviceId]/page.tsx

@@ -0,0 +1,18 @@
+import { ServiceRecordPage } from '@/features/vehicles/Components/service-page/ServiceRecordPage'
+
+// Counter sales (service records without a vehicle) live here; the shared
+// ServiceRecordPage renders vehicle-linked records identically under
+// /vehicles/[id]/service/[serviceId].
+export default async function SalesDetailPage({
+  params,
+  searchParams,
+}: {
+  params: Promise<{ serviceId: string }>
+  searchParams: Promise<Record<string, string | string[] | undefined>>
+}) {
+  const { serviceId } = await params
+  const sp = await searchParams
+  const initialTab = typeof sp.tab === 'string' ? sp.tab : undefined
+
+  return <ServiceRecordPage serviceId={serviceId} initialTab={initialTab} />
+}

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

@@ -1,20 +1,4 @@
-import { getServiceRecord } from '@/features/vehicles/Actions/serviceActions'
-import { getSettings } from '@/features/settings/Actions/settingsActions'
-import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
-import { readPartsPricingSettings } from '@/features/inventory/Lib/partPricing'
-import { getInventoryPartsList } from '@/features/inventory/Actions/inventoryActions'
-import { getLaborPresetsList } from '@/features/labor-presets/Actions/laborPresetActions'
-
-import { getTechnicians, getOrgMembers } from '@/features/workboard/Actions/technicianActions'
-import { getAuthContext } from '@/lib/get-auth-context'
-import { getFeatures } from '@/lib/features'
-import { getStatusReportsForService } from '@/features/status-reports/Actions/getStatusReportsForService'
-import { getServiceFindings } from '@/features/vehicles/Actions/findingActions'
-import { db } from '@/lib/db'
-import { getCachedSession, getCachedMembership } from '@/lib/cached-session'
-import { ServicePageClient } from '@/features/vehicles/Components/service-page/ServicePageClient'
-import { PageHeader } from '@/components/page-header'
-import { getTranslations } from 'next-intl/server'
+import { ServiceRecordPage } from '@/features/vehicles/Components/service-page/ServiceRecordPage'
 
 
 export default async function ServiceDetailPage({
 export default async function ServiceDetailPage({
   params,
   params,
@@ -23,262 +7,9 @@ export default async function ServiceDetailPage({
   params: Promise<{ id: string; serviceId: string }>
   params: Promise<{ id: string; serviceId: string }>
   searchParams: Promise<Record<string, string | string[] | undefined>>
   searchParams: Promise<Record<string, string | string[] | undefined>>
 }) {
 }) {
-  const { id, serviceId } = await params
+  const { serviceId } = await params
   const sp = await searchParams
   const sp = await searchParams
   const initialTab = typeof sp.tab === 'string' ? sp.tab : undefined
   const initialTab = typeof sp.tab === 'string' ? sp.tab : undefined
 
 
-  const [
-    result,
-    settingsResult,
-    inventoryResult,
-    techniciansResult,
-    presetsResult,
-    authContext,
-    session,
-    orgMembersResult,
-    statusReportsResult,
-    findingsResult,
-  ] = await Promise.all([
-    getServiceRecord(serviceId),
-    getSettings([
-      SETTING_KEYS.CURRENCY_CODE,
-      SETTING_KEYS.UNIT_SYSTEM,
-      SETTING_KEYS.DEFAULT_TAX_RATE,
-      SETTING_KEYS.TAX_ENABLED,
-      SETTING_KEYS.DEFAULT_LABOR_RATE,
-      SETTING_KEYS.INVOICE_DUE_DAYS,
-      SETTING_KEYS.PARTS_DEFAULT_MARKUP_PERCENT,
-      SETTING_KEYS.PARTS_MARKUP_APPLIES_TO_INVENTORY,
-    ]),
-    getInventoryPartsList(),
-    getTechnicians(),
-    getLaborPresetsList(),
-    getAuthContext(),
-    getCachedSession(),
-    getOrgMembers(),
-    getStatusReportsForService(serviceId),
-    getServiceFindings(serviceId),
-  ])
-
-  if (!result.success || !result.data) {
-    return (
-      <>
-        <PageHeader />
-        <div className="flex h-[50vh] items-center justify-center">
-          <p className="text-muted-foreground">
-            {result.error || (await getTranslations('service.page'))('notFound')}
-          </p>
-        </div>
-      </>
-    )
-  }
-
-  const record = result.data
-  const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {}
-  const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || 'USD'
-  const unitSystem = (settings[SETTING_KEYS.UNIT_SYSTEM] || 'imperial') as 'metric' | 'imperial'
-  const taxEnabled = settings[SETTING_KEYS.TAX_ENABLED] !== 'false'
-  const defaultTaxRate = taxEnabled ? Number(settings[SETTING_KEYS.DEFAULT_TAX_RATE]) || 0 : 0
-  const defaultLaborRate = Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0
-  const defaultDueDays = Number(settings[SETTING_KEYS.INVOICE_DUE_DAYS]) || 0
-  const { defaultMarkupPercent, markupAppliesToInventory } = readPartsPricingSettings(settings, {
-    defaultMarkupPercent: SETTING_KEYS.PARTS_DEFAULT_MARKUP_PERCENT,
-    markupAppliesToInventory: SETTING_KEYS.PARTS_MARKUP_APPLIES_TO_INVENTORY,
-  })
-  const inventoryParts = inventoryResult.success && inventoryResult.data ? inventoryResult.data : []
-  const laborPresets = presetsResult.success && presetsResult.data ? presetsResult.data : []
-  const initialVehicle = {
-    id: record.vehicle.id,
-    make: record.vehicle.make,
-    model: record.vehicle.model,
-    year: record.vehicle.year,
-    licensePlate: record.vehicle.licensePlate,
-  }
-  const boardTechnicians = (
-    techniciansResult.success && techniciansResult.data ? techniciansResult.data : []
-  ).map((t) => ({ id: t.id, name: t.name, userId: t.userId }))
-  const organizationId = authContext?.organizationId || ''
-
-  // Fetch team members and features
-  const membership = session?.user?.id ? await getCachedMembership(session.user.id) : null
-  const orgId = membership?.organizationId
-
-  const [currentUser, features, aiSettings] = await Promise.all([
-    session?.user?.id
-      ? db.user.findUnique({
-          where: { id: session.user.id },
-          select: { name: true },
-        })
-      : Promise.resolve(null),
-    orgId ? getFeatures(orgId) : Promise.resolve(null),
-    orgId
-      ? db.appSetting.findMany({
-          where: {
-            organizationId: orgId,
-            key: { in: [SETTING_KEYS.AI_ENABLED, SETTING_KEYS.AI_API_KEY] },
-          },
-          select: { key: true, value: true },
-        })
-      : Promise.resolve([]),
-  ])
-
-  const currentUserName = currentUser?.name || ''
-  const aiSettingsMap = Object.fromEntries(aiSettings.map((s) => [s.key, s.value]))
-  const aiEnabled =
-    features?.ai === true &&
-    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,
-    description: record.description || '',
-    type: record.type,
-    status: record.status,
-    mileage: record.mileage,
-    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: effectiveInvoiceDate.toISOString().split('T')[0],
-    invoiceDueDate: isRenderable(record.invoiceDueDate)
-      ? record.invoiceDueDate.toISOString().split('T')[0]
-      : defaultDueDays > 0
-        ? new Date(effectiveInvoiceDate.getTime() + defaultDueDays * 86400000)
-            .toISOString()
-            .split('T')[0]
-        : '',
-    partItems: record.partItems.map((p) => ({
-      partNumber: p.partNumber || '',
-      name: p.name,
-      quantity: p.quantity,
-      unitPrice: p.unitPrice,
-      total: p.total,
-      unitCost: p.unitCost ?? 0,
-      markupPercent: p.markupPercent ?? 0,
-      // Preserve the inventory link so editing/saving a work order keeps
-      // deducting the right stock. Without this, a saved edit sends unlinked
-      // parts and inventory reconciliation restocks the "removed" part.
-      inventoryPartId: p.inventoryPartId ?? undefined,
-    })),
-    laborItems: record.laborItems.map((l) => ({
-      description: l.description,
-      hours: l.hours,
-      rate: l.rate,
-      total: l.total,
-      pricingType: (l.pricingType as 'hourly' | 'service') || 'hourly',
-    })),
-    attachments: [],
-    subtotal: record.subtotal,
-    taxRate: record.taxRate,
-    taxAmount: record.taxAmount,
-    taxInclusive: record.taxInclusive,
-    totalAmount: record.totalAmount,
-    discountType: record.discountType || undefined,
-    discountValue: record.discountValue,
-    discountAmount: record.discountAmount,
-    warrantyMonths: record.warrantyMonths ?? null,
-    warrantyMileage: record.warrantyMileage ?? null,
-    warrantyNotes: record.warrantyNotes ?? null,
-  }
-
-  // Prepare media attachments for managers
-  // The Prisma query returns includeInInvoice on each attachment; cast to include it
-  const allAttachments =
-    (record.attachments as ((typeof record.attachments)[number] & {
-      includeInInvoice: boolean
-    })[]) || []
-  const imageAttachmentsForManager = allAttachments
-    .filter((a) => a.category === 'image')
-    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
-  const videoAttachments = allAttachments
-    .filter((a) => a.category === 'video')
-    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
-  const documentAttachments = allAttachments
-    .filter((a) => a.category === 'document' || a.category === 'diagnostic')
-    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
-
-  // Fetch notification history for this service record
-  const notificationHistory = await db.smsMessage.findMany({
-    where: {
-      relatedEntityId: serviceId,
-      relatedEntityType: 'service-record',
-      direction: 'outbound',
-    },
-    select: { id: true, body: true, status: true, createdAt: true, toNumber: true },
-    orderBy: { createdAt: 'desc' },
-    take: 10,
-  })
-
-  // Fetch open observations for this vehicle (not just this service)
-  const openObservations = await db.vehicleFinding.findMany({
-    where: { vehicleId: id, status: { not: 'resolved' } },
-    select: { id: true, description: true, severity: true, notes: true, serviceRecordId: true },
-    orderBy: { createdAt: 'desc' },
-  })
-
-  return (
-    <div className="flex h-svh flex-col overflow-hidden">
-      <PageHeader />
-      <ServicePageClient
-        record={result.data}
-        vehicleId={id}
-        organizationId={organizationId}
-        initialTab={initialTab}
-        currencyCode={currencyCode}
-        unitSystem={unitSystem}
-        defaultTaxRate={defaultTaxRate}
-        taxEnabled={taxEnabled}
-        defaultLaborRate={defaultLaborRate}
-        initialData={initialData}
-        inventoryParts={inventoryParts}
-        laborPresets={laborPresets}
-        initialVehicle={initialVehicle}
-        boardTechnicians={boardTechnicians}
-        orgMembers={orgMembersResult.success && orgMembersResult.data ? orgMembersResult.data : []}
-        currentUserName={currentUserName}
-        imageAttachmentsForManager={imageAttachmentsForManager}
-        videoAttachments={videoAttachments}
-        documentAttachments={documentAttachments}
-        maxImagesPerService={features?.maxImagesPerService ?? 999999}
-        maxDiagnosticsPerService={features?.maxDiagnosticsPerService ?? 999999}
-        maxDocumentsPerService={features?.maxDocumentsPerService ?? 999999}
-        smsEnabled={features?.sms ?? false}
-        emailEnabled={features?.smtp ?? false}
-        telegramEnabled={features?.telegram ?? false}
-        aiEnabled={aiEnabled}
-        defaultDueDays={defaultDueDays}
-        defaultMarkupPercent={defaultMarkupPercent}
-        markupAppliesToInventory={markupAppliesToInventory}
-        statusReports={(statusReportsResult.success && statusReportsResult.data
-          ? statusReportsResult.data
-          : []
-        ).map((r) => ({
-          ...r,
-          createdAt: r.createdAt.toISOString(),
-          expiresAt: r.expiresAt?.toISOString() || null,
-          feedbackAt: r.feedbackAt?.toISOString() || null,
-          sentAt: r.sentAt?.toISOString() || null,
-        }))}
-        findings={findingsResult.success && findingsResult.data ? findingsResult.data : []}
-        openObservations={openObservations}
-        notificationHistory={notificationHistory.map((n) => ({
-          ...n,
-          createdAt: n.createdAt.toISOString(),
-        }))}
-      />
-    </div>
-  )
+  return <ServiceRecordPage serviceId={serviceId} initialTab={initialTab} />
 }
 }

+ 74 - 45
src/app/(authenticated)/work-orders/work-orders-client.tsx

@@ -18,11 +18,12 @@ import {
   TableRow,
   TableRow,
 } from "@/components/ui/table";
 } from "@/components/ui/table";
 import {
 import {
-  DropdownMenu,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+  ContextMenu,
+  ContextMenuContent,
+  ContextMenuItem,
+  ContextMenuSeparator,
+  ContextMenuTrigger,
+} from "@/components/ui/context-menu";
 import { DataTablePagination } from "@/components/data-table-pagination";
 import { DataTablePagination } from "@/components/data-table-pagination";
 import { statusColors } from "@/lib/table-utils";
 import { statusColors } from "@/lib/table-utils";
 import { updateServiceStatus } from "@/features/vehicles/Actions/serviceActions";
 import { updateServiceStatus } from "@/features/vehicles/Actions/serviceActions";
@@ -30,10 +31,13 @@ import {
   ArrowDown,
   ArrowDown,
   ArrowUp,
   ArrowUp,
   ArrowUpDown,
   ArrowUpDown,
-  ChevronDown,
+  Car,
+  ExternalLink,
   Loader2,
   Loader2,
+  MousePointerClick,
   Plus,
   Plus,
   Search,
   Search,
+  User,
 } from "lucide-react";
 } from "lucide-react";
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import { VehiclePickerDialog } from "@/components/vehicle-picker-dialog";
 import { VehiclePickerDialog } from "@/components/vehicle-picker-dialog";
@@ -54,6 +58,7 @@ interface WorkOrder {
   startDateTime: Date | null;
   startDateTime: Date | null;
   techName: string | null;
   techName: string | null;
   invoiceNumber: string | null;
   invoiceNumber: string | null;
+  customer: { id: string; name: string; email: string | null; phone: string | null } | null;
   vehicle: {
   vehicle: {
     id: string;
     id: string;
     make: string;
     make: string;
@@ -61,7 +66,7 @@ interface WorkOrder {
     year: number;
     year: number;
     licensePlate: string | null;
     licensePlate: string | null;
     customer: { id: string; name: string; email: string | null; phone: string | null } | null;
     customer: { id: string; name: string; email: string | null; phone: string | null } | null;
-  };
+  } | null;
 }
 }
 
 
 interface VehicleOption {
 interface VehicleOption {
@@ -208,18 +213,19 @@ export function WorkOrdersClient({
     router.refresh();
     router.refresh();
 
 
     const templateKey = statusTemplateKeys[newStatus];
     const templateKey = statusTemplateKeys[newStatus];
-    if (workOrder.vehicle.customer && templateKey) {
+    const notifyTarget = workOrder.customer ?? workOrder.vehicle?.customer;
+    if (notifyTarget && templateKey) {
       const tplResult = await getSmsTemplates();
       const tplResult = await getSmsTemplates();
       const tplData = tplResult.success && tplResult.data ? tplResult.data : null;
       const tplData = tplResult.success && tplResult.data ? tplResult.data : null;
       const tpl = tplData?.templates[templateKey] || SMS_TEMPLATE_DEFAULTS[templateKey] || "";
       const tpl = tplData?.templates[templateKey] || SMS_TEMPLATE_DEFAULTS[templateKey] || "";
-      const vehicle = `${workOrder.vehicle.year} ${workOrder.vehicle.make} ${workOrder.vehicle.model}`;
+      const vehicle = workOrder.vehicle ? `${workOrder.vehicle.year} ${workOrder.vehicle.make} ${workOrder.vehicle.model}` : workOrder.title;
       const message = interpolateSmsTemplate(tpl, {
       const message = interpolateSmsTemplate(tpl, {
-        customer_name: workOrder.vehicle.customer.name,
+        customer_name: notifyTarget.name,
         vehicle,
         vehicle,
         company_name: tplData?.companyName || "",
         company_name: tplData?.companyName || "",
         current_user: tplData?.currentUser || "",
         current_user: tplData?.currentUser || "",
       });
       });
-      setNotifyCustomer(workOrder.vehicle.customer);
+      setNotifyCustomer(notifyTarget);
       setNotifyMessage(message);
       setNotifyMessage(message);
       setNotifyStatus(newStatus);
       setNotifyStatus(newStatus);
       setShowNotifyDialog(true);
       setShowNotifyDialog(true);
@@ -275,6 +281,10 @@ export function WorkOrdersClient({
 
 
       {/* Table */}
       {/* Table */}
       <div className="rounded-lg border">
       <div className="rounded-lg border">
+        <div className="hidden items-center gap-1.5 border-b bg-muted/30 px-3 py-1.5 text-xs text-muted-foreground md:flex">
+          <MousePointerClick className="h-3.5 w-3.5" />
+          {t("contextMenu.hint")}
+        </div>
         <Table className="table-fixed">
         <Table className="table-fixed">
           <TableHeader>
           <TableHeader>
             <TableRow>
             <TableRow>
@@ -318,13 +328,12 @@ export function WorkOrdersClient({
                   {t("table.total")}<SortIcon column="totalAmount" />
                   {t("table.total")}<SortIcon column="totalAmount" />
                 </button>
                 </button>
               </TableHead>
               </TableHead>
-              <TableHead className="w-[50px]" />
             </TableRow>
             </TableRow>
           </TableHeader>
           </TableHeader>
           <TableBody>
           <TableBody>
             {data.records.length === 0 ? (
             {data.records.length === 0 ? (
               <TableRow>
               <TableRow>
-                <TableCell colSpan={9} className="h-32 text-center text-muted-foreground">
+                <TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
                   {t("empty")}
                   {t("empty")}
                 </TableCell>
                 </TableCell>
               </TableRow>
               </TableRow>
@@ -332,13 +341,16 @@ export function WorkOrdersClient({
               data.records.map((r) => {
               data.records.map((r) => {
                 const displayTotal = r.totalAmount > 0 ? r.totalAmount : r.cost;
                 const displayTotal = r.totalAmount > 0 ? r.totalAmount : r.cost;
                 const transitions = statusTransitions[r.status] || [];
                 const transitions = statusTransitions[r.status] || [];
+                const recordHref = r.vehicle ? `/vehicles/${r.vehicle.id}/service/${r.id}` : `/sales/${r.id}`;
+                const rowCustomer = r.customer ?? r.vehicle?.customer;
                 return (
                 return (
+                  <ContextMenu key={r.id} modal={false}>
+                  <ContextMenuTrigger asChild>
                   <TableRow
                   <TableRow
-                    key={r.id}
                     className={`cursor-pointer transition-opacity ${navigatingId === r.id ? "opacity-50" : ""}`}
                     className={`cursor-pointer transition-opacity ${navigatingId === r.id ? "opacity-50" : ""}`}
                     onClick={() => {
                     onClick={() => {
                       setNavigatingId(r.id);
                       setNavigatingId(r.id);
-                      router.push(`/vehicles/${r.vehicle.id}/service/${r.id}`);
+                      router.push(recordHref);
                     }}
                     }}
                   >
                   >
                     <TableCell className="hidden sm:table-cell font-mono text-xs text-muted-foreground">
                     <TableCell className="hidden sm:table-cell font-mono text-xs text-muted-foreground">
@@ -346,16 +358,16 @@ export function WorkOrdersClient({
                     </TableCell>
                     </TableCell>
                     <TableCell>
                     <TableCell>
                       <div className="min-w-0">
                       <div className="min-w-0">
-                        {r.vehicle.licensePlate && (
+                        {r.vehicle?.licensePlate && (
                           <span className="font-mono text-sm font-medium">{r.vehicle.licensePlate}</span>
                           <span className="font-mono text-sm font-medium">{r.vehicle.licensePlate}</span>
                         )}
                         )}
                         <p className="truncate text-xs text-muted-foreground">
                         <p className="truncate text-xs text-muted-foreground">
-                          {r.vehicle.year} {r.vehicle.make} {r.vehicle.model}
+                          {r.vehicle ? `${r.vehicle.year} ${r.vehicle.make} ${r.vehicle.model}` : "-"}
                         </p>
                         </p>
                       </div>
                       </div>
                     </TableCell>
                     </TableCell>
                     <TableCell className="hidden truncate md:table-cell text-muted-foreground">
                     <TableCell className="hidden truncate md:table-cell text-muted-foreground">
-                      {r.vehicle.customer?.name || "-"}
+                      {(r.customer ?? r.vehicle?.customer)?.name || "-"}
                     </TableCell>
                     </TableCell>
                     <TableCell className="truncate">
                     <TableCell className="truncate">
                       <span className="font-medium">{r.title}</span>
                       <span className="font-medium">{r.title}</span>
@@ -372,35 +384,52 @@ export function WorkOrdersClient({
                       {formatDate(new Date(r.startDateTime ?? r.serviceDate))}
                       {formatDate(new Date(r.startDateTime ?? r.serviceDate))}
                     </TableCell>
                     </TableCell>
                     <TableCell className="text-right font-semibold">
                     <TableCell className="text-right font-semibold">
-                      {formatCurrency(displayTotal, currencyCode)}
-                    </TableCell>
-                    <TableCell>
-                      {navigatingId === r.id ? (
-                        <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
-                      ) : transitions.length > 0 ? (
-                        <DropdownMenu>
-                          <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
-                            <Button variant="ghost" size="icon" className="h-8 w-8" aria-label={t("changeStatus")}>
-                              <ChevronDown className="h-4 w-4" />
-                            </Button>
-                          </DropdownMenuTrigger>
-                          <DropdownMenuContent align="end">
-                            {transitions.map((tr) => (
-                              <DropdownMenuItem
-                                key={tr.target}
-                                onClick={(e) => {
-                                  e.stopPropagation();
-                                  handleStatusChange(r, tr.target);
-                                }}
-                              >
-                                {t(`statusActions.${tr.actionKey}`)}
-                              </DropdownMenuItem>
-                            ))}
-                          </DropdownMenuContent>
-                        </DropdownMenu>
-                      ) : null}
+                      <span className="inline-flex items-center gap-2">
+                        {navigatingId === r.id && (
+                          <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
+                        )}
+                        {formatCurrency(displayTotal, currencyCode)}
+                      </span>
                     </TableCell>
                     </TableCell>
                   </TableRow>
                   </TableRow>
+                  </ContextMenuTrigger>
+                  <ContextMenuContent className="w-52">
+                    <ContextMenuItem
+                      onClick={() => {
+                        setNavigatingId(r.id);
+                        router.push(recordHref);
+                      }}
+                    >
+                      <ExternalLink className="mr-2 h-4 w-4" />
+                      {t("contextMenu.open")}
+                    </ContextMenuItem>
+                    {r.vehicle && (
+                      <ContextMenuItem onClick={() => router.push(`/vehicles/${r.vehicle?.id}`)}>
+                        <Car className="mr-2 h-4 w-4" />
+                        {t("contextMenu.openVehicle")}
+                      </ContextMenuItem>
+                    )}
+                    {rowCustomer && (
+                      <ContextMenuItem onClick={() => router.push(`/customers/${rowCustomer.id}`)}>
+                        <User className="mr-2 h-4 w-4" />
+                        {t("contextMenu.openCustomer")}
+                      </ContextMenuItem>
+                    )}
+                    {transitions.length > 0 && (
+                      <>
+                        <ContextMenuSeparator />
+                        {transitions.map((tr) => (
+                          <ContextMenuItem
+                            key={tr.target}
+                            onClick={() => handleStatusChange(r, tr.target)}
+                          >
+                            {t(`statusActions.${tr.actionKey}`)}
+                          </ContextMenuItem>
+                        ))}
+                      </>
+                    )}
+                  </ContextMenuContent>
+                  </ContextMenu>
                 );
                 );
               })
               })
             )}
             )}

+ 6 - 3
src/app/(public)/portal/[orgId]/invoices/[invoiceId]/pdf/route.ts

@@ -41,7 +41,9 @@ export async function GET(
       select: {
       select: {
         id: true,
         id: true,
         status: true,
         status: true,
-        vehicle: { select: { customerId: true, organizationId: true } },
+        customerId: true,
+        organizationId: true,
+        vehicle: { select: { customerId: true } },
       },
       },
     })
     })
 
 
@@ -49,9 +51,10 @@ export async function GET(
       return NextResponse.json({ error: 'Not found' }, { status: 404 })
       return NextResponse.json({ error: 'Not found' }, { status: 404 })
     }
     }
 
 
+    const recordCustomerId = record.customerId ?? record.vehicle?.customerId ?? null
     if (
     if (
-      record.vehicle.customerId !== session.customerId ||
-      record.vehicle.organizationId !== session.organizationId
+      recordCustomerId !== session.customerId ||
+      record.organizationId !== session.organizationId
     ) {
     ) {
       return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
       return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
     }
     }

+ 17 - 7
src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx

@@ -78,6 +78,14 @@ interface InvoiceRecord {
     category: string
     category: string
     description: string | null
     description: string | null
   }[]
   }[]
+  customer?: {
+    name: string
+    email: string | null
+    phone: string | null
+    address: string | null
+    company: string | null
+    taxId?: string | null
+  } | null
   vehicle: {
   vehicle: {
     make: string
     make: string
     model: string
     model: string
@@ -93,7 +101,7 @@ interface InvoiceRecord {
       company: string | null
       company: string | null
       taxId?: string | null
       taxId?: string | null
     } | null
     } | null
-  }
+  } | null
 }
 }
 
 
 interface InvoiceSettings {
 interface InvoiceSettings {
@@ -255,7 +263,7 @@ export function InvoiceView({
     }).catch(() => { /* fire-and-forget */ })
     }).catch(() => { /* fire-and-forget */ })
   }, [token])
   }, [token])
 
 
-  const vehicleName = `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`
+  const vehicleName = record.vehicle ? `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}` : ''
   const partsSubtotalStored = record.partItems.reduce((sum, p) => sum + p.total, 0)
   const partsSubtotalStored = record.partItems.reduce((sum, p) => sum + p.total, 0)
   const laborSubtotalStored = record.laborItems.reduce((sum, l) => sum + l.total, 0)
   const laborSubtotalStored = record.laborItems.reduce((sum, l) => sum + l.total, 0)
   const computedSubtotalStored = partsSubtotalStored + laborSubtotalStored
   const computedSubtotalStored = partsSubtotalStored + laborSubtotalStored
@@ -730,7 +738,7 @@ export function InvoiceView({
             case 'service': {
             case 'service': {
               const renderInfoCard = (sid: string) => {
               const renderInfoCard = (sid: string) => {
                 if (sid === 'customer') {
                 if (sid === 'customer') {
-                  const c = record.vehicle.customer
+                  const c = record.customer ?? record.vehicle?.customer
                   if (!c) return null
                   if (!c) return null
                   const vf = getVisibleFieldsForSection(layoutConfig, 'customer')
                   const vf = getVisibleFieldsForSection(layoutConfig, 'customer')
                   const show = (fid: string) => !vf || vf.has(fid)
                   const show = (fid: string) => !vf || vf.has(fid)
@@ -759,6 +767,8 @@ export function InvoiceView({
                   );
                   );
                 }
                 }
                 if (sid === 'vehicle') {
                 if (sid === 'vehicle') {
+                  const vehicle = record.vehicle
+                  if (!vehicle) return null
                   const vf = getVisibleFieldsForSection(layoutConfig, 'vehicle')
                   const vf = getVisibleFieldsForSection(layoutConfig, 'vehicle')
                   const show = (fid: string) => !vf || vf.has(fid)
                   const show = (fid: string) => !vf || vf.has(fid)
                   if (!(show('vehicle_name') || show('vin') || show('license_plate') || show('mileage'))) return null
                   if (!(show('vehicle_name') || show('vin') || show('license_plate') || show('mileage'))) return null
@@ -767,9 +777,9 @@ export function InvoiceView({
                     if (!show(fid)) return null
                     if (!show(fid)) return null
                     switch (fid) {
                     switch (fid) {
                       case 'vehicle_name': return <p key={fid} className="font-semibold">{vehicleName}</p>
                       case 'vehicle_name': return <p key={fid} className="font-semibold">{vehicleName}</p>
-                      case 'vin': return record.vehicle.vin ? <p key={fid} className="text-sm text-gray-500">{serviceType === 'marine' ? `HIN: ${record.vehicle.vin}` : t('vin', { vin: record.vehicle.vin })}</p> : null
-                      case 'license_plate': return record.vehicle.licensePlate ? <p key={fid} className="text-sm text-gray-500">{serviceType === 'marine' ? `Reg: ${record.vehicle.licensePlate}` : t('plate', { plate: record.vehicle.licensePlate })}</p> : null
-                      case 'mileage': return record.vehicle.mileage > 0 ? <p key={fid} className="text-sm text-gray-500">{serviceType === 'marine' ? `Engine Hours: ${record.vehicle.mileage.toLocaleString(locale)}` : record.vehicle.mileage.toLocaleString(locale)}</p> : null
+                      case 'vin': return vehicle.vin ? <p key={fid} className="text-sm text-gray-500">{serviceType === 'marine' ? `HIN: ${vehicle.vin}` : t('vin', { vin: vehicle.vin })}</p> : null
+                      case 'license_plate': return vehicle.licensePlate ? <p key={fid} className="text-sm text-gray-500">{serviceType === 'marine' ? `Reg: ${vehicle.licensePlate}` : t('plate', { plate: vehicle.licensePlate })}</p> : null
+                      case 'mileage': return vehicle.mileage > 0 ? <p key={fid} className="text-sm text-gray-500">{serviceType === 'marine' ? `Engine Hours: ${vehicle.mileage.toLocaleString(locale)}` : vehicle.mileage.toLocaleString(locale)}</p> : null
                       default: return null
                       default: return null
                     }
                     }
                   }
                   }
@@ -792,7 +802,7 @@ export function InvoiceView({
                     if (!show(fid)) return null
                     if (!show(fid)) return null
                     switch (fid) {
                     switch (fid) {
                       case 'service_title': return <p key={fid} className="font-semibold">{record.title}</p>
                       case 'service_title': return <p key={fid} className="font-semibold">{record.title}</p>
-                      case 'service_type': return <p key={fid} className="text-sm text-gray-500">{t('type', { type: record.type })}</p>
+                      case 'service_type': return record.vehicle ? <p key={fid} className="text-sm text-gray-500">{t('type', { type: record.type })}</p> : null
                       case 'tech_name': return record.techName ? <p key={fid} className="text-sm text-gray-500">{t('tech', { tech: record.techName })}</p> : null
                       case 'tech_name': return record.techName ? <p key={fid} className="text-sm text-gray-500">{t('tech', { tech: record.techName })}</p> : null
                       default: return null
                       default: return null
                     }
                     }

+ 14 - 4
src/app/(public)/share/invoice/[orgId]/[token]/page.tsx

@@ -49,6 +49,16 @@ export default async function PublicInvoicePage({
       laborItems: true,
       laborItems: true,
       attachments: true,
       attachments: true,
       payments: { orderBy: { date: "desc" } },
       payments: { orderBy: { date: "desc" } },
+      customer: {
+        select: {
+          name: true,
+          email: true,
+          phone: true,
+          address: true,
+          company: true,
+          taxId: true,
+        },
+      },
       vehicle: {
       vehicle: {
         select: {
         select: {
           make: true,
           make: true,
@@ -74,7 +84,7 @@ export default async function PublicInvoicePage({
     },
     },
   });
   });
 
 
-  if (!record || record.vehicle.organizationId !== orgId) {
+  if (!record || record.organizationId !== orgId) {
     notFound();
     notFound();
   }
   }
 
 
@@ -82,7 +92,7 @@ export default async function PublicInvoicePage({
   const [settings, org, features] = await Promise.all([
   const [settings, org, features] = await Promise.all([
     db.appSetting.findMany({
     db.appSetting.findMany({
       where: {
       where: {
-        organizationId: record.vehicle.organizationId,
+        organizationId: record.organizationId,
         key: {
         key: {
           in: [
           in: [
             "workshop.address",
             "workshop.address",
@@ -117,9 +127,9 @@ export default async function PublicInvoicePage({
         },
         },
       },
       },
     }),
     }),
-    record.vehicle.organizationId
+    record.organizationId
       ? db.organization.findUnique({
       ? db.organization.findUnique({
-          where: { id: record.vehicle.organizationId },
+          where: { id: record.organizationId },
           select: { name: true, portalSlug: true },
           select: { name: true, portalSlug: true },
         })
         })
       : null,
       : null,

+ 13 - 11
src/app/(public)/share/status-report/[orgId]/[token]/status-report-view.tsx

@@ -19,7 +19,7 @@ interface StatusReportViewProps {
     customerFeedback: string | null;
     customerFeedback: string | null;
     feedbackAt: string | null;
     feedbackAt: string | null;
   };
   };
-  vehicle: { make: string; model: string; year: number; licensePlate: string | null };
+  vehicle: { make: string; model: string; year: number; licensePlate: string | null } | null;
   serviceTitle: string;
   serviceTitle: string;
   technicianName: string | null;
   technicianName: string | null;
   workshopName: string;
   workshopName: string;
@@ -40,7 +40,7 @@ export function StatusReportView({
   const createdDate = new Date(report.createdAt).toLocaleDateString(undefined, {
   const createdDate = new Date(report.createdAt).toLocaleDateString(undefined, {
     year: "numeric", month: "long", day: "numeric",
     year: "numeric", month: "long", day: "numeric",
   });
   });
-  const vehicleLabel = `${vehicle.year} ${vehicle.make} ${vehicle.model}`;
+  const vehicleLabel = vehicle ? `${vehicle.year} ${vehicle.make} ${vehicle.model}` : null;
 
 
   async function handleSubmitFeedback() {
   async function handleSubmitFeedback() {
     if (!feedback.trim() || submitting) return;
     if (!feedback.trim() || submitting) return;
@@ -74,16 +74,18 @@ export function StatusReportView({
                 <p className="font-medium">{serviceTitle}</p>
                 <p className="font-medium">{serviceTitle}</p>
               </div>
               </div>
             </div>
             </div>
-            <div className="flex items-start gap-3">
-              <CarFront className="mt-0.5 h-5 w-5 text-gray-400 shrink-0" />
-              <div>
-                <p className="text-sm text-gray-500">Vehicle</p>
-                <p className="font-medium">{vehicleLabel}</p>
-                {vehicle.licensePlate && (
-                  <p className="text-sm text-gray-500">Plate: {vehicle.licensePlate}</p>
-                )}
+            {vehicle && (
+              <div className="flex items-start gap-3">
+                <CarFront className="mt-0.5 h-5 w-5 text-gray-400 shrink-0" />
+                <div>
+                  <p className="text-sm text-gray-500">Vehicle</p>
+                  <p className="font-medium">{vehicleLabel}</p>
+                  {vehicle.licensePlate && (
+                    <p className="text-sm text-gray-500">Plate: {vehicle.licensePlate}</p>
+                  )}
+                </div>
               </div>
               </div>
-            </div>
+            )}
             {report.title && (
             {report.title && (
               <div className="pt-2 border-t">
               <div className="pt-2 border-t">
                 <p className="font-semibold text-lg">{report.title}</p>
                 <p className="font-semibold text-lg">{report.title}</p>

+ 32 - 1
src/app/api/protected/backup/export/route.ts

@@ -126,6 +126,27 @@ export async function POST(request: NextRequest) {
     );
     );
   }
   }
 
 
+  if (options.vehicles) {
+    // Counter sales: service records without a vehicle, linked directly to a
+    // customer. They are not nested under any vehicle, so export them
+    // separately or they would be lost from backups.
+    queries.push(
+      db.serviceRecord
+        .findMany({
+          where: { organizationId: ctx.organizationId, vehicleId: null },
+          include: {
+            partItems: true,
+            laborItems: true,
+            attachments: true,
+            payments: true,
+          },
+        })
+        .then((result) => {
+          data.counterSales = result;
+        })
+    );
+  }
+
   if (options.quotes) {
   if (options.quotes) {
     queries.push(
     queries.push(
       db.quote
       db.quote
@@ -277,7 +298,7 @@ export async function GET() {
   }
   }
 
 
   const [
   const [
-    settings, customers, customFieldDefinitions, inventoryParts, vehicles, quotes,
+    settings, customers, customFieldDefinitions, inventoryParts, vehicles, counterSales, quotes,
     technicians, inspectionTemplates, inspections, auditLogs, smsMessages, notifications,
     technicians, inspectionTemplates, inspections, auditLogs, smsMessages, notifications,
   ] = await Promise.all([
   ] = await Promise.all([
       db.appSetting.findMany({ where: { organizationId: ctx.organizationId } }),
       db.appSetting.findMany({ where: { organizationId: ctx.organizationId } }),
@@ -303,6 +324,15 @@ export async function GET() {
           },
           },
         },
         },
       }),
       }),
+      db.serviceRecord.findMany({
+        where: { organizationId: ctx.organizationId, vehicleId: null },
+        include: {
+          partItems: true,
+          laborItems: true,
+          attachments: true,
+          payments: true,
+        },
+      }),
       db.quote.findMany({
       db.quote.findMany({
         where: { organizationId: ctx.organizationId },
         where: { organizationId: ctx.organizationId },
         include: {
         include: {
@@ -333,6 +363,7 @@ export async function GET() {
       customFieldDefinitions,
       customFieldDefinitions,
       inventoryParts,
       inventoryParts,
       vehicles,
       vehicles,
+      counterSales,
       quotes,
       quotes,
       technicians,
       technicians,
       inspectionTemplates,
       inspectionTemplates,

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

@@ -298,7 +298,7 @@ export async function POST(request: NextRequest) {
 
 
     // Get the current highest invoice number
     // Get the current highest invoice number
     const lastRecord = await db.serviceRecord.findFirst({
     const lastRecord = await db.serviceRecord.findFirst({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       orderBy: { createdAt: "desc" },
       orderBy: { createdAt: "desc" },
       select: { invoiceNumber: true },
       select: { invoiceNumber: true },
     });
     });
@@ -417,6 +417,7 @@ export async function POST(request: NextRequest) {
 
 
         const record = await tx.serviceRecord.create({
         const record = await tx.serviceRecord.create({
           data: {
           data: {
+            organizationId,
             title,
             title,
             description: invoice.private_notes || invoice.public_notes || null,
             description: invoice.private_notes || invoice.public_notes || null,
             invoiceNotes: invoice.terms || null,
             invoiceNotes: invoice.terms || null,

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

@@ -258,7 +258,7 @@ export async function POST(request: NextRequest) {
 
 
     // Get the current highest invoice number to continue the sequence
     // Get the current highest invoice number to continue the sequence
     const lastRecord = await db.serviceRecord.findFirst({
     const lastRecord = await db.serviceRecord.findFirst({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       orderBy: { createdAt: "desc" },
       orderBy: { createdAt: "desc" },
       select: { invoiceNumber: true },
       select: { invoiceNumber: true },
     });
     });
@@ -380,6 +380,7 @@ export async function POST(request: NextRequest) {
 
 
         const created = await tx.serviceRecord.create({
         const created = await tx.serviceRecord.create({
           data: {
           data: {
+            organizationId,
             title: sr.Description,
             title: sr.Description,
             description: sr.Notes || null,
             description: sr.Notes || null,
             type: "repair",
             type: "repair",

+ 159 - 129
src/app/api/protected/backup/import/route.ts

@@ -51,6 +51,147 @@ async function parseBackup(
   return { backup, files: null };
   return { backup, files: null };
 }
 }
 
 
+/**
+ * Restores one service record with its nested parts/labor/attachments/payments.
+ * Used for both vehicle-linked records (nested under vehicles in the backup)
+ * and counter sales (top-level records without a vehicle).
+ */
+async function importServiceRecordTree(
+  tx: Prisma.TransactionClient,
+  sr: Record<string, unknown>,
+  opts: {
+    organizationId: string;
+    vehicleId: string | null;
+    customerId: string | null;
+    workDayStartTime: string;
+  }
+) {
+  // Derive startDateTime/endDateTime from backup or fall back to serviceDate + work day start
+  let startDT: Date | undefined;
+  let endDT: Date | undefined;
+  if (sr.startDateTime) {
+    startDT = toSafeDate(sr.startDateTime as string);
+  }
+  if (!startDT && sr.serviceDate) {
+    const sd = toSafeDate(sr.serviceDate as string);
+    if (sd) {
+      const [h, m] = opts.workDayStartTime.split(":").map(Number);
+      startDT = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate(), h, m, 0, 0);
+    }
+  }
+  if (sr.endDateTime) {
+    endDT = toSafeDate(sr.endDateTime as string);
+  }
+  if (!endDT && startDT) {
+    endDT = new Date(startDT.getTime() + 3600000);
+  }
+
+  await tx.serviceRecord.create({
+    data: {
+      id: sr.id as string,
+      organizationId: opts.organizationId,
+      title: sr.title as string,
+      description: (sr.description as string) || null,
+      type: (sr.type as string) || "maintenance",
+      status: (sr.status as string) || "completed",
+      cost: (sr.cost as number) || 0,
+      mileage: (sr.mileage as number) || null,
+      serviceDate: toSafeDate(sr.serviceDate as string),
+      startDateTime: startDT ?? undefined,
+      endDateTime: endDT ?? undefined,
+      shopName: (sr.shopName as string) || null,
+      techName: (sr.techName as string) || null,
+      parts: (sr.parts as string) || null,
+      laborHours: (sr.laborHours as number) || null,
+      diagnosticNotes: (sr.diagnosticNotes as string) || null,
+      invoiceNotes: (sr.invoiceNotes as string) || null,
+      subtotal: (sr.subtotal as number) || 0,
+      taxRate: (sr.taxRate as number) || 0,
+      taxAmount: (sr.taxAmount as number) || 0,
+      taxInclusive: (sr.taxInclusive as boolean) ?? false,
+      totalAmount: (sr.totalAmount as number) || 0,
+      invoiceNumber: (sr.invoiceNumber as string) || null,
+      discountType: (sr.discountType as string) || null,
+      discountValue: (sr.discountValue as number) || 0,
+      discountAmount: (sr.discountAmount as number) || 0,
+      publicToken: (sr.publicToken as string) || null,
+      technicianId: (sr.technicianId as string) || null,
+      sortOrder: (sr.sortOrder as number) || 0,
+      createdAt: toSafeDate(sr.createdAt as string),
+      updatedAt: toSafeDate(sr.updatedAt as string),
+      vehicleId: opts.vehicleId,
+      customerId: opts.customerId,
+    },
+  });
+
+  // Service parts
+  const partItems = sr.partItems as Record<string, unknown>[] | undefined;
+  if (partItems?.length) {
+    await tx.servicePart.createMany({
+      data: partItems.map((p) => ({
+        id: p.id as string,
+        partNumber: (p.partNumber as string) || null,
+        name: p.name as string,
+        quantity: (p.quantity as number) || 1,
+        unitPrice: (p.unitPrice as number) || 0,
+        total: (p.total as number) || 0,
+        serviceRecordId: sr.id as string,
+      })),
+    });
+  }
+
+  // Service labor
+  const laborItems = sr.laborItems as Record<string, unknown>[] | undefined;
+  if (laborItems?.length) {
+    await tx.serviceLabor.createMany({
+      data: laborItems.map((l) => ({
+        id: l.id as string,
+        description: l.description as string,
+        hours: (l.hours as number) || 0,
+        rate: (l.rate as number) || 0,
+        total: (l.total as number) || 0,
+        serviceRecordId: sr.id as string,
+      })),
+    });
+  }
+
+  // Service attachments
+  const attachments = sr.attachments as Record<string, unknown>[] | undefined;
+  if (attachments?.length) {
+    await tx.serviceAttachment.createMany({
+      data: attachments.map((a) => ({
+        id: a.id as string,
+        fileName: a.fileName as string,
+        fileUrl: rewriteFileUrl(a.fileUrl as string, opts.organizationId) || (a.fileUrl as string),
+        fileType: a.fileType as string,
+        fileSize: (a.fileSize as number) || 0,
+        category: (a.category as string) || "diagnostic",
+        description: (a.description as string) || null,
+        includeInInvoice: a.includeInInvoice !== false,
+        createdAt: toSafeDate(a.createdAt as string),
+        serviceRecordId: sr.id as string,
+      })),
+    });
+  }
+
+  // Payments
+  const payments = sr.payments as Record<string, unknown>[] | undefined;
+  if (payments?.length) {
+    await tx.payment.createMany({
+      data: payments.map((p) => ({
+        id: p.id as string,
+        amount: p.amount as number,
+        date: toSafeDate(p.date as string),
+        method: (p.method as string) || "other",
+        note: (p.note as string) || null,
+        createdAt: toSafeDate(p.createdAt as string),
+        updatedAt: toSafeDate(p.updatedAt as string),
+        serviceRecordId: sr.id as string,
+      })),
+    });
+  }
+}
+
 async function restoreFiles(zip: JSZip, organizationId: string) {
 async function restoreFiles(zip: JSZip, organizationId: string) {
   const uploadsDir = path.join(
   const uploadsDir = path.join(
     process.cwd(),
     process.cwd(),
@@ -424,141 +565,30 @@ export async function POST(request: NextRequest) {
             | undefined;
             | undefined;
           if (serviceRecords?.length) {
           if (serviceRecords?.length) {
             for (const sr of serviceRecords) {
             for (const sr of serviceRecords) {
-              // Derive startDateTime/endDateTime from backup or fall back to serviceDate + work day start
-              let startDT: Date | undefined;
-              let endDT: Date | undefined;
-              if (sr.startDateTime) {
-                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 = toSafeDate(sr.endDateTime as string);
-              }
-              if (!endDT && startDT) {
-                endDT = new Date(startDT.getTime() + 3600000);
-              }
-
-              await tx.serviceRecord.create({
-                data: {
-                  id: sr.id as string,
-                  title: sr.title as string,
-                  description: (sr.description as string) || null,
-                  type: (sr.type as string) || "maintenance",
-                  status: (sr.status as string) || "completed",
-                  cost: (sr.cost as number) || 0,
-                  mileage: (sr.mileage as number) || null,
-                  serviceDate: toSafeDate(sr.serviceDate as string),
-                  startDateTime: startDT ?? undefined,
-                  endDateTime: endDT ?? undefined,
-                  shopName: (sr.shopName as string) || null,
-                  techName: (sr.techName as string) || null,
-                  parts: (sr.parts as string) || null,
-                  laborHours: (sr.laborHours as number) || null,
-                  diagnosticNotes: (sr.diagnosticNotes as string) || null,
-                  invoiceNotes: (sr.invoiceNotes as string) || null,
-                  subtotal: (sr.subtotal as number) || 0,
-                  taxRate: (sr.taxRate as number) || 0,
-                  taxAmount: (sr.taxAmount as number) || 0,
-                  taxInclusive: (sr.taxInclusive as boolean) ?? false,
-                  totalAmount: (sr.totalAmount as number) || 0,
-                  invoiceNumber: (sr.invoiceNumber as string) || null,
-                  discountType: (sr.discountType as string) || null,
-                  discountValue: (sr.discountValue as number) || 0,
-                  discountAmount: (sr.discountAmount as number) || 0,
-                  publicToken: (sr.publicToken as string) || null,
-                  technicianId: (sr.technicianId as string) || null,
-                  sortOrder: (sr.sortOrder as number) || 0,
-                  createdAt: toSafeDate(sr.createdAt as string),
-                  updatedAt: toSafeDate(sr.updatedAt as string),
-                  vehicleId: v.id as string,
-                },
+              await importServiceRecordTree(tx, sr, {
+                organizationId: ctx.organizationId,
+                vehicleId: v.id as string,
+                customerId: null,
+                workDayStartTime,
               });
               });
-
-              // Service parts
-              const partItems = sr.partItems as
-                | Record<string, unknown>[]
-                | undefined;
-              if (partItems?.length) {
-                await tx.servicePart.createMany({
-                  data: partItems.map((p) => ({
-                    id: p.id as string,
-                    partNumber: (p.partNumber as string) || null,
-                    name: p.name as string,
-                    quantity: (p.quantity as number) || 1,
-                    unitPrice: (p.unitPrice as number) || 0,
-                    total: (p.total as number) || 0,
-                    serviceRecordId: sr.id as string,
-                  })),
-                });
-              }
-
-              // Service labor
-              const laborItems = sr.laborItems as
-                | Record<string, unknown>[]
-                | undefined;
-              if (laborItems?.length) {
-                await tx.serviceLabor.createMany({
-                  data: laborItems.map((l) => ({
-                    id: l.id as string,
-                    description: l.description as string,
-                    hours: (l.hours as number) || 0,
-                    rate: (l.rate as number) || 0,
-                    total: (l.total as number) || 0,
-                    serviceRecordId: sr.id as string,
-                  })),
-                });
-              }
-
-              // Service attachments
-              const attachments = sr.attachments as
-                | Record<string, unknown>[]
-                | undefined;
-              if (attachments?.length) {
-                await tx.serviceAttachment.createMany({
-                  data: attachments.map((a) => ({
-                    id: a.id as string,
-                    fileName: a.fileName as string,
-                    fileUrl: rewriteFileUrl(a.fileUrl as string, ctx.organizationId) || (a.fileUrl as string),
-                    fileType: a.fileType as string,
-                    fileSize: (a.fileSize as number) || 0,
-                    category: (a.category as string) || "diagnostic",
-                    description: (a.description as string) || null,
-                    includeInInvoice: a.includeInInvoice !== false,
-                    createdAt: toSafeDate(a.createdAt as string),
-                    serviceRecordId: sr.id as string,
-                  })),
-                });
-              }
-
-              // Payments
-              const payments = sr.payments as
-                | Record<string, unknown>[]
-                | undefined;
-              if (payments?.length) {
-                await tx.payment.createMany({
-                  data: payments.map((p) => ({
-                    id: p.id as string,
-                    amount: p.amount as number,
-                    date: toSafeDate(p.date as string),
-                    method: (p.method as string) || "other",
-                    note: (p.note as string) || null,
-                    createdAt: toSafeDate(p.createdAt as string),
-                    updatedAt: toSafeDate(p.updatedAt as string),
-                    serviceRecordId: sr.id as string,
-                  })),
-                });
-              }
             }
             }
           }
           }
         }
         }
       }
       }
 
 
+      // 6b. Counter sales (service records without a vehicle, linked directly
+      // to a customer). Older backups don't have this key.
+      if (data.counterSales?.length) {
+        for (const sr of data.counterSales as Record<string, unknown>[]) {
+          await importServiceRecordTree(tx, sr, {
+            organizationId: ctx.organizationId,
+            vehicleId: null,
+            customerId: (sr.customerId as string) || null,
+            workDayStartTime,
+          });
+        }
+      }
+
       // 7. Insert quotes with nested data
       // 7. Insert quotes with nested data
       if (data.quotes?.length) {
       if (data.quotes?.length) {
         for (const q of data.quotes as Record<string, unknown>[]) {
         for (const q of data.quotes as Record<string, unknown>[]) {

+ 11 - 1
src/app/api/protected/services/[id]/pdf/route.ts

@@ -43,7 +43,7 @@ export async function GET(
 
 
     const [record, settings, org] = await Promise.all([
     const [record, settings, org] = await Promise.all([
       db.serviceRecord.findFirst({
       db.serviceRecord.findFirst({
-        where: { id, vehicle: { organizationId: ctx.organizationId } },
+        where: { id, organizationId: ctx.organizationId },
         include: {
         include: {
           partItems: true,
           partItems: true,
           laborItems: true,
           laborItems: true,
@@ -54,6 +54,16 @@ export async function GET(
           // relation is always correct. The PDF prefers technician.name and
           // relation is always correct. The PDF prefers technician.name and
           // falls back to techName for legacy records that have no FK.
           // falls back to techName for legacy records that have no FK.
           technician: { select: { name: true } },
           technician: { select: { name: true } },
+          customer: {
+            select: {
+              name: true,
+              email: true,
+              phone: true,
+              address: true,
+              company: true,
+              taxId: true,
+            },
+          },
           vehicle: {
           vehicle: {
             select: {
             select: {
               make: true,
               make: true,

+ 3 - 3
src/app/api/public/files/[token]/[...path]/route.ts

@@ -46,11 +46,11 @@ export async function GET(
 
 
   const serviceRecord = await db.serviceRecord.findUnique({
   const serviceRecord = await db.serviceRecord.findUnique({
     where: { publicToken: token },
     where: { publicToken: token },
-    select: { vehicle: { select: { organizationId: true } } },
+    select: { organizationId: true },
   });
   });
 
 
-  if (serviceRecord?.vehicle.organizationId) {
-    orgId = serviceRecord.vehicle.organizationId;
+  if (serviceRecord?.organizationId) {
+    orgId = serviceRecord.organizationId;
   } else {
   } else {
     const quote = await db.quote.findFirst({
     const quote = await db.quote.findFirst({
       where: { publicToken: token },
       where: { publicToken: token },

+ 1 - 1
src/app/api/public/share/invoice/[orgId]/[token]/checkout/route.ts

@@ -37,7 +37,7 @@ export async function POST(
       },
       },
     });
     });
 
 
-    if (!record || record.vehicle.organizationId !== orgId) {
+    if (!record || record.organizationId !== orgId) {
       return NextResponse.json({ error: "Not found" }, { status: 404 });
       return NextResponse.json({ error: "Not found" }, { status: 404 });
     }
     }
 
 

+ 2 - 2
src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts

@@ -26,11 +26,11 @@ export async function GET(
       where: { publicToken: token },
       where: { publicToken: token },
       select: {
       select: {
         id: true,
         id: true,
-        vehicle: { select: { organizationId: true } },
+        organizationId: true,
       },
       },
     })
     })
 
 
-    if (!record || record.vehicle.organizationId !== orgId) {
+    if (!record || record.organizationId !== orgId) {
       return NextResponse.json({ error: 'Not found' }, { status: 404 })
       return NextResponse.json({ error: 'Not found' }, { status: 404 })
     }
     }
 
 

+ 7 - 4
src/app/api/public/share/invoice/[orgId]/[token]/verify/route.ts

@@ -28,11 +28,12 @@ export async function POST(
     const record = await db.serviceRecord.findUnique({
     const record = await db.serviceRecord.findUnique({
       where: { publicToken: token },
       where: { publicToken: token },
       include: {
       include: {
-        vehicle: { select: { id: true, organizationId: true, customer: { select: { name: true } } } },
+        customer: { select: { name: true } },
+        vehicle: { select: { id: true, customer: { select: { name: true } } } },
       },
       },
     });
     });
 
 
-    if (!record || record.vehicle.organizationId !== orgId) {
+    if (!record || record.organizationId !== orgId) {
       return NextResponse.json({ error: "Not found" }, { status: 404 });
       return NextResponse.json({ error: "Not found" }, { status: 404 });
     }
     }
 
 
@@ -89,10 +90,12 @@ export async function POST(
         organizationId: orgId,
         organizationId: orgId,
         type: "invoice_payment",
         type: "invoice_payment",
         title: "Invoice Payment Received",
         title: "Invoice Payment Received",
-        message: `${record.vehicle.customer?.name || "A customer"} paid ${result.amount.toFixed(2)} for invoice ${record.invoiceNumber || record.title}`,
+        message: `${(record.customer ?? record.vehicle?.customer)?.name || "A customer"} paid ${result.amount.toFixed(2)} for invoice ${record.invoiceNumber || record.title}`,
         entityType: "invoice",
         entityType: "invoice",
         entityId: record.id,
         entityId: record.id,
-        entityUrl: `/vehicles/${record.vehicle.id}?tab=service&record=${record.id}`,
+        entityUrl: record.vehicle
+          ? `/vehicles/${record.vehicle.id}?tab=service&record=${record.id}`
+          : `/sales/${record.id}`,
       });
       });
     }
     }
 
 

+ 2 - 2
src/app/api/webhooks/paypal/route.ts

@@ -92,10 +92,10 @@ async function processPayPalPayment(
   // Verify service record exists and belongs to this org
   // Verify service record exists and belongs to this org
   const record = await db.serviceRecord.findUnique({
   const record = await db.serviceRecord.findUnique({
     where: { id: serviceRecordId },
     where: { id: serviceRecordId },
-    include: { vehicle: { select: { organizationId: true } } },
+    select: { id: true, organizationId: true },
   });
   });
 
 
-  if (record && record.vehicle.organizationId === orgId) {
+  if (record && record.organizationId === orgId) {
     await db.payment.create({
     await db.payment.create({
       data: {
       data: {
         amount: result.amount,
         amount: result.amount,

+ 2 - 2
src/app/api/webhooks/stripe/route.ts

@@ -133,10 +133,10 @@ export async function POST(request: Request) {
       // Verify service record exists and belongs to this org
       // Verify service record exists and belongs to this org
       const record = await db.serviceRecord.findUnique({
       const record = await db.serviceRecord.findUnique({
         where: { id: serviceRecordId },
         where: { id: serviceRecordId },
-        include: { vehicle: { select: { organizationId: true } } },
+        select: { id: true, organizationId: true },
       });
       });
 
 
-      if (record && record.vehicle.organizationId === orgId) {
+      if (record && record.organizationId === orgId) {
         await db.payment.create({
         await db.payment.create({
           data: {
           data: {
             amount: (session.amount_total ?? 0) / 100,
             amount: (session.amount_total ?? 0) / 100,

+ 4 - 4
src/app/api/webhooks/vipps/route.ts

@@ -20,13 +20,13 @@ export async function POST(request: Request) {
           // We need orgId from the service record lookup
           // We need orgId from the service record lookup
           const record = await db.serviceRecord.findUnique({
           const record = await db.serviceRecord.findUnique({
             where: { id: parts[1] },
             where: { id: parts[1] },
-            include: { vehicle: { select: { organizationId: true } } },
+            select: { id: true, organizationId: true },
           });
           });
 
 
           if (record) {
           if (record) {
             return await processVippsPayment(
             return await processVippsPayment(
               reference,
               reference,
-              record.vehicle.organizationId!,
+              record.organizationId!,
               record.id,
               record.id,
             );
             );
           }
           }
@@ -85,10 +85,10 @@ async function processVippsPayment(
   // Verify service record exists and belongs to this org
   // Verify service record exists and belongs to this org
   const record = await db.serviceRecord.findUnique({
   const record = await db.serviceRecord.findUnique({
     where: { id: serviceRecordId },
     where: { id: serviceRecordId },
-    include: { vehicle: { select: { organizationId: true } } },
+    select: { id: true, organizationId: true },
   });
   });
 
 
-  if (record && record.vehicle.organizationId === orgId) {
+  if (record && record.organizationId === orgId) {
     await db.payment.create({
     await db.payment.create({
       data: {
       data: {
         amount: result.amount,
         amount: result.amount,

+ 154 - 5
src/components/vehicle-picker-dialog.tsx

@@ -25,8 +25,11 @@ import {
   Search,
   Search,
   ArrowLeft,
   ArrowLeft,
   UserPlus,
   UserPlus,
+  ShoppingCart,
 } from "lucide-react";
 } from "lucide-react";
 import { createVehicle } from "@/features/vehicles/Actions/vehicleActions";
 import { createVehicle } from "@/features/vehicles/Actions/vehicleActions";
+import { createDraftCounterSale } from "@/features/vehicles/Actions/createDraftServiceRecord";
+import { CustomerCombobox } from "@/features/quotes/Components/CustomerCombobox";
 import { createCustomer } from "@/features/customers/Actions/customerActions";
 import { createCustomer } from "@/features/customers/Actions/customerActions";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { useTranslations } from "next-intl";
 import { useTranslations } from "next-intl";
@@ -69,7 +72,7 @@ export function VehiclePickerDialog({
   const displayTitle = title || t("title");
   const displayTitle = title || t("title");
 
 
   const [vehicleSearch, setVehicleSearch] = useState("");
   const [vehicleSearch, setVehicleSearch] = useState("");
-  const [pickerStep, setPickerStep] = useState<"select" | "create">("select");
+  const [pickerStep, setPickerStep] = useState<"select" | "create" | "sale">("select");
 
 
   const [newMake, setNewMake] = useState("");
   const [newMake, setNewMake] = useState("");
   const [newModel, setNewModel] = useState("");
   const [newModel, setNewModel] = useState("");
@@ -106,6 +109,46 @@ export function VehiclePickerDialog({
     setCreating(false);
     setCreating(false);
   };
   };
 
 
+  const handleCreateSale = async () => {
+    if (showNewCustomer && !newCustomerName.trim()) {
+      toast.error(t("customerNameRequired"));
+      return;
+    }
+    if (!showNewCustomer && !selectedCustomerId) return;
+
+    setCreating(true);
+    try {
+      let customerId = selectedCustomerId;
+
+      if (showNewCustomer && newCustomerName.trim()) {
+        const customerResult = await createCustomer({
+          name: newCustomerName.trim(),
+          phone: newCustomerPhone.trim() || undefined,
+        });
+        if (!customerResult.success || !customerResult.data) {
+          toast.error(customerResult.error || t("failedCreateCustomer"));
+          setCreating(false);
+          return;
+        }
+        customerId = customerResult.data.id;
+      }
+
+      const result = await createDraftCounterSale(customerId);
+      if (!result.success || !result.data) {
+        toast.error(result.error || t("somethingWentWrong"));
+        setCreating(false);
+        return;
+      }
+
+      onOpenChange(false);
+      resetState();
+      router.push(`/sales/${result.data.id}`);
+    } catch {
+      toast.error(t("somethingWentWrong"));
+      setCreating(false);
+    }
+  };
+
   const handleSelect = (vehicleId: string) => {
   const handleSelect = (vehicleId: string) => {
     onOpenChange(false);
     onOpenChange(false);
     resetState();
     resetState();
@@ -229,13 +272,119 @@ export function VehiclePickerDialog({
                   </div>
                   </div>
                 )}
                 )}
               </div>
               </div>
+              <div className="flex gap-2">
+                <Button
+                  variant="ghost"
+                  className="flex-1"
+                  onClick={() => setPickerStep("create")}
+                >
+                  <Plus className="h-4 w-4 mr-1" />
+                  {t("addNewVehicle")}
+                </Button>
+                <Button
+                  variant="ghost"
+                  className="flex-1"
+                  onClick={() => setPickerStep("sale")}
+                >
+                  <ShoppingCart className="h-4 w-4 mr-1" />
+                  {t("partsSale")}
+                </Button>
+              </div>
+            </div>
+          </>
+        ) : pickerStep === "sale" ? (
+          <>
+            <DialogHeader>
+              <div className="flex items-center gap-2">
+                <Button
+                  variant="ghost"
+                  size="icon"
+                  className="h-7 w-7 shrink-0"
+                  onClick={() => setPickerStep("select")}
+                >
+                  <ArrowLeft className="h-4 w-4" />
+                </Button>
+                <DialogTitle>{t("partsSale")}</DialogTitle>
+              </div>
+            </DialogHeader>
+            <div className="space-y-4">
+              <p className="text-sm text-muted-foreground">{t("partsSaleDescription")}</p>
+              <div className="space-y-1.5">
+                <Label>{t("customer")}</Label>
+                {!showNewCustomer ? (
+                  <>
+                    <CustomerCombobox
+                      value={selectedCustomerId}
+                      placeholder={t("selectCustomer")}
+                      noneLabel={t("none")}
+                      onChange={(id) => setSelectedCustomerId(id)}
+                    />
+                    <Button
+                      variant="ghost"
+                      size="sm"
+                      className="h-8 px-2 text-xs text-muted-foreground"
+                      onClick={() => {
+                        setShowNewCustomer(true);
+                        setSelectedCustomerId("");
+                      }}
+                    >
+                      <UserPlus className="h-3.5 w-3.5 mr-1" />
+                      {t("createNewCustomer")}
+                    </Button>
+                  </>
+                ) : (
+                  <div className="space-y-3 rounded-md border p-3">
+                    <div className="flex items-center justify-between">
+                      <p className="text-sm font-medium">{t("newCustomer")}</p>
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        className="h-7 text-xs"
+                        onClick={() => {
+                          setShowNewCustomer(false);
+                          setNewCustomerName("");
+                          setNewCustomerPhone("");
+                        }}
+                      >
+                        {tc("cancel")}
+                      </Button>
+                    </div>
+                    <div className="grid grid-cols-2 gap-3">
+                      <div className="space-y-1.5">
+                        <Label htmlFor="sale-customer-name">{t("customerName")}</Label>
+                        <Input
+                          id="sale-customer-name"
+                          placeholder={t("customerNamePlaceholder")}
+                          value={newCustomerName}
+                          onChange={(e) => setNewCustomerName(e.target.value)}
+                        />
+                      </div>
+                      <div className="space-y-1.5">
+                        <Label htmlFor="sale-customer-phone">{t("phone")}</Label>
+                        <Input
+                          id="sale-customer-phone"
+                          placeholder={t("phonePlaceholder")}
+                          value={newCustomerPhone}
+                          onChange={(e) => setNewCustomerPhone(e.target.value)}
+                        />
+                      </div>
+                    </div>
+                  </div>
+                )}
+              </div>
               <Button
               <Button
-                variant="ghost"
                 className="w-full"
                 className="w-full"
-                onClick={() => setPickerStep("create")}
+                disabled={creating || (showNewCustomer ? !newCustomerName.trim() : !selectedCustomerId)}
+                onClick={handleCreateSale}
               >
               >
-                <Plus className="h-4 w-4 mr-1" />
-                Add New Vehicle
+                {creating ? (
+                  <>
+                    <Loader2 className="h-4 w-4 mr-1 animate-spin" />
+                    {t("creating")}
+                  </>
+                ) : (
+                  t("createSale")
+                )}
               </Button>
               </Button>
             </div>
             </div>
           </>
           </>

+ 2 - 2
src/features/admin/Actions/getOrganizationDetails.ts

@@ -34,8 +34,8 @@ export async function getOrganizationDetails(organizationId: string) {
 
 
     // Service records and inspections are linked via Vehicle.organizationId
     // Service records and inspections are linked via Vehicle.organizationId
     const [serviceRecordCount, inspectionCount, members] = await Promise.all([
     const [serviceRecordCount, inspectionCount, members] = await Promise.all([
-      db.serviceRecord.count({ where: { vehicle: { organizationId } } }),
-      db.inspection.count({ where: { vehicle: { organizationId } } }),
+      db.serviceRecord.count({ where: { organizationId } }),
+      db.inspection.count({ where: { organizationId } }),
       db.organizationMember.findMany({
       db.organizationMember.findMany({
         where: { organizationId },
         where: { organizationId },
         select: {
         select: {

+ 5 - 5
src/features/ai/Actions/aiActions.ts

@@ -18,7 +18,7 @@ export async function aiGenerateServiceNotes(serviceRecordId: string) {
     async ({ organizationId }) => {
     async ({ organizationId }) => {
       const locale = (await getLocale()) as Locale;
       const locale = (await getLocale()) as Locale;
       const sr = await db.serviceRecord.findFirst({
       const sr = await db.serviceRecord.findFirst({
-        where: { id: serviceRecordId, vehicle: { organizationId } },
+        where: { id: serviceRecordId, organizationId },
         include: {
         include: {
           vehicle: { select: { make: true, model: true, year: true, licensePlate: true } },
           vehicle: { select: { make: true, model: true, year: true, licensePlate: true } },
           partItems: { select: { name: true, quantity: true } },
           partItems: { select: { name: true, quantity: true } },
@@ -29,10 +29,10 @@ export async function aiGenerateServiceNotes(serviceRecordId: string) {
       if (!sr) throw new Error("Service record not found");
       if (!sr) throw new Error("Service record not found");
 
 
       return generateServiceDescription(organizationId, {
       return generateServiceDescription(organizationId, {
-        vehicleMake: sr.vehicle.make,
-        vehicleModel: sr.vehicle.model,
-        vehicleYear: sr.vehicle.year,
-        licensePlate: sr.vehicle.licensePlate,
+        vehicleMake: sr.vehicle?.make ?? null,
+        vehicleModel: sr.vehicle?.model ?? null,
+        vehicleYear: sr.vehicle?.year ?? null,
+        licensePlate: sr.vehicle?.licensePlate ?? null,
         serviceType: sr.type,
         serviceType: sr.type,
         serviceTitle: sr.title,
         serviceTitle: sr.title,
         parts: sr.partItems.map((p) => ({ name: p.name, quantity: p.quantity })),
         parts: sr.partItems.map((p) => ({ name: p.name, quantity: p.quantity })),

+ 6 - 3
src/features/ai/tools/workshop-tools.ts

@@ -205,6 +205,9 @@ async function executeOrgScopedQuery(sql: string, orgId: string): Promise<unknow
       "technicians",
       "technicians",
       "inspections",
       "inspections",
       "inspection_quote_requests",
       "inspection_quote_requests",
+      // Service records carry their own organizationId (counter sales have no
+      // vehicle, so scoping through vehicles would hide them).
+      "service_records",
     ];
     ];
 
 
     for (const table of orgDirectTables) {
     for (const table of orgDirectTables) {
@@ -219,7 +222,6 @@ async function executeOrgScopedQuery(sql: string, orgId: string): Promise<unknow
 
 
     // Children of vehicles (via vehicleId → vehicles.id)
     // Children of vehicles (via vehicleId → vehicles.id)
     const vehicleChildren = [
     const vehicleChildren = [
-      "service_records",
       "notes",
       "notes",
       "fuel_logs",
       "fuel_logs",
       "reminders",
       "reminders",
@@ -317,7 +319,7 @@ PostgreSQL database schema (use these exact table/column names in queries):
 
 
 vehicles (id, make, model, year, vin, "licensePlate", color, mileage, "fuelType", transmission, "engineSize", "purchaseDate", "purchasePrice", "isArchived", "createdAt", "updatedAt", "customerId")
 vehicles (id, make, model, year, vin, "licensePlate", color, mileage, "fuelType", transmission, "engineSize", "purchaseDate", "purchasePrice", "isArchived", "createdAt", "updatedAt", "customerId")
 
 
-service_records (id, title, description, type, status, cost, mileage, "serviceDate", "startDateTime", "endDateTime", "shopName", "techName", parts, "laborHours", "diagnosticNotes", "invoiceNotes", subtotal, "taxRate", "taxAmount", "totalAmount", "invoiceNumber", "discountType", "discountValue", "discountAmount", "manuallyPaid", "createdAt", "updatedAt", "vehicleId", "technicianId", "sortOrder")
+service_records (id, title, description, type, status, cost, mileage, "serviceDate", "startDateTime", "endDateTime", "shopName", "techName", parts, "laborHours", "diagnosticNotes", "invoiceNotes", subtotal, "taxRate", "taxAmount", "totalAmount", "invoiceNumber", "discountType", "discountValue", "discountAmount", "manuallyPaid", "createdAt", "updatedAt", "vehicleId", "customerId", "technicianId", "sortOrder") -- "vehicleId" is NULL for parts-only counter sales; those link "customerId" directly
 
 
 service_parts (id, "partNumber", name, quantity, "unitPrice", total, "serviceRecordId")
 service_parts (id, "partNumber", name, quantity, "unitPrice", total, "serviceRecordId")
 
 
@@ -357,7 +359,8 @@ service_attachments (id, "fileName", "fileUrl", "fileType", "fileSize", category
 
 
 Key relationships:
 Key relationships:
 - vehicles."customerId" → customers.id
 - vehicles."customerId" → customers.id
-- service_records."vehicleId" → vehicles.id
+- service_records."vehicleId" → vehicles.id (NULL for counter sales)
+- service_records."customerId" → customers.id (set only for counter sales)
 - service_parts."serviceRecordId" → service_records.id
 - service_parts."serviceRecordId" → service_records.id
 - service_labor."serviceRecordId" → service_records.id
 - service_labor."serviceRecordId" → service_records.id
 - payments."serviceRecordId" → service_records.id
 - payments."serviceRecordId" → service_records.id

+ 17 - 16
src/features/billing/Actions/billingActions.ts

@@ -41,8 +41,7 @@ async function getBillingSummary(organizationId: string): Promise<BillingSummary
           ELSE COALESCE((SELECT SUM(p.amount) FROM payments p WHERE p."serviceRecordId" = sr.id), 0)
           ELSE COALESCE((SELECT SUM(p.amount) FROM payments p WHERE p."serviceRecordId" = sr.id), 0)
         END AS paid
         END AS paid
       FROM service_records sr
       FROM service_records sr
-      JOIN vehicles v ON v.id = sr."vehicleId"
-      WHERE v."organizationId" = ${organizationId}
+      WHERE sr."organizationId" = ${organizationId}
     ) sub
     ) sub
   `);
   `);
 
 
@@ -110,8 +109,8 @@ export async function getBillingHistory(params: {
               ELSE COALESCE((SELECT SUM(p.amount) FROM payments p WHERE p."serviceRecordId" = sr.id), 0)
               ELSE COALESCE((SELECT SUM(p.amount) FROM payments p WHERE p."serviceRecordId" = sr.id), 0)
             END AS paid_amount
             END AS paid_amount
           FROM service_records sr
           FROM service_records sr
-          JOIN vehicles v ON v.id = sr."vehicleId"
-          WHERE v."organizationId" = ${organizationId}
+          LEFT JOIN vehicles v ON v.id = sr."vehicleId"
+          WHERE sr."organizationId" = ${organizationId}
           ${searchCondition}
           ${searchCondition}
         ) sub
         ) sub
         WHERE 1=1 ${statusCondition}
         WHERE 1=1 ${statusCondition}
@@ -130,7 +129,7 @@ export async function getBillingHistory(params: {
           effective_total: number;
           effective_total: number;
           paid_amount: number;
           paid_amount: number;
           manually_paid: boolean;
           manually_paid: boolean;
-          vehicle_id: string;
+          vehicle_id: string | null;
           vehicle_make: string | null;
           vehicle_make: string | null;
           vehicle_model: string | null;
           vehicle_model: string | null;
           vehicle_year: number | null;
           vehicle_year: number | null;
@@ -178,9 +177,9 @@ export async function getBillingHistory(params: {
             c.id AS customer_id,
             c.id AS customer_id,
             c.name AS customer_name
             c.name AS customer_name
           FROM service_records sr
           FROM service_records sr
-          JOIN vehicles v ON v.id = sr."vehicleId"
-          LEFT JOIN customers c ON c.id = v."customerId"
-          WHERE v."organizationId" = ${organizationId}
+          LEFT JOIN vehicles v ON v.id = sr."vehicleId"
+          LEFT JOIN customers c ON c.id = COALESCE(sr."customerId", v."customerId")
+          WHERE sr."organizationId" = ${organizationId}
           ${searchCondition}
           ${searchCondition}
         ) sub
         ) sub
         WHERE 1=1 ${statusCondition}
         WHERE 1=1 ${statusCondition}
@@ -218,14 +217,16 @@ export async function getBillingHistory(params: {
           totalAmount: effectiveTotal,
           totalAmount: effectiveTotal,
           totalPaid: paidAmount,
           totalPaid: paidAmount,
           status: paymentStatus,
           status: paymentStatus,
-          vehicle: {
-            id: r.vehicle_id,
-            make: r.vehicle_make || "",
-            model: r.vehicle_model || "",
-            year: r.vehicle_year || 0,
-            licensePlate: r.vehicle_license_plate,
-            customer: r.customer_id ? { id: r.customer_id, name: r.customer_name || "" } : null,
-          },
+          vehicle: r.vehicle_id
+            ? {
+                id: r.vehicle_id,
+                make: r.vehicle_make || "",
+                model: r.vehicle_model || "",
+                year: r.vehicle_year || 0,
+                licensePlate: r.vehicle_license_plate,
+              }
+            : null,
+          customer: r.customer_id ? { id: r.customer_id, name: r.customer_name || "" } : null,
         };
         };
         }),
         }),
         total,
         total,

+ 2 - 1
src/features/billing/Actions/recurringInvoiceActions.ts

@@ -258,7 +258,7 @@ async function generateInvoiceNumber(organizationId: string): Promise<string> {
   const startNumber = parseInt(settingsMap["workshop.invoiceStartNumber"] || "0", 10);
   const startNumber = parseInt(settingsMap["workshop.invoiceStartNumber"] || "0", 10);
 
 
   const lastRecord = await db.serviceRecord.findFirst({
   const lastRecord = await db.serviceRecord.findFirst({
-    where: { vehicle: { organizationId } },
+    where: { organizationId },
     orderBy: { createdAt: "desc" },
     orderBy: { createdAt: "desc" },
     select: { invoiceNumber: true },
     select: { invoiceNumber: true },
   });
   });
@@ -317,6 +317,7 @@ export async function processRecurringInvoices() {
       const serviceRecord = await db.$transaction(async (tx) => {
       const serviceRecord = await db.$transaction(async (tx) => {
         const sr = await tx.serviceRecord.create({
         const sr = await tx.serviceRecord.create({
           data: {
           data: {
+            organizationId,
             title: ri.title,
             title: ri.title,
             description: ri.description,
             description: ri.description,
             type: ri.type,
             type: ri.type,

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

@@ -11,7 +11,7 @@ export type CalendarEvent = {
   time: string | null; // HH:MM or null
   time: string | null; // HH:MM or null
   type: "service" | "reminder" | "quote";
   type: "service" | "reminder" | "quote";
   status: string;
   status: string;
-  vehicleId: string;
+  vehicleId: string | null;
   vehicleLabel: string;
   vehicleLabel: string;
   customerName: string | null;
   customerName: string | null;
   invoiceNumber: string | null;
   invoiceNumber: string | null;
@@ -42,7 +42,7 @@ export async function getCalendarEvents(params: {
     const [services, reminders, quotes] = await Promise.all([
     const [services, reminders, quotes] = await Promise.all([
       db.serviceRecord.findMany({
       db.serviceRecord.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           startDateTime: { gte: start, lte: end },
           startDateTime: { gte: start, lte: end },
         },
         },
         select: {
         select: {
@@ -55,6 +55,7 @@ export async function getCalendarEvents(params: {
           totalAmount: true,
           totalAmount: true,
           cost: true,
           cost: true,
           vehicleId: true,
           vehicleId: true,
+          customer: { select: { name: true } },
           vehicle: {
           vehicle: {
             select: {
             select: {
               make: true,
               make: true,
@@ -125,8 +126,8 @@ export async function getCalendarEvents(params: {
         type: "service" as const,
         type: "service" as const,
         status: s.status,
         status: s.status,
         vehicleId: s.vehicleId,
         vehicleId: s.vehicleId,
-        vehicleLabel: `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}`,
-        customerName: s.vehicle.customer?.name ?? null,
+        vehicleLabel: s.vehicle ? `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}` : "",
+        customerName: (s.customer ?? s.vehicle?.customer)?.name ?? null,
         invoiceNumber: s.invoiceNumber,
         invoiceNumber: s.invoiceNumber,
         amount: s.totalAmount > 0 ? s.totalAmount : s.cost > 0 ? s.cost : null,
         amount: s.totalAmount > 0 ? s.totalAmount : s.cost > 0 ? s.cost : null,
       })),
       })),

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

@@ -59,6 +59,7 @@ function getTypeBadge(type: CalendarEvent["type"], t: (key: string) => string) {
 
 
 function getEventLink(event: CalendarEvent) {
 function getEventLink(event: CalendarEvent) {
   if (event.type === "quote") return `/quotes/${event.id}`;
   if (event.type === "quote") return `/quotes/${event.id}`;
+  if (!event.vehicleId) return `/sales/${event.id}`;
   return `/vehicles/${event.vehicleId}`;
   return `/vehicles/${event.vehicleId}`;
 }
 }
 
 

+ 6 - 4
src/features/dashboard/custom-cards/server-registry.ts

@@ -1,6 +1,7 @@
 import { db } from "@/lib/db";
 import { db } from "@/lib/db";
 import { toSafeDate } from "@/lib/invoice-utils";
 import { toSafeDate } from "@/lib/invoice-utils";
 import { PermissionSubject } from "@/lib/permissions";
 import { PermissionSubject } from "@/lib/permissions";
+import { serviceRecordHref } from "@/lib/service-record";
 import type { CardEntity, CardFilter, CustomCardConfig } from "./registry";
 import type { CardEntity, CardFilter, CustomCardConfig } from "./registry";
 import { getField } from "./registry";
 import { getField } from "./registry";
 
 
@@ -168,11 +169,12 @@ export async function runEntityQuery(
     }
     }
     case "workOrders": {
     case "workOrders": {
       const rows = await db.serviceRecord.findMany({
       const rows = await db.serviceRecord.findMany({
-        where: buildWhere(config, { vehicle: { organizationId } }),
+        where: buildWhere(config, { organizationId }),
         select: {
         select: {
           id: true, invoiceNumber: true, title: true, type: true, status: true,
           id: true, invoiceNumber: true, title: true, type: true, status: true,
           techName: true, totalAmount: true, serviceDate: true,
           techName: true, totalAmount: true, serviceDate: true,
           invoiceDate: true, startDateTime: true,
           invoiceDate: true, startDateTime: true,
+          customer: { select: { name: true } },
           vehicle: {
           vehicle: {
             select: {
             select: {
               id: true, make: true, model: true, year: true,
               id: true, make: true, model: true, year: true,
@@ -185,13 +187,13 @@ export async function runEntityQuery(
       });
       });
       return rows.map((r) => ({
       return rows.map((r) => ({
         id: r.id,
         id: r.id,
-        href: `/vehicles/${r.vehicle.id}/service/${r.id}`,
+        href: serviceRecordHref(r),
         cells: {
         cells: {
           invoiceNumber: r.invoiceNumber, title: r.title, type: r.type,
           invoiceNumber: r.invoiceNumber, title: r.title, type: r.type,
           status: r.status, techName: r.techName, totalAmount: r.totalAmount,
           status: r.status, techName: r.techName, totalAmount: r.totalAmount,
           serviceDate: iso(r.invoiceDate ?? r.startDateTime ?? r.serviceDate),
           serviceDate: iso(r.invoiceDate ?? r.startDateTime ?? r.serviceDate),
-          vehicle: `${r.vehicle.year} ${r.vehicle.make} ${r.vehicle.model}`,
-          customer: r.vehicle.customer?.name ?? null,
+          vehicle: r.vehicle ? `${r.vehicle.year} ${r.vehicle.make} ${r.vehicle.model}` : null,
+          customer: (r.customer ?? r.vehicle?.customer)?.name ?? null,
         },
         },
       }));
       }));
     }
     }

+ 2 - 1
src/features/email/Actions/emailActions.ts

@@ -219,10 +219,11 @@ export async function sendInvoiceEmail(input: {
     const { serviceRecordId, recipientEmail, message } = input;
     const { serviceRecordId, recipientEmail, message } = input;
 
 
     const record = await db.serviceRecord.findFirst({
     const record = await db.serviceRecord.findFirst({
-      where: { id: serviceRecordId, vehicle: { organizationId } },
+      where: { id: serviceRecordId, organizationId },
       include: {
       include: {
         partItems: true,
         partItems: true,
         laborItems: true,
         laborItems: true,
+        customer: { select: { name: true, email: true, phone: true, address: true, company: true } },
         vehicle: {
         vehicle: {
           include: {
           include: {
             customer: { select: { name: true, email: true, phone: true, address: true, company: true } },
             customer: { select: { name: true, email: true, phone: true, address: true, company: true } },

+ 11 - 1
src/features/invoices/Pdf/buildInvoicePdfBuffer.ts

@@ -78,6 +78,16 @@ export async function buildInvoicePdfBuffer(
       // Linked technician (FK) is the source of truth for the tech name;
       // Linked technician (FK) is the source of truth for the tech name;
       // the legacy `techName` string is only a fallback for old records.
       // the legacy `techName` string is only a fallback for old records.
       technician: { select: { name: true } },
       technician: { select: { name: true } },
+      customer: {
+        select: {
+          name: true,
+          email: true,
+          phone: true,
+          address: true,
+          company: true,
+          taxId: true,
+        },
+      },
       vehicle: {
       vehicle: {
         select: {
         select: {
           make: true,
           make: true,
@@ -105,7 +115,7 @@ export async function buildInvoicePdfBuffer(
 
 
   if (!record) return null
   if (!record) return null
 
 
-  const orgId = record.vehicle.organizationId
+  const orgId = record.organizationId
   if (!orgId) return null
   if (!orgId) return null
 
 
   // Resolve locale + load PDF translations
   // Resolve locale + load PDF translations

+ 10 - 4
src/features/payments/Actions/paymentActions.ts

@@ -13,7 +13,7 @@ export async function createPayment(input: unknown) {
 
 
     // Verify ownership: serviceRecord -> vehicle -> organizationId
     // Verify ownership: serviceRecord -> vehicle -> organizationId
     const serviceRecord = await db.serviceRecord.findFirst({
     const serviceRecord = await db.serviceRecord.findFirst({
-      where: { id: data.serviceRecordId, vehicle: { organizationId } },
+      where: { id: data.serviceRecordId, organizationId },
       select: { id: true, vehicleId: true },
       select: { id: true, vehicleId: true },
     });
     });
     if (!serviceRecord) throw new Error("Service record not found");
     if (!serviceRecord) throw new Error("Service record not found");
@@ -28,7 +28,11 @@ export async function createPayment(input: unknown) {
       },
       },
     });
     });
 
 
-    revalidatePath(`/vehicles/${serviceRecord.vehicleId}/service/${data.serviceRecordId}`);
+    revalidatePath(
+      serviceRecord.vehicleId
+        ? `/vehicles/${serviceRecord.vehicleId}/service/${data.serviceRecordId}`
+        : `/sales/${data.serviceRecordId}`
+    );
     return { ...payment, serviceRecordId: data.serviceRecordId, amount: data.amount, method: data.method };
     return { ...payment, serviceRecordId: data.serviceRecordId, amount: data.amount, method: data.method };
   }, {
   }, {
     requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.BILLING }],
     requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.BILLING }],
@@ -45,7 +49,7 @@ export async function createPayment(input: unknown) {
 export async function deletePayment(paymentId: string) {
 export async function deletePayment(paymentId: string) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const payment = await db.payment.findFirst({
     const payment = await db.payment.findFirst({
-      where: { id: paymentId, serviceRecord: { vehicle: { organizationId } } },
+      where: { id: paymentId, serviceRecord: { organizationId } },
       include: { serviceRecord: { select: { vehicleId: true, id: true } } },
       include: { serviceRecord: { select: { vehicleId: true, id: true } } },
     });
     });
     if (!payment) throw new Error("Payment not found");
     if (!payment) throw new Error("Payment not found");
@@ -53,7 +57,9 @@ export async function deletePayment(paymentId: string) {
     await db.payment.delete({ where: { id: paymentId } });
     await db.payment.delete({ where: { id: paymentId } });
 
 
     const { vehicleId, id: serviceId } = payment.serviceRecord;
     const { vehicleId, id: serviceId } = payment.serviceRecord;
-    revalidatePath(`/vehicles/${vehicleId}/service/${serviceId}`);
+    revalidatePath(
+      vehicleId ? `/vehicles/${vehicleId}/service/${serviceId}` : `/sales/${serviceId}`
+    );
     return { deleted: true, paymentId };
     return { deleted: true, paymentId };
   }, {
   }, {
     requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.BILLING }],
     requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.BILLING }],

+ 7 - 4
src/features/portal/Actions/portalActions.ts

@@ -52,7 +52,8 @@ export async function getPortalDashboard() {
         // is still a work-order in progress and not really an invoice yet.
         // is still a work-order in progress and not really an invoice yet.
         db.serviceRecord.findMany({
         db.serviceRecord.findMany({
           where: {
           where: {
-            vehicle: { customerId, organizationId },
+            organizationId,
+            OR: [{ vehicle: { customerId } }, { customerId }],
             status: 'completed',
             status: 'completed',
           },
           },
           select: {
           select: {
@@ -72,7 +73,8 @@ export async function getPortalDashboard() {
         // Active jobs: work currently being done on the customer's vehicles.
         // Active jobs: work currently being done on the customer's vehicles.
         db.serviceRecord.findMany({
         db.serviceRecord.findMany({
           where: {
           where: {
-            vehicle: { customerId, organizationId },
+            organizationId,
+            OR: [{ vehicle: { customerId } }, { customerId }],
             status: { in: ['pending', 'in-progress', 'waiting-parts'] },
             status: { in: ['pending', 'in-progress', 'waiting-parts'] },
           },
           },
           select: {
           select: {
@@ -118,7 +120,7 @@ export async function getPortalDashboard() {
     // Open invoices: completed records that the customer still owes money on.
     // Open invoices: completed records that the customer still owes money on.
     // Pre-completion records are not counted — they're not yet invoices.
     // Pre-completion records are not counted — they're not yet invoices.
     const completedInvoices = await db.serviceRecord.findMany({
     const completedInvoices = await db.serviceRecord.findMany({
-      where: { vehicle: { customerId, organizationId }, status: 'completed' },
+      where: { organizationId, OR: [{ vehicle: { customerId } }, { customerId }], status: 'completed' },
       select: {
       select: {
         totalAmount: true,
         totalAmount: true,
         manuallyPaid: true,
         manuallyPaid: true,
@@ -243,7 +245,8 @@ export async function getPortalInvoices() {
     // change, customer can't act on them).
     // change, customer can't act on them).
     const invoices = await db.serviceRecord.findMany({
     const invoices = await db.serviceRecord.findMany({
       where: {
       where: {
-        vehicle: { customerId, organizationId },
+        organizationId,
+        OR: [{ vehicle: { customerId } }, { customerId }],
         status: 'completed',
         status: 'completed',
       },
       },
       select: {
       select: {

+ 2 - 1
src/features/quotes/Actions/quoteActions.ts

@@ -385,7 +385,7 @@ export async function convertQuoteToServiceRecord(quoteId: string, vehicleId: st
     const prefix = resolveInvoicePrefix(settingsMap["workshop.invoicePrefix"] ?? "{year}-");
     const prefix = resolveInvoicePrefix(settingsMap["workshop.invoicePrefix"] ?? "{year}-");
 
 
     const lastRecord = await db.serviceRecord.findFirst({
     const lastRecord = await db.serviceRecord.findFirst({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       orderBy: { createdAt: "desc" },
       orderBy: { createdAt: "desc" },
       select: { invoiceNumber: true },
       select: { invoiceNumber: true },
     });
     });
@@ -399,6 +399,7 @@ export async function convertQuoteToServiceRecord(quoteId: string, vehicleId: st
     const record = await db.$transaction(async (tx) => {
     const record = await db.$transaction(async (tx) => {
       const created = await tx.serviceRecord.create({
       const created = await tx.serviceRecord.create({
         data: {
         data: {
+          organizationId,
           title: quote.title,
           title: quote.title,
           description: quote.description,
           description: quote.description,
           type: "repair",
           type: "repair",

+ 13 - 11
src/features/reports/Actions/reportActions.ts

@@ -16,7 +16,7 @@ export async function getRevenueReport(params: {
 
 
     const records = await db.serviceRecord.findMany({
     const records = await db.serviceRecord.findMany({
       where: {
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
         startDateTime: { gte: start, lte: end },
       },
       },
       select: {
       select: {
@@ -119,7 +119,7 @@ export async function getServiceReport(params: {
 
 
     const records = await db.serviceRecord.findMany({
     const records = await db.serviceRecord.findMany({
       where: {
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
         startDateTime: { gte: start, lte: end },
       },
       },
       select: {
       select: {
@@ -206,7 +206,7 @@ export async function getTechnicianReport(params: {
 
 
     const records = await db.serviceRecord.findMany({
     const records = await db.serviceRecord.findMany({
       where: {
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
         startDateTime: { gte: start, lte: end },
         OR: [{ technicianId: { not: null } }, { techName: { not: null } }],
         OR: [{ technicianId: { not: null } }, { techName: { not: null } }],
       },
       },
@@ -263,7 +263,7 @@ export async function getPartsUsageReport(params: {
     const parts = await db.servicePart.findMany({
     const parts = await db.servicePart.findMany({
       where: {
       where: {
         serviceRecord: {
         serviceRecord: {
-          vehicle: { organizationId },
+          organizationId,
           startDateTime: { gte: start, lte: end },
           startDateTime: { gte: start, lte: end },
         },
         },
       },
       },
@@ -339,7 +339,7 @@ export async function getJobAnalyticsReport(params: {
 
 
     const records = await db.serviceRecord.findMany({
     const records = await db.serviceRecord.findMany({
       where: {
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
         startDateTime: { gte: start, lte: end },
       },
       },
       select: {
       select: {
@@ -540,7 +540,7 @@ export async function getPastDueInvoicesReport() {
 
 
     const records = await db.serviceRecord.findMany({
     const records = await db.serviceRecord.findMany({
       where: {
       where: {
-        vehicle: { organizationId },
+        organizationId,
         invoiceDueDate: { lt: now },
         invoiceDueDate: { lt: now },
         status: { not: "cancelled" },
         status: { not: "cancelled" },
       },
       },
@@ -552,6 +552,7 @@ export async function getPastDueInvoicesReport() {
         cost: true,
         cost: true,
         manuallyPaid: true,
         manuallyPaid: true,
         payments: { select: { amount: true } },
         payments: { select: { amount: true } },
+        customer: { select: { name: true, company: true } },
         vehicle: {
         vehicle: {
           select: {
           select: {
             year: true,
             year: true,
@@ -592,14 +593,15 @@ export async function getPastDueInvoicesReport() {
       const dueDate = r.invoiceDueDate!;
       const dueDate = r.invoiceDueDate!;
       const daysPastDue = Math.floor((now.getTime() - dueDate.getTime()) / (1000 * 60 * 60 * 24));
       const daysPastDue = Math.floor((now.getTime() - dueDate.getTime()) / (1000 * 60 * 60 * 24));
 
 
-      const vehicleParts = [r.vehicle.year, r.vehicle.make, r.vehicle.model].filter(Boolean);
+      const vehicleParts = r.vehicle ? [r.vehicle.year, r.vehicle.make, r.vehicle.model].filter(Boolean) : [];
       const vehicleInfo = vehicleParts.length > 0 ? vehicleParts.join(" ") : "N/A";
       const vehicleInfo = vehicleParts.length > 0 ? vehicleParts.join(" ") : "N/A";
+      const invoiceCustomer = r.customer ?? r.vehicle?.customer;
 
 
       invoices.push({
       invoices.push({
         id: r.id,
         id: r.id,
         invoiceNumber: r.invoiceNumber,
         invoiceNumber: r.invoiceNumber,
-        customerName: r.vehicle.customer?.name ?? "Unknown",
-        customerCompany: r.vehicle.customer?.company ?? null,
+        customerName: invoiceCustomer?.name ?? "Unknown",
+        customerCompany: invoiceCustomer?.company ?? null,
         vehicleInfo,
         vehicleInfo,
         totalAmount: total,
         totalAmount: total,
         amountPaid: paid,
         amountPaid: paid,
@@ -651,7 +653,7 @@ export async function getVehicleReport(params: {
     const records = await db.serviceRecord.findMany({
     const records = await db.serviceRecord.findMany({
       where: {
       where: {
         vehicleId: params.vehicleId,
         vehicleId: params.vehicleId,
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
         startDateTime: { gte: start, lte: end },
       },
       },
       select: {
       select: {
@@ -769,7 +771,7 @@ export async function getTaxReport(params: {
 
 
     const records = await db.serviceRecord.findMany({
     const records = await db.serviceRecord.findMany({
       where: {
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
         startDateTime: { gte: start, lte: end },
       },
       },
       select: {
       select: {

+ 2 - 2
src/features/search/Actions/searchActions.ts

@@ -133,7 +133,7 @@ export async function globalSearch(query: string) {
       })(),
       })(),
       db.serviceRecord.findMany({
       db.serviceRecord.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           AND: words.map((word) => ({
           AND: words.map((word) => ({
             OR: [
             OR: [
               { title: { contains: word, mode } },
               { title: { contains: word, mode } },
@@ -229,7 +229,7 @@ export async function globalSearch(query: string) {
       }),
       }),
       db.inspection.findMany({
       db.inspection.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           AND: words.map((word) => ({
           AND: words.map((word) => ({
             OR: [
             OR: [
               { template: { name: { contains: word, mode } } },
               { template: { name: { contains: word, mode } } },

+ 5 - 5
src/features/search/Components/SearchCommand.tsx

@@ -45,7 +45,7 @@ interface SearchResult {
       model: string;
       model: string;
       year: number;
       year: number;
       licensePlate: string | null;
       licensePlate: string | null;
-    };
+    } | null;
   }[];
   }[];
   parts: {
   parts: {
     id: string;
     id: string;
@@ -312,8 +312,8 @@ export function SearchCommand() {
                 {results.services.map((s) => (
                 {results.services.map((s) => (
                   <CommandItem
                   <CommandItem
                     key={s.id}
                     key={s.id}
-                    value={`${s.title} ${s.invoiceNumber || ""} ${s.vehicle.make} ${s.vehicle.model}`}
-                    onSelect={() => handleSelect(`/vehicles/${s.vehicle.id}/service/${s.id}`)}
+                    value={`${s.title} ${s.invoiceNumber || ""} ${s.vehicle ? `${s.vehicle.make} ${s.vehicle.model}` : ""}`}
+                    onSelect={() => handleSelect(s.vehicle ? `/vehicles/${s.vehicle.id}/service/${s.id}` : `/sales/${s.id}`)}
                   >
                   >
                     <Wrench className="mr-2 h-4 w-4 text-muted-foreground" />
                     <Wrench className="mr-2 h-4 w-4 text-muted-foreground" />
                     <div className="flex flex-col">
                     <div className="flex flex-col">
@@ -321,8 +321,8 @@ export function SearchCommand() {
                       <span className="text-xs text-muted-foreground">
                       <span className="text-xs text-muted-foreground">
                         {[
                         {[
                           s.invoiceNumber,
                           s.invoiceNumber,
-                          `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}`,
-                          s.vehicle.licensePlate,
+                          s.vehicle ? `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}` : null,
+                          s.vehicle?.licensePlate,
                         ].filter(Boolean).join(" · ")}
                         ].filter(Boolean).join(" · ")}
                       </span>
                       </span>
                     </div>
                     </div>

+ 6 - 6
src/features/settings/Actions/applyTaxRateToExisting.ts

@@ -41,7 +41,7 @@ export async function applyTaxRateToExisting() {
       // --- Service records (work orders / invoices) ---
       // --- Service records (work orders / invoices) ---
       const serviceRecords = await db.serviceRecord.findMany({
       const serviceRecords = await db.serviceRecord.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           taxRate: 0,
           taxRate: 0,
         },
         },
         select: {
         select: {
@@ -144,7 +144,7 @@ export async function getTaxBackfillCounts() {
     async ({ organizationId }) => {
     async ({ organizationId }) => {
       const [serviceRecords, quotes] = await Promise.all([
       const [serviceRecords, quotes] = await Promise.all([
         db.serviceRecord.count({
         db.serviceRecord.count({
-          where: { vehicle: { organizationId }, taxRate: 0 },
+          where: { organizationId, taxRate: 0 },
         }),
         }),
         db.quote.count({
         db.quote.count({
           where: { organizationId, taxRate: 0 },
           where: { organizationId, taxRate: 0 },
@@ -168,7 +168,7 @@ export async function getInclusiveBackfillCounts() {
     async ({ organizationId }) => {
     async ({ organizationId }) => {
       const [serviceRecords, quotes] = await Promise.all([
       const [serviceRecords, quotes] = await Promise.all([
         db.serviceRecord.count({
         db.serviceRecord.count({
-          where: { vehicle: { organizationId }, taxInclusive: false },
+          where: { organizationId, taxInclusive: false },
         }),
         }),
         db.quote.count({
         db.quote.count({
           where: { organizationId, taxInclusive: false },
           where: { organizationId, taxInclusive: false },
@@ -192,7 +192,7 @@ export async function getExclusiveBackfillCounts() {
     async ({ organizationId }) => {
     async ({ organizationId }) => {
       const [serviceRecords, quotes] = await Promise.all([
       const [serviceRecords, quotes] = await Promise.all([
         db.serviceRecord.count({
         db.serviceRecord.count({
-          where: { vehicle: { organizationId }, taxInclusive: true },
+          where: { organizationId, taxInclusive: true },
         }),
         }),
         db.quote.count({
         db.quote.count({
           where: { organizationId, taxInclusive: true },
           where: { organizationId, taxInclusive: true },
@@ -233,7 +233,7 @@ export async function convertRecordsToInclusive() {
       // --- Service records ---
       // --- Service records ---
       const serviceRecords = await db.serviceRecord.findMany({
       const serviceRecords = await db.serviceRecord.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           taxInclusive: false,
           taxInclusive: false,
         },
         },
         select: {
         select: {
@@ -402,7 +402,7 @@ export async function convertRecordsToExclusive() {
     async ({ organizationId }) => {
     async ({ organizationId }) => {
       const serviceRecords = await db.serviceRecord.findMany({
       const serviceRecords = await db.serviceRecord.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           taxInclusive: true,
           taxInclusive: true,
         },
         },
         select: {
         select: {

+ 5 - 1
src/features/settings/Actions/deleteContent.ts

@@ -49,7 +49,7 @@ export async function deleteContent(input: unknown) {
       }
       }
 
 
       const attachments = await db.serviceAttachment.findMany({
       const attachments = await db.serviceAttachment.findMany({
-        where: { serviceRecord: { vehicle: { organizationId } } },
+        where: { serviceRecord: { organizationId } },
         select: { fileUrl: true },
         select: { fileUrl: true },
       });
       });
       for (const att of attachments) {
       for (const att of attachments) {
@@ -57,6 +57,10 @@ export async function deleteContent(input: unknown) {
       }
       }
 
 
       await db.vehicle.deleteMany({ where: { organizationId } });
       await db.vehicle.deleteMany({ where: { organizationId } });
+      // Counter sales have no vehicle so the cascade above misses them, but
+      // their attachment files were already collected for cleanup — delete the
+      // records too so no orphaned work orders survive.
+      await db.serviceRecord.deleteMany({ where: { organizationId } });
       deleted.push("vehicles");
       deleted.push("vehicles");
     }
     }
 
 

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

@@ -14,7 +14,7 @@ export async function createStatusReport(input: unknown) {
 
 
       // Verify the service record belongs to this org
       // Verify the service record belongs to this org
       const serviceRecord = await db.serviceRecord.findFirst({
       const serviceRecord = await db.serviceRecord.findFirst({
-        where: { id: data.serviceRecordId, vehicle: { organizationId } },
+        where: { id: data.serviceRecordId, organizationId },
         select: { id: true, technicianId: true },
         select: { id: true, technicianId: true },
       });
       });
       if (!serviceRecord) throw new Error("Service record not found");
       if (!serviceRecord) throw new Error("Service record not found");

+ 8 - 6
src/features/status-reports/Actions/getPublicStatusReport.ts

@@ -63,12 +63,14 @@ export async function getPublicStatusReport(token: string) {
       description: report.serviceRecord.description,
       description: report.serviceRecord.description,
       status: report.serviceRecord.status,
       status: report.serviceRecord.status,
     },
     },
-    vehicle: {
-      make: report.serviceRecord.vehicle.make,
-      model: report.serviceRecord.vehicle.model,
-      year: report.serviceRecord.vehicle.year,
-      licensePlate: report.serviceRecord.vehicle.licensePlate,
-    },
+    vehicle: report.serviceRecord.vehicle
+      ? {
+          make: report.serviceRecord.vehicle.make,
+          model: report.serviceRecord.vehicle.model,
+          year: report.serviceRecord.vehicle.year,
+          licensePlate: report.serviceRecord.vehicle.licensePlate,
+        }
+      : null,
     technician: report.technician ? { name: report.technician.name } : null,
     technician: report.technician ? { name: report.technician.name } : null,
     organization: { name: report.organization.name },
     organization: { name: report.organization.name },
   };
   };

+ 1 - 1
src/features/status-reports/Actions/getStatusReportsForService.ts

@@ -12,7 +12,7 @@ export async function getStatusReportsForService(serviceRecordId: string) {
       // runs during page renders of just-deleted records (see deleteServiceRecord
       // runs during page renders of just-deleted records (see deleteServiceRecord
       // revalidating the current route).
       // revalidating the current route).
       const serviceRecord = await db.serviceRecord.findFirst({
       const serviceRecord = await db.serviceRecord.findFirst({
-        where: { id: serviceRecordId, vehicle: { organizationId } },
+        where: { id: serviceRecordId, organizationId },
         select: { id: true },
         select: { id: true },
       });
       });
       if (!serviceRecord) return [];
       if (!serviceRecord) return [];

+ 11 - 2
src/features/status-reports/Actions/sendStatusReport.ts

@@ -20,6 +20,15 @@ export async function sendStatusReport(input: unknown) {
         include: {
         include: {
           serviceRecord: {
           serviceRecord: {
             include: {
             include: {
+              customer: {
+                select: {
+                  id: true,
+                  name: true,
+                  email: true,
+                  phone: true,
+                  telegramChatId: true,
+                },
+              },
               vehicle: {
               vehicle: {
                 include: {
                 include: {
                   customer: {
                   customer: {
@@ -40,7 +49,7 @@ export async function sendStatusReport(input: unknown) {
 
 
       if (!report) throw new Error("Status report not found");
       if (!report) throw new Error("Status report not found");
 
 
-      const customer = report.serviceRecord.vehicle.customer;
+      const customer = report.serviceRecord.customer ?? report.serviceRecord.vehicle?.customer;
       if (!customer) throw new Error("No customer linked to this vehicle");
       if (!customer) throw new Error("No customer linked to this vehicle");
 
 
       // Build the public URL for the status report
       // Build the public URL for the status report
@@ -48,7 +57,7 @@ export async function sendStatusReport(input: unknown) {
       const publicUrl = `${appUrl}/share/status-report/${organizationId}/${report.publicToken}`;
       const publicUrl = `${appUrl}/share/status-report/${organizationId}/${report.publicToken}`;
 
 
       const vehicle = report.serviceRecord.vehicle;
       const vehicle = report.serviceRecord.vehicle;
-      const vehicleName = `${vehicle.year} ${vehicle.make} ${vehicle.model}`;
+      const vehicleName = vehicle ? `${vehicle.year} ${vehicle.make} ${vehicle.model}` : report.serviceRecord.title;
 
 
       const t = await getTranslations("statusReport.notify");
       const t = await getTranslations("statusReport.notify");
       const messageBody = data.customMessage
       const messageBody = data.customMessage

+ 8 - 3
src/features/status-reports/Actions/submitFeedback.ts

@@ -15,6 +15,7 @@ export async function submitStatusReportFeedback(input: unknown) {
           id: true,
           id: true,
           title: true,
           title: true,
           vehicleId: true,
           vehicleId: true,
+          customer: { select: { name: true } },
           vehicle: {
           vehicle: {
             select: {
             select: {
               year: true,
               year: true,
@@ -42,8 +43,10 @@ export async function submitStatusReportFeedback(input: unknown) {
 
 
   // Notify the organization about the feedback
   // Notify the organization about the feedback
   const vehicle = report.serviceRecord.vehicle;
   const vehicle = report.serviceRecord.vehicle;
-  const vehicleName = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean).join(" ");
-  const customerName = vehicle.customer?.name ?? "Customer";
+  const vehicleName = vehicle
+    ? [vehicle.year, vehicle.make, vehicle.model].filter(Boolean).join(" ")
+    : report.serviceRecord.title;
+  const customerName = (report.serviceRecord.customer ?? vehicle?.customer)?.name ?? "Customer";
 
 
   await notify({
   await notify({
     organizationId: report.organizationId,
     organizationId: report.organizationId,
@@ -52,7 +55,9 @@ export async function submitStatusReportFeedback(input: unknown) {
     message: `${customerName} responded to the status report for ${vehicleName}: "${data.feedback.length > 100 ? data.feedback.slice(0, 100) + "..." : data.feedback}"`,
     message: `${customerName} responded to the status report for ${vehicleName}: "${data.feedback.length > 100 ? data.feedback.slice(0, 100) + "..." : data.feedback}"`,
     entityType: "ServiceRecord",
     entityType: "ServiceRecord",
     entityId: report.serviceRecord.id,
     entityId: report.serviceRecord.id,
-    entityUrl: `/vehicles/${report.serviceRecord.vehicleId}/service/${report.serviceRecord.id}?tab=statusReports`,
+    entityUrl: report.serviceRecord.vehicleId
+      ? `/vehicles/${report.serviceRecord.vehicleId}/service/${report.serviceRecord.id}?tab=statusReports`
+      : `/sales/${report.serviceRecord.id}?tab=statusReports`,
   });
   });
 
 
   return { success: true };
   return { success: true };

+ 6 - 2
src/features/vehicles/Actions/addPartToServiceRecord.ts

@@ -21,7 +21,7 @@ export async function addPartToServiceRecord(input: {
   return withAuth(
   return withAuth(
     async ({ userId, organizationId }) => {
     async ({ userId, organizationId }) => {
       const record = await db.serviceRecord.findFirst({
       const record = await db.serviceRecord.findFirst({
-        where: { id: input.serviceRecordId, vehicle: { organizationId } },
+        where: { id: input.serviceRecordId, organizationId },
         select: { id: true, vehicleId: true, subtotal: true, taxRate: true, taxInclusive: true, discountType: true, discountValue: true, title: true, invoiceNumber: true },
         select: { id: true, vehicleId: true, subtotal: true, taxRate: true, taxInclusive: true, discountType: true, discountValue: true, title: true, invoiceNumber: true },
       });
       });
       if (!record) throw new Error("Service record not found");
       if (!record) throw new Error("Service record not found");
@@ -90,7 +90,11 @@ export async function addPartToServiceRecord(input: {
         return created;
         return created;
       });
       });
 
 
-      revalidatePath(`/vehicles/${record.vehicleId}/service/${record.id}`);
+      revalidatePath(
+        record.vehicleId
+          ? `/vehicles/${record.vehicleId}/service/${record.id}`
+          : `/sales/${record.id}`
+      );
       if (input.inventoryPartId) await onInventoryChanged(organizationId);
       if (input.inventoryPartId) await onInventoryChanged(organizationId);
 
 
       return part;
       return part;

+ 1 - 1
src/features/vehicles/Actions/addServiceAttachment.ts

@@ -27,7 +27,7 @@ export async function addServiceAttachment(input: unknown) {
       const record = await db.serviceRecord.findFirst({
       const record = await db.serviceRecord.findFirst({
         where: {
         where: {
           id: data.serviceRecordId,
           id: data.serviceRecordId,
-          vehicle: { organizationId },
+          organizationId,
         },
         },
         select: { id: true, vehicleId: true },
         select: { id: true, vehicleId: true },
       });
       });

+ 201 - 140
src/features/vehicles/Actions/createDraftServiceRecord.ts

@@ -4,6 +4,166 @@ import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 
+/**
+ * Shared draft-record creation for both work orders (with a vehicle) and
+ * counter sales (no vehicle, direct customer link). Resolves invoice number,
+ * default technician, tax settings and schedule times identically.
+ */
+async function createDraft(
+  { organizationId, userId }: { organizationId: string; userId: string },
+  opts: {
+    vehicleId: string | null;
+    customerId: string | null;
+    customerExempt: boolean;
+    title: string;
+    startDateTime?: Date;
+    endDateTime?: Date;
+    technicianId?: string;
+  },
+) {
+  const [settings, org, currentUser] = await Promise.all([
+    db.appSetting.findMany({
+      where: {
+        organizationId,
+        key: {
+          in: [
+            "workshop.invoicePrefix",
+            "workshop.invoiceStartNumber",
+            "workshop.defaultTechnician",
+            "workshop.defaultTechnicianId",
+            "workshop.defaultTaxRate",
+            "workshop.taxEnabled",
+            "workshop.taxInclusive",
+            "workboard.workDayStart",
+          ],
+        },
+      },
+    }),
+    db.organization.findUnique({
+      where: { id: organizationId },
+      select: { name: true },
+    }),
+    db.user.findUnique({
+      where: { id: userId },
+      select: { name: true },
+    }),
+  ]);
+  const settingsMap: Record<string, string> = {};
+  for (const s of settings) settingsMap[s.key] = s.value;
+
+  const shopName = org?.name || undefined;
+  let techName = currentUser?.name || undefined;
+
+  // Resolve technician: explicit param > default setting by ID > legacy default by name
+  let resolvedTechId = opts.technicianId;
+  if (!resolvedTechId) {
+    const defaultId = settingsMap["workshop.defaultTechnicianId"];
+    if (defaultId) {
+      const defaultTech = await db.technician.findFirst({
+        where: { id: defaultId, organizationId, isActive: true },
+        select: { id: true, name: true },
+      });
+      if (defaultTech) {
+        resolvedTechId = defaultTech.id;
+        techName = defaultTech.name;
+      }
+    }
+    // Legacy fallback: look up by name
+    if (!resolvedTechId && settingsMap["workshop.defaultTechnician"]) {
+      const defaultTech = await db.technician.findFirst({
+        where: { organizationId, name: settingsMap["workshop.defaultTechnician"], isActive: true },
+        select: { id: true, name: true },
+      });
+      if (defaultTech) {
+        resolvedTechId = defaultTech.id;
+        techName = defaultTech.name;
+      }
+    }
+  }
+
+  // If a technician is resolved (explicit or default), use their name
+  if (resolvedTechId) {
+    const tech = await db.technician.findFirst({
+      where: { id: resolvedTechId, organizationId },
+      select: { name: true },
+    });
+    if (tech) techName = tech.name;
+  }
+
+  const rawPrefix = settingsMap["workshop.invoicePrefix"] ?? "{year}-";
+  const now = new Date();
+  const prefix = rawPrefix
+    .replace("{year}", now.getFullYear().toString())
+    .replace("{month}", String(now.getMonth() + 1).padStart(2, "0"));
+
+  const startNumber = parseInt(
+    settingsMap["workshop.invoiceStartNumber"] || "0",
+    10,
+  );
+  const lastRecord = await db.serviceRecord.findFirst({
+    where: { organizationId },
+    orderBy: { createdAt: "desc" },
+    select: { invoiceNumber: true },
+  });
+  let nextNum = startNumber || 1001;
+  if (lastRecord?.invoiceNumber) {
+    const match = lastRecord.invoiceNumber.match(/(\d+)$/);
+    if (match) {
+      const lastNum = parseInt(match[1], 10) + 1;
+      nextNum = Math.max(nextNum, lastNum);
+    }
+  }
+  const invoiceNumber = `${prefix}${nextNum}`;
+
+  if (startNumber && nextNum === startNumber) {
+    await db.appSetting.updateMany({
+      where: { organizationId, key: "workshop.invoiceStartNumber" },
+      data: { value: "" },
+    });
+  }
+
+  // Apply default tax rate from settings (if tax is enabled).
+  // Tax-exempt customers always get a 0% rate regardless of org default.
+  const taxEnabled = settingsMap["workshop.taxEnabled"] !== "false";
+  const defaultTaxRate = taxEnabled && !opts.customerExempt
+    ? Number(settingsMap["workshop.defaultTaxRate"]) || 0
+    : 0;
+  const taxInclusive = settingsMap["workshop.taxInclusive"] === "true";
+
+  // Default to today's date at work day start time (from settings, fallback 07:00)
+  let defaultStart: Date;
+  if (!opts.startDateTime) {
+    const workDayStart = settingsMap["workboard.workDayStart"] || "07:00";
+    const [h, m] = workDayStart.split(":").map(Number);
+    defaultStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0, 0);
+  } else {
+    defaultStart = opts.startDateTime;
+  }
+  // serviceDate should be date-only (start of day)
+  const serviceDate = new Date(defaultStart.getFullYear(), defaultStart.getMonth(), defaultStart.getDate());
+
+  return db.serviceRecord.create({
+    data: {
+      organizationId,
+      title: opts.title,
+      type: "maintenance",
+      status: "pending",
+      vehicleId: opts.vehicleId,
+      customerId: opts.customerId,
+      shopName,
+      techName,
+      technicianId: resolvedTechId || undefined,
+      invoiceNumber,
+      taxRate: defaultTaxRate,
+      taxInclusive,
+      serviceDate,
+      invoiceDate: serviceDate,
+      startDateTime: defaultStart,
+      endDateTime: opts.endDateTime ?? new Date(defaultStart.getTime() + 3600000),
+    },
+  });
+}
+
 export async function createDraftServiceRecord(
 export async function createDraftServiceRecord(
   vehicleId: string,
   vehicleId: string,
   startDateTime?: Date,
   startDateTime?: Date,
@@ -18,148 +178,49 @@ export async function createDraftServiceRecord(
       });
       });
       if (!vehicle) throw new Error("Vehicle not found");
       if (!vehicle) throw new Error("Vehicle not found");
 
 
-      const [settings, org, currentUser] = await Promise.all([
-        db.appSetting.findMany({
-          where: {
-            organizationId,
-            key: {
-              in: [
-                "workshop.invoicePrefix",
-                "workshop.invoiceStartNumber",
-                "workshop.defaultTechnician",
-                "workshop.defaultTechnicianId",
-                "workshop.defaultTaxRate",
-                "workshop.taxEnabled",
-                "workshop.taxInclusive",
-                "workboard.workDayStart",
-              ],
-            },
-          },
-        }),
-        db.organization.findUnique({
-          where: { id: organizationId },
-          select: { name: true },
-        }),
-        db.user.findUnique({
-          where: { id: userId },
-          select: { name: true },
-        }),
-      ]);
-      const settingsMap: Record<string, string> = {};
-      for (const s of settings) settingsMap[s.key] = s.value;
-
-      const shopName = org?.name || undefined;
-      let techName = currentUser?.name || undefined;
-
-      // Resolve technician: explicit param > default setting by ID > legacy default by name
-      let resolvedTechId = technicianId;
-      if (!resolvedTechId) {
-        const defaultId = settingsMap["workshop.defaultTechnicianId"];
-        if (defaultId) {
-          const defaultTech = await db.technician.findFirst({
-            where: { id: defaultId, organizationId, isActive: true },
-            select: { id: true, name: true },
-          });
-          if (defaultTech) {
-            resolvedTechId = defaultTech.id;
-            techName = defaultTech.name;
-          }
-        }
-        // Legacy fallback: look up by name
-        if (!resolvedTechId && settingsMap["workshop.defaultTechnician"]) {
-          const defaultTech = await db.technician.findFirst({
-            where: { organizationId, name: settingsMap["workshop.defaultTechnician"], isActive: true },
-            select: { id: true, name: true },
-          });
-          if (defaultTech) {
-            resolvedTechId = defaultTech.id;
-            techName = defaultTech.name;
-          }
-        }
-      }
-
-      // If a technician is resolved (explicit or default), use their name
-      if (resolvedTechId) {
-        const tech = await db.technician.findFirst({
-          where: { id: resolvedTechId, organizationId },
-          select: { name: true },
-        });
-        if (tech) techName = tech.name;
-      }
-
-      const rawPrefix = settingsMap["workshop.invoicePrefix"] ?? "{year}-";
-      const now = new Date();
-      const prefix = rawPrefix
-        .replace("{year}", now.getFullYear().toString())
-        .replace("{month}", String(now.getMonth() + 1).padStart(2, "0"));
-
-      const startNumber = parseInt(
-        settingsMap["workshop.invoiceStartNumber"] || "0",
-        10,
-      );
-      const lastRecord = await db.serviceRecord.findFirst({
-        where: { vehicle: { organizationId } },
-        orderBy: { createdAt: "desc" },
-        select: { invoiceNumber: true },
+      return createDraft({ organizationId, userId }, {
+        vehicleId,
+        customerId: null,
+        customerExempt: vehicle.customer?.taxExempt ?? false,
+        title: "New Service Record",
+        startDateTime,
+        endDateTime,
+        technicianId,
       });
       });
-      let nextNum = startNumber || 1001;
-      if (lastRecord?.invoiceNumber) {
-        const match = lastRecord.invoiceNumber.match(/(\d+)$/);
-        if (match) {
-          const lastNum = parseInt(match[1], 10) + 1;
-          nextNum = Math.max(nextNum, lastNum);
-        }
-      }
-      const invoiceNumber = `${prefix}${nextNum}`;
-
-      if (startNumber && nextNum === startNumber) {
-        await db.appSetting.updateMany({
-          where: { organizationId, key: "workshop.invoiceStartNumber" },
-          data: { value: "" },
-        });
-      }
-
-      // Apply default tax rate from settings (if tax is enabled).
-      // Tax-exempt customers always get a 0% rate regardless of org default.
-      const taxEnabled = settingsMap["workshop.taxEnabled"] !== "false";
-      const customerExempt = vehicle.customer?.taxExempt ?? false;
-      const defaultTaxRate = taxEnabled && !customerExempt
-        ? Number(settingsMap["workshop.defaultTaxRate"]) || 0
-        : 0;
-      const taxInclusive = settingsMap["workshop.taxInclusive"] === "true";
-
-      // Default to today's date at work day start time (from settings, fallback 07:00)
-      let defaultStart: Date;
-      if (!startDateTime) {
-        const workDayStart = settingsMap["workboard.workDayStart"] || "07:00";
-        const [h, m] = workDayStart.split(":").map(Number);
-        defaultStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0, 0);
-      } else {
-        defaultStart = startDateTime;
-      }
-      // serviceDate should be date-only (start of day)
-      const serviceDate = new Date(defaultStart.getFullYear(), defaultStart.getMonth(), defaultStart.getDate());
-
-      const record = await db.serviceRecord.create({
-        data: {
-          title: "New Service Record",
-          type: "maintenance",
-          status: "pending",
-          vehicleId,
-          shopName,
-          techName,
-          technicianId: resolvedTechId || undefined,
-          invoiceNumber,
-          taxRate: defaultTaxRate,
-          taxInclusive,
-          serviceDate,
-          invoiceDate: serviceDate,
-          startDateTime: defaultStart,
-          endDateTime: endDateTime ?? new Date(defaultStart.getTime() + 3600000),
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.CREATE,
+          subject: PermissionSubject.SERVICES,
         },
         },
+      ],
+      audit: ({ result }) => ({
+        action: "service.create",
+        entity: "ServiceRecord",
+        entityId: result.id,
+        message: `Created draft service record ${result.invoiceNumber || result.id}`,
+        metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
+      }),
+    },
+  );
+}
+
+export async function createDraftCounterSale(customerId: string) {
+  return withAuth(
+    async ({ organizationId, userId }) => {
+      const customer = await db.customer.findFirst({
+        where: { id: customerId, organizationId },
+        select: { id: true, taxExempt: true },
       });
       });
+      if (!customer) throw new Error("Customer not found");
 
 
-      return record;
+      return createDraft({ organizationId, userId }, {
+        vehicleId: null,
+        customerId: customer.id,
+        customerExempt: customer.taxExempt,
+        title: "Parts Sale",
+      });
     },
     },
     {
     {
       requiredPermissions: [
       requiredPermissions: [
@@ -172,8 +233,8 @@ export async function createDraftServiceRecord(
         action: "service.create",
         action: "service.create",
         entity: "ServiceRecord",
         entity: "ServiceRecord",
         entityId: result.id,
         entityId: result.id,
-        message: `Created draft service record ${result.invoiceNumber || result.id}`,
-        metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
+        message: `Created counter sale ${result.invoiceNumber || result.id}`,
+        metadata: { serviceRecordId: result.id, customerId: result.customerId },
       }),
       }),
     },
     },
   );
   );

+ 6 - 4
src/features/vehicles/Actions/dashboardActions.ts

@@ -28,10 +28,10 @@ export async function getDashboardStats() {
       recentServices,
       recentServices,
     ] = await Promise.all([
     ] = await Promise.all([
       db.serviceRecord.count({
       db.serviceRecord.count({
-        where: { vehicle: { organizationId }, status: { not: "completed" } },
+        where: { organizationId, status: { not: "completed" } },
       }),
       }),
       db.serviceRecord.count({
       db.serviceRecord.count({
-        where: { vehicle: { organizationId }, status: "pending" },
+        where: { organizationId, status: "pending" },
       }),
       }),
       db.inventoryPart.count({
       db.inventoryPart.count({
         where: { organizationId, isArchived: false },
         where: { organizationId, isArchived: false },
@@ -51,10 +51,11 @@ export async function getDashboardStats() {
       db.customer.count({ where: { organizationId } }),
       db.customer.count({ where: { organizationId } }),
       db.serviceRecord.findMany({
       db.serviceRecord.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           status: { in: ["pending", "in-progress", "waiting-parts"] },
           status: { in: ["pending", "in-progress", "waiting-parts"] },
         },
         },
         include: {
         include: {
+          customer: { select: { id: true, name: true } },
           vehicle: {
           vehicle: {
             select: {
             select: {
               id: true,
               id: true,
@@ -71,10 +72,11 @@ export async function getDashboardStats() {
       }),
       }),
       db.serviceRecord.findMany({
       db.serviceRecord.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           status: "completed",
           status: "completed",
         },
         },
         include: {
         include: {
+          customer: { select: { id: true, name: true } },
           vehicle: {
           vehicle: {
             select: {
             select: {
               id: true,
               id: true,

+ 2 - 0
src/features/vehicles/Actions/findingActions.ts

@@ -24,6 +24,8 @@ export async function getObservationsPaginated(params: {
       const skip = (page - 1) * pageSize;
       const skip = (page - 1) * pageSize;
 
 
       const search = params.search?.trim();
       const search = params.search?.trim();
+      // VehicleFinding has no organizationId of its own — findings are always
+      // vehicle-scoped, so the org check must go through the vehicle relation.
       const where = {
       const where = {
         vehicle: { organizationId },
         vehicle: { organizationId },
         ...(params.status && params.status !== "all" ? { status: params.status } : {}),
         ...(params.status && params.status !== "all" ? { status: params.status } : {}),

+ 19 - 5
src/features/vehicles/Actions/getMyActiveJobs.ts

@@ -8,13 +8,13 @@ export interface MyActiveJob {
   id: string;
   id: string;
   title: string;
   title: string;
   status: string;
   status: string;
-  vehicleId: string;
+  vehicleId: string | null;
   vehicle: {
   vehicle: {
     make: string;
     make: string;
     model: string;
     model: string;
     year: number;
     year: number;
     licensePlate: string | null;
     licensePlate: string | null;
-  };
+  } | null;
   customer: {
   customer: {
     id: string;
     id: string;
     name: string;
     name: string;
@@ -42,7 +42,7 @@ export async function getMyActiveJobs() {
 
 
       const records = await db.serviceRecord.findMany({
       const records = await db.serviceRecord.findMany({
         where: {
         where: {
-          vehicle: { organizationId },
+          organizationId,
           status: { in: ["in-progress", "pending", "waiting-parts"] },
           status: { in: ["in-progress", "pending", "waiting-parts"] },
           technicianId: { in: techIds },
           technicianId: { in: techIds },
         },
         },
@@ -51,6 +51,15 @@ export async function getMyActiveJobs() {
           title: true,
           title: true,
           status: true,
           status: true,
           vehicleId: true,
           vehicleId: true,
+          customer: {
+            select: {
+              id: true,
+              name: true,
+              email: true,
+              phone: true,
+              telegramChatId: true,
+            },
+          },
           vehicle: {
           vehicle: {
             select: {
             select: {
               make: true,
               make: true,
@@ -86,8 +95,13 @@ export async function getMyActiveJobs() {
         title: r.title,
         title: r.title,
         status: r.status,
         status: r.status,
         vehicleId: r.vehicleId,
         vehicleId: r.vehicleId,
-        vehicle: r.vehicle,
-        customer: r.vehicle.customer,
+        vehicle: r.vehicle ? {
+          make: r.vehicle.make,
+          model: r.vehicle.model,
+          year: r.vehicle.year,
+          licensePlate: r.vehicle.licensePlate,
+        } : null,
+        customer: r.customer ?? r.vehicle?.customer ?? null,
         imageCount: r.attachments.filter((a) => a.category === "image").length,
         imageCount: r.attachments.filter((a) => a.category === "image").length,
         videoCount: r.attachments.filter((a) => a.category === "video").length,
         videoCount: r.attachments.filter((a) => a.category === "video").length,
         partCount: r._count.partItems,
         partCount: r._count.partItems,

+ 1 - 1
src/features/vehicles/Actions/predictedMaintenanceActions.ts

@@ -45,7 +45,7 @@ export async function getVehiclePredictedMileage(vehicleId: string) {
     const records = await db.serviceRecord.findMany({
     const records = await db.serviceRecord.findMany({
       where: {
       where: {
         vehicleId,
         vehicleId,
-        vehicle: { organizationId },
+        organizationId,
         mileage: { not: null },
         mileage: { not: null },
         status: "completed",
         status: "completed",
       },
       },

+ 108 - 40
src/features/vehicles/Actions/serviceActions.ts

@@ -98,7 +98,7 @@ export async function getServiceRecordsPaginated(
 export async function getAllServiceRecords() {
 export async function getAllServiceRecords() {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     return db.serviceRecord.findMany({
     return db.serviceRecord.findMany({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       include: {
       include: {
         vehicle: { select: { make: true, model: true, year: true } },
         vehicle: { select: { make: true, model: true, year: true } },
         _count: { select: { partItems: true, laborItems: true } },
         _count: { select: { partItems: true, laborItems: true } },
@@ -122,7 +122,7 @@ export async function getAllServiceRecordsPaginated(params: {
     const skip = (page - 1) * pageSize;
     const skip = (page - 1) * pageSize;
 
 
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const where: any = { vehicle: { organizationId } };
+    const where: any = { organizationId };
 
 
     if (params.search) {
     if (params.search) {
       where.OR = [
       where.OR = [
@@ -178,12 +178,23 @@ export async function getAllServiceRecordsPaginated(params: {
 export async function getServiceRecord(recordId: string) {
 export async function getServiceRecord(recordId: string) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
     const record = await db.serviceRecord.findFirst({
-      where: { id: recordId, vehicle: { organizationId } },
+      where: { id: recordId, organizationId },
       include: {
       include: {
         partItems: true,
         partItems: true,
         laborItems: true,
         laborItems: true,
         attachments: true,
         attachments: true,
         payments: { orderBy: { date: "desc" } },
         payments: { orderBy: { date: "desc" } },
+        customer: {
+          select: {
+            id: true,
+            name: true,
+            email: true,
+            phone: true,
+            address: true,
+            company: true,
+            telegramChatId: true,
+          },
+        },
         vehicle: {
         vehicle: {
           select: {
           select: {
             id: true,
             id: true,
@@ -216,11 +227,25 @@ export async function getServiceRecord(recordId: string) {
 export async function createServiceRecord(input: unknown) {
 export async function createServiceRecord(input: unknown) {
   return withAuth(async ({ userId, organizationId }) => {
   return withAuth(async ({ userId, organizationId }) => {
     const data = createServiceSchema.parse(input);
     const data = createServiceSchema.parse(input);
-    const vehicle = await db.vehicle.findFirst({
-      where: { id: data.vehicleId, organizationId },
-      include: { customer: { select: { taxExempt: true } } },
-    });
-    if (!vehicle) throw new Error("Vehicle not found");
+
+    // Two shapes: a vehicle-linked work order, or a counter sale (no vehicle)
+    // that must be linked directly to a customer instead.
+    let vehicle: { id: string; mileage: number; customer: { taxExempt: boolean } | null } | null = null;
+    let directCustomer: { id: string; taxExempt: boolean } | null = null;
+    if (data.vehicleId) {
+      vehicle = await db.vehicle.findFirst({
+        where: { id: data.vehicleId, organizationId },
+        select: { id: true, mileage: true, customer: { select: { taxExempt: true } } },
+      });
+      if (!vehicle) throw new Error("Vehicle not found");
+    } else {
+      if (!data.customerId) throw new Error("A customer is required for a sale without a vehicle");
+      directCustomer = await db.customer.findFirst({
+        where: { id: data.customerId, organizationId },
+        select: { id: true, taxExempt: true },
+      });
+      if (!directCustomer) throw new Error("Customer not found");
+    }
 
 
     // Auto-populate shop name and invoice prefix from settings
     // Auto-populate shop name and invoice prefix from settings
     const [settings, org] = await Promise.all([
     const [settings, org] = await Promise.all([
@@ -251,7 +276,7 @@ export async function createServiceRecord(input: unknown) {
         : settingsMap["workshop.taxInclusive"] === "true";
         : settingsMap["workshop.taxInclusive"] === "true";
 
 
     // Tax-exempt customer: force taxRate to 0 (overrides whatever the caller sent).
     // Tax-exempt customer: force taxRate to 0 (overrides whatever the caller sent).
-    if (vehicle.customer?.taxExempt) {
+    if (vehicle?.customer?.taxExempt || directCustomer?.taxExempt) {
       data.taxRate = 0;
       data.taxRate = 0;
       data.taxAmount = 0;
       data.taxAmount = 0;
     }
     }
@@ -259,7 +284,7 @@ export async function createServiceRecord(input: unknown) {
     // Generate sequential invoice number
     // Generate sequential invoice number
     const startNumber = parseInt(settingsMap["workshop.invoiceStartNumber"] || "0", 10);
     const startNumber = parseInt(settingsMap["workshop.invoiceStartNumber"] || "0", 10);
     const lastRecord = await db.serviceRecord.findFirst({
     const lastRecord = await db.serviceRecord.findFirst({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       orderBy: { createdAt: "desc" },
       orderBy: { createdAt: "desc" },
       select: { invoiceNumber: true },
       select: { invoiceNumber: true },
     });
     });
@@ -287,6 +312,10 @@ export async function createServiceRecord(input: unknown) {
       const created = await tx.serviceRecord.create({
       const created = await tx.serviceRecord.create({
         data: {
         data: {
           ...recordData,
           ...recordData,
+          organizationId,
+          // Only vehicle-less records link a customer directly; vehicle-linked
+          // records always resolve their customer through the vehicle.
+          customerId: data.vehicleId ? null : data.customerId,
           taxInclusive,
           taxInclusive,
           shopName,
           shopName,
           invoiceNumber,
           invoiceNumber,
@@ -372,20 +401,22 @@ export async function createServiceRecord(input: unknown) {
     }
     }
 
 
     // Update vehicle mileage if service mileage is higher, and reset maintenance dismissed
     // Update vehicle mileage if service mileage is higher, and reset maintenance dismissed
-    const vehicleUpdate: { mileage?: number; maintenanceDismissed: boolean; maintenanceDismissedAt: null } = {
-      maintenanceDismissed: false,
-      maintenanceDismissedAt: null,
-    };
-    if (data.mileage && data.mileage > vehicle.mileage) {
-      vehicleUpdate.mileage = data.mileage;
+    if (vehicle) {
+      const vehicleUpdate: { mileage?: number; maintenanceDismissed: boolean; maintenanceDismissedAt: null } = {
+        maintenanceDismissed: false,
+        maintenanceDismissedAt: null,
+      };
+      if (data.mileage && data.mileage > vehicle.mileage) {
+        vehicleUpdate.mileage = data.mileage;
+      }
+      await db.vehicle.update({
+        where: { id: vehicle.id },
+        data: vehicleUpdate,
+      });
     }
     }
-    await db.vehicle.update({
-      where: { id: vehicle.id },
-      data: vehicleUpdate,
-    });
 
 
     revalidatePath("/");
     revalidatePath("/");
-    revalidatePath(`/vehicles/${data.vehicleId}`);
+    if (data.vehicleId) revalidatePath(`/vehicles/${data.vehicleId}`);
     revalidatePath("/services");
     revalidatePath("/services");
     return record;
     return record;
   }, {
   }, {
@@ -404,7 +435,7 @@ export async function updateServiceRecord(input: unknown) {
   return withAuth(async ({ userId, organizationId }) => {
   return withAuth(async ({ userId, organizationId }) => {
     const data = updateServiceSchema.parse(input);
     const data = updateServiceSchema.parse(input);
     const existing = await db.serviceRecord.findFirst({
     const existing = await db.serviceRecord.findFirst({
-      where: { id: data.id, vehicle: { organizationId } },
+      where: { id: data.id, organizationId },
       include: {
       include: {
         attachments: { select: { fileUrl: true, category: true } },
         attachments: { select: { fileUrl: true, category: true } },
         vehicle: { select: { id: true, mileage: true, make: true, model: true, year: true, licensePlate: true } },
         vehicle: { select: { id: true, mileage: true, make: true, model: true, year: true, licensePlate: true } },
@@ -412,7 +443,21 @@ export async function updateServiceRecord(input: unknown) {
     });
     });
     if (!existing) throw new Error("Service record not found");
     if (!existing) throw new Error("Service record not found");
 
 
-    const { id, partItems, laborItems, attachments, serviceDate: _sd, invoiceDate: _id, invoiceDueDate: _idd, warrantyMonths: _wm, warrantyMileage: _wmil, warrantyNotes: _wn, ...recordData } = data;
+    const { id, partItems, laborItems, attachments, customerId: _cid, serviceDate: _sd, invoiceDate: _id, invoiceDueDate: _idd, warrantyMonths: _wm, warrantyMileage: _wmil, warrantyNotes: _wn, ...recordData } = data;
+
+    // A null vehicleId from the client means "no vehicle" (counter sale) —
+    // treat it as no-change rather than detaching an existing vehicle.
+    if (recordData.vehicleId == null) {
+      delete recordData.vehicleId;
+    } else if (recordData.vehicleId !== existing.vehicleId) {
+      // Moving the record to another vehicle: it must belong to this org, and
+      // the record's customer then follows the vehicle again.
+      const targetVehicle = await db.vehicle.findFirst({
+        where: { id: recordData.vehicleId, organizationId },
+        select: { id: true },
+      });
+      if (!targetVehicle) throw new Error("Vehicle not found");
+    }
 
 
     // Determine which categories are being replaced and which files were removed
     // Determine which categories are being replaced and which files were removed
     let removedFileUrls: string[] = [];
     let removedFileUrls: string[] = [];
@@ -433,6 +478,12 @@ export async function updateServiceRecord(input: unknown) {
         where: { id },
         where: { id },
         data: {
         data: {
           ...recordData,
           ...recordData,
+          // Attaching a vehicle to a counter sale: the direct customer link is
+          // cleared so the invoice follows the vehicle's customer again.
+          customerId:
+            recordData.vehicleId && recordData.vehicleId !== existing.vehicleId && existing.customerId
+              ? null
+              : undefined,
           description: recordData.description !== undefined ? (recordData.description || null) : undefined,
           description: recordData.description !== undefined ? (recordData.description || null) : undefined,
           techName: recordData.techName !== undefined ? (recordData.techName || null) : undefined,
           techName: recordData.techName !== undefined ? (recordData.techName || null) : undefined,
           diagnosticNotes: recordData.diagnosticNotes !== undefined ? (recordData.diagnosticNotes || null) : undefined,
           diagnosticNotes: recordData.diagnosticNotes !== undefined ? (recordData.diagnosticNotes || null) : undefined,
@@ -528,7 +579,7 @@ export async function updateServiceRecord(input: unknown) {
     });
     });
 
 
     // Update vehicle mileage if this is the latest service record and mileage is higher
     // Update vehicle mileage if this is the latest service record and mileage is higher
-    if (recordData.mileage && recordData.mileage > existing.vehicle.mileage) {
+    if (existing.vehicle && recordData.mileage && recordData.mileage > existing.vehicle.mileage) {
       const latestRecord = await db.serviceRecord.findFirst({
       const latestRecord = await db.serviceRecord.findFirst({
         where: { vehicleId: existing.vehicle.id },
         where: { vehicleId: existing.vehicle.id },
         orderBy: [{ startDateTime: { sort: 'desc', nulls: 'last' } }, { serviceDate: 'desc' }],
         orderBy: [{ startDateTime: { sort: 'desc', nulls: 'last' } }, { serviceDate: 'desc' }],
@@ -568,8 +619,12 @@ export async function updateServiceRecord(input: unknown) {
     }
     }
 
 
     revalidatePath("/");
     revalidatePath("/");
-    revalidatePath(`/vehicles/${existing.vehicleId}`);
-    revalidatePath(`/vehicles/${existing.vehicleId}/service/${id}`);
+    if (existing.vehicleId) {
+      revalidatePath(`/vehicles/${existing.vehicleId}`);
+      revalidatePath(`/vehicles/${existing.vehicleId}/service/${id}`);
+    } else {
+      revalidatePath(`/sales/${id}`);
+    }
     revalidatePath("/services");
     revalidatePath("/services");
     // Parts may have been added, changed or removed on this record.
     // Parts may have been added, changed or removed on this record.
     await onInventoryChanged(organizationId);
     await onInventoryChanged(organizationId);
@@ -589,7 +644,7 @@ export async function updateServiceRecord(input: unknown) {
 export async function updateServiceStatus(recordId: string, status: string) {
 export async function updateServiceStatus(recordId: string, status: string) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
     const record = await db.serviceRecord.findFirst({
-      where: { id: recordId, vehicle: { organizationId } },
+      where: { id: recordId, organizationId },
       include: {
       include: {
         vehicle: {
         vehicle: {
           select: { id: true, make: true, model: true, year: true, licensePlate: true },
           select: { id: true, make: true, model: true, year: true, licensePlate: true },
@@ -619,7 +674,8 @@ export async function updateServiceStatus(recordId: string, status: string) {
     revalidatePath("/");
     revalidatePath("/");
     revalidatePath("/work-orders");
     revalidatePath("/work-orders");
     revalidatePath("/services");
     revalidatePath("/services");
-    revalidatePath(`/vehicles/${record.vehicleId}`);
+    if (record.vehicleId) revalidatePath(`/vehicles/${record.vehicleId}`);
+    else revalidatePath(`/sales/${recordId}`);
     return { success: true, recordId, status };
     return { success: true, recordId, status };
   }, {
   }, {
     requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }],
     requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }],
@@ -636,7 +692,7 @@ export async function updateServiceStatus(recordId: string, status: string) {
 export async function toggleManuallyPaid(recordId: string) {
 export async function toggleManuallyPaid(recordId: string) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
     const record = await db.serviceRecord.findFirst({
-      where: { id: recordId, vehicle: { organizationId } },
+      where: { id: recordId, organizationId },
     });
     });
     if (!record) throw new Error("Record not found");
     if (!record) throw new Error("Record not found");
 
 
@@ -647,8 +703,12 @@ export async function toggleManuallyPaid(recordId: string) {
 
 
     revalidatePath("/");
     revalidatePath("/");
     revalidatePath("/services");
     revalidatePath("/services");
-    revalidatePath(`/vehicles/${record.vehicleId}`);
-    revalidatePath(`/vehicles/${record.vehicleId}/service/${recordId}`);
+    if (record.vehicleId) {
+      revalidatePath(`/vehicles/${record.vehicleId}`);
+      revalidatePath(`/vehicles/${record.vehicleId}/service/${recordId}`);
+    } else {
+      revalidatePath(`/sales/${recordId}`);
+    }
     return { success: true, manuallyPaid: !record.manuallyPaid };
     return { success: true, manuallyPaid: !record.manuallyPaid };
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
 }
 }
@@ -667,7 +727,7 @@ export async function getWorkOrders(params: {
     const skip = (page - 1) * pageSize;
     const skip = (page - 1) * pageSize;
 
 
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const where: any = { vehicle: { organizationId } };
+    const where: any = { organizationId };
 
 
     if (params.status === "active") {
     if (params.status === "active") {
       where.status = { not: "completed" };
       where.status = { not: "completed" };
@@ -682,6 +742,7 @@ export async function getWorkOrders(params: {
         { techName: { contains: params.search, mode: "insensitive" } },
         { techName: { contains: params.search, mode: "insensitive" } },
         { vehicle: { licensePlate: { contains: params.search, mode: "insensitive" } } },
         { vehicle: { licensePlate: { contains: params.search, mode: "insensitive" } } },
         { vehicle: { customer: { name: { contains: params.search, mode: "insensitive" } } } },
         { vehicle: { customer: { name: { contains: params.search, mode: "insensitive" } } } },
+        { customer: { name: { contains: params.search, mode: "insensitive" } } },
       ];
       ];
     }
     }
 
 
@@ -689,6 +750,7 @@ export async function getWorkOrders(params: {
       db.serviceRecord.findMany({
       db.serviceRecord.findMany({
         where,
         where,
         include: {
         include: {
+          customer: { select: { id: true, name: true, email: true, phone: true } },
           vehicle: {
           vehicle: {
             select: {
             select: {
               id: true,
               id: true,
@@ -726,7 +788,7 @@ export async function getWorkOrders(params: {
       db.serviceRecord.count({ where }),
       db.serviceRecord.count({ where }),
       db.serviceRecord.groupBy({
       db.serviceRecord.groupBy({
         by: ["status"],
         by: ["status"],
-        where: { vehicle: { organizationId } },
+        where: { organizationId },
         _count: true,
         _count: true,
       }),
       }),
     ]);
     ]);
@@ -750,7 +812,7 @@ export async function getWorkOrders(params: {
 export async function deleteServiceRecord(recordId: string) {
 export async function deleteServiceRecord(recordId: string) {
   return withAuth(async ({ userId, organizationId }) => {
   return withAuth(async ({ userId, organizationId }) => {
     const record = await db.serviceRecord.findFirst({
     const record = await db.serviceRecord.findFirst({
-      where: { id: recordId, vehicle: { organizationId } },
+      where: { id: recordId, organizationId },
       include: { attachments: true },
       include: { attachments: true },
     });
     });
     if (!record) throw new Error("Record not found");
     if (!record) throw new Error("Record not found");
@@ -785,7 +847,7 @@ export async function deleteServiceRecord(recordId: string) {
     });
     });
 
 
     revalidatePath("/");
     revalidatePath("/");
-    revalidatePath(`/vehicles/${record.vehicleId}`);
+    if (record.vehicleId) revalidatePath(`/vehicles/${record.vehicleId}`);
     revalidatePath("/services");
     revalidatePath("/services");
     // Deleting the record restocked its linked parts.
     // Deleting the record restocked its linked parts.
     await onInventoryChanged(organizationId);
     await onInventoryChanged(organizationId);
@@ -805,7 +867,7 @@ export async function deleteServiceRecord(recordId: string) {
 export async function deleteServiceAttachment(attachmentId: string) {
 export async function deleteServiceAttachment(attachmentId: string) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const attachment = await db.serviceAttachment.findFirst({
     const attachment = await db.serviceAttachment.findFirst({
-      where: { id: attachmentId, serviceRecord: { vehicle: { organizationId } } },
+      where: { id: attachmentId, serviceRecord: { organizationId } },
       include: { serviceRecord: { select: { vehicleId: true, id: true } } },
       include: { serviceRecord: { select: { vehicleId: true, id: true } } },
     });
     });
     if (!attachment) throw new Error("Attachment not found");
     if (!attachment) throw new Error("Attachment not found");
@@ -821,7 +883,9 @@ export async function deleteServiceAttachment(attachmentId: string) {
     await db.serviceAttachment.delete({ where: { id: attachmentId } });
     await db.serviceAttachment.delete({ where: { id: attachmentId } });
 
 
     const { vehicleId, id: serviceId } = attachment.serviceRecord;
     const { vehicleId, id: serviceId } = attachment.serviceRecord;
-    revalidatePath(`/vehicles/${vehicleId}/service/${serviceId}`);
+    revalidatePath(
+      vehicleId ? `/vehicles/${vehicleId}/service/${serviceId}` : `/sales/${serviceId}`
+    );
     return { deleted: true };
     return { deleted: true };
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
 }
 }
@@ -829,7 +893,7 @@ export async function deleteServiceAttachment(attachmentId: string) {
 export async function generatePublicLink(serviceRecordId: string) {
 export async function generatePublicLink(serviceRecordId: string) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
     const record = await db.serviceRecord.findFirst({
-      where: { id: serviceRecordId, vehicle: { organizationId } },
+      where: { id: serviceRecordId, organizationId },
     });
     });
     if (!record) throw new Error("Record not found");
     if (!record) throw new Error("Record not found");
 
 
@@ -839,7 +903,11 @@ export async function generatePublicLink(serviceRecordId: string) {
       data: { publicToken: token, sharedAt: new Date() },
       data: { publicToken: token, sharedAt: new Date() },
     });
     });
 
 
-    revalidatePath(`/vehicles/${record.vehicleId}/service/${serviceRecordId}`);
+    revalidatePath(
+      record.vehicleId
+        ? `/vehicles/${record.vehicleId}/service/${serviceRecordId}`
+        : `/sales/${serviceRecordId}`
+    );
     return { token, organizationId };
     return { token, organizationId };
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
 }
 }
@@ -847,7 +915,7 @@ export async function generatePublicLink(serviceRecordId: string) {
 export async function revokePublicLink(serviceRecordId: string) {
 export async function revokePublicLink(serviceRecordId: string) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
     const record = await db.serviceRecord.findFirst({
-      where: { id: serviceRecordId, vehicle: { organizationId } },
+      where: { id: serviceRecordId, organizationId },
     });
     });
     if (!record) throw new Error("Record not found");
     if (!record) throw new Error("Record not found");
 
 

+ 1 - 1
src/features/vehicles/Actions/updateServiceAttachment.ts

@@ -20,7 +20,7 @@ export async function updateServiceAttachment(input: unknown) {
       const attachment = await db.serviceAttachment.findFirst({
       const attachment = await db.serviceAttachment.findFirst({
         where: {
         where: {
           id: data.id,
           id: data.id,
-          serviceRecord: { vehicle: { organizationId } },
+          serviceRecord: { organizationId },
         },
         },
         include: {
         include: {
           serviceRecord: { select: { vehicleId: true, id: true } },
           serviceRecord: { select: { vehicleId: true, id: true } },

+ 31 - 26
src/features/vehicles/Components/MyActiveJobs.tsx

@@ -326,7 +326,7 @@ export function MyActiveJobs({
                   <div className="flex items-center justify-between">
                   <div className="flex items-center justify-between">
                     <div
                     <div
                       className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
                       className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
-                      onClick={() => router.push(`/vehicles/${job.vehicleId}/service/${job.id}`)}
+                      onClick={() => router.push(job.vehicleId ? `/vehicles/${job.vehicleId}/service/${job.id}` : `/sales/${job.id}`)}
                     >
                     >
                       <div
                       <div
                         className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${statusColor}`}
                         className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${statusColor}`}
@@ -336,8 +336,9 @@ export function MyActiveJobs({
                       <div className="min-w-0">
                       <div className="min-w-0">
                         <p className="font-medium text-sm truncate">{job.title}</p>
                         <p className="font-medium text-sm truncate">{job.title}</p>
                         <p className="text-xs text-muted-foreground truncate">
                         <p className="text-xs text-muted-foreground truncate">
-                          {job.vehicle.year} {job.vehicle.make} {job.vehicle.model}
-                          {job.vehicle.licensePlate && ` · ${job.vehicle.licensePlate}`}
+                          {job.vehicle
+                            ? `${job.vehicle.year} ${job.vehicle.make} ${job.vehicle.model}${job.vehicle.licensePlate ? ` · ${job.vehicle.licensePlate}` : ''}`
+                            : job.customer?.name ?? ''}
                         </p>
                         </p>
                       </div>
                       </div>
                     </div>
                     </div>
@@ -424,17 +425,19 @@ export function MyActiveJobs({
                         <FileVideo className="mr-1.5 h-4 w-4" />
                         <FileVideo className="mr-1.5 h-4 w-4" />
                         {t('statusReport')}
                         {t('statusReport')}
                       </Button>
                       </Button>
-                      <Button
-                        variant="outline"
-                        size="sm"
-                        className="h-9"
-                        onClick={() =>
-                          setObservationTarget({ vehicleId: job.vehicleId, serviceRecordId: job.id })
-                        }
-                      >
-                        <ClipboardCheck className="mr-1.5 h-4 w-4" />
-                        {t('observation')}
-                      </Button>
+                      {job.vehicleId && (
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          className="h-9"
+                          onClick={() =>
+                            setObservationTarget({ vehicleId: job.vehicleId as string, serviceRecordId: job.id })
+                          }
+                        >
+                          <ClipboardCheck className="mr-1.5 h-4 w-4" />
+                          {t('observation')}
+                        </Button>
+                      )}
                     </div>
                     </div>
                     {/* Mobile: counters only */}
                     {/* Mobile: counters only */}
                     <div className="shrink-0 ml-3 flex sm:hidden items-center gap-1.5 text-xs text-muted-foreground">
                     <div className="shrink-0 ml-3 flex sm:hidden items-center gap-1.5 text-xs text-muted-foreground">
@@ -524,17 +527,19 @@ export function MyActiveJobs({
                         <FileVideo className="mr-1 h-3.5 w-3.5" />
                         <FileVideo className="mr-1 h-3.5 w-3.5" />
                         {t('report')}
                         {t('report')}
                       </Button>
                       </Button>
-                      <Button
-                        variant="outline"
-                        size="sm"
-                        className="flex-1 h-9"
-                        onClick={() =>
-                          setObservationTarget({ vehicleId: job.vehicleId, serviceRecordId: job.id })
-                        }
-                      >
-                        <ClipboardCheck className="mr-1 h-3.5 w-3.5" />
-                        {t('observation')}
-                      </Button>
+                      {job.vehicleId && (
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          className="flex-1 h-9"
+                          onClick={() =>
+                            setObservationTarget({ vehicleId: job.vehicleId as string, serviceRecordId: job.id })
+                          }
+                        >
+                          <ClipboardCheck className="mr-1 h-3.5 w-3.5" />
+                          {t('observation')}
+                        </Button>
+                      )}
                     </ButtonGroup>
                     </ButtonGroup>
                   </div>
                   </div>
                   <input
                   <input
@@ -664,7 +669,7 @@ export function MyActiveJobs({
                 if (!open) setStatusReportJobId(null)
                 if (!open) setStatusReportJobId(null)
               }}
               }}
               serviceRecordId={statusReportJobId}
               serviceRecordId={statusReportJobId}
-              vehicleName={`${job.vehicle.year} ${job.vehicle.make} ${job.vehicle.model}`}
+              vehicleName={job.vehicle ? `${job.vehicle.year} ${job.vehicle.make} ${job.vehicle.model}` : job.title}
               customer={job.customer}
               customer={job.customer}
               smsEnabled={smsEnabled}
               smsEnabled={smsEnabled}
               emailEnabled={emailEnabled}
               emailEnabled={emailEnabled}

+ 14 - 7
src/features/vehicles/Components/invoice-pdf/InfoSection.tsx

@@ -61,7 +61,7 @@ interface CustomerRenderCtx {
 
 
 function renderCustomerField(fieldId: string, ctx: CustomerRenderCtx): React.ReactNode {
 function renderCustomerField(fieldId: string, ctx: CustomerRenderCtx): React.ReactNode {
   const { data, styles, labels } = ctx
   const { data, styles, labels } = ctx
-  const c = data.vehicle.customer
+  const c = data.customer ?? data.vehicle?.customer
   if (!c) return null
   if (!c) return null
 
 
   switch (fieldId) {
   switch (fieldId) {
@@ -96,6 +96,7 @@ interface VehicleRenderCtx {
 
 
 function renderVehicleField(fieldId: string, ctx: VehicleRenderCtx): React.ReactNode {
 function renderVehicleField(fieldId: string, ctx: VehicleRenderCtx): React.ReactNode {
   const { data, vehicleName, invoiceSettings, styles, labels } = ctx
   const { data, vehicleName, invoiceSettings, styles, labels } = ctx
+  if (!data.vehicle) return null
 
 
   switch (fieldId) {
   switch (fieldId) {
     case 'vehicle_name':
     case 'vehicle_name':
@@ -137,6 +138,9 @@ function renderServiceField(fieldId: string, ctx: ServiceRenderCtx): React.React
     case 'service_title':
     case 'service_title':
       return <Text key={fieldId} style={styles.infoTextBold}>{data.title}</Text>
       return <Text key={fieldId} style={styles.infoTextBold}>{data.title}</Text>
     case 'service_type':
     case 'service_type':
+      // Counter sales (no vehicle) aren't a service — the default
+      // "maintenance" type would just be noise on the invoice.
+      if (!data.vehicle) return null
       return (
       return (
         <Text key={fieldId} style={styles.infoTextSmall}>
         <Text key={fieldId} style={styles.infoTextSmall}>
           {labels.type ? fillTemplate(labels.type, { type: data.type }) : `Type: ${data.type}`}
           {labels.type ? fillTemplate(labels.type, { type: data.type }) : `Type: ${data.type}`}
@@ -189,12 +193,13 @@ export function CustomerSection({
   const show = (id: string) => !visibleFields || visibleFields.has(id)
   const show = (id: string) => !visibleFields || visibleFields.has(id)
   const fieldOrder = getOrderedFieldIds(visibleFields, DEFAULT_CUSTOMER_FIELDS)
   const fieldOrder = getOrderedFieldIds(visibleFields, DEFAULT_CUSTOMER_FIELDS)
 
 
-  const hasCustomer = data.vehicle.customer &&
+  const invoiceCustomer = data.customer ?? data.vehicle?.customer
+  const hasCustomer = invoiceCustomer &&
     (show('customer_name') ||
     (show('customer_name') ||
-      (show('customer_company') && data.vehicle.customer.company) ||
-      (show('customer_address') && data.vehicle.customer.address) ||
-      (show('customer_email') && data.vehicle.customer.email) ||
-      (show('customer_phone') && data.vehicle.customer.phone))
+      (show('customer_company') && invoiceCustomer.company) ||
+      (show('customer_address') && invoiceCustomer.address) ||
+      (show('customer_email') && invoiceCustomer.email) ||
+      (show('customer_phone') && invoiceCustomer.phone))
 
 
   const hasCf = customFields?.some(cf => cf.value !== '' && cf.value != null)
   const hasCf = customFields?.some(cf => cf.value !== '' && cf.value != null)
 
 
@@ -205,7 +210,7 @@ export function CustomerSection({
   return (
   return (
     <View style={styles.infoBox}>
     <View style={styles.infoBox}>
       <Text style={styles.infoLabel}>{labels.billTo || 'Bill To'}</Text>
       <Text style={styles.infoLabel}>{labels.billTo || 'Bill To'}</Text>
-      {data.vehicle.customer && (
+      {invoiceCustomer && (
         <>
         <>
           {fieldOrder.filter(id => show(id)).map(id => renderCustomerField(id, ctx))}
           {fieldOrder.filter(id => show(id)).map(id => renderCustomerField(id, ctx))}
         </>
         </>
@@ -238,6 +243,8 @@ export function VehicleSection({
   const show = (id: string) => !visibleFields || visibleFields.has(id)
   const show = (id: string) => !visibleFields || visibleFields.has(id)
   const fieldOrder = getOrderedFieldIds(visibleFields, DEFAULT_VEHICLE_FIELDS)
   const fieldOrder = getOrderedFieldIds(visibleFields, DEFAULT_VEHICLE_FIELDS)
 
 
+  if (!data.vehicle) return null
+
   const hasVehicle =
   const hasVehicle =
     show('vehicle_name') ||
     show('vehicle_name') ||
     (show('vin') && data.vehicle.vin) ||
     (show('vin') && data.vehicle.vin) ||

+ 1 - 1
src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx

@@ -90,7 +90,7 @@ export function InvoicePDF({
 
 
   const cc = invoiceSettings?.currencyCode || 'USD'
   const cc = invoiceSettings?.currencyCode || 'USD'
   const cf: 'symbol' | 'code' = invoiceSettings?.currencyFormat === 'code' ? 'code' : 'symbol'
   const cf: 'symbol' | 'code' = invoiceSettings?.currencyFormat === 'code' ? 'code' : 'symbol'
-  const vehicleName = `${data.vehicle.year} ${data.vehicle.make} ${data.vehicle.model}`
+  const vehicleName = data.vehicle ? `${data.vehicle.year} ${data.vehicle.make} ${data.vehicle.model}` : ''
   const partsSubtotal = data.partItems.reduce((sum, p) => sum + p.total, 0)
   const partsSubtotal = data.partItems.reduce((sum, p) => sum + p.total, 0)
   const laborSubtotal = data.laborItems.reduce((sum, l) => sum + l.total, 0)
   const laborSubtotal = data.laborItems.reduce((sum, l) => sum + l.total, 0)
   const computedSubtotal = partsSubtotal + laborSubtotal
   const computedSubtotal = partsSubtotal + laborSubtotal

+ 9 - 1
src/features/vehicles/Components/invoice-pdf/types.ts

@@ -55,6 +55,14 @@ export interface InvoiceData {
   warrantyMileage?: number | null;
   warrantyMileage?: number | null;
   warrantyExpiresAt?: Date | string | null;
   warrantyExpiresAt?: Date | string | null;
   warrantyNotes?: string | null;
   warrantyNotes?: string | null;
+  customer?: {
+    name: string
+    email: string | null
+    phone: string | null
+    address: string | null
+    company: string | null
+    taxId?: string | null
+  } | null
   vehicle: {
   vehicle: {
     make: string
     make: string
     model: string
     model: string
@@ -70,7 +78,7 @@ export interface InvoiceData {
       company: string | null
       company: string | null
       taxId?: string | null
       taxId?: string | null
     } | null
     } | null
-  }
+  } | null
 }
 }
 
 
 export interface WorkshopInfo {
 export interface WorkshopInfo {

+ 3 - 1
src/features/vehicles/Components/service-detail/types.ts

@@ -50,7 +50,9 @@ export interface ServiceDetail {
   laborItems: LaborItem[];
   laborItems: LaborItem[];
   attachments: Attachment[];
   attachments: Attachment[];
   payments: Payment[];
   payments: Payment[];
-  vehicle: Vehicle;
+  // Counter sales (parts-only) have no vehicle and link a customer directly.
+  vehicle: Vehicle | null;
+  customer: Vehicle["customer"];
 }
 }
 
 
 export interface PartItem {
 export interface PartItem {

+ 24 - 20
src/features/vehicles/Components/service-edit/BasicInfoSection.tsx

@@ -17,9 +17,9 @@ interface CustomerInfo {
 
 
 interface BasicInfoSectionProps {
 interface BasicInfoSectionProps {
   initialData: InitialData
   initialData: InitialData
-  vehicleId: string
+  vehicleId: string | null
   vehicleName: string
   vehicleName: string
-  selectedVehicleId: string
+  selectedVehicleId: string | null
   setSelectedVehicleId: (id: string) => void
   setSelectedVehicleId: (id: string) => void
   techName: string
   techName: string
   customer?: CustomerInfo | null
   customer?: CustomerInfo | null
@@ -45,16 +45,18 @@ export function BasicInfoSection({
       <div className="space-y-1">
       <div className="space-y-1">
         <div className="flex items-center justify-between">
         <div className="flex items-center justify-between">
           <Label className="text-xs">{t('vehicle')}</Label>
           <Label className="text-xs">{t('vehicle')}</Label>
-          <Link
-            href={`/vehicles/${selectedVehicleId}`}
-            className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
-          >
-            {t('open')}
-            <ExternalLink className="h-3 w-3" />
-          </Link>
+          {selectedVehicleId && (
+            <Link
+              href={`/vehicles/${selectedVehicleId}`}
+              className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
+            >
+              {t('open')}
+              <ExternalLink className="h-3 w-3" />
+            </Link>
+          )}
         </div>
         </div>
         <VehicleCombobox
         <VehicleCombobox
-          value={selectedVehicleId}
+          value={selectedVehicleId ?? ''}
           initialVehicle={initialVehicle ? { ...initialVehicle, customerId: null, customer: null } : null}
           initialVehicle={initialVehicle ? { ...initialVehicle, customerId: null, customer: null } : null}
           placeholder={vehicleName || t('searchVehicles')}
           placeholder={vehicleName || t('searchVehicles')}
           noneLabel={t('noVehicleFound')}
           noneLabel={t('noVehicleFound')}
@@ -88,16 +90,18 @@ export function BasicInfoSection({
         </div>
         </div>
       )}
       )}
 
 
-      <div className="space-y-1">
-        <Label htmlFor="mileage" className="text-xs">{serviceType === 'marine' ? t('mileageMarine') : t('mileage')}</Label>
-        <Input
-          id="mileage"
-          name="mileage"
-          type="number"
-          placeholder="50000"
-          defaultValue={initialData.mileage ?? ''}
-        />
-      </div>
+      {selectedVehicleId && (
+        <div className="space-y-1">
+          <Label htmlFor="mileage" className="text-xs">{serviceType === 'marine' ? t('mileageMarine') : t('mileage')}</Label>
+          <Input
+            id="mileage"
+            name="mileage"
+            type="number"
+            placeholder="50000"
+            defaultValue={initialData.mileage ?? ''}
+          />
+        </div>
+      )}
 
 
       <input type="hidden" name="techName" value={techName} />
       <input type="hidden" name="techName" value={techName} />
     </div>
     </div>

+ 20 - 15
src/features/vehicles/Components/service-edit/InvoiceDetailsSection.tsx

@@ -21,6 +21,8 @@ interface InvoiceDetailsSectionProps {
   initialData: InitialData
   initialData: InitialData
   type: string
   type: string
   setType: (type: string) => void
   setType: (type: string) => void
+  /** Counter sales (no vehicle) have no meaningful service type; hide the field. */
+  showType?: boolean
   status: string
   status: string
   setStatus: (status: string) => void
   setStatus: (status: string) => void
   onDirty?: () => void
   onDirty?: () => void
@@ -33,6 +35,7 @@ export function InvoiceDetailsSection({
   initialData,
   initialData,
   type,
   type,
   setType,
   setType,
+  showType = true,
   status,
   status,
   setStatus,
   setStatus,
   onDirty,
   onDirty,
@@ -93,21 +96,23 @@ export function InvoiceDetailsSection({
         value={initialData.serviceDate || new Date().toISOString().split('T')[0]}
         value={initialData.serviceDate || new Date().toISOString().split('T')[0]}
       />
       />
 
 
-      <div className="grid grid-cols-2 gap-2">
-        <div className="space-y-1">
-          <Label className="text-xs">{t('type')}</Label>
-          <Select value={type} onValueChange={setType}>
-            <SelectTrigger className="w-full">
-              <SelectValue />
-            </SelectTrigger>
-            <SelectContent>
-              <SelectItem value="maintenance">{t('typeOptions.maintenance')}</SelectItem>
-              <SelectItem value="repair">{t('typeOptions.repair')}</SelectItem>
-              <SelectItem value="upgrade">{t('typeOptions.upgrade')}</SelectItem>
-              <SelectItem value="inspection">{t('typeOptions.inspection')}</SelectItem>
-            </SelectContent>
-          </Select>
-        </div>
+      <div className={showType ? "grid grid-cols-2 gap-2" : "grid grid-cols-1 gap-2"}>
+        {showType && (
+          <div className="space-y-1">
+            <Label className="text-xs">{t('type')}</Label>
+            <Select value={type} onValueChange={setType}>
+              <SelectTrigger className="w-full">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="maintenance">{t('typeOptions.maintenance')}</SelectItem>
+                <SelectItem value="repair">{t('typeOptions.repair')}</SelectItem>
+                <SelectItem value="upgrade">{t('typeOptions.upgrade')}</SelectItem>
+                <SelectItem value="inspection">{t('typeOptions.inspection')}</SelectItem>
+              </SelectContent>
+            </Select>
+          </div>
+        )}
         <div className="space-y-1">
         <div className="space-y-1">
           <Label className="text-xs">{t('status')}</Label>
           <Label className="text-xs">{t('status')}</Label>
           <Select value={status} onValueChange={setStatus}>
           <Select value={status} onValueChange={setStatus}>

+ 10 - 8
src/features/vehicles/Components/service-page/DetailsLeftColumn.tsx

@@ -22,7 +22,7 @@ interface DetailsLeftColumnProps {
   onOpenPresets?: () => void
   onOpenPresets?: () => void
   onScanBarcode?: () => void
   onScanBarcode?: () => void
   aiEnabled?: boolean
   aiEnabled?: boolean
-  vehicleId: string
+  vehicleId: string | null
   findings?: { id: string; description: string; severity: string; status: string; notes: string | null }[]
   findings?: { id: string; description: string; severity: string; status: string; notes: string | null }[]
   onAddFinding?: () => void
   onAddFinding?: () => void
   onEditFinding?: (finding: { id: string; description: string; severity: string; status: string; notes: string | null }) => void
   onEditFinding?: (finding: { id: string; description: string; severity: string; status: string; notes: string | null }) => void
@@ -84,13 +84,15 @@ export function DetailsLeftColumn({
         serviceRecordId={record.id}
         serviceRecordId={record.id}
         aiEnabled={aiEnabled}
         aiEnabled={aiEnabled}
       />
       />
-      <ServiceFindingsSection
-        vehicleId={vehicleId}
-        serviceRecordId={record.id}
-        findings={findings}
-        onAddFinding={onAddFinding}
-        onEditFinding={onEditFinding}
-      />
+      {vehicleId && (
+        <ServiceFindingsSection
+          vehicleId={vehicleId}
+          serviceRecordId={record.id}
+          findings={findings}
+          onAddFinding={onAddFinding}
+          onEditFinding={onEditFinding}
+        />
+      )}
       <div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
       <div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
         <PaymentsSection
         <PaymentsSection
           payments={record.payments || []}
           payments={record.payments || []}

+ 4 - 3
src/features/vehicles/Components/service-page/DetailsRightColumn.tsx

@@ -19,7 +19,7 @@ interface DetailsRightColumnProps {
   formState: ReturnType<typeof useServiceFormState>
   formState: ReturnType<typeof useServiceFormState>
   actions: ReturnType<typeof useServiceActions>
   actions: ReturnType<typeof useServiceActions>
   record: ServiceDetail
   record: ServiceDetail
-  vehicleId: string
+  vehicleId: string | null
   organizationId: string
   organizationId: string
   currencyCode: string
   currencyCode: string
   taxEnabled: boolean
   taxEnabled: boolean
@@ -29,7 +29,7 @@ interface DetailsRightColumnProps {
     model: string
     model: string
     year: number
     year: number
     licensePlate: string | null
     licensePlate: string | null
-  }
+  } | null
   boardTechnicians: BoardTechnicianOption[]
   boardTechnicians: BoardTechnicianOption[]
   orgMembers?: OrgMemberOption[]
   orgMembers?: OrgMemberOption[]
   notificationHistory?: {
   notificationHistory?: {
@@ -74,6 +74,7 @@ export function DetailsRightColumn({
       )}
       )}
       <InvoiceDetailsSection
       <InvoiceDetailsSection
         initialData={formState.initialData}
         initialData={formState.initialData}
+        showType={!!record.vehicle}
         type={formState.type}
         type={formState.type}
         setType={formState.dirtySetType}
         setType={formState.dirtySetType}
         status={formState.status}
         status={formState.status}
@@ -90,7 +91,7 @@ export function DetailsRightColumn({
         selectedVehicleId={formState.selectedVehicleId}
         selectedVehicleId={formState.selectedVehicleId}
         setSelectedVehicleId={formState.dirtySetSelectedVehicleId}
         setSelectedVehicleId={formState.dirtySetSelectedVehicleId}
         techName={formState.techName}
         techName={formState.techName}
-        customer={record.vehicle.customer}
+        customer={record.customer ?? record.vehicle?.customer}
         initialVehicle={initialVehicle}
         initialVehicle={initialVehicle}
       />
       />
       <ScheduleTimesSection
       <ScheduleTimesSection

+ 25 - 19
src/features/vehicles/Components/service-page/ServicePageClient.tsx

@@ -89,6 +89,9 @@ export function ServicePageClient({
   const t = useTranslations('service')
   const t = useTranslations('service')
   const router = useRouter()
   const router = useRouter()
 
 
+  // Counter sales carry the customer directly; vehicle jobs resolve it via the vehicle.
+  const customer = record.customer ?? record.vehicle?.customer ?? null
+
   const validTabs: ServiceTab[] = ['details', 'images', 'video', 'documents', 'statusReports']
   const validTabs: ServiceTab[] = ['details', 'images', 'video', 'documents', 'statusReports']
   const resolvedInitialTab =
   const resolvedInitialTab =
     initialTab && validTabs.includes(initialTab as ServiceTab)
     initialTab && validTabs.includes(initialTab as ServiceTab)
@@ -203,15 +206,17 @@ export function ServicePageClient({
   const [notifyMessage, setNotifyMessage] = useState('')
   const [notifyMessage, setNotifyMessage] = useState('')
 
 
   const handleNotifyCustomer = useCallback(async () => {
   const handleNotifyCustomer = useCallback(async () => {
-    if (!record.vehicle.customer) return
+    if (!customer) return
     const templateKey =
     const templateKey =
       statusTemplateKeys[formState.status] || SETTING_KEYS.SMS_TEMPLATE_STATUS_READY
       statusTemplateKeys[formState.status] || SETTING_KEYS.SMS_TEMPLATE_STATUS_READY
     const tplResult = await getSmsTemplates()
     const tplResult = await getSmsTemplates()
     const tplData = tplResult.success && tplResult.data ? tplResult.data : null
     const tplData = tplResult.success && tplResult.data ? tplResult.data : null
     const tpl = tplData?.templates[templateKey] || SMS_TEMPLATE_DEFAULTS[templateKey] || ''
     const tpl = tplData?.templates[templateKey] || SMS_TEMPLATE_DEFAULTS[templateKey] || ''
-    const vehicle = `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`
+    const vehicle = record.vehicle
+      ? `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`
+      : record.title
     const message = interpolateSmsTemplate(tpl, {
     const message = interpolateSmsTemplate(tpl, {
-      customer_name: record.vehicle.customer.name,
+      customer_name: customer.name,
       vehicle,
       vehicle,
       company_name: tplData?.companyName || '',
       company_name: tplData?.companyName || '',
       current_user: tplData?.currentUser || '',
       current_user: tplData?.currentUser || '',
@@ -220,10 +225,9 @@ export function ServicePageClient({
     setShowNotifyDialog(true)
     setShowNotifyDialog(true)
   }, [
   }, [
     formState.status,
     formState.status,
-    record.vehicle.customer,
-    record.vehicle.year,
-    record.vehicle.make,
-    record.vehicle.model,
+    customer,
+    record.vehicle,
+    record.title,
   ]) // eslint-disable-line react-hooks/exhaustive-deps
   ]) // eslint-disable-line react-hooks/exhaustive-deps
 
 
   // Observations state
   // Observations state
@@ -326,7 +330,7 @@ export function ServicePageClient({
           actions.setShowShareDialog(true)
           actions.setShowShareDialog(true)
         }}
         }}
         onNotifyCustomer={handleNotifyCustomer}
         onNotifyCustomer={handleNotifyCustomer}
-        hasCustomer={!!record.vehicle.customer}
+        hasCustomer={!!customer}
       />
       />
 
 
       {activeTab === 'details' && (
       {activeTab === 'details' && (
@@ -380,6 +384,7 @@ export function ServicePageClient({
               }
               }
             />
             />
           </form>
           </form>
+          {vehicleId && (
           <ObservationsManager
           <ObservationsManager
             vehicleId={vehicleId}
             vehicleId={vehicleId}
             serviceRecordId={record.id}
             serviceRecordId={record.id}
@@ -390,6 +395,7 @@ export function ServicePageClient({
               obsControlsRef.current = c
               obsControlsRef.current = c
             }}
             }}
           />
           />
+          )}
         </>
         </>
       )}
       )}
 
 
@@ -427,13 +433,13 @@ export function ServicePageClient({
             organizationId={organizationId}
             organizationId={organizationId}
             vehicleName={formState.vehicleName}
             vehicleName={formState.vehicleName}
             customer={
             customer={
-              record.vehicle.customer
+              customer
                 ? {
                 ? {
-                    id: record.vehicle.customer.id,
-                    name: record.vehicle.customer.name,
-                    email: record.vehicle.customer.email,
-                    phone: record.vehicle.customer.phone,
-                    telegramChatId: record.vehicle.customer.telegramChatId || null,
+                    id: customer.id,
+                    name: customer.name,
+                    email: customer.email,
+                    phone: customer.phone,
+                    telegramChatId: customer.telegramChatId || null,
                   }
                   }
                 : null
                 : null
             }
             }
@@ -479,7 +485,7 @@ export function ServicePageClient({
       <SendEmailDialog
       <SendEmailDialog
         open={actions.showEmailDialog}
         open={actions.showEmailDialog}
         onOpenChange={actions.setShowEmailDialog}
         onOpenChange={actions.setShowEmailDialog}
-        defaultEmail={record.vehicle.customer?.email || ''}
+        defaultEmail={customer?.email || ''}
         entityLabel={t('invoice.entityLabel')}
         entityLabel={t('invoice.entityLabel')}
         onSend={async (email, message) =>
         onSend={async (email, message) =>
           sendInvoiceEmail({ serviceRecordId: record.id, recipientEmail: email, message })
           sendInvoiceEmail({ serviceRecordId: record.id, recipientEmail: email, message })
@@ -492,17 +498,17 @@ export function ServicePageClient({
         recordId={record.id}
         recordId={record.id}
         organizationId={organizationId}
         organizationId={organizationId}
         initialToken={record.publicToken}
         initialToken={record.publicToken}
-        customer={record.vehicle.customer}
+        customer={customer}
         smsEnabled={smsEnabled}
         smsEnabled={smsEnabled}
         emailEnabled={emailEnabled}
         emailEnabled={emailEnabled}
       />
       />
 
 
-      {record.vehicle.customer && (
+      {customer && (
         <>
         <>
           <NotifyCustomerDialog
           <NotifyCustomerDialog
             open={actions.showPaymentNotifyDialog}
             open={actions.showPaymentNotifyDialog}
             onOpenChange={actions.setShowPaymentNotifyDialog}
             onOpenChange={actions.setShowPaymentNotifyDialog}
-            customer={record.vehicle.customer}
+            customer={customer}
             defaultMessage={actions.paymentNotifyMessage}
             defaultMessage={actions.paymentNotifyMessage}
             emailSubject={t('invoice.emailSubject')}
             emailSubject={t('invoice.emailSubject')}
             smsEnabled={smsEnabled}
             smsEnabled={smsEnabled}
@@ -513,7 +519,7 @@ export function ServicePageClient({
           <NotifyCustomerDialog
           <NotifyCustomerDialog
             open={showNotifyDialog}
             open={showNotifyDialog}
             onOpenChange={setShowNotifyDialog}
             onOpenChange={setShowNotifyDialog}
-            customer={record.vehicle.customer}
+            customer={customer}
             defaultMessage={notifyMessage}
             defaultMessage={notifyMessage}
             emailSubject={t('invoice.statusEmailSubject')}
             emailSubject={t('invoice.statusEmailSubject')}
             smsEnabled={smsEnabled}
             smsEnabled={smsEnabled}

+ 291 - 0
src/features/vehicles/Components/service-page/ServiceRecordPage.tsx

@@ -0,0 +1,291 @@
+import { getServiceRecord } from '@/features/vehicles/Actions/serviceActions'
+import { getSettings } from '@/features/settings/Actions/settingsActions'
+import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
+import { readPartsPricingSettings } from '@/features/inventory/Lib/partPricing'
+import { getInventoryPartsList } from '@/features/inventory/Actions/inventoryActions'
+import { getLaborPresetsList } from '@/features/labor-presets/Actions/laborPresetActions'
+
+import { getTechnicians, getOrgMembers } from '@/features/workboard/Actions/technicianActions'
+import { getAuthContext } from '@/lib/get-auth-context'
+import { getFeatures } from '@/lib/features'
+import { getStatusReportsForService } from '@/features/status-reports/Actions/getStatusReportsForService'
+import { getServiceFindings } from '@/features/vehicles/Actions/findingActions'
+import { db } from '@/lib/db'
+import { getCachedSession, getCachedMembership } from '@/lib/cached-session'
+import { ServicePageClient } from '@/features/vehicles/Components/service-page/ServicePageClient'
+import { PageHeader } from '@/components/page-header'
+import { getTranslations } from 'next-intl/server'
+
+/**
+ * Shared server component behind both service-record routes:
+ * /vehicles/[id]/service/[serviceId] and /sales/[serviceId] (counter sales
+ * without a vehicle). The record itself decides which mode applies.
+ */
+export async function ServiceRecordPage({
+  serviceId,
+  initialTab,
+}: {
+  serviceId: string
+  initialTab?: string
+}) {
+  const [
+    result,
+    settingsResult,
+    inventoryResult,
+    techniciansResult,
+    presetsResult,
+    authContext,
+    session,
+    orgMembersResult,
+    statusReportsResult,
+    findingsResult,
+  ] = await Promise.all([
+    getServiceRecord(serviceId),
+    getSettings([
+      SETTING_KEYS.CURRENCY_CODE,
+      SETTING_KEYS.UNIT_SYSTEM,
+      SETTING_KEYS.DEFAULT_TAX_RATE,
+      SETTING_KEYS.TAX_ENABLED,
+      SETTING_KEYS.DEFAULT_LABOR_RATE,
+      SETTING_KEYS.INVOICE_DUE_DAYS,
+      SETTING_KEYS.PARTS_DEFAULT_MARKUP_PERCENT,
+      SETTING_KEYS.PARTS_MARKUP_APPLIES_TO_INVENTORY,
+    ]),
+    getInventoryPartsList(),
+    getTechnicians(),
+    getLaborPresetsList(),
+    getAuthContext(),
+    getCachedSession(),
+    getOrgMembers(),
+    getStatusReportsForService(serviceId),
+    getServiceFindings(serviceId),
+  ])
+
+  if (!result.success || !result.data) {
+    return (
+      <>
+        <PageHeader />
+        <div className="flex h-[50vh] items-center justify-center">
+          <p className="text-muted-foreground">
+            {result.error || (await getTranslations('service.page'))('notFound')}
+          </p>
+        </div>
+      </>
+    )
+  }
+
+  const record = result.data
+  const vehicleId = record.vehicleId
+  const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {}
+  const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || 'USD'
+  const unitSystem = (settings[SETTING_KEYS.UNIT_SYSTEM] || 'imperial') as 'metric' | 'imperial'
+  const taxEnabled = settings[SETTING_KEYS.TAX_ENABLED] !== 'false'
+  const defaultTaxRate = taxEnabled ? Number(settings[SETTING_KEYS.DEFAULT_TAX_RATE]) || 0 : 0
+  const defaultLaborRate = Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0
+  const defaultDueDays = Number(settings[SETTING_KEYS.INVOICE_DUE_DAYS]) || 0
+  const { defaultMarkupPercent, markupAppliesToInventory } = readPartsPricingSettings(settings, {
+    defaultMarkupPercent: SETTING_KEYS.PARTS_DEFAULT_MARKUP_PERCENT,
+    markupAppliesToInventory: SETTING_KEYS.PARTS_MARKUP_APPLIES_TO_INVENTORY,
+  })
+  const inventoryParts = inventoryResult.success && inventoryResult.data ? inventoryResult.data : []
+  const laborPresets = presetsResult.success && presetsResult.data ? presetsResult.data : []
+  const initialVehicle = record.vehicle
+    ? {
+        id: record.vehicle.id,
+        make: record.vehicle.make,
+        model: record.vehicle.model,
+        year: record.vehicle.year,
+        licensePlate: record.vehicle.licensePlate,
+      }
+    : null
+  const boardTechnicians = (
+    techniciansResult.success && techniciansResult.data ? techniciansResult.data : []
+  ).map((t) => ({ id: t.id, name: t.name, userId: t.userId }))
+  const organizationId = authContext?.organizationId || ''
+
+  // Fetch team members and features
+  const membership = session?.user?.id ? await getCachedMembership(session.user.id) : null
+  const orgId = membership?.organizationId
+
+  const [currentUser, features, aiSettings] = await Promise.all([
+    session?.user?.id
+      ? db.user.findUnique({
+          where: { id: session.user.id },
+          select: { name: true },
+        })
+      : Promise.resolve(null),
+    orgId ? getFeatures(orgId) : Promise.resolve(null),
+    orgId
+      ? db.appSetting.findMany({
+          where: {
+            organizationId: orgId,
+            key: { in: [SETTING_KEYS.AI_ENABLED, SETTING_KEYS.AI_API_KEY] },
+          },
+          select: { key: true, value: true },
+        })
+      : Promise.resolve([]),
+  ])
+
+  const currentUserName = currentUser?.name || ''
+  const aiSettingsMap = Object.fromEntries(aiSettings.map((s) => [s.key, s.value]))
+  const aiEnabled =
+    features?.ai === true &&
+    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,
+    description: record.description || '',
+    type: record.type,
+    status: record.status,
+    mileage: record.mileage,
+    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: effectiveInvoiceDate.toISOString().split('T')[0],
+    invoiceDueDate: isRenderable(record.invoiceDueDate)
+      ? record.invoiceDueDate.toISOString().split('T')[0]
+      : defaultDueDays > 0
+        ? new Date(effectiveInvoiceDate.getTime() + defaultDueDays * 86400000)
+            .toISOString()
+            .split('T')[0]
+        : '',
+    partItems: record.partItems.map((p) => ({
+      partNumber: p.partNumber || '',
+      name: p.name,
+      quantity: p.quantity,
+      unitPrice: p.unitPrice,
+      total: p.total,
+      unitCost: p.unitCost ?? 0,
+      markupPercent: p.markupPercent ?? 0,
+      // Preserve the inventory link so editing/saving a work order keeps
+      // deducting the right stock. Without this, a saved edit sends unlinked
+      // parts and inventory reconciliation restocks the "removed" part.
+      inventoryPartId: p.inventoryPartId ?? undefined,
+    })),
+    laborItems: record.laborItems.map((l) => ({
+      description: l.description,
+      hours: l.hours,
+      rate: l.rate,
+      total: l.total,
+      pricingType: (l.pricingType as 'hourly' | 'service') || 'hourly',
+    })),
+    attachments: [],
+    subtotal: record.subtotal,
+    taxRate: record.taxRate,
+    taxAmount: record.taxAmount,
+    taxInclusive: record.taxInclusive,
+    totalAmount: record.totalAmount,
+    discountType: record.discountType || undefined,
+    discountValue: record.discountValue,
+    discountAmount: record.discountAmount,
+    warrantyMonths: record.warrantyMonths ?? null,
+    warrantyMileage: record.warrantyMileage ?? null,
+    warrantyNotes: record.warrantyNotes ?? null,
+  }
+
+  // Prepare media attachments for managers
+  // The Prisma query returns includeInInvoice on each attachment; cast to include it
+  const allAttachments =
+    (record.attachments as ((typeof record.attachments)[number] & {
+      includeInInvoice: boolean
+    })[]) || []
+  const imageAttachmentsForManager = allAttachments
+    .filter((a) => a.category === 'image')
+    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
+  const videoAttachments = allAttachments
+    .filter((a) => a.category === 'video')
+    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
+  const documentAttachments = allAttachments
+    .filter((a) => a.category === 'document' || a.category === 'diagnostic')
+    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
+
+  // Fetch notification history for this service record
+  const notificationHistory = await db.smsMessage.findMany({
+    where: {
+      relatedEntityId: serviceId,
+      relatedEntityType: 'service-record',
+      direction: 'outbound',
+    },
+    select: { id: true, body: true, status: true, createdAt: true, toNumber: true },
+    orderBy: { createdAt: 'desc' },
+    take: 10,
+  })
+
+  // Fetch open observations for this vehicle (not just this service).
+  // Counter sales have no vehicle, so there is nothing to look up.
+  const openObservations = vehicleId
+    ? await db.vehicleFinding.findMany({
+        where: { vehicleId, status: { not: 'resolved' } },
+        select: { id: true, description: true, severity: true, notes: true, serviceRecordId: true },
+        orderBy: { createdAt: 'desc' },
+      })
+    : []
+
+  return (
+    <div className="flex h-svh flex-col overflow-hidden">
+      <PageHeader />
+      <ServicePageClient
+        record={result.data}
+        vehicleId={vehicleId}
+        organizationId={organizationId}
+        initialTab={initialTab}
+        currencyCode={currencyCode}
+        unitSystem={unitSystem}
+        defaultTaxRate={defaultTaxRate}
+        taxEnabled={taxEnabled}
+        defaultLaborRate={defaultLaborRate}
+        initialData={initialData}
+        inventoryParts={inventoryParts}
+        laborPresets={laborPresets}
+        initialVehicle={initialVehicle}
+        boardTechnicians={boardTechnicians}
+        orgMembers={orgMembersResult.success && orgMembersResult.data ? orgMembersResult.data : []}
+        currentUserName={currentUserName}
+        imageAttachmentsForManager={imageAttachmentsForManager}
+        videoAttachments={videoAttachments}
+        documentAttachments={documentAttachments}
+        maxImagesPerService={features?.maxImagesPerService ?? 999999}
+        maxDiagnosticsPerService={features?.maxDiagnosticsPerService ?? 999999}
+        maxDocumentsPerService={features?.maxDocumentsPerService ?? 999999}
+        smsEnabled={features?.sms ?? false}
+        emailEnabled={features?.smtp ?? false}
+        telegramEnabled={features?.telegram ?? false}
+        aiEnabled={aiEnabled}
+        defaultDueDays={defaultDueDays}
+        defaultMarkupPercent={defaultMarkupPercent}
+        markupAppliesToInventory={markupAppliesToInventory}
+        statusReports={(statusReportsResult.success && statusReportsResult.data
+          ? statusReportsResult.data
+          : []
+        ).map((r) => ({
+          ...r,
+          createdAt: r.createdAt.toISOString(),
+          expiresAt: r.expiresAt?.toISOString() || null,
+          feedbackAt: r.feedbackAt?.toISOString() || null,
+          sentAt: r.sentAt?.toISOString() || null,
+        }))}
+        findings={findingsResult.success && findingsResult.data ? findingsResult.data : []}
+        openObservations={openObservations}
+        notificationHistory={notificationHistory.map((n) => ({
+          ...n,
+          createdAt: n.createdAt.toISOString(),
+        }))}
+      />
+    </div>
+  )
+}

+ 2 - 2
src/features/vehicles/Components/service-page/UnifiedServiceHeader.tsx

@@ -28,7 +28,7 @@ export interface TabCounts {
 }
 }
 
 
 interface UnifiedServiceHeaderProps {
 interface UnifiedServiceHeaderProps {
-  vehicleId: string
+  vehicleId: string | null
   vehicleName: string
   vehicleName: string
   title: string
   title: string
   status: string
   status: string
@@ -81,7 +81,7 @@ export function UnifiedServiceHeader({
     <div className="shrink-0 border-b bg-background px-4 pt-2 pb-0">
     <div className="shrink-0 border-b bg-background px-4 pt-2 pb-0">
       <div className="mb-2 flex flex-wrap items-center justify-between gap-2">
       <div className="mb-2 flex flex-wrap items-center justify-between gap-2">
         <Link
         <Link
-          href={`/vehicles/${vehicleId}`}
+          href={vehicleId ? `/vehicles/${vehicleId}` : '/work-orders'}
           className="flex min-w-0 items-center gap-3 text-foreground transition-colors hover:text-muted-foreground"
           className="flex min-w-0 items-center gap-3 text-foreground transition-colors hover:text-muted-foreground"
         >
         >
           <ArrowLeft className="h-4 w-4 shrink-0" />
           <ArrowLeft className="h-4 w-4 shrink-0" />

+ 2 - 2
src/features/vehicles/Components/service-page/service-page-types.ts

@@ -29,7 +29,7 @@ export interface Attachment {
 
 
 export interface ServicePageClientProps {
 export interface ServicePageClientProps {
   record: ServiceDetail
   record: ServiceDetail
-  vehicleId: string
+  vehicleId: string | null
   organizationId: string
   organizationId: string
   currencyCode: string
   currencyCode: string
   unitSystem: 'metric' | 'imperial'
   unitSystem: 'metric' | 'imperial'
@@ -38,7 +38,7 @@ export interface ServicePageClientProps {
   defaultLaborRate: number
   defaultLaborRate: number
   initialData: InitialData
   initialData: InitialData
   inventoryParts: InventoryPartOption[]
   inventoryParts: InventoryPartOption[]
-  initialVehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null }
+  initialVehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null } | null
   boardTechnicians?: BoardTechnicianOption[]
   boardTechnicians?: BoardTechnicianOption[]
   orgMembers?: OrgMemberOption[]
   orgMembers?: OrgMemberOption[]
   currentUserName: string
   currentUserName: string

+ 6 - 6
src/features/vehicles/Components/service-page/useServiceActions.ts

@@ -27,7 +27,7 @@ export function useServiceActions({
   formState,
   formState,
 }: {
 }: {
   record: ServiceDetail
   record: ServiceDetail
-  vehicleId: string
+  vehicleId: string | null
   currencyCode: string
   currencyCode: string
   formState: FormState
   formState: FormState
 }) {
 }) {
@@ -151,7 +151,7 @@ export function useServiceActions({
     if (result.success) {
     if (result.success) {
       setHasUnsavedChanges(false)
       setHasUnsavedChanges(false)
       flashSaved()
       flashSaved()
-      if (selectedVehicleId !== vehicleId) {
+      if (selectedVehicleId && selectedVehicleId !== vehicleId) {
         router.push(`/vehicles/${selectedVehicleId}/service/${initialData.id}`)
         router.push(`/vehicles/${selectedVehicleId}/service/${initialData.id}`)
       } else {
       } else {
         router.refresh()
         router.refresh()
@@ -179,7 +179,7 @@ export function useServiceActions({
     const result = await deleteServiceRecord(record.id)
     const result = await deleteServiceRecord(record.id)
     if (result.success) {
     if (result.success) {
       setHasUnsavedChanges(false)
       setHasUnsavedChanges(false)
-      router.push(`/vehicles/${vehicleId}`)
+      router.push(vehicleId ? `/vehicles/${vehicleId}` : '/work-orders')
       router.refresh()
       router.refresh()
     } else {
     } else {
       isSavingRef.current = false
       isSavingRef.current = false
@@ -238,7 +238,7 @@ export function useServiceActions({
       interpolateSmsTemplate(tpl, {
       interpolateSmsTemplate(tpl, {
         amount,
         amount,
         invoice_number: invoiceNum,
         invoice_number: invoiceNum,
-        customer_name: record.vehicle.customer?.name || '',
+        customer_name: (record.customer ?? record.vehicle?.customer)?.name || '',
         company_name: tplData?.companyName || '',
         company_name: tplData?.companyName || '',
         current_user: tplData?.currentUser || '',
         current_user: tplData?.currentUser || '',
       })
       })
@@ -258,7 +258,7 @@ export function useServiceActions({
     if (result.success) {
     if (result.success) {
       toast.success(t('payments.recorded'))
       toast.success(t('payments.recorded'))
       router.refresh()
       router.refresh()
-      if (record.vehicle.customer) {
+      if (record.customer ?? record.vehicle?.customer) {
         await buildPaymentNotifyMessage(formatCurrency(data.amount, currencyCode))
         await buildPaymentNotifyMessage(formatCurrency(data.amount, currencyCode))
       }
       }
       return true
       return true
@@ -277,7 +277,7 @@ export function useServiceActions({
         result.data?.manuallyPaid ? t('payments.markedPaid') : t('payments.markedUnpaid')
         result.data?.manuallyPaid ? t('payments.markedPaid') : t('payments.markedUnpaid')
       )
       )
       router.refresh()
       router.refresh()
-      if (result.data?.manuallyPaid && record.vehicle.customer) {
+      if (result.data?.manuallyPaid && (record.customer ?? record.vehicle?.customer)) {
         await buildPaymentNotifyMessage('')
         await buildPaymentNotifyMessage('')
       }
       }
     } else {
     } else {

+ 3 - 3
src/features/vehicles/Components/service-page/useServiceFormState.ts

@@ -15,7 +15,7 @@ export function useServiceFormState({
   currentUserName,
   currentUserName,
   record,
   record,
 }: {
 }: {
-  vehicleId: string
+  vehicleId: string | null
   initialData: InitialData
   initialData: InitialData
   defaultTaxRate: number
   defaultTaxRate: number
   currentUserName: string
   currentUserName: string
@@ -23,7 +23,7 @@ export function useServiceFormState({
 }) {
 }) {
   // Form state
   // Form state
   const [loading, setLoading] = useState(false)
   const [loading, setLoading] = useState(false)
-  const [selectedVehicleId, setSelectedVehicleId] = useState(vehicleId)
+  const [selectedVehicleId, setSelectedVehicleId] = useState<string | null>(vehicleId)
 
 
   const [techName] = useState(initialData.techName || currentUserName)
   const [techName] = useState(initialData.techName || currentUserName)
   const [type, setType] = useState(initialData.type || 'maintenance')
   const [type, setType] = useState(initialData.type || 'maintenance')
@@ -143,7 +143,7 @@ export function useServiceFormState({
         ? 'paid'
         ? 'paid'
         : 'partial'
         : 'partial'
   const imageAttachments = record.attachments?.filter((a) => a.category === 'image') || []
   const imageAttachments = record.attachments?.filter((a) => a.category === 'image') || []
-  const vehicleName = `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`
+  const vehicleName = record.vehicle ? `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}` : ''
 
 
   // Part/labor update helpers
   // Part/labor update helpers
   //
   //

+ 3 - 1
src/features/vehicles/Schema/serviceSchema.ts

@@ -30,7 +30,9 @@ export const serviceAttachmentSchema = z.object({
 });
 });
 
 
 export const createServiceSchema = z.object({
 export const createServiceSchema = z.object({
-  vehicleId: z.string(),
+  // null = parts-only / counter sale (no vehicle); customerId is required then
+  vehicleId: z.string().nullable(),
+  customerId: z.string().nullable().optional(),
   title: z.string().min(1, "Title is required"),
   title: z.string().min(1, "Title is required"),
   description: z.string().optional(),
   description: z.string().optional(),
   type: z.enum(["maintenance", "repair", "upgrade", "inspection"]).default("maintenance"),
   type: z.enum(["maintenance", "repair", "upgrade", "inspection"]).default("maintenance"),

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio