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

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 1 месяц назад
Родитель
Сommit
2f66d718bb
100 измененных файлов с 1973 добавлено и 972 удалено
  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_update": "Kunde aktualisiert",
     "customer_delete": "Kunde gelöscht",
+    "customer_backfillNumbers": "Kundennummern nachgetragen",
     "inventory_create": "Lagerteil erstellt",
     "inventory_update": "Lagerteil aktualisiert",
     "inventory_delete": "Lagerteil gelöscht",

+ 12 - 2
messages/de/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Gesamt"
     },
     "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": {
     "title": "Fahrzeug für Arbeitsauftrag auswählen",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Kundenname ist erforderlich",
     "failedCreateCustomer": "Kunde 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": {
     "title": "{name} benachrichtigen",

+ 1 - 0
messages/en/audit.json

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

+ 12 - 2
messages/en/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
     },
     "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": {
     "title": "Select Vehicle for Work Order",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Customer name is required",
     "failedCreateCustomer": "Failed to create customer",
     "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": {
     "title": "Notify {name}",

+ 1 - 0
messages/es/audit.json

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

+ 12 - 2
messages/es/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
     },
     "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": {
     "title": "Seleccionar Vehículo para Orden de Trabajo",
@@ -61,7 +67,11 @@
     "customerNameRequired": "El nombre del cliente es requerido",
     "failedCreateCustomer": "Error al crear cliente",
     "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": {
     "title": "Notificar a {name}",

+ 1 - 0
messages/fr/audit.json

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

+ 12 - 2
messages/fr/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
     },
     "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": {
     "title": "Sélectionner un Véhicule pour l'Ordre de Travail",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Le nom du client est requis",
     "failedCreateCustomer": "Impossible de créer le client",
     "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": {
     "title": "Notifier {name}",

+ 1 - 0
messages/it/audit.json

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

+ 12 - 2
messages/it/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Totale"
     },
     "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": {
     "title": "Seleziona Veicolo per Ordine di Lavoro",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Il nome del cliente è obbligatorio",
     "failedCreateCustomer": "Impossibile creare il cliente",
     "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": {
     "title": "Notifica {name}",

+ 1 - 0
messages/lt/audit.json

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

+ 12 - 2
messages/lt/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Suma"
     },
     "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": {
     "title": "Pasirinkite transporto priemonę darbo užsakymui",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Kliento vardas yra privalomas",
     "failedCreateCustomer": "Nepavyko sukurti kliento",
     "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": {
     "title": "Pranešti {name}",

+ 1 - 0
messages/nb/audit.json

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

+ 12 - 2
messages/nb/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Total"
     },
     "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": {
     "title": "Velg kjøretøy for arbeidsordre",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Kundenavn er påkrevd",
     "failedCreateCustomer": "Kunne ikke opprette kunde",
     "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": {
     "title": "Varsle {name}",

+ 1 - 0
messages/nl/audit.json

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

+ 12 - 2
messages/nl/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Totaal"
     },
     "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": {
     "title": "Voertuig selecteren voor werkorder",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Klantnaam is verplicht",
     "failedCreateCustomer": "Klant 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": {
     "title": "Stuur bericht naar {name}",

+ 1 - 0
messages/pl/audit.json

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

+ 12 - 2
messages/pl/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Razem"
     },
     "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": {
     "title": "Wybierz Pojazd do Zlecenia Pracy",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Nazwa klienta jest wymagana",
     "failedCreateCustomer": "Nie udało się utworzyć klienta",
     "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": {
     "title": "Powiadom {name}",

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

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

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

@@ -34,7 +34,13 @@
       "total": "Total"
     },
     "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": {
     "title": "Selecionar Veículo para Ordem de Serviço",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Nome do cliente é obrigatório",
     "failedCreateCustomer": "Falha ao criar cliente",
     "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": {
     "title": "Notificar {name}",

+ 1 - 0
messages/ru/audit.json

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

+ 12 - 2
messages/ru/workOrders.json

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

+ 1 - 0
messages/tr/audit.json

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

+ 12 - 2
messages/tr/workOrders.json

@@ -34,7 +34,13 @@
       "total": "Toplam"
     },
     "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": {
     "title": "İş Emri için Araç Seç",
@@ -61,7 +67,11 @@
     "customerNameRequired": "Müşteri adı gereklidir",
     "failedCreateCustomer": "Müşteri 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": {
     "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())
   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?
   technician   Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
@@ -247,6 +260,8 @@ model ServiceRecord {
 
   @@index([vehicleId])
   @@index([technicianId])
+  @@index([organizationId])
+  @@index([customerId])
   @@map("service_records")
 }
 
@@ -357,8 +372,10 @@ model Customer {
   customerSessions CustomerSession[]
   serviceRequests  ServiceRequest[]
 
-  @@index([organizationId])
+  serviceRecords ServiceRecord[]
+
   @@unique([organizationId, customerNumber])
+  @@index([organizationId])
   @@map("customers")
 }
 
@@ -479,23 +496,23 @@ model LaborPresetPart {
 }
 
 model InventoryPart {
-  id            String   @id @default(cuid())
+  id            String  @id @default(cuid())
   partNumber    String?
   barcode       String?
   name          String
   description   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?
   supplierPhone String?
   supplierEmail String?
   supplierUrl   String?
   imageUrl      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
   /// `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.
   lowStockAlertedAt DateTime?
 
-  createdAt     DateTime @default(now())
-  updatedAt     DateTime @updatedAt
+  createdAt DateTime @default(now())
+  updatedAt DateTime @updatedAt
 
   userId String
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -530,12 +547,12 @@ model StockMovement {
   id String @id @default(cuid())
 
   /// Signed change in whole units. Negative = consumed, positive = restocked.
-  delta Int
+  delta         Int
   /// Part balance immediately after this movement was applied.
   quantityAfter Int
   /// Why the stock moved — see StockMovementReason in src/features/inventory/Lib/stockMovement.ts
-  reason String
-  note   String?
+  reason        String
+  note          String?
 
   inventoryPartId String
   inventoryPart   InventoryPart @relation(fields: [inventoryPartId], references: [id], onDelete: Cascade)
@@ -645,17 +662,17 @@ model QuoteAttachment {
 }
 
 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
   /// 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
   /// ServicePart on conversion so the job deducts the right inventory item.
@@ -751,6 +768,7 @@ model Organization {
   members                OrganizationMember[]
   roles                  Role[]
   vehicles               Vehicle[]
+  serviceRecords         ServiceRecord[]
   customers              Customer[]
   quotes                 Quote[]
   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" },
 };
 
+// Service records carry their own organizationId (counter sales have no
+// vehicle), so every ownership check scopes on the record directly.
 const orgWhereClause = expect.objectContaining({
-  where: expect.objectContaining({
-    vehicle: expect.objectContaining({ organizationId: ORG_A }),
-  }),
+  where: expect.objectContaining({ organizationId: ORG_A }),
 });
 
 beforeEach(() => { vi.resetAllMocks(); });
@@ -78,7 +78,7 @@ describe("getServiceRecord — cross-org isolation", () => {
     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();
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     await getServiceRecord("sr-a");
@@ -103,7 +103,7 @@ describe("updateServiceRecord — cross-org isolation", () => {
     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();
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     await updateServiceRecord({ id: "sr-x" });
@@ -132,7 +132,7 @@ describe("deleteServiceRecord — cross-org isolation", () => {
     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();
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
     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();
   });
 
-  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();
     vi.mocked(db.serviceRecord.findFirst).mockResolvedValue(null);
 
     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.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");
   });
 
-  it("attachment lookup always scopes through vehicle's organizationId", async () => {
+  it("attachment lookup always scopes through the record's organizationId", async () => {
     setupOrgAOwner();
     vi.mocked(db.serviceAttachment.findFirst).mockResolvedValue(null);
 
@@ -332,9 +332,7 @@ describe("deleteServiceAttachment — cross-org isolation", () => {
     expect(vi.mocked(db.serviceAttachment.findFirst)).toHaveBeenCalledWith(
       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;
     year: number;
     licensePlate: string | null;
-    customer: {
-      id: string;
-      name: string;
-    } | null;
-  };
+  } | null;
+  customer: {
+    id: string;
+    name: string;
+  } | null;
 }
 
 interface PaginatedBillingData {
@@ -179,7 +179,9 @@ export default function BillingClient({
 
   const handleRowClick = (record: BillingRecord) => {
     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 className="truncate">{record.title}</TableCell>
                     <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 className="truncate">
-                      {record.vehicle.customer?.name || "\u2014"}
+                      {record.customer?.name || "\u2014"}
                     </TableCell>
                     <TableCell>
                       {formatDate(new Date(record.serviceDate))}

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

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

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

@@ -231,9 +231,9 @@ export function PartDetailClient({
                     </TableCell>
                     <TableCell className="font-medium">{m.quantityAfter}</TableCell>
                     <TableCell>
-                      {m.serviceRecordId && m.vehicleId ? (
+                      {m.serviceRecordId ? (
                         <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"
                         >
                           {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({
   params,
@@ -23,262 +7,9 @@ export default async function ServiceDetailPage({
   params: Promise<{ id: string; serviceId: string }>
   searchParams: Promise<Record<string, string | string[] | undefined>>
 }) {
-  const { id, serviceId } = await params
+  const { serviceId } = await params
   const sp = await searchParams
   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,
 } from "@/components/ui/table";
 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 { statusColors } from "@/lib/table-utils";
 import { updateServiceStatus } from "@/features/vehicles/Actions/serviceActions";
@@ -30,10 +31,13 @@ import {
   ArrowDown,
   ArrowUp,
   ArrowUpDown,
-  ChevronDown,
+  Car,
+  ExternalLink,
   Loader2,
+  MousePointerClick,
   Plus,
   Search,
+  User,
 } from "lucide-react";
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import { VehiclePickerDialog } from "@/components/vehicle-picker-dialog";
@@ -54,6 +58,7 @@ interface WorkOrder {
   startDateTime: Date | null;
   techName: string | null;
   invoiceNumber: string | null;
+  customer: { id: string; name: string; email: string | null; phone: string | null } | null;
   vehicle: {
     id: string;
     make: string;
@@ -61,7 +66,7 @@ interface WorkOrder {
     year: number;
     licensePlate: string | null;
     customer: { id: string; name: string; email: string | null; phone: string | null } | null;
-  };
+  } | null;
 }
 
 interface VehicleOption {
@@ -208,18 +213,19 @@ export function WorkOrdersClient({
     router.refresh();
 
     const templateKey = statusTemplateKeys[newStatus];
-    if (workOrder.vehicle.customer && templateKey) {
+    const notifyTarget = workOrder.customer ?? workOrder.vehicle?.customer;
+    if (notifyTarget && templateKey) {
       const tplResult = await getSmsTemplates();
       const tplData = tplResult.success && tplResult.data ? tplResult.data : null;
       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, {
-        customer_name: workOrder.vehicle.customer.name,
+        customer_name: notifyTarget.name,
         vehicle,
         company_name: tplData?.companyName || "",
         current_user: tplData?.currentUser || "",
       });
-      setNotifyCustomer(workOrder.vehicle.customer);
+      setNotifyCustomer(notifyTarget);
       setNotifyMessage(message);
       setNotifyStatus(newStatus);
       setShowNotifyDialog(true);
@@ -275,6 +281,10 @@ export function WorkOrdersClient({
 
       {/* Table */}
       <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">
           <TableHeader>
             <TableRow>
@@ -318,13 +328,12 @@ export function WorkOrdersClient({
                   {t("table.total")}<SortIcon column="totalAmount" />
                 </button>
               </TableHead>
-              <TableHead className="w-[50px]" />
             </TableRow>
           </TableHeader>
           <TableBody>
             {data.records.length === 0 ? (
               <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")}
                 </TableCell>
               </TableRow>
@@ -332,13 +341,16 @@ export function WorkOrdersClient({
               data.records.map((r) => {
                 const displayTotal = r.totalAmount > 0 ? r.totalAmount : r.cost;
                 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 (
+                  <ContextMenu key={r.id} modal={false}>
+                  <ContextMenuTrigger asChild>
                   <TableRow
-                    key={r.id}
                     className={`cursor-pointer transition-opacity ${navigatingId === r.id ? "opacity-50" : ""}`}
                     onClick={() => {
                       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">
@@ -346,16 +358,16 @@ export function WorkOrdersClient({
                     </TableCell>
                     <TableCell>
                       <div className="min-w-0">
-                        {r.vehicle.licensePlate && (
+                        {r.vehicle?.licensePlate && (
                           <span className="font-mono text-sm font-medium">{r.vehicle.licensePlate}</span>
                         )}
                         <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>
                       </div>
                     </TableCell>
                     <TableCell className="hidden truncate md:table-cell text-muted-foreground">
-                      {r.vehicle.customer?.name || "-"}
+                      {(r.customer ?? r.vehicle?.customer)?.name || "-"}
                     </TableCell>
                     <TableCell className="truncate">
                       <span className="font-medium">{r.title}</span>
@@ -372,35 +384,52 @@ export function WorkOrdersClient({
                       {formatDate(new Date(r.startDateTime ?? r.serviceDate))}
                     </TableCell>
                     <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>
                   </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: {
         id: 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 })
     }
 
+    const recordCustomerId = record.customerId ?? record.vehicle?.customerId ?? null
     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 })
     }

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

@@ -78,6 +78,14 @@ interface InvoiceRecord {
     category: string
     description: string | null
   }[]
+  customer?: {
+    name: string
+    email: string | null
+    phone: string | null
+    address: string | null
+    company: string | null
+    taxId?: string | null
+  } | null
   vehicle: {
     make: string
     model: string
@@ -93,7 +101,7 @@ interface InvoiceRecord {
       company: string | null
       taxId?: string | null
     } | null
-  }
+  } | null
 }
 
 interface InvoiceSettings {
@@ -255,7 +263,7 @@ export function InvoiceView({
     }).catch(() => { /* fire-and-forget */ })
   }, [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 laborSubtotalStored = record.laborItems.reduce((sum, l) => sum + l.total, 0)
   const computedSubtotalStored = partsSubtotalStored + laborSubtotalStored
@@ -730,7 +738,7 @@ export function InvoiceView({
             case 'service': {
               const renderInfoCard = (sid: string) => {
                 if (sid === 'customer') {
-                  const c = record.vehicle.customer
+                  const c = record.customer ?? record.vehicle?.customer
                   if (!c) return null
                   const vf = getVisibleFieldsForSection(layoutConfig, 'customer')
                   const show = (fid: string) => !vf || vf.has(fid)
@@ -759,6 +767,8 @@ export function InvoiceView({
                   );
                 }
                 if (sid === 'vehicle') {
+                  const vehicle = record.vehicle
+                  if (!vehicle) return null
                   const vf = getVisibleFieldsForSection(layoutConfig, 'vehicle')
                   const show = (fid: string) => !vf || vf.has(fid)
                   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
                     switch (fid) {
                       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
                     }
                   }
@@ -792,7 +802,7 @@ export function InvoiceView({
                     if (!show(fid)) return null
                     switch (fid) {
                       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
                       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,
       attachments: true,
       payments: { orderBy: { date: "desc" } },
+      customer: {
+        select: {
+          name: true,
+          email: true,
+          phone: true,
+          address: true,
+          company: true,
+          taxId: true,
+        },
+      },
       vehicle: {
         select: {
           make: true,
@@ -74,7 +84,7 @@ export default async function PublicInvoicePage({
     },
   });
 
-  if (!record || record.vehicle.organizationId !== orgId) {
+  if (!record || record.organizationId !== orgId) {
     notFound();
   }
 
@@ -82,7 +92,7 @@ export default async function PublicInvoicePage({
   const [settings, org, features] = await Promise.all([
     db.appSetting.findMany({
       where: {
-        organizationId: record.vehicle.organizationId,
+        organizationId: record.organizationId,
         key: {
           in: [
             "workshop.address",
@@ -117,9 +127,9 @@ export default async function PublicInvoicePage({
         },
       },
     }),
-    record.vehicle.organizationId
+    record.organizationId
       ? db.organization.findUnique({
-          where: { id: record.vehicle.organizationId },
+          where: { id: record.organizationId },
           select: { name: true, portalSlug: true },
         })
       : null,

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

@@ -19,7 +19,7 @@ interface StatusReportViewProps {
     customerFeedback: 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;
   technicianName: string | null;
   workshopName: string;
@@ -40,7 +40,7 @@ export function StatusReportView({
   const createdDate = new Date(report.createdAt).toLocaleDateString(undefined, {
     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() {
     if (!feedback.trim() || submitting) return;
@@ -74,16 +74,18 @@ export function StatusReportView({
                 <p className="font-medium">{serviceTitle}</p>
               </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>
+            )}
             {report.title && (
               <div className="pt-2 border-t">
                 <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) {
     queries.push(
       db.quote
@@ -277,7 +298,7 @@ export async function GET() {
   }
 
   const [
-    settings, customers, customFieldDefinitions, inventoryParts, vehicles, quotes,
+    settings, customers, customFieldDefinitions, inventoryParts, vehicles, counterSales, quotes,
     technicians, inspectionTemplates, inspections, auditLogs, smsMessages, notifications,
   ] = await Promise.all([
       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({
         where: { organizationId: ctx.organizationId },
         include: {
@@ -333,6 +363,7 @@ export async function GET() {
       customFieldDefinitions,
       inventoryParts,
       vehicles,
+      counterSales,
       quotes,
       technicians,
       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
     const lastRecord = await db.serviceRecord.findFirst({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       orderBy: { createdAt: "desc" },
       select: { invoiceNumber: true },
     });
@@ -417,6 +417,7 @@ export async function POST(request: NextRequest) {
 
         const record = await tx.serviceRecord.create({
           data: {
+            organizationId,
             title,
             description: invoice.private_notes || invoice.public_notes || 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
     const lastRecord = await db.serviceRecord.findFirst({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       orderBy: { createdAt: "desc" },
       select: { invoiceNumber: true },
     });
@@ -380,6 +380,7 @@ export async function POST(request: NextRequest) {
 
         const created = await tx.serviceRecord.create({
           data: {
+            organizationId,
             title: sr.Description,
             description: sr.Notes || null,
             type: "repair",

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

@@ -51,6 +51,147 @@ async function parseBackup(
   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) {
   const uploadsDir = path.join(
     process.cwd(),
@@ -424,141 +565,30 @@ export async function POST(request: NextRequest) {
             | undefined;
           if (serviceRecords?.length) {
             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
       if (data.quotes?.length) {
         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([
       db.serviceRecord.findFirst({
-        where: { id, vehicle: { organizationId: ctx.organizationId } },
+        where: { id, organizationId: ctx.organizationId },
         include: {
           partItems: true,
           laborItems: true,
@@ -54,6 +54,16 @@ export async function GET(
           // relation is always correct. The PDF prefers technician.name and
           // falls back to techName for legacy records that have no FK.
           technician: { select: { name: true } },
+          customer: {
+            select: {
+              name: true,
+              email: true,
+              phone: true,
+              address: true,
+              company: true,
+              taxId: true,
+            },
+          },
           vehicle: {
             select: {
               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({
     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 {
     const quote = await db.quote.findFirst({
       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 });
     }
 

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

@@ -26,11 +26,11 @@ export async function GET(
       where: { publicToken: token },
       select: {
         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 })
     }
 

+ 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({
       where: { publicToken: token },
       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 });
     }
 
@@ -89,10 +90,12 @@ export async function POST(
         organizationId: orgId,
         type: "invoice_payment",
         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",
         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
   const record = await db.serviceRecord.findUnique({
     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({
       data: {
         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
       const record = await db.serviceRecord.findUnique({
         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({
           data: {
             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
           const record = await db.serviceRecord.findUnique({
             where: { id: parts[1] },
-            include: { vehicle: { select: { organizationId: true } } },
+            select: { id: true, organizationId: true },
           });
 
           if (record) {
             return await processVippsPayment(
               reference,
-              record.vehicle.organizationId!,
+              record.organizationId!,
               record.id,
             );
           }
@@ -85,10 +85,10 @@ async function processVippsPayment(
   // Verify service record exists and belongs to this org
   const record = await db.serviceRecord.findUnique({
     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({
       data: {
         amount: result.amount,

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

@@ -25,8 +25,11 @@ import {
   Search,
   ArrowLeft,
   UserPlus,
+  ShoppingCart,
 } from "lucide-react";
 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 { toast } from "sonner";
 import { useTranslations } from "next-intl";
@@ -69,7 +72,7 @@ export function VehiclePickerDialog({
   const displayTitle = title || t("title");
 
   const [vehicleSearch, setVehicleSearch] = useState("");
-  const [pickerStep, setPickerStep] = useState<"select" | "create">("select");
+  const [pickerStep, setPickerStep] = useState<"select" | "create" | "sale">("select");
 
   const [newMake, setNewMake] = useState("");
   const [newModel, setNewModel] = useState("");
@@ -106,6 +109,46 @@ export function VehiclePickerDialog({
     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) => {
     onOpenChange(false);
     resetState();
@@ -229,13 +272,119 @@ export function VehiclePickerDialog({
                   </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
-                variant="ghost"
                 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>
             </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
     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({
         where: { organizationId },
         select: {

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

@@ -18,7 +18,7 @@ export async function aiGenerateServiceNotes(serviceRecordId: string) {
     async ({ organizationId }) => {
       const locale = (await getLocale()) as Locale;
       const sr = await db.serviceRecord.findFirst({
-        where: { id: serviceRecordId, vehicle: { organizationId } },
+        where: { id: serviceRecordId, organizationId },
         include: {
           vehicle: { select: { make: true, model: true, year: true, licensePlate: 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");
 
       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,
         serviceTitle: sr.title,
         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",
       "inspections",
       "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) {
@@ -219,7 +222,6 @@ async function executeOrgScopedQuery(sql: string, orgId: string): Promise<unknow
 
     // Children of vehicles (via vehicleId → vehicles.id)
     const vehicleChildren = [
-      "service_records",
       "notes",
       "fuel_logs",
       "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")
 
-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")
 
@@ -357,7 +359,8 @@ service_attachments (id, "fileName", "fileUrl", "fileType", "fileSize", category
 
 Key relationships:
 - 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_labor."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)
         END AS paid
       FROM service_records sr
-      JOIN vehicles v ON v.id = sr."vehicleId"
-      WHERE v."organizationId" = ${organizationId}
+      WHERE sr."organizationId" = ${organizationId}
     ) 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)
             END AS paid_amount
           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}
         ) sub
         WHERE 1=1 ${statusCondition}
@@ -130,7 +129,7 @@ export async function getBillingHistory(params: {
           effective_total: number;
           paid_amount: number;
           manually_paid: boolean;
-          vehicle_id: string;
+          vehicle_id: string | null;
           vehicle_make: string | null;
           vehicle_model: string | null;
           vehicle_year: number | null;
@@ -178,9 +177,9 @@ export async function getBillingHistory(params: {
             c.id AS customer_id,
             c.name AS customer_name
           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}
         ) sub
         WHERE 1=1 ${statusCondition}
@@ -218,14 +217,16 @@ export async function getBillingHistory(params: {
           totalAmount: effectiveTotal,
           totalPaid: paidAmount,
           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,

+ 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 lastRecord = await db.serviceRecord.findFirst({
-    where: { vehicle: { organizationId } },
+    where: { organizationId },
     orderBy: { createdAt: "desc" },
     select: { invoiceNumber: true },
   });
@@ -317,6 +317,7 @@ export async function processRecurringInvoices() {
       const serviceRecord = await db.$transaction(async (tx) => {
         const sr = await tx.serviceRecord.create({
           data: {
+            organizationId,
             title: ri.title,
             description: ri.description,
             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
   type: "service" | "reminder" | "quote";
   status: string;
-  vehicleId: string;
+  vehicleId: string | null;
   vehicleLabel: string;
   customerName: string | null;
   invoiceNumber: string | null;
@@ -42,7 +42,7 @@ export async function getCalendarEvents(params: {
     const [services, reminders, quotes] = await Promise.all([
       db.serviceRecord.findMany({
         where: {
-          vehicle: { organizationId },
+          organizationId,
           startDateTime: { gte: start, lte: end },
         },
         select: {
@@ -55,6 +55,7 @@ export async function getCalendarEvents(params: {
           totalAmount: true,
           cost: true,
           vehicleId: true,
+          customer: { select: { name: true } },
           vehicle: {
             select: {
               make: true,
@@ -125,8 +126,8 @@ export async function getCalendarEvents(params: {
         type: "service" as const,
         status: s.status,
         vehicleId: s.vehicleId,
-        vehicleLabel: `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}`,
-        customerName: s.vehicle.customer?.name ?? null,
+        vehicleLabel: s.vehicle ? `${s.vehicle.year} ${s.vehicle.make} ${s.vehicle.model}` : "",
+        customerName: (s.customer ?? s.vehicle?.customer)?.name ?? null,
         invoiceNumber: s.invoiceNumber,
         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) {
   if (event.type === "quote") return `/quotes/${event.id}`;
+  if (!event.vehicleId) return `/sales/${event.id}`;
   return `/vehicles/${event.vehicleId}`;
 }
 

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

@@ -1,6 +1,7 @@
 import { db } from "@/lib/db";
 import { toSafeDate } from "@/lib/invoice-utils";
 import { PermissionSubject } from "@/lib/permissions";
+import { serviceRecordHref } from "@/lib/service-record";
 import type { CardEntity, CardFilter, CustomCardConfig } from "./registry";
 import { getField } from "./registry";
 
@@ -168,11 +169,12 @@ export async function runEntityQuery(
     }
     case "workOrders": {
       const rows = await db.serviceRecord.findMany({
-        where: buildWhere(config, { vehicle: { organizationId } }),
+        where: buildWhere(config, { organizationId }),
         select: {
           id: true, invoiceNumber: true, title: true, type: true, status: true,
           techName: true, totalAmount: true, serviceDate: true,
           invoiceDate: true, startDateTime: true,
+          customer: { select: { name: true } },
           vehicle: {
             select: {
               id: true, make: true, model: true, year: true,
@@ -185,13 +187,13 @@ export async function runEntityQuery(
       });
       return rows.map((r) => ({
         id: r.id,
-        href: `/vehicles/${r.vehicle.id}/service/${r.id}`,
+        href: serviceRecordHref(r),
         cells: {
           invoiceNumber: r.invoiceNumber, title: r.title, type: r.type,
           status: r.status, techName: r.techName, totalAmount: r.totalAmount,
           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 record = await db.serviceRecord.findFirst({
-      where: { id: serviceRecordId, vehicle: { organizationId } },
+      where: { id: serviceRecordId, organizationId },
       include: {
         partItems: true,
         laborItems: true,
+        customer: { select: { name: true, email: true, phone: true, address: true, company: true } },
         vehicle: {
           include: {
             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;
       // the legacy `techName` string is only a fallback for old records.
       technician: { select: { name: true } },
+      customer: {
+        select: {
+          name: true,
+          email: true,
+          phone: true,
+          address: true,
+          company: true,
+          taxId: true,
+        },
+      },
       vehicle: {
         select: {
           make: true,
@@ -105,7 +115,7 @@ export async function buildInvoicePdfBuffer(
 
   if (!record) return null
 
-  const orgId = record.vehicle.organizationId
+  const orgId = record.organizationId
   if (!orgId) return null
 
   // 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
     const serviceRecord = await db.serviceRecord.findFirst({
-      where: { id: data.serviceRecordId, vehicle: { organizationId } },
+      where: { id: data.serviceRecordId, organizationId },
       select: { id: true, vehicleId: true },
     });
     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 };
   }, {
     requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.BILLING }],
@@ -45,7 +49,7 @@ export async function createPayment(input: unknown) {
 export async function deletePayment(paymentId: string) {
   return withAuth(async ({ organizationId }) => {
     const payment = await db.payment.findFirst({
-      where: { id: paymentId, serviceRecord: { vehicle: { organizationId } } },
+      where: { id: paymentId, serviceRecord: { organizationId } },
       include: { serviceRecord: { select: { vehicleId: true, id: true } } },
     });
     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 } });
 
     const { vehicleId, id: serviceId } = payment.serviceRecord;
-    revalidatePath(`/vehicles/${vehicleId}/service/${serviceId}`);
+    revalidatePath(
+      vehicleId ? `/vehicles/${vehicleId}/service/${serviceId}` : `/sales/${serviceId}`
+    );
     return { deleted: true, paymentId };
   }, {
     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.
         db.serviceRecord.findMany({
           where: {
-            vehicle: { customerId, organizationId },
+            organizationId,
+            OR: [{ vehicle: { customerId } }, { customerId }],
             status: 'completed',
           },
           select: {
@@ -72,7 +73,8 @@ export async function getPortalDashboard() {
         // Active jobs: work currently being done on the customer's vehicles.
         db.serviceRecord.findMany({
           where: {
-            vehicle: { customerId, organizationId },
+            organizationId,
+            OR: [{ vehicle: { customerId } }, { customerId }],
             status: { in: ['pending', 'in-progress', 'waiting-parts'] },
           },
           select: {
@@ -118,7 +120,7 @@ export async function getPortalDashboard() {
     // Open invoices: completed records that the customer still owes money on.
     // Pre-completion records are not counted — they're not yet invoices.
     const completedInvoices = await db.serviceRecord.findMany({
-      where: { vehicle: { customerId, organizationId }, status: 'completed' },
+      where: { organizationId, OR: [{ vehicle: { customerId } }, { customerId }], status: 'completed' },
       select: {
         totalAmount: true,
         manuallyPaid: true,
@@ -243,7 +245,8 @@ export async function getPortalInvoices() {
     // change, customer can't act on them).
     const invoices = await db.serviceRecord.findMany({
       where: {
-        vehicle: { customerId, organizationId },
+        organizationId,
+        OR: [{ vehicle: { customerId } }, { customerId }],
         status: 'completed',
       },
       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 lastRecord = await db.serviceRecord.findFirst({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       orderBy: { createdAt: "desc" },
       select: { invoiceNumber: true },
     });
@@ -399,6 +399,7 @@ export async function convertQuoteToServiceRecord(quoteId: string, vehicleId: st
     const record = await db.$transaction(async (tx) => {
       const created = await tx.serviceRecord.create({
         data: {
+          organizationId,
           title: quote.title,
           description: quote.description,
           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({
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
       },
       select: {
@@ -119,7 +119,7 @@ export async function getServiceReport(params: {
 
     const records = await db.serviceRecord.findMany({
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
       },
       select: {
@@ -206,7 +206,7 @@ export async function getTechnicianReport(params: {
 
     const records = await db.serviceRecord.findMany({
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
         OR: [{ technicianId: { not: null } }, { techName: { not: null } }],
       },
@@ -263,7 +263,7 @@ export async function getPartsUsageReport(params: {
     const parts = await db.servicePart.findMany({
       where: {
         serviceRecord: {
-          vehicle: { organizationId },
+          organizationId,
           startDateTime: { gte: start, lte: end },
         },
       },
@@ -339,7 +339,7 @@ export async function getJobAnalyticsReport(params: {
 
     const records = await db.serviceRecord.findMany({
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
       },
       select: {
@@ -540,7 +540,7 @@ export async function getPastDueInvoicesReport() {
 
     const records = await db.serviceRecord.findMany({
       where: {
-        vehicle: { organizationId },
+        organizationId,
         invoiceDueDate: { lt: now },
         status: { not: "cancelled" },
       },
@@ -552,6 +552,7 @@ export async function getPastDueInvoicesReport() {
         cost: true,
         manuallyPaid: true,
         payments: { select: { amount: true } },
+        customer: { select: { name: true, company: true } },
         vehicle: {
           select: {
             year: true,
@@ -592,14 +593,15 @@ export async function getPastDueInvoicesReport() {
       const dueDate = r.invoiceDueDate!;
       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 invoiceCustomer = r.customer ?? r.vehicle?.customer;
 
       invoices.push({
         id: r.id,
         invoiceNumber: r.invoiceNumber,
-        customerName: r.vehicle.customer?.name ?? "Unknown",
-        customerCompany: r.vehicle.customer?.company ?? null,
+        customerName: invoiceCustomer?.name ?? "Unknown",
+        customerCompany: invoiceCustomer?.company ?? null,
         vehicleInfo,
         totalAmount: total,
         amountPaid: paid,
@@ -651,7 +653,7 @@ export async function getVehicleReport(params: {
     const records = await db.serviceRecord.findMany({
       where: {
         vehicleId: params.vehicleId,
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
       },
       select: {
@@ -769,7 +771,7 @@ export async function getTaxReport(params: {
 
     const records = await db.serviceRecord.findMany({
       where: {
-        vehicle: { organizationId },
+        organizationId,
         startDateTime: { gte: start, lte: end },
       },
       select: {

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

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

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

@@ -45,7 +45,7 @@ interface SearchResult {
       model: string;
       year: number;
       licensePlate: string | null;
-    };
+    } | null;
   }[];
   parts: {
     id: string;
@@ -312,8 +312,8 @@ export function SearchCommand() {
                 {results.services.map((s) => (
                   <CommandItem
                     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" />
                     <div className="flex flex-col">
@@ -321,8 +321,8 @@ export function SearchCommand() {
                       <span className="text-xs text-muted-foreground">
                         {[
                           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(" · ")}
                       </span>
                     </div>

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

@@ -41,7 +41,7 @@ export async function applyTaxRateToExisting() {
       // --- Service records (work orders / invoices) ---
       const serviceRecords = await db.serviceRecord.findMany({
         where: {
-          vehicle: { organizationId },
+          organizationId,
           taxRate: 0,
         },
         select: {
@@ -144,7 +144,7 @@ export async function getTaxBackfillCounts() {
     async ({ organizationId }) => {
       const [serviceRecords, quotes] = await Promise.all([
         db.serviceRecord.count({
-          where: { vehicle: { organizationId }, taxRate: 0 },
+          where: { organizationId, taxRate: 0 },
         }),
         db.quote.count({
           where: { organizationId, taxRate: 0 },
@@ -168,7 +168,7 @@ export async function getInclusiveBackfillCounts() {
     async ({ organizationId }) => {
       const [serviceRecords, quotes] = await Promise.all([
         db.serviceRecord.count({
-          where: { vehicle: { organizationId }, taxInclusive: false },
+          where: { organizationId, taxInclusive: false },
         }),
         db.quote.count({
           where: { organizationId, taxInclusive: false },
@@ -192,7 +192,7 @@ export async function getExclusiveBackfillCounts() {
     async ({ organizationId }) => {
       const [serviceRecords, quotes] = await Promise.all([
         db.serviceRecord.count({
-          where: { vehicle: { organizationId }, taxInclusive: true },
+          where: { organizationId, taxInclusive: true },
         }),
         db.quote.count({
           where: { organizationId, taxInclusive: true },
@@ -233,7 +233,7 @@ export async function convertRecordsToInclusive() {
       // --- Service records ---
       const serviceRecords = await db.serviceRecord.findMany({
         where: {
-          vehicle: { organizationId },
+          organizationId,
           taxInclusive: false,
         },
         select: {
@@ -402,7 +402,7 @@ export async function convertRecordsToExclusive() {
     async ({ organizationId }) => {
       const serviceRecords = await db.serviceRecord.findMany({
         where: {
-          vehicle: { organizationId },
+          organizationId,
           taxInclusive: true,
         },
         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({
-        where: { serviceRecord: { vehicle: { organizationId } } },
+        where: { serviceRecord: { organizationId } },
         select: { fileUrl: true },
       });
       for (const att of attachments) {
@@ -57,6 +57,10 @@ export async function deleteContent(input: unknown) {
       }
 
       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");
     }
 

+ 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
       const serviceRecord = await db.serviceRecord.findFirst({
-        where: { id: data.serviceRecordId, vehicle: { organizationId } },
+        where: { id: data.serviceRecordId, organizationId },
         select: { id: true, technicianId: true },
       });
       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,
       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,
     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
       // revalidating the current route).
       const serviceRecord = await db.serviceRecord.findFirst({
-        where: { id: serviceRecordId, vehicle: { organizationId } },
+        where: { id: serviceRecordId, organizationId },
         select: { id: true },
       });
       if (!serviceRecord) return [];

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

@@ -20,6 +20,15 @@ export async function sendStatusReport(input: unknown) {
         include: {
           serviceRecord: {
             include: {
+              customer: {
+                select: {
+                  id: true,
+                  name: true,
+                  email: true,
+                  phone: true,
+                  telegramChatId: true,
+                },
+              },
               vehicle: {
                 include: {
                   customer: {
@@ -40,7 +49,7 @@ export async function sendStatusReport(input: unknown) {
 
       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");
 
       // 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 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 messageBody = data.customMessage

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

@@ -15,6 +15,7 @@ export async function submitStatusReportFeedback(input: unknown) {
           id: true,
           title: true,
           vehicleId: true,
+          customer: { select: { name: true } },
           vehicle: {
             select: {
               year: true,
@@ -42,8 +43,10 @@ export async function submitStatusReportFeedback(input: unknown) {
 
   // Notify the organization about the feedback
   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({
     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}"`,
     entityType: "ServiceRecord",
     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 };

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

@@ -21,7 +21,7 @@ export async function addPartToServiceRecord(input: {
   return withAuth(
     async ({ userId, organizationId }) => {
       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 },
       });
       if (!record) throw new Error("Service record not found");
@@ -90,7 +90,11 @@ export async function addPartToServiceRecord(input: {
         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);
 
       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({
         where: {
           id: data.serviceRecordId,
-          vehicle: { organizationId },
+          organizationId,
         },
         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 { 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(
   vehicleId: string,
   startDateTime?: Date,
@@ -18,148 +178,49 @@ export async function createDraftServiceRecord(
       });
       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: [
@@ -172,8 +233,8 @@ export async function createDraftServiceRecord(
         action: "service.create",
         entity: "ServiceRecord",
         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,
     ] = await Promise.all([
       db.serviceRecord.count({
-        where: { vehicle: { organizationId }, status: { not: "completed" } },
+        where: { organizationId, status: { not: "completed" } },
       }),
       db.serviceRecord.count({
-        where: { vehicle: { organizationId }, status: "pending" },
+        where: { organizationId, status: "pending" },
       }),
       db.inventoryPart.count({
         where: { organizationId, isArchived: false },
@@ -51,10 +51,11 @@ export async function getDashboardStats() {
       db.customer.count({ where: { organizationId } }),
       db.serviceRecord.findMany({
         where: {
-          vehicle: { organizationId },
+          organizationId,
           status: { in: ["pending", "in-progress", "waiting-parts"] },
         },
         include: {
+          customer: { select: { id: true, name: true } },
           vehicle: {
             select: {
               id: true,
@@ -71,10 +72,11 @@ export async function getDashboardStats() {
       }),
       db.serviceRecord.findMany({
         where: {
-          vehicle: { organizationId },
+          organizationId,
           status: "completed",
         },
         include: {
+          customer: { select: { id: true, name: true } },
           vehicle: {
             select: {
               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 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 = {
         vehicle: { organizationId },
         ...(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;
   title: string;
   status: string;
-  vehicleId: string;
+  vehicleId: string | null;
   vehicle: {
     make: string;
     model: string;
     year: number;
     licensePlate: string | null;
-  };
+  } | null;
   customer: {
     id: string;
     name: string;
@@ -42,7 +42,7 @@ export async function getMyActiveJobs() {
 
       const records = await db.serviceRecord.findMany({
         where: {
-          vehicle: { organizationId },
+          organizationId,
           status: { in: ["in-progress", "pending", "waiting-parts"] },
           technicianId: { in: techIds },
         },
@@ -51,6 +51,15 @@ export async function getMyActiveJobs() {
           title: true,
           status: true,
           vehicleId: true,
+          customer: {
+            select: {
+              id: true,
+              name: true,
+              email: true,
+              phone: true,
+              telegramChatId: true,
+            },
+          },
           vehicle: {
             select: {
               make: true,
@@ -86,8 +95,13 @@ export async function getMyActiveJobs() {
         title: r.title,
         status: r.status,
         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,
         videoCount: r.attachments.filter((a) => a.category === "video").length,
         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({
       where: {
         vehicleId,
-        vehicle: { organizationId },
+        organizationId,
         mileage: { not: null },
         status: "completed",
       },

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

@@ -98,7 +98,7 @@ export async function getServiceRecordsPaginated(
 export async function getAllServiceRecords() {
   return withAuth(async ({ organizationId }) => {
     return db.serviceRecord.findMany({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       include: {
         vehicle: { select: { make: true, model: true, year: true } },
         _count: { select: { partItems: true, laborItems: true } },
@@ -122,7 +122,7 @@ export async function getAllServiceRecordsPaginated(params: {
     const skip = (page - 1) * pageSize;
 
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const where: any = { vehicle: { organizationId } };
+    const where: any = { organizationId };
 
     if (params.search) {
       where.OR = [
@@ -178,12 +178,23 @@ export async function getAllServiceRecordsPaginated(params: {
 export async function getServiceRecord(recordId: string) {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
-      where: { id: recordId, vehicle: { organizationId } },
+      where: { id: recordId, organizationId },
       include: {
         partItems: true,
         laborItems: true,
         attachments: true,
         payments: { orderBy: { date: "desc" } },
+        customer: {
+          select: {
+            id: true,
+            name: true,
+            email: true,
+            phone: true,
+            address: true,
+            company: true,
+            telegramChatId: true,
+          },
+        },
         vehicle: {
           select: {
             id: true,
@@ -216,11 +227,25 @@ export async function getServiceRecord(recordId: string) {
 export async function createServiceRecord(input: unknown) {
   return withAuth(async ({ userId, organizationId }) => {
     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
     const [settings, org] = await Promise.all([
@@ -251,7 +276,7 @@ export async function createServiceRecord(input: unknown) {
         : settingsMap["workshop.taxInclusive"] === "true";
 
     // 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.taxAmount = 0;
     }
@@ -259,7 +284,7 @@ export async function createServiceRecord(input: unknown) {
     // Generate sequential invoice number
     const startNumber = parseInt(settingsMap["workshop.invoiceStartNumber"] || "0", 10);
     const lastRecord = await db.serviceRecord.findFirst({
-      where: { vehicle: { organizationId } },
+      where: { organizationId },
       orderBy: { createdAt: "desc" },
       select: { invoiceNumber: true },
     });
@@ -287,6 +312,10 @@ export async function createServiceRecord(input: unknown) {
       const created = await tx.serviceRecord.create({
         data: {
           ...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,
           shopName,
           invoiceNumber,
@@ -372,20 +401,22 @@ export async function createServiceRecord(input: unknown) {
     }
 
     // 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(`/vehicles/${data.vehicleId}`);
+    if (data.vehicleId) revalidatePath(`/vehicles/${data.vehicleId}`);
     revalidatePath("/services");
     return record;
   }, {
@@ -404,7 +435,7 @@ export async function updateServiceRecord(input: unknown) {
   return withAuth(async ({ userId, organizationId }) => {
     const data = updateServiceSchema.parse(input);
     const existing = await db.serviceRecord.findFirst({
-      where: { id: data.id, vehicle: { organizationId } },
+      where: { id: data.id, organizationId },
       include: {
         attachments: { select: { fileUrl: true, category: 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");
 
-    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
     let removedFileUrls: string[] = [];
@@ -433,6 +478,12 @@ export async function updateServiceRecord(input: unknown) {
         where: { id },
         data: {
           ...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,
           techName: recordData.techName !== undefined ? (recordData.techName || 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
-    if (recordData.mileage && recordData.mileage > existing.vehicle.mileage) {
+    if (existing.vehicle && recordData.mileage && recordData.mileage > existing.vehicle.mileage) {
       const latestRecord = await db.serviceRecord.findFirst({
         where: { vehicleId: existing.vehicle.id },
         orderBy: [{ startDateTime: { sort: 'desc', nulls: 'last' } }, { serviceDate: 'desc' }],
@@ -568,8 +619,12 @@ export async function updateServiceRecord(input: unknown) {
     }
 
     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");
     // Parts may have been added, changed or removed on this record.
     await onInventoryChanged(organizationId);
@@ -589,7 +644,7 @@ export async function updateServiceRecord(input: unknown) {
 export async function updateServiceStatus(recordId: string, status: string) {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
-      where: { id: recordId, vehicle: { organizationId } },
+      where: { id: recordId, organizationId },
       include: {
         vehicle: {
           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("/work-orders");
     revalidatePath("/services");
-    revalidatePath(`/vehicles/${record.vehicleId}`);
+    if (record.vehicleId) revalidatePath(`/vehicles/${record.vehicleId}`);
+    else revalidatePath(`/sales/${recordId}`);
     return { success: true, recordId, status };
   }, {
     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) {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
-      where: { id: recordId, vehicle: { organizationId } },
+      where: { id: recordId, organizationId },
     });
     if (!record) throw new Error("Record not found");
 
@@ -647,8 +703,12 @@ export async function toggleManuallyPaid(recordId: string) {
 
     revalidatePath("/");
     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 };
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
 }
@@ -667,7 +727,7 @@ export async function getWorkOrders(params: {
     const skip = (page - 1) * pageSize;
 
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const where: any = { vehicle: { organizationId } };
+    const where: any = { organizationId };
 
     if (params.status === "active") {
       where.status = { not: "completed" };
@@ -682,6 +742,7 @@ export async function getWorkOrders(params: {
         { techName: { contains: params.search, mode: "insensitive" } },
         { vehicle: { licensePlate: { 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({
         where,
         include: {
+          customer: { select: { id: true, name: true, email: true, phone: true } },
           vehicle: {
             select: {
               id: true,
@@ -726,7 +788,7 @@ export async function getWorkOrders(params: {
       db.serviceRecord.count({ where }),
       db.serviceRecord.groupBy({
         by: ["status"],
-        where: { vehicle: { organizationId } },
+        where: { organizationId },
         _count: true,
       }),
     ]);
@@ -750,7 +812,7 @@ export async function getWorkOrders(params: {
 export async function deleteServiceRecord(recordId: string) {
   return withAuth(async ({ userId, organizationId }) => {
     const record = await db.serviceRecord.findFirst({
-      where: { id: recordId, vehicle: { organizationId } },
+      where: { id: recordId, organizationId },
       include: { attachments: true },
     });
     if (!record) throw new Error("Record not found");
@@ -785,7 +847,7 @@ export async function deleteServiceRecord(recordId: string) {
     });
 
     revalidatePath("/");
-    revalidatePath(`/vehicles/${record.vehicleId}`);
+    if (record.vehicleId) revalidatePath(`/vehicles/${record.vehicleId}`);
     revalidatePath("/services");
     // Deleting the record restocked its linked parts.
     await onInventoryChanged(organizationId);
@@ -805,7 +867,7 @@ export async function deleteServiceRecord(recordId: string) {
 export async function deleteServiceAttachment(attachmentId: string) {
   return withAuth(async ({ organizationId }) => {
     const attachment = await db.serviceAttachment.findFirst({
-      where: { id: attachmentId, serviceRecord: { vehicle: { organizationId } } },
+      where: { id: attachmentId, serviceRecord: { organizationId } },
       include: { serviceRecord: { select: { vehicleId: true, id: true } } },
     });
     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 } });
 
     const { vehicleId, id: serviceId } = attachment.serviceRecord;
-    revalidatePath(`/vehicles/${vehicleId}/service/${serviceId}`);
+    revalidatePath(
+      vehicleId ? `/vehicles/${vehicleId}/service/${serviceId}` : `/sales/${serviceId}`
+    );
     return { deleted: true };
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
 }
@@ -829,7 +893,7 @@ export async function deleteServiceAttachment(attachmentId: string) {
 export async function generatePublicLink(serviceRecordId: string) {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
-      where: { id: serviceRecordId, vehicle: { organizationId } },
+      where: { id: serviceRecordId, organizationId },
     });
     if (!record) throw new Error("Record not found");
 
@@ -839,7 +903,11 @@ export async function generatePublicLink(serviceRecordId: string) {
       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 };
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
 }
@@ -847,7 +915,7 @@ export async function generatePublicLink(serviceRecordId: string) {
 export async function revokePublicLink(serviceRecordId: string) {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
-      where: { id: serviceRecordId, vehicle: { organizationId } },
+      where: { id: serviceRecordId, organizationId },
     });
     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({
         where: {
           id: data.id,
-          serviceRecord: { vehicle: { organizationId } },
+          serviceRecord: { organizationId },
         },
         include: {
           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 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
                         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">
                         <p className="font-medium text-sm truncate">{job.title}</p>
                         <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>
                       </div>
                     </div>
@@ -424,17 +425,19 @@ export function MyActiveJobs({
                         <FileVideo className="mr-1.5 h-4 w-4" />
                         {t('statusReport')}
                       </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>
                     {/* Mobile: counters only */}
                     <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" />
                         {t('report')}
                       </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>
                   </div>
                   <input
@@ -664,7 +669,7 @@ export function MyActiveJobs({
                 if (!open) setStatusReportJobId(null)
               }}
               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}
               smsEnabled={smsEnabled}
               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 {
   const { data, styles, labels } = ctx
-  const c = data.vehicle.customer
+  const c = data.customer ?? data.vehicle?.customer
   if (!c) return null
 
   switch (fieldId) {
@@ -96,6 +96,7 @@ interface VehicleRenderCtx {
 
 function renderVehicleField(fieldId: string, ctx: VehicleRenderCtx): React.ReactNode {
   const { data, vehicleName, invoiceSettings, styles, labels } = ctx
+  if (!data.vehicle) return null
 
   switch (fieldId) {
     case 'vehicle_name':
@@ -137,6 +138,9 @@ function renderServiceField(fieldId: string, ctx: ServiceRenderCtx): React.React
     case 'service_title':
       return <Text key={fieldId} style={styles.infoTextBold}>{data.title}</Text>
     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 (
         <Text key={fieldId} style={styles.infoTextSmall}>
           {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 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_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)
 
@@ -205,7 +210,7 @@ export function CustomerSection({
   return (
     <View style={styles.infoBox}>
       <Text style={styles.infoLabel}>{labels.billTo || 'Bill To'}</Text>
-      {data.vehicle.customer && (
+      {invoiceCustomer && (
         <>
           {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 fieldOrder = getOrderedFieldIds(visibleFields, DEFAULT_VEHICLE_FIELDS)
 
+  if (!data.vehicle) return null
+
   const hasVehicle =
     show('vehicle_name') ||
     (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 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 laborSubtotal = data.laborItems.reduce((sum, l) => sum + l.total, 0)
   const computedSubtotal = partsSubtotal + laborSubtotal

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

@@ -55,6 +55,14 @@ export interface InvoiceData {
   warrantyMileage?: number | null;
   warrantyExpiresAt?: Date | 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: {
     make: string
     model: string
@@ -70,7 +78,7 @@ export interface InvoiceData {
       company: string | null
       taxId?: string | null
     } | null
-  }
+  } | null
 }
 
 export interface WorkshopInfo {

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

@@ -50,7 +50,9 @@ export interface ServiceDetail {
   laborItems: LaborItem[];
   attachments: Attachment[];
   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 {

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

@@ -17,9 +17,9 @@ interface CustomerInfo {
 
 interface BasicInfoSectionProps {
   initialData: InitialData
-  vehicleId: string
+  vehicleId: string | null
   vehicleName: string
-  selectedVehicleId: string
+  selectedVehicleId: string | null
   setSelectedVehicleId: (id: string) => void
   techName: string
   customer?: CustomerInfo | null
@@ -45,16 +45,18 @@ export function BasicInfoSection({
       <div className="space-y-1">
         <div className="flex items-center justify-between">
           <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>
         <VehicleCombobox
-          value={selectedVehicleId}
+          value={selectedVehicleId ?? ''}
           initialVehicle={initialVehicle ? { ...initialVehicle, customerId: null, customer: null } : null}
           placeholder={vehicleName || t('searchVehicles')}
           noneLabel={t('noVehicleFound')}
@@ -88,16 +90,18 @@ export function BasicInfoSection({
         </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} />
     </div>

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

@@ -21,6 +21,8 @@ interface InvoiceDetailsSectionProps {
   initialData: InitialData
   type: string
   setType: (type: string) => void
+  /** Counter sales (no vehicle) have no meaningful service type; hide the field. */
+  showType?: boolean
   status: string
   setStatus: (status: string) => void
   onDirty?: () => void
@@ -33,6 +35,7 @@ export function InvoiceDetailsSection({
   initialData,
   type,
   setType,
+  showType = true,
   status,
   setStatus,
   onDirty,
@@ -93,21 +96,23 @@ export function InvoiceDetailsSection({
         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">
           <Label className="text-xs">{t('status')}</Label>
           <Select value={status} onValueChange={setStatus}>

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

@@ -22,7 +22,7 @@ interface DetailsLeftColumnProps {
   onOpenPresets?: () => void
   onScanBarcode?: () => void
   aiEnabled?: boolean
-  vehicleId: string
+  vehicleId: string | null
   findings?: { id: string; description: string; severity: string; status: string; notes: string | null }[]
   onAddFinding?: () => 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}
         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">
         <PaymentsSection
           payments={record.payments || []}

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

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

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

@@ -89,6 +89,9 @@ export function ServicePageClient({
   const t = useTranslations('service')
   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 resolvedInitialTab =
     initialTab && validTabs.includes(initialTab as ServiceTab)
@@ -203,15 +206,17 @@ export function ServicePageClient({
   const [notifyMessage, setNotifyMessage] = useState('')
 
   const handleNotifyCustomer = useCallback(async () => {
-    if (!record.vehicle.customer) return
+    if (!customer) return
     const templateKey =
       statusTemplateKeys[formState.status] || SETTING_KEYS.SMS_TEMPLATE_STATUS_READY
     const tplResult = await getSmsTemplates()
     const tplData = tplResult.success && tplResult.data ? tplResult.data : null
     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, {
-      customer_name: record.vehicle.customer.name,
+      customer_name: customer.name,
       vehicle,
       company_name: tplData?.companyName || '',
       current_user: tplData?.currentUser || '',
@@ -220,10 +225,9 @@ export function ServicePageClient({
     setShowNotifyDialog(true)
   }, [
     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
 
   // Observations state
@@ -326,7 +330,7 @@ export function ServicePageClient({
           actions.setShowShareDialog(true)
         }}
         onNotifyCustomer={handleNotifyCustomer}
-        hasCustomer={!!record.vehicle.customer}
+        hasCustomer={!!customer}
       />
 
       {activeTab === 'details' && (
@@ -380,6 +384,7 @@ export function ServicePageClient({
               }
             />
           </form>
+          {vehicleId && (
           <ObservationsManager
             vehicleId={vehicleId}
             serviceRecordId={record.id}
@@ -390,6 +395,7 @@ export function ServicePageClient({
               obsControlsRef.current = c
             }}
           />
+          )}
         </>
       )}
 
@@ -427,13 +433,13 @@ export function ServicePageClient({
             organizationId={organizationId}
             vehicleName={formState.vehicleName}
             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
             }
@@ -479,7 +485,7 @@ export function ServicePageClient({
       <SendEmailDialog
         open={actions.showEmailDialog}
         onOpenChange={actions.setShowEmailDialog}
-        defaultEmail={record.vehicle.customer?.email || ''}
+        defaultEmail={customer?.email || ''}
         entityLabel={t('invoice.entityLabel')}
         onSend={async (email, message) =>
           sendInvoiceEmail({ serviceRecordId: record.id, recipientEmail: email, message })
@@ -492,17 +498,17 @@ export function ServicePageClient({
         recordId={record.id}
         organizationId={organizationId}
         initialToken={record.publicToken}
-        customer={record.vehicle.customer}
+        customer={customer}
         smsEnabled={smsEnabled}
         emailEnabled={emailEnabled}
       />
 
-      {record.vehicle.customer && (
+      {customer && (
         <>
           <NotifyCustomerDialog
             open={actions.showPaymentNotifyDialog}
             onOpenChange={actions.setShowPaymentNotifyDialog}
-            customer={record.vehicle.customer}
+            customer={customer}
             defaultMessage={actions.paymentNotifyMessage}
             emailSubject={t('invoice.emailSubject')}
             smsEnabled={smsEnabled}
@@ -513,7 +519,7 @@ export function ServicePageClient({
           <NotifyCustomerDialog
             open={showNotifyDialog}
             onOpenChange={setShowNotifyDialog}
-            customer={record.vehicle.customer}
+            customer={customer}
             defaultMessage={notifyMessage}
             emailSubject={t('invoice.statusEmailSubject')}
             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 {
-  vehicleId: string
+  vehicleId: string | null
   vehicleName: string
   title: 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="mb-2 flex flex-wrap items-center justify-between gap-2">
         <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"
         >
           <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 {
   record: ServiceDetail
-  vehicleId: string
+  vehicleId: string | null
   organizationId: string
   currencyCode: string
   unitSystem: 'metric' | 'imperial'
@@ -38,7 +38,7 @@ export interface ServicePageClientProps {
   defaultLaborRate: number
   initialData: InitialData
   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[]
   orgMembers?: OrgMemberOption[]
   currentUserName: string

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

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

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

@@ -15,7 +15,7 @@ export function useServiceFormState({
   currentUserName,
   record,
 }: {
-  vehicleId: string
+  vehicleId: string | null
   initialData: InitialData
   defaultTaxRate: number
   currentUserName: string
@@ -23,7 +23,7 @@ export function useServiceFormState({
 }) {
   // Form state
   const [loading, setLoading] = useState(false)
-  const [selectedVehicleId, setSelectedVehicleId] = useState(vehicleId)
+  const [selectedVehicleId, setSelectedVehicleId] = useState<string | null>(vehicleId)
 
   const [techName] = useState(initialData.techName || currentUserName)
   const [type, setType] = useState(initialData.type || 'maintenance')
@@ -143,7 +143,7 @@ export function useServiceFormState({
         ? 'paid'
         : 'partial'
   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
   //

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

@@ -30,7 +30,9 @@ export const serviceAttachmentSchema = 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"),
   description: z.string().optional(),
   type: z.enum(["maintenance", "repair", "upgrade", "inspection"]).default("maintenance"),

Некоторые файлы не были показаны из-за большого количества измененных файлов