Quellcode durchsuchen

Add vehicle observations feature (#121)

* outstanding items

* translations

* translations

* layout

* layout

* translations

* translations

* show note if observations exist

* dropdown

* delete observations when added to workorder

* dialog

* diloag

* lint

* inputs

* layout

* table layout

* description

* criticality

* delsum

* discount

* invoice

* test
Bernt Christian Egeland vor 6 Monaten
Ursprung
Commit
f2fe6b8799
95 geänderte Dateien mit 2360 neuen und 67 gelöschten Zeilen
  1. 4 0
      messages/de/audit.json
  2. 7 0
      messages/de/pdf.json
  3. 3 0
      messages/de/service.json
  4. 1 0
      messages/de/settings.json
  5. 7 0
      messages/de/share.json
  6. 50 1
      messages/de/vehicles.json
  7. 4 0
      messages/en/audit.json
  8. 7 0
      messages/en/pdf.json
  9. 3 0
      messages/en/service.json
  10. 1 0
      messages/en/settings.json
  11. 7 0
      messages/en/share.json
  12. 50 1
      messages/en/vehicles.json
  13. 4 0
      messages/es/audit.json
  14. 7 0
      messages/es/pdf.json
  15. 3 0
      messages/es/service.json
  16. 1 0
      messages/es/settings.json
  17. 7 0
      messages/es/share.json
  18. 50 1
      messages/es/vehicles.json
  19. 4 0
      messages/fr/audit.json
  20. 7 0
      messages/fr/pdf.json
  21. 3 0
      messages/fr/service.json
  22. 1 0
      messages/fr/settings.json
  23. 7 0
      messages/fr/share.json
  24. 50 1
      messages/fr/vehicles.json
  25. 4 0
      messages/it/audit.json
  26. 7 0
      messages/it/pdf.json
  27. 3 0
      messages/it/service.json
  28. 1 0
      messages/it/settings.json
  29. 7 0
      messages/it/share.json
  30. 50 1
      messages/it/vehicles.json
  31. 4 0
      messages/nb/audit.json
  32. 7 0
      messages/nb/pdf.json
  33. 3 0
      messages/nb/service.json
  34. 1 0
      messages/nb/settings.json
  35. 7 0
      messages/nb/share.json
  36. 50 1
      messages/nb/vehicles.json
  37. 4 0
      messages/nl/audit.json
  38. 7 0
      messages/nl/pdf.json
  39. 3 0
      messages/nl/service.json
  40. 1 0
      messages/nl/settings.json
  41. 7 0
      messages/nl/share.json
  42. 50 1
      messages/nl/vehicles.json
  43. 4 0
      messages/pl/audit.json
  44. 7 0
      messages/pl/pdf.json
  45. 3 0
      messages/pl/service.json
  46. 1 0
      messages/pl/settings.json
  47. 7 0
      messages/pl/share.json
  48. 50 1
      messages/pl/vehicles.json
  49. 4 0
      messages/pt-BR/audit.json
  50. 7 0
      messages/pt-BR/pdf.json
  51. 3 0
      messages/pt-BR/service.json
  52. 1 0
      messages/pt-BR/settings.json
  53. 7 0
      messages/pt-BR/share.json
  54. 50 1
      messages/pt-BR/vehicles.json
  55. 4 0
      messages/ru/audit.json
  56. 7 0
      messages/ru/pdf.json
  57. 3 0
      messages/ru/service.json
  58. 1 0
      messages/ru/settings.json
  59. 7 0
      messages/ru/share.json
  60. 50 1
      messages/ru/vehicles.json
  61. 4 0
      messages/tr/audit.json
  62. 7 0
      messages/tr/pdf.json
  63. 3 0
      messages/tr/service.json
  64. 1 0
      messages/tr/settings.json
  65. 7 0
      messages/tr/share.json
  66. 50 1
      messages/tr/vehicles.json
  67. 31 0
      prisma/migrations/20260403135129_vehicle_outstanding_items/migration.sql
  68. 28 0
      prisma/schema.prisma
  69. 358 0
      src/app/(authenticated)/vehicles/[id]/findings-table.tsx
  70. 10 1
      src/app/(authenticated)/vehicles/[id]/page.tsx
  71. 12 1
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  72. 111 2
      src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx
  73. 64 7
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  74. 8 0
      src/app/(public)/share/invoice/[orgId]/[token]/page.tsx
  75. 8 1
      src/app/api/protected/services/[id]/pdf/route.ts
  76. 8 1
      src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts
  77. 1 0
      src/features/settings/Actions/invoiceLayoutActions.ts
  78. 4 0
      src/features/settings/Components/InvoiceLayoutPreviewRenderer.tsx
  79. 24 5
      src/features/settings/Schema/invoiceLayoutSchema.ts
  80. 218 0
      src/features/vehicles/Actions/findingActions.ts
  81. 1 0
      src/features/vehicles/Actions/vehicleActions.ts
  82. 167 0
      src/features/vehicles/Components/FindingForm.tsx
  83. 19 2
      src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx
  84. 1 0
      src/features/vehicles/Components/invoice-pdf/Notes.tsx
  85. 58 0
      src/features/vehicles/Components/invoice-pdf/Tables.tsx
  86. 1 0
      src/features/vehicles/Components/invoice-pdf/types.ts
  87. 1 1
      src/features/vehicles/Components/service-detail/InvoiceSummary.tsx
  88. 129 0
      src/features/vehicles/Components/service-detail/ServiceFindingsSection.tsx
  89. 50 1
      src/features/vehicles/Components/service-edit/LaborEditor.tsx
  90. 23 0
      src/features/vehicles/Components/service-page/DetailsLeftColumn.tsx
  91. 128 0
      src/features/vehicles/Components/service-page/ObservationsManager.tsx
  92. 76 31
      src/features/vehicles/Components/service-page/ServicePageClient.tsx
  93. 2 0
      src/features/vehicles/Components/service-page/service-page-types.ts
  94. 3 3
      src/features/vehicles/Components/service-page/useServiceActions.ts
  95. 24 0
      src/features/vehicles/Schema/findingSchema.ts

+ 4 - 0
messages/de/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Lagerteil aktualisiert",
     "inventory_delete": "Lagerteil gelöscht",
     "inventory_bulkDelete": "Lagerteile massengelöscht",
+    "finding_create": "Beobachtung erstellt",
+    "finding_update": "Beobachtung aktualisiert",
+    "finding_resolve": "Beobachtung erledigt",
+    "finding_delete": "Beobachtung gelöscht",
     "team_invite": "Teammitglied eingeladen",
     "team_sendInvitation": "Einladung gesendet",
     "team_cancelInvitation": "Einladung storniert",

+ 7 - 0
messages/de/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Zahlungsbedingungen",
     "dueDateLabel": "Fälligkeitsdatum",
     "diagnosticNotes": "Diagnosehinweise",
+    "findings": "Beobachtungen",
+    "findingsDescription": "Die folgenden Punkte wurden bei diesem Service festgestellt und erfordern möglicherweise Aufmerksamkeit.",
+    "findingSeverityLabel": "Schweregrad",
+    "findingSeverityUrgent": "Hoch",
+    "findingSeverityNeedsWork": "Mittel",
+    "findingSeverityMonitor": "Niedrig",
+    "findingNotesLabel": "Hinweise",
     "attachedDocuments": "Beigefügte Dokumente",
     "seeAppendedPages": "{name} (siehe angehängte Seiten)",
     "serviceImages": "Service-Bilder",

+ 3 - 0
messages/de/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Beschreibung *",
     "subtotal": "Arbeitssumme: {amount}",
     "addService": "Add Service",
+    "addFinding": "Beobachtung",
+    "newObservation": "Neue Beobachtung",
+    "addExisting": "Vorhandene anzeigen ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

+ 1 - 0
messages/de/settings.json

@@ -409,6 +409,7 @@
       "totals": "Summen",
       "notes": "Notizen",
       "diagnostic_notes": "Diagnosenotizen",
+      "findings": "Beobachtungen",
       "bank_account": "Bankkonto",
       "footer": "Fußzeile",
       "general": "Allgemein",

+ 7 - 0
messages/de/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Diagnoseberichte ({count})",
     "notes": "Hinweise",
     "diagnosticNotes": "Diagnosehinweise",
+    "findings": "Beobachtungen",
+    "findingsDescription": "Die folgenden Punkte wurden bei diesem Service festgestellt und erfordern möglicherweise Aufmerksamkeit.",
+    "severity": "Schweregrad",
+    "findingNotes": "Hinweise",
+    "findingSeverityUrgent": "Hoch",
+    "findingSeverityNeedsWork": "Mittel",
+    "findingSeverityMonitor": "Niedrig",
     "tel": "Tel: {phone}"
   },
   "quote": {

+ 50 - 1
messages/de/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Angebote",
       "inspections": "Inspektionen",
       "notes": "Notizen",
-      "reminders": "Erinnerungen"
+      "reminders": "Erinnerungen",
+      "findings": "Beobachtungen"
     },
     "noteDeleted": "Notiz gel\u00f6scht",
     "noteDeleteError": "Fehler beim L\u00f6schen",
@@ -180,6 +181,54 @@
     "titleRequired": "Titel und Inhalt sind erforderlich",
     "saveError": "Fehler beim {action} der Notiz"
   },
+  "findings": {
+    "sectionTitle": "Beobachtungen",
+    "addFinding": "Beobachtung hinzufügen",
+    "editTitle": "Beobachtung bearbeiten",
+    "addTitle": "Beobachtung hinzufügen",
+    "empty": "Noch keine Beobachtungen.",
+    "descriptionLabel": "Beschreibung *",
+    "descriptionPlaceholder": "z.B. Bremsbeläge vorne am Minimum",
+    "severityLabel": "Schweregrad",
+    "notesLabel": "Zusätzliche Anmerkungen",
+    "notesPlaceholder": "Optionale Details...",
+    "table": {
+      "severity": "Schweregrad",
+      "description": "Beschreibung",
+      "status": "Status",
+      "date": "Datum"
+    },
+    "severity": {
+      "urgent": "Hoch",
+      "needs_work": "Mittel",
+      "monitor": "Niedrig"
+    },
+    "status": {
+      "open": "Offen",
+      "quoted": "Angeboten",
+      "resolved": "Erledigt"
+    },
+    "edit": "Bearbeiten",
+    "resolve": "Als erledigt markieren",
+    "discoveredIn": "Entdeckt in",
+    "resolvedIn": "Behoben in",
+    "createWorkOrder": "Arbeitsauftrag erstellen ({count})",
+    "vehicleHasObservations": "Dieses Fahrzeug hat {count} offene Beobachtungen",
+    "addToWorkOrder": "Zum Auftrag hinzufügen ({count})",
+    "dismiss": "Ausblenden",
+    "observationsAdded": "{count} Beobachtungen zum Auftrag hinzugefügt",
+    "delete": "Löschen",
+    "findingCreated": "Beobachtung hinzugefügt",
+    "findingUpdated": "Beobachtung aktualisiert",
+    "findingResolved": "Beobachtung erledigt",
+    "findingDeleted": "Beobachtung gelöscht",
+    "saveError": "Beobachtung konnte nicht gespeichert werden",
+    "deleteError": "Beobachtung konnte nicht gelöscht werden",
+    "resolveError": "Beobachtung konnte nicht erledigt werden",
+    "showing": "Zeige {start}-{end} von {total}",
+    "perPage": "pro Seite",
+    "page": "Seite {page} von {totalPages}"
+  },
   "form": {
     "editTitle": "Fahrzeug bearbeiten",
     "addTitle": "Neues Fahrzeug hinzuf\u00fcgen",

+ 4 - 0
messages/en/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Updated Inventory Part",
     "inventory_delete": "Deleted Inventory Part",
     "inventory_bulkDelete": "Bulk Deleted Inventory Parts",
+    "finding_create": "Created Observation",
+    "finding_update": "Updated Observation",
+    "finding_resolve": "Resolved Observation",
+    "finding_delete": "Deleted Observation",
     "team_invite": "Invited Team Member",
     "team_sendInvitation": "Sent Invitation",
     "team_cancelInvitation": "Cancelled Invitation",

+ 7 - 0
messages/en/pdf.json

@@ -40,6 +40,13 @@
     "paymentTermsLabel": "Payment Terms",
     "dueDateLabel": "Due Date",
     "diagnosticNotes": "Diagnostic Notes",
+    "findings": "Observations",
+    "findingsDescription": "The following items were observed during this service and may require attention.",
+    "findingSeverityLabel": "Severity",
+    "findingSeverityUrgent": "High",
+    "findingSeverityNeedsWork": "Medium",
+    "findingSeverityMonitor": "Low",
+    "findingNotesLabel": "Notes",
     "attachedDocuments": "Attached Documents",
     "seeAppendedPages": "{name} (see appended pages)",
     "serviceImages": "Service Images",

+ 3 - 0
messages/en/service.json

@@ -100,6 +100,9 @@
     "title": "Labor & Services",
     "addLabor": "Add Labor",
     "addService": "Add Service",
+    "addFinding": "Observation",
+    "newObservation": "New Observation",
+    "addExisting": "Show Existing ({count})",
     "fromPresets": "From Presets",
     "description": "Description",
     "hours": "Hours",

+ 1 - 0
messages/en/settings.json

@@ -409,6 +409,7 @@
       "totals": "Totals",
       "notes": "Notes",
       "diagnostic_notes": "Diagnostic Notes",
+      "findings": "Observations",
       "bank_account": "Bank Account",
       "footer": "Footer",
       "general": "General",

+ 7 - 0
messages/en/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Diagnostic Reports ({count})",
     "notes": "Notes",
     "diagnosticNotes": "Diagnostic Notes",
+    "findings": "Observations",
+    "findingsDescription": "The following items were observed during this service and may require attention.",
+    "severity": "Severity",
+    "findingNotes": "Notes",
+    "findingSeverityUrgent": "High",
+    "findingSeverityNeedsWork": "Medium",
+    "findingSeverityMonitor": "Low",
     "tel": "Tel: {phone}"
   },
   "quote": {

+ 50 - 1
messages/en/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Quotes",
       "inspections": "Inspections",
       "notes": "Notes",
-      "reminders": "Reminders"
+      "reminders": "Reminders",
+      "findings": "Observations"
     },
     "noteDeleted": "Note deleted",
     "noteDeleteError": "Failed to delete",
@@ -180,6 +181,54 @@
     "titleRequired": "Title and content are required",
     "saveError": "Failed to {action} note"
   },
+  "findings": {
+    "sectionTitle": "Observations",
+    "addFinding": "Add Observation",
+    "editTitle": "Edit Observation",
+    "addTitle": "Add Observation",
+    "empty": "No observations yet.",
+    "descriptionLabel": "Description *",
+    "descriptionPlaceholder": "e.g. Brake pads front at minimum",
+    "severityLabel": "Severity",
+    "notesLabel": "Additional Notes",
+    "notesPlaceholder": "Optional details...",
+    "table": {
+      "severity": "Severity",
+      "description": "Description",
+      "status": "Status",
+      "date": "Date"
+    },
+    "severity": {
+      "urgent": "High",
+      "needs_work": "Medium",
+      "monitor": "Low"
+    },
+    "status": {
+      "open": "Open",
+      "quoted": "Quoted",
+      "resolved": "Resolved"
+    },
+    "edit": "Edit",
+    "resolve": "Mark Resolved",
+    "discoveredIn": "Discovered in",
+    "resolvedIn": "Resolved in",
+    "createWorkOrder": "Create Work Order ({count})",
+    "vehicleHasObservations": "This vehicle has {count} open observations",
+    "addToWorkOrder": "Add to Work Order ({count})",
+    "dismiss": "Dismiss",
+    "observationsAdded": "{count} observations added to work order",
+    "delete": "Delete",
+    "findingCreated": "Observation added",
+    "findingUpdated": "Observation updated",
+    "findingResolved": "Observation resolved",
+    "findingDeleted": "Observation deleted",
+    "saveError": "Failed to {action} observation",
+    "deleteError": "Failed to delete observation",
+    "resolveError": "Failed to resolve observation",
+    "showing": "Showing {start}-{end} of {total}",
+    "perPage": "per page",
+    "page": "Page {page} of {totalPages}"
+  },
   "form": {
     "editTitle": "Edit Vehicle",
     "addTitle": "Add New Vehicle",

+ 4 - 0
messages/es/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Pieza de inventario actualizada",
     "inventory_delete": "Pieza de inventario eliminada",
     "inventory_bulkDelete": "Piezas de inventario eliminadas en masa",
+    "finding_create": "Observación creada",
+    "finding_update": "Observación actualizada",
+    "finding_resolve": "Observación resuelta",
+    "finding_delete": "Observación eliminada",
     "team_invite": "Miembro del equipo invitado",
     "team_sendInvitation": "Invitación enviada",
     "team_cancelInvitation": "Invitación cancelada",

+ 7 - 0
messages/es/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Condiciones de pago",
     "dueDateLabel": "Fecha de vencimiento",
     "diagnosticNotes": "Notas de diagnóstico",
+    "findings": "Observaciones",
+    "findingsDescription": "Los siguientes elementos fueron observados durante este servicio y pueden requerir atención.",
+    "findingSeverityLabel": "Gravedad",
+    "findingSeverityUrgent": "Alta",
+    "findingSeverityNeedsWork": "Media",
+    "findingSeverityMonitor": "Baja",
+    "findingNotesLabel": "Notas",
     "attachedDocuments": "Documentos adjuntos",
     "seeAppendedPages": "{name} (ver páginas adjuntas)",
     "serviceImages": "Imágenes del servicio",

+ 3 - 0
messages/es/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Descripción *",
     "subtotal": "Subtotal de Mano de Obra: {amount}",
     "addService": "Add Service",
+    "addFinding": "Observación",
+    "newObservation": "Nueva observación",
+    "addExisting": "Ver existentes ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

+ 1 - 0
messages/es/settings.json

@@ -409,6 +409,7 @@
       "totals": "Totales",
       "notes": "Notas",
       "diagnostic_notes": "Notas de diagnóstico",
+      "findings": "Observaciones",
       "bank_account": "Cuenta bancaria",
       "footer": "Pie de página",
       "general": "General",

+ 7 - 0
messages/es/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Informes de diagnóstico ({count})",
     "notes": "Notas",
     "diagnosticNotes": "Notas de diagnóstico",
+    "findings": "Observaciones",
+    "findingsDescription": "Los siguientes elementos fueron observados durante este servicio y pueden requerir atención.",
+    "severity": "Gravedad",
+    "findingNotes": "Notas",
+    "findingSeverityUrgent": "Alta",
+    "findingSeverityNeedsWork": "Media",
+    "findingSeverityMonitor": "Baja",
     "tel": "Tel: {phone}"
   },
   "quote": {

+ 50 - 1
messages/es/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Cotizaciones",
       "inspections": "Inspecciones",
       "notes": "Notas",
-      "reminders": "Recordatorios"
+      "reminders": "Recordatorios",
+      "findings": "Observaciones"
     },
     "noteDeleted": "Nota eliminada",
     "noteDeleteError": "Error al eliminar",
@@ -180,6 +181,54 @@
     "titleRequired": "El titulo y el contenido son obligatorios",
     "saveError": "Error al {action} nota"
   },
+  "findings": {
+    "sectionTitle": "Observaciones",
+    "addFinding": "Agregar observación",
+    "editTitle": "Editar observación",
+    "addTitle": "Agregar observación",
+    "empty": "Aún no hay observaciones.",
+    "descriptionLabel": "Descripción *",
+    "descriptionPlaceholder": "ej. Pastillas de freno delanteras al mínimo",
+    "severityLabel": "Gravedad",
+    "notesLabel": "Notas adicionales",
+    "notesPlaceholder": "Detalles opcionales...",
+    "table": {
+      "severity": "Gravedad",
+      "description": "Descripción",
+      "status": "Estado",
+      "date": "Fecha"
+    },
+    "severity": {
+      "urgent": "Alta",
+      "needs_work": "Media",
+      "monitor": "Baja"
+    },
+    "status": {
+      "open": "Abierto",
+      "quoted": "Cotizado",
+      "resolved": "Resuelto"
+    },
+    "edit": "Editar",
+    "resolve": "Marcar como resuelto",
+    "discoveredIn": "Descubierto en",
+    "resolvedIn": "Resuelto en",
+    "createWorkOrder": "Crear orden de trabajo ({count})",
+    "vehicleHasObservations": "Este vehículo tiene {count} observaciones abiertas",
+    "addToWorkOrder": "Agregar a la orden ({count})",
+    "dismiss": "Descartar",
+    "observationsAdded": "{count} observaciones agregadas a la orden",
+    "delete": "Eliminar",
+    "findingCreated": "Observación agregada",
+    "findingUpdated": "Observación actualizada",
+    "findingResolved": "Observación resuelta",
+    "findingDeleted": "Observación eliminada",
+    "saveError": "Error al {action} observación",
+    "deleteError": "Error al eliminar observación",
+    "resolveError": "Error al resolver observación",
+    "showing": "Mostrando {start}-{end} de {total}",
+    "perPage": "por página",
+    "page": "Página {page} de {totalPages}"
+  },
   "form": {
     "editTitle": "Editar vehiculo",
     "addTitle": "Agregar nuevo vehiculo",

+ 4 - 0
messages/fr/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Pièce d'inventaire mise à jour",
     "inventory_delete": "Pièce d'inventaire supprimée",
     "inventory_bulkDelete": "Pièces d'inventaire supprimées en masse",
+    "finding_create": "Observation créée",
+    "finding_update": "Observation mise à jour",
+    "finding_resolve": "Observation résolue",
+    "finding_delete": "Observation supprimée",
     "team_invite": "Membre d'équipe invité",
     "team_sendInvitation": "Invitation envoyée",
     "team_cancelInvitation": "Invitation annulée",

+ 7 - 0
messages/fr/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Conditions de paiement",
     "dueDateLabel": "Date d'échéance",
     "diagnosticNotes": "Notes de diagnostic",
+    "findings": "Observations",
+    "findingsDescription": "Les éléments suivants ont été observés lors de ce service et peuvent nécessiter une attention particulière.",
+    "findingSeverityLabel": "Gravité",
+    "findingSeverityUrgent": "Haute",
+    "findingSeverityNeedsWork": "Moyenne",
+    "findingSeverityMonitor": "Basse",
+    "findingNotesLabel": "Remarques",
     "attachedDocuments": "Documents joints",
     "seeAppendedPages": "{name} (voir pages annexées)",
     "serviceImages": "Images de service",

+ 3 - 0
messages/fr/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Description *",
     "subtotal": "Sous-total Main-d'œuvre: {amount}",
     "addService": "Add Service",
+    "addFinding": "Observation",
+    "newObservation": "Nouvelle observation",
+    "addExisting": "Voir existantes ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

+ 1 - 0
messages/fr/settings.json

@@ -409,6 +409,7 @@
       "totals": "Totaux",
       "notes": "Notes",
       "diagnostic_notes": "Notes de diagnostic",
+      "findings": "Observations",
       "bank_account": "Compte bancaire",
       "footer": "Pied de page",
       "general": "Général",

+ 7 - 0
messages/fr/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Rapports de diagnostic ({count})",
     "notes": "Notes",
     "diagnosticNotes": "Notes de diagnostic",
+    "findings": "Observations",
+    "findingsDescription": "Les éléments suivants ont été observés lors de ce service et peuvent nécessiter une attention particulière.",
+    "severity": "Gravité",
+    "findingNotes": "Remarques",
+    "findingSeverityUrgent": "Haute",
+    "findingSeverityNeedsWork": "Moyenne",
+    "findingSeverityMonitor": "Basse",
     "tel": "Tél. : {phone}"
   },
   "quote": {

+ 50 - 1
messages/fr/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Devis",
       "inspections": "Inspections",
       "notes": "Notes",
-      "reminders": "Rappels"
+      "reminders": "Rappels",
+      "findings": "Observations"
     },
     "noteDeleted": "Note supprim\u00e9e",
     "noteDeleteError": "\u00c9chec de la suppression",
@@ -180,6 +181,54 @@
     "titleRequired": "Le titre et le contenu sont obligatoires",
     "saveError": "\u00c9chec de {action} de la note"
   },
+  "findings": {
+    "sectionTitle": "Observations",
+    "addFinding": "Ajouter une observation",
+    "editTitle": "Modifier l'observation",
+    "addTitle": "Ajouter une observation",
+    "empty": "Aucune observation pour le moment.",
+    "descriptionLabel": "Description *",
+    "descriptionPlaceholder": "ex. Plaquettes de frein avant au minimum",
+    "severityLabel": "Gravité",
+    "notesLabel": "Notes supplémentaires",
+    "notesPlaceholder": "Détails optionnels...",
+    "table": {
+      "severity": "Gravité",
+      "description": "Description",
+      "status": "Statut",
+      "date": "Date"
+    },
+    "severity": {
+      "urgent": "Haute",
+      "needs_work": "Moyenne",
+      "monitor": "Basse"
+    },
+    "status": {
+      "open": "Ouvert",
+      "quoted": "Devisé",
+      "resolved": "Résolu"
+    },
+    "edit": "Modifier",
+    "resolve": "Marquer comme résolu",
+    "discoveredIn": "Découvert dans",
+    "resolvedIn": "Résolu dans",
+    "createWorkOrder": "Créer un ordre de travail ({count})",
+    "vehicleHasObservations": "Ce véhicule a {count} observations en cours",
+    "addToWorkOrder": "Ajouter à l'ordre de travail ({count})",
+    "dismiss": "Ignorer",
+    "observationsAdded": "{count} observations ajoutées à l'ordre de travail",
+    "delete": "Supprimer",
+    "findingCreated": "Observation ajoutée",
+    "findingUpdated": "Observation mise à jour",
+    "findingResolved": "Observation résolue",
+    "findingDeleted": "Observation supprimée",
+    "saveError": "Échec de l'observation",
+    "deleteError": "Échec de suppression",
+    "resolveError": "Échec de résolution",
+    "showing": "Affichage de {start}-{end} sur {total}",
+    "perPage": "par page",
+    "page": "Page {page} sur {totalPages}"
+  },
   "form": {
     "editTitle": "Modifier le v\u00e9hicule",
     "addTitle": "Ajouter un nouveau v\u00e9hicule",

+ 4 - 0
messages/it/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Pezzo di inventario aggiornato",
     "inventory_delete": "Pezzo di inventario eliminato",
     "inventory_bulkDelete": "Pezzi di inventario eliminati in massa",
+    "finding_create": "Osservazione creata",
+    "finding_update": "Osservazione aggiornata",
+    "finding_resolve": "Osservazione risolta",
+    "finding_delete": "Osservazione eliminata",
     "team_invite": "Membro del team invitato",
     "team_sendInvitation": "Invito inviato",
     "team_cancelInvitation": "Invito annullato",

+ 7 - 0
messages/it/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Condizioni di pagamento",
     "dueDateLabel": "Data di scadenza",
     "diagnosticNotes": "Note Diagnostiche",
+    "findings": "Osservazioni",
+    "findingsDescription": "I seguenti elementi sono stati osservati durante questo servizio e potrebbero richiedere attenzione.",
+    "findingSeverityLabel": "Gravità",
+    "findingSeverityUrgent": "Alta",
+    "findingSeverityNeedsWork": "Media",
+    "findingSeverityMonitor": "Bassa",
+    "findingNotesLabel": "Note",
     "attachedDocuments": "Documenti Allegati",
     "seeAppendedPages": "{name} (vedi pagine allegate)",
     "serviceImages": "Immagini del Servizio",

+ 3 - 0
messages/it/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Descrizione *",
     "subtotal": "Subtotale Manodopera: {amount}",
     "addService": "Add Service",
+    "addFinding": "Osservazione",
+    "newObservation": "Nuova osservazione",
+    "addExisting": "Mostra esistenti ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

+ 1 - 0
messages/it/settings.json

@@ -409,6 +409,7 @@
       "totals": "Totali",
       "notes": "Note",
       "diagnostic_notes": "Note diagnostiche",
+      "findings": "Osservazioni",
       "bank_account": "Conto bancario",
       "footer": "Piè di pagina",
       "general": "Generale",

+ 7 - 0
messages/it/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Rapporti Diagnostici ({count})",
     "notes": "Note",
     "diagnosticNotes": "Note Diagnostiche",
+    "findings": "Osservazioni",
+    "findingsDescription": "I seguenti elementi sono stati osservati durante questo servizio e potrebbero richiedere attenzione.",
+    "severity": "Gravità",
+    "findingNotes": "Note",
+    "findingSeverityUrgent": "Alta",
+    "findingSeverityNeedsWork": "Media",
+    "findingSeverityMonitor": "Bassa",
     "tel": "Tel: {phone}"
   },
   "quote": {

+ 50 - 1
messages/it/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Preventivi",
       "inspections": "Ispezioni",
       "notes": "Note",
-      "reminders": "Promemoria"
+      "reminders": "Promemoria",
+      "findings": "Osservazioni"
     },
     "noteDeleted": "Nota eliminata",
     "noteDeleteError": "Impossibile eliminare",
@@ -180,6 +181,54 @@
     "titleRequired": "Titolo e contenuto sono obbligatori",
     "saveError": "Impossibile {action} la nota"
   },
+  "findings": {
+    "sectionTitle": "Osservazioni",
+    "addFinding": "Aggiungi osservazione",
+    "editTitle": "Modifica osservazione",
+    "addTitle": "Aggiungi osservazione",
+    "empty": "Nessuna osservazione ancora.",
+    "descriptionLabel": "Descrizione *",
+    "descriptionPlaceholder": "es. Pastiglie freno anteriori al minimo",
+    "severityLabel": "Gravità",
+    "notesLabel": "Note aggiuntive",
+    "notesPlaceholder": "Dettagli facoltativi...",
+    "table": {
+      "severity": "Gravità",
+      "description": "Descrizione",
+      "status": "Stato",
+      "date": "Data"
+    },
+    "severity": {
+      "urgent": "Alta",
+      "needs_work": "Media",
+      "monitor": "Bassa"
+    },
+    "status": {
+      "open": "Aperto",
+      "quoted": "Preventivato",
+      "resolved": "Risolto"
+    },
+    "edit": "Modifica",
+    "resolve": "Segna come risolto",
+    "discoveredIn": "Scoperto in",
+    "resolvedIn": "Risolto in",
+    "createWorkOrder": "Crea ordine di lavoro ({count})",
+    "vehicleHasObservations": "Questo veicolo ha {count} osservazioni aperte",
+    "addToWorkOrder": "Aggiungi all'ordine di lavoro ({count})",
+    "dismiss": "Ignora",
+    "observationsAdded": "{count} osservazioni aggiunte all'ordine di lavoro",
+    "delete": "Elimina",
+    "findingCreated": "Osservazione aggiunta",
+    "findingUpdated": "Osservazione aggiornata",
+    "findingResolved": "Osservazione risolta",
+    "findingDeleted": "Osservazione eliminata",
+    "saveError": "Impossibile salvare l'osservazione",
+    "deleteError": "Impossibile eliminare l'osservazione",
+    "resolveError": "Impossibile risolvere l'osservazione",
+    "showing": "Visualizzando {start}-{end} di {total}",
+    "perPage": "per pagina",
+    "page": "Pagina {page} di {totalPages}"
+  },
   "form": {
     "editTitle": "Modifica Veicolo",
     "addTitle": "Aggiungi Nuovo Veicolo",

+ 4 - 0
messages/nb/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Oppdaterte lagerdel",
     "inventory_delete": "Slettet lagerdel",
     "inventory_bulkDelete": "Masseslettet lagerdeler",
+    "finding_create": "Opprettet observasjon",
+    "finding_update": "Oppdaterte observasjon",
+    "finding_resolve": "Løste observasjon",
+    "finding_delete": "Slettet observasjon",
     "team_invite": "Inviterte teammedlem",
     "team_sendInvitation": "Sendte invitasjon",
     "team_cancelInvitation": "Kansellerte invitasjon",

+ 7 - 0
messages/nb/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Betalingsbetingelser",
     "dueDateLabel": "Forfallsdato",
     "diagnosticNotes": "Diagnosenotater",
+    "findings": "Observasjoner",
+    "findingsDescription": "Følgende punkter ble observert under denne servicen og kan kreve oppmerksomhet.",
+    "findingSeverityLabel": "Alvorlighet",
+    "findingSeverityUrgent": "Høy",
+    "findingSeverityNeedsWork": "Middels",
+    "findingSeverityMonitor": "Lav",
+    "findingNotesLabel": "Merknader",
     "attachedDocuments": "Vedlagte dokumenter",
     "seeAppendedPages": "{name} (se vedlagte sider)",
     "serviceImages": "Servicebilder",

+ 3 - 0
messages/nb/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Beskrivelse *",
     "subtotal": "Arbeid delsum: {amount}",
     "addService": "Add Service",
+    "addFinding": "Observasjon",
+    "newObservation": "Ny observasjon",
+    "addExisting": "Vis eksisterende ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

+ 1 - 0
messages/nb/settings.json

@@ -409,6 +409,7 @@
       "totals": "Totaler",
       "notes": "Notater",
       "diagnostic_notes": "Diagnostiske notater",
+      "findings": "Observasjoner",
       "bank_account": "Bankkonto",
       "footer": "Bunntekst",
       "general": "Generelt",

+ 7 - 0
messages/nb/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Diagnoserapporter ({count})",
     "notes": "Notater",
     "diagnosticNotes": "Diagnosenotater",
+    "findings": "Observasjoner",
+    "findingsDescription": "Følgende punkter ble observert under denne servicen og kan kreve oppmerksomhet.",
+    "severity": "Alvorlighet",
+    "findingNotes": "Merknader",
+    "findingSeverityUrgent": "Høy",
+    "findingSeverityNeedsWork": "Middels",
+    "findingSeverityMonitor": "Lav",
     "tel": "Tlf: {phone}"
   },
   "quote": {

+ 50 - 1
messages/nb/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Tilbud",
       "inspections": "Inspeksjoner",
       "notes": "Notater",
-      "reminders": "Paminnelser"
+      "reminders": "Paminnelser",
+      "findings": "Observasjoner"
     },
     "noteDeleted": "Notat slettet",
     "noteDeleteError": "Kunne ikke slette",
@@ -180,6 +181,54 @@
     "titleRequired": "Tittel og innhold er pakrevd",
     "saveError": "Kunne ikke {action} notat"
   },
+  "findings": {
+    "sectionTitle": "Observasjoner",
+    "addFinding": "Legg til observasjon",
+    "editTitle": "Rediger observasjon",
+    "addTitle": "Legg til observasjon",
+    "empty": "Ingen observasjoner ennå.",
+    "descriptionLabel": "Beskrivelse *",
+    "descriptionPlaceholder": "f.eks. Bremseklosser foran på minimum",
+    "severityLabel": "Alvorlighetsgrad",
+    "notesLabel": "Tilleggsnotater",
+    "notesPlaceholder": "Valgfrie detaljer...",
+    "table": {
+      "severity": "Alvorlighetsgrad",
+      "description": "Beskrivelse",
+      "status": "Status",
+      "date": "Dato"
+    },
+    "severity": {
+      "urgent": "Høy",
+      "needs_work": "Middels",
+      "monitor": "Lav"
+    },
+    "status": {
+      "open": "Åpen",
+      "quoted": "Tilbudt",
+      "resolved": "Løst"
+    },
+    "edit": "Rediger",
+    "resolve": "Merk som løst",
+    "discoveredIn": "Oppdaget i",
+    "resolvedIn": "Løst i",
+    "createWorkOrder": "Opprett arbeidsordre ({count})",
+    "vehicleHasObservations": "Dette kjøretøyet har {count} åpne observasjoner",
+    "addToWorkOrder": "Legg til i arbeidsordre ({count})",
+    "dismiss": "Avvis",
+    "observationsAdded": "{count} observasjoner lagt til i arbeidsordre",
+    "delete": "Slett",
+    "findingCreated": "Observasjon lagt til",
+    "findingUpdated": "Observasjon oppdatert",
+    "findingResolved": "Observasjon løst",
+    "findingDeleted": "Observasjon slettet",
+    "saveError": "Kunne ikke lagre observasjon",
+    "deleteError": "Kunne ikke slette observasjon",
+    "resolveError": "Kunne ikke løse observasjon",
+    "showing": "Viser {start}-{end} av {total}",
+    "perPage": "per side",
+    "page": "Side {page} av {totalPages}"
+  },
   "form": {
     "editTitle": "Rediger kjoretoy",
     "addTitle": "Legg til nytt kjoretoy",

+ 4 - 0
messages/nl/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Inventarisonderdeel bijgewerkt",
     "inventory_delete": "Inventarisonderdeel verwijderd",
     "inventory_bulkDelete": "Inventarisonderdelen in bulk verwijderd",
+    "finding_create": "Observatie aangemaakt",
+    "finding_update": "Observatie bijgewerkt",
+    "finding_resolve": "Observatie opgelost",
+    "finding_delete": "Observatie verwijderd",
     "team_invite": "Teamlid uitgenodigd",
     "team_sendInvitation": "Uitnodiging verzonden",
     "team_cancelInvitation": "Uitnodiging geannuleerd",

+ 7 - 0
messages/nl/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Betalingsvoorwaarden",
     "dueDateLabel": "Vervaldatum",
     "diagnosticNotes": "Diagnosenotities",
+    "findings": "Observaties",
+    "findingsDescription": "De volgende punten zijn waargenomen tijdens deze service en vereisen mogelijk aandacht.",
+    "findingSeverityLabel": "Ernst",
+    "findingSeverityUrgent": "Hoog",
+    "findingSeverityNeedsWork": "Gemiddeld",
+    "findingSeverityMonitor": "Laag",
+    "findingNotesLabel": "Opmerkingen",
     "attachedDocuments": "Bijgevoegde documenten",
     "seeAppendedPages": "{name} (zie bijgevoegde pagina's)",
     "serviceImages": "Serviceafbeeldingen",

+ 3 - 0
messages/nl/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Beschrijving *",
     "subtotal": "Arbeidskosten totaal: {amount}",
     "addService": "Add Service",
+    "addFinding": "Observatie",
+    "newObservation": "Nieuwe observatie",
+    "addExisting": "Bestaande tonen ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

+ 1 - 0
messages/nl/settings.json

@@ -409,6 +409,7 @@
       "totals": "Totalen",
       "notes": "Notities",
       "diagnostic_notes": "Diagnostische notities",
+      "findings": "Observaties",
       "bank_account": "Bankrekening",
       "footer": "Voettekst",
       "general": "Algemeen",

+ 7 - 0
messages/nl/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Diagnoserapporten ({count})",
     "notes": "Notities",
     "diagnosticNotes": "Diagnosenotities",
+    "findings": "Observaties",
+    "findingsDescription": "De volgende punten zijn waargenomen tijdens deze service en vereisen mogelijk aandacht.",
+    "severity": "Ernst",
+    "findingNotes": "Opmerkingen",
+    "findingSeverityUrgent": "Hoog",
+    "findingSeverityNeedsWork": "Gemiddeld",
+    "findingSeverityMonitor": "Laag",
     "tel": "Tel: {phone}"
   },
   "quote": {

+ 50 - 1
messages/nl/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Offertes",
       "inspections": "Keuringen",
       "notes": "Notities",
-      "reminders": "Herinneringen"
+      "reminders": "Herinneringen",
+      "findings": "Observaties"
     },
     "noteDeleted": "Notitie verwijderd",
     "noteDeleteError": "Verwijderen mislukt",
@@ -180,6 +181,54 @@
     "titleRequired": "Titel en inhoud zijn verplicht",
     "saveError": "{action} van notitie mislukt"
   },
+  "findings": {
+    "sectionTitle": "Observaties",
+    "addFinding": "Observatie toevoegen",
+    "editTitle": "Observatie bewerken",
+    "addTitle": "Observatie toevoegen",
+    "empty": "Nog geen observaties.",
+    "descriptionLabel": "Beschrijving *",
+    "descriptionPlaceholder": "bijv. Remblokken voor op minimum",
+    "severityLabel": "Ernst",
+    "notesLabel": "Aanvullende opmerkingen",
+    "notesPlaceholder": "Optionele details...",
+    "table": {
+      "severity": "Ernst",
+      "description": "Beschrijving",
+      "status": "Status",
+      "date": "Datum"
+    },
+    "severity": {
+      "urgent": "Hoog",
+      "needs_work": "Gemiddeld",
+      "monitor": "Laag"
+    },
+    "status": {
+      "open": "Open",
+      "quoted": "Geoffreerd",
+      "resolved": "Opgelost"
+    },
+    "edit": "Bewerken",
+    "resolve": "Markeer als opgelost",
+    "discoveredIn": "Ontdekt in",
+    "resolvedIn": "Opgelost in",
+    "createWorkOrder": "Werkorder aanmaken ({count})",
+    "vehicleHasObservations": "Dit voertuig heeft {count} openstaande observaties",
+    "addToWorkOrder": "Toevoegen aan werkorder ({count})",
+    "dismiss": "Negeren",
+    "observationsAdded": "{count} observaties toegevoegd aan werkorder",
+    "delete": "Verwijderen",
+    "findingCreated": "Observatie toegevoegd",
+    "findingUpdated": "Observatie bijgewerkt",
+    "findingResolved": "Observatie opgelost",
+    "findingDeleted": "Observatie verwijderd",
+    "saveError": "Kan observatie niet opslaan",
+    "deleteError": "Kan observatie niet verwijderen",
+    "resolveError": "Kan observatie niet oplossen",
+    "showing": "{start}-{end} van {total} weergegeven",
+    "perPage": "per pagina",
+    "page": "Pagina {page} van {totalPages}"
+  },
   "form": {
     "editTitle": "Voertuig bewerken",
     "addTitle": "Nieuw voertuig toevoegen",

+ 4 - 0
messages/pl/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Zaktualizowano część magazynową",
     "inventory_delete": "Usunięto część magazynową",
     "inventory_bulkDelete": "Masowo usunięto części magazynowe",
+    "finding_create": "Utworzono obserwację",
+    "finding_update": "Zaktualizowano obserwację",
+    "finding_resolve": "Rozwiązano obserwację",
+    "finding_delete": "Usunięto obserwację",
     "team_invite": "Zaproszono członka zespołu",
     "team_sendInvitation": "Wysłano zaproszenie",
     "team_cancelInvitation": "Anulowano zaproszenie",

+ 7 - 0
messages/pl/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Warunki płatności",
     "dueDateLabel": "Termin płatności",
     "diagnosticNotes": "Uwagi diagnostyczne",
+    "findings": "Obserwacje",
+    "findingsDescription": "Następujące elementy zostały zaobserwowane podczas tego serwisu i mogą wymagać uwagi.",
+    "findingSeverityLabel": "Priorytet",
+    "findingSeverityUrgent": "Wysoki",
+    "findingSeverityNeedsWork": "Średni",
+    "findingSeverityMonitor": "Niski",
+    "findingNotesLabel": "Uwagi",
     "attachedDocuments": "Załączone dokumenty",
     "seeAppendedPages": "{name} (patrz dołączone strony)",
     "serviceImages": "Zdjęcia serwisowe",

+ 3 - 0
messages/pl/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Opis *",
     "subtotal": "Razem Praca: {amount}",
     "addService": "Add Service",
+    "addFinding": "Obserwacja",
+    "newObservation": "Nowa obserwacja",
+    "addExisting": "Pokaż istniejące ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

+ 1 - 0
messages/pl/settings.json

@@ -409,6 +409,7 @@
       "totals": "Podsumowanie",
       "notes": "Notatki",
       "diagnostic_notes": "Notatki diagnostyczne",
+      "findings": "Obserwacje",
       "bank_account": "Konto bankowe",
       "footer": "Stopka",
       "general": "Ogólne",

+ 7 - 0
messages/pl/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Raporty diagnostyczne ({count})",
     "notes": "Uwagi",
     "diagnosticNotes": "Uwagi diagnostyczne",
+    "findings": "Obserwacje",
+    "findingsDescription": "Następujące elementy zostały zaobserwowane podczas tego serwisu i mogą wymagać uwagi.",
+    "severity": "Priorytet",
+    "findingNotes": "Uwagi",
+    "findingSeverityUrgent": "Wysoki",
+    "findingSeverityNeedsWork": "Średni",
+    "findingSeverityMonitor": "Niski",
     "tel": "Tel: {phone}"
   },
   "quote": {

+ 50 - 1
messages/pl/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Wyceny",
       "inspections": "Przeglądy",
       "notes": "Notatki",
-      "reminders": "Przypomnienia"
+      "reminders": "Przypomnienia",
+      "findings": "Obserwacje"
     },
     "noteDeleted": "Notatka usunięta",
     "noteDeleteError": "Nie udało się usunąć",
@@ -180,6 +181,54 @@
     "titleRequired": "Tytuł i treść są wymagane",
     "saveError": "Nie udało się {action} notatki"
   },
+  "findings": {
+    "sectionTitle": "Obserwacje",
+    "addFinding": "Dodaj obserwację",
+    "editTitle": "Edytuj obserwację",
+    "addTitle": "Dodaj obserwację",
+    "empty": "Brak obserwacji.",
+    "descriptionLabel": "Opis *",
+    "descriptionPlaceholder": "np. Klocki hamulcowe przednie na minimum",
+    "severityLabel": "Ważność",
+    "notesLabel": "Dodatkowe uwagi",
+    "notesPlaceholder": "Opcjonalne szczegóły...",
+    "table": {
+      "severity": "Ważność",
+      "description": "Opis",
+      "status": "Status",
+      "date": "Data"
+    },
+    "severity": {
+      "urgent": "Wysoki",
+      "needs_work": "Średni",
+      "monitor": "Niski"
+    },
+    "status": {
+      "open": "Otwarte",
+      "quoted": "Wycenione",
+      "resolved": "Rozwiązane"
+    },
+    "edit": "Edytuj",
+    "resolve": "Oznacz jako rozwiązane",
+    "discoveredIn": "Odkryto w",
+    "resolvedIn": "Rozwiązano w",
+    "createWorkOrder": "Utwórz zlecenie ({count})",
+    "vehicleHasObservations": "Ten pojazd ma {count} otwartych obserwacji",
+    "addToWorkOrder": "Dodaj do zlecenia ({count})",
+    "dismiss": "Odrzuć",
+    "observationsAdded": "{count} obserwacji dodanych do zlecenia",
+    "delete": "Usuń",
+    "findingCreated": "Obserwacja dodana",
+    "findingUpdated": "Obserwacja zaktualizowana",
+    "findingResolved": "Obserwacja rozwiązana",
+    "findingDeleted": "Obserwacja usunięta",
+    "saveError": "Nie udało się zapisać obserwacji",
+    "deleteError": "Nie udało się usunąć obserwacji",
+    "resolveError": "Nie udało się rozwiązać obserwacji",
+    "showing": "Wyświetlanie {start}-{end} z {total}",
+    "perPage": "na stronę",
+    "page": "Strona {page} z {totalPages}"
+  },
   "form": {
     "editTitle": "Edytuj pojazd",
     "addTitle": "Dodaj nowy pojazd",

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

@@ -42,6 +42,10 @@
     "inventory_update": "Peça de inventário atualizada",
     "inventory_delete": "Peça de inventário excluída",
     "inventory_bulkDelete": "Peças de inventário excluídas em massa",
+    "finding_create": "Observação criada",
+    "finding_update": "Observação atualizada",
+    "finding_resolve": "Observação resolvida",
+    "finding_delete": "Observação excluída",
     "team_invite": "Membro da equipe convidado",
     "team_sendInvitation": "Convite enviado",
     "team_cancelInvitation": "Convite cancelado",

+ 7 - 0
messages/pt-BR/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Condições de pagamento",
     "dueDateLabel": "Data de vencimento",
     "diagnosticNotes": "Notas de Diagnóstico",
+    "findings": "Observações",
+    "findingsDescription": "Os seguintes itens foram observados durante este serviço e podem necessitar de atenção.",
+    "findingSeverityLabel": "Gravidade",
+    "findingSeverityUrgent": "Alta",
+    "findingSeverityNeedsWork": "Média",
+    "findingSeverityMonitor": "Baixa",
+    "findingNotesLabel": "Observações",
     "attachedDocuments": "Documentos Anexados",
     "seeAppendedPages": "{name} (ver páginas anexadas)",
     "serviceImages": "Imagens do Serviço",

+ 3 - 0
messages/pt-BR/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Descrição *",
     "subtotal": "Subtotal de Mão de Obra: {amount}",
     "addService": "Add Service",
+    "addFinding": "Observação",
+    "newObservation": "Nova observação",
+    "addExisting": "Ver existentes ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

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

@@ -409,6 +409,7 @@
       "totals": "Totais",
       "notes": "Notas",
       "diagnostic_notes": "Notas de diagnóstico",
+      "findings": "Observações",
       "bank_account": "Conta bancária",
       "footer": "Rodapé",
       "general": "Geral",

+ 7 - 0
messages/pt-BR/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Relatórios de Diagnóstico ({count})",
     "notes": "Observações",
     "diagnosticNotes": "Notas de Diagnóstico",
+    "findings": "Observações",
+    "findingsDescription": "Os seguintes itens foram observados durante este serviço e podem necessitar de atenção.",
+    "severity": "Gravidade",
+    "findingNotes": "Observações",
+    "findingSeverityUrgent": "Alta",
+    "findingSeverityNeedsWork": "Média",
+    "findingSeverityMonitor": "Baixa",
     "tel": "Tel: {phone}"
   },
   "quote": {

+ 50 - 1
messages/pt-BR/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Orçamentos",
       "inspections": "Inspeções",
       "notes": "Notas",
-      "reminders": "Lembretes"
+      "reminders": "Lembretes",
+      "findings": "Observações"
     },
     "noteDeleted": "Nota excluída",
     "noteDeleteError": "Falha ao excluir",
@@ -180,6 +181,54 @@
     "titleRequired": "Título e conteúdo são obrigatórios",
     "saveError": "Falha ao {action} nota"
   },
+  "findings": {
+    "sectionTitle": "Observações",
+    "addFinding": "Adicionar observação",
+    "editTitle": "Editar observação",
+    "addTitle": "Adicionar observação",
+    "empty": "Nenhuma observação ainda.",
+    "descriptionLabel": "Descrição *",
+    "descriptionPlaceholder": "ex. Pastilhas de freio dianteiras no mínimo",
+    "severityLabel": "Gravidade",
+    "notesLabel": "Observações adicionais",
+    "notesPlaceholder": "Detalhes opcionais...",
+    "table": {
+      "severity": "Gravidade",
+      "description": "Descrição",
+      "status": "Status",
+      "date": "Data"
+    },
+    "severity": {
+      "urgent": "Alta",
+      "needs_work": "Média",
+      "monitor": "Baixa"
+    },
+    "status": {
+      "open": "Aberto",
+      "quoted": "Orçado",
+      "resolved": "Resolvido"
+    },
+    "edit": "Editar",
+    "resolve": "Marcar como resolvido",
+    "discoveredIn": "Descoberto em",
+    "resolvedIn": "Resolvido em",
+    "createWorkOrder": "Criar ordem de serviço ({count})",
+    "vehicleHasObservations": "Este veículo tem {count} observações em aberto",
+    "addToWorkOrder": "Adicionar à ordem de serviço ({count})",
+    "dismiss": "Dispensar",
+    "observationsAdded": "{count} observações adicionadas à ordem de serviço",
+    "delete": "Excluir",
+    "findingCreated": "Observação adicionada",
+    "findingUpdated": "Observação atualizada",
+    "findingResolved": "Observação resolvida",
+    "findingDeleted": "Observação excluída",
+    "saveError": "Falha ao {action} observação",
+    "deleteError": "Falha ao excluir observação",
+    "resolveError": "Falha ao resolver observação",
+    "showing": "Exibindo {start}-{end} de {total}",
+    "perPage": "por página",
+    "page": "Página {page} de {totalPages}"
+  },
   "form": {
     "editTitle": "Editar Veículo",
     "addTitle": "Adicionar Novo Veículo",

+ 4 - 0
messages/ru/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Обновлена складская запчасть",
     "inventory_delete": "Удалена складская запчасть",
     "inventory_bulkDelete": "Массовое удаление складских запчастей",
+    "finding_create": "Создано наблюдение",
+    "finding_update": "Обновлено наблюдение",
+    "finding_resolve": "Решено наблюдение",
+    "finding_delete": "Удалено наблюдение",
     "team_invite": "Приглашён участник команды",
     "team_sendInvitation": "Отправлено приглашение",
     "team_cancelInvitation": "Отменено приглашение",

+ 7 - 0
messages/ru/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Условия оплаты",
     "dueDateLabel": "Срок оплаты",
     "diagnosticNotes": "Диагностические заметки",
+    "findings": "Наблюдения",
+    "findingsDescription": "Следующие элементы были обнаружены во время данного обслуживания и могут потребовать внимания.",
+    "findingSeverityLabel": "Приоритет",
+    "findingSeverityUrgent": "Высокий",
+    "findingSeverityNeedsWork": "Средний",
+    "findingSeverityMonitor": "Низкий",
+    "findingNotesLabel": "Примечания",
     "attachedDocuments": "Прикреплённые документы",
     "seeAppendedPages": "{name} (см. приложенные страницы)",
     "serviceImages": "Фотографии обслуживания",

+ 3 - 0
messages/ru/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Описание *",
     "subtotal": "Подитог работ: {amount}",
     "addService": "Добавить услугу",
+    "addFinding": "Наблюдение",
+    "newObservation": "Новое наблюдение",
+    "addExisting": "Показать существующие ({count})",
     "qty": "кол-во",
     "qtyOrHours": "Услуга / Часы",
     "hourlyTag": "н/ч",

+ 1 - 0
messages/ru/settings.json

@@ -409,6 +409,7 @@
       "totals": "Итоги",
       "notes": "Заметки",
       "diagnostic_notes": "Диагностические заметки",
+      "findings": "Наблюдения",
       "bank_account": "Банковский счёт",
       "footer": "Подвал",
       "general": "Общие",

+ 7 - 0
messages/ru/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Диагностические отчёты ({count})",
     "notes": "Заметки",
     "diagnosticNotes": "Диагностические заметки",
+    "findings": "Наблюдения",
+    "findingsDescription": "Следующие элементы были обнаружены во время данного обслуживания и могут потребовать внимания.",
+    "severity": "Приоритет",
+    "findingNotes": "Примечания",
+    "findingSeverityUrgent": "Высокий",
+    "findingSeverityNeedsWork": "Средний",
+    "findingSeverityMonitor": "Низкий",
     "tel": "Тел: {phone}"
   },
   "quote": {

+ 50 - 1
messages/ru/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Предложения",
       "inspections": "Осмотры",
       "notes": "Заметки",
-      "reminders": "Напоминания"
+      "reminders": "Напоминания",
+      "findings": "Наблюдения"
     },
     "noteDeleted": "Заметка удалена",
     "noteDeleteError": "Не удалось удалить",
@@ -180,6 +181,54 @@
     "titleRequired": "Название и содержание обязательны",
     "saveError": "Не удалось {action} заметку"
   },
+  "findings": {
+    "sectionTitle": "Наблюдения",
+    "addFinding": "Добавить наблюдение",
+    "editTitle": "Редактировать наблюдение",
+    "addTitle": "Добавить наблюдение",
+    "empty": "Пока нет наблюдений.",
+    "descriptionLabel": "Описание *",
+    "descriptionPlaceholder": "напр. Передние тормозные колодки на минимуме",
+    "severityLabel": "Серьёзность",
+    "notesLabel": "Дополнительные примечания",
+    "notesPlaceholder": "Дополнительные сведения...",
+    "table": {
+      "severity": "Серьёзность",
+      "description": "Описание",
+      "status": "Статус",
+      "date": "Дата"
+    },
+    "severity": {
+      "urgent": "Высокий",
+      "needs_work": "Средний",
+      "monitor": "Низкий"
+    },
+    "status": {
+      "open": "Открыто",
+      "quoted": "Оценено",
+      "resolved": "Решено"
+    },
+    "edit": "Редактировать",
+    "resolve": "Отметить как решённое",
+    "discoveredIn": "Обнаружено в",
+    "resolvedIn": "Решено в",
+    "createWorkOrder": "Создать заказ-наряд ({count})",
+    "vehicleHasObservations": "У этого транспортного средства {count} открытых наблюдений",
+    "addToWorkOrder": "Добавить в заказ-наряд ({count})",
+    "dismiss": "Скрыть",
+    "observationsAdded": "{count} наблюдений добавлено в заказ-наряд",
+    "delete": "Удалить",
+    "findingCreated": "Наблюдение добавлено",
+    "findingUpdated": "Наблюдение обновлено",
+    "findingResolved": "Наблюдение решено",
+    "findingDeleted": "Наблюдение удалено",
+    "saveError": "Не удалось сохранить наблюдение",
+    "deleteError": "Не удалось удалить наблюдение",
+    "resolveError": "Не удалось решить наблюдение",
+    "showing": "Показано {start}-{end} из {total}",
+    "perPage": "на странице",
+    "page": "Страница {page} из {totalPages}"
+  },
   "form": {
     "editTitle": "Редактировать транспортное средство",
     "addTitle": "Добавить новое транспортное средство",

+ 4 - 0
messages/tr/audit.json

@@ -42,6 +42,10 @@
     "inventory_update": "Envanter parçası güncellendi",
     "inventory_delete": "Envanter parçası silindi",
     "inventory_bulkDelete": "Envanter parçaları toplu silindi",
+    "finding_create": "Gözlem oluşturuldu",
+    "finding_update": "Gözlem güncellendi",
+    "finding_resolve": "Gözlem çözüldü",
+    "finding_delete": "Gözlem silindi",
     "team_invite": "Ekip üyesi davet edildi",
     "team_sendInvitation": "Davet gönderildi",
     "team_cancelInvitation": "Davet iptal edildi",

+ 7 - 0
messages/tr/pdf.json

@@ -39,6 +39,13 @@
     "paymentTermsLabel": "Ödeme koşulları",
     "dueDateLabel": "Vade tarihi",
     "diagnosticNotes": "Tanı Notları",
+    "findings": "Gözlemler",
+    "findingsDescription": "Aşağıdaki maddeler bu servis sırasında gözlemlenmiştir ve ilgi gerektirebilir.",
+    "findingSeverityLabel": "Önem",
+    "findingSeverityUrgent": "Yüksek",
+    "findingSeverityNeedsWork": "Orta",
+    "findingSeverityMonitor": "Düşük",
+    "findingNotesLabel": "Notlar",
     "attachedDocuments": "Ekli Belgeler",
     "seeAppendedPages": "{name} (eklenen sayfalara bakınız)",
     "serviceImages": "Servis Görselleri",

+ 3 - 0
messages/tr/service.json

@@ -107,6 +107,9 @@
     "descriptionPlaceholder": "Açıklama *",
     "subtotal": "İşçilik Ara Toplamı: {amount}",
     "addService": "Add Service",
+    "addFinding": "Gözlem",
+    "newObservation": "Yeni gözlem",
+    "addExisting": "Mevcut göster ({count})",
     "qty": "Qty",
     "qtyOrHours": "Unit / Hours",
     "hourlyTag": "HR",

+ 1 - 0
messages/tr/settings.json

@@ -409,6 +409,7 @@
       "totals": "Toplamlar",
       "notes": "Notlar",
       "diagnostic_notes": "Teşhis notları",
+      "findings": "Gözlemler",
       "bank_account": "Banka hesabı",
       "footer": "Alt bilgi",
       "general": "Genel",

+ 7 - 0
messages/tr/share.json

@@ -55,6 +55,13 @@
     "diagnosticReports": "Tanı Raporları ({count})",
     "notes": "Notlar",
     "diagnosticNotes": "Tanı Notları",
+    "findings": "Gözlemler",
+    "findingsDescription": "Aşağıdaki maddeler bu servis sırasında gözlemlenmiştir ve ilgi gerektirebilir.",
+    "severity": "Önem",
+    "findingNotes": "Notlar",
+    "findingSeverityUrgent": "Yüksek",
+    "findingSeverityNeedsWork": "Orta",
+    "findingSeverityMonitor": "Düşük",
     "tel": "Tel: {phone}"
   },
   "quote": {

+ 50 - 1
messages/tr/vehicles.json

@@ -52,7 +52,8 @@
       "quotes": "Teklifler",
       "inspections": "Muayeneler",
       "notes": "Notlar",
-      "reminders": "Hatırlatıcılar"
+      "reminders": "Hatırlatıcılar",
+      "findings": "Gözlemler"
     },
     "noteDeleted": "Not silindi",
     "noteDeleteError": "Silinemedi",
@@ -180,6 +181,54 @@
     "titleRequired": "Başlık ve içerik gereklidir",
     "saveError": "Not {action} başarısız oldu"
   },
+  "findings": {
+    "sectionTitle": "Gözlemler",
+    "addFinding": "Gözlem ekle",
+    "editTitle": "Gözlemi düzenle",
+    "addTitle": "Gözlem ekle",
+    "empty": "Henüz gözlem yok.",
+    "descriptionLabel": "Açıklama *",
+    "descriptionPlaceholder": "örn. Ön fren balataları minimumda",
+    "severityLabel": "Önem Derecesi",
+    "notesLabel": "Ek Notlar",
+    "notesPlaceholder": "İsteğe bağlı detaylar...",
+    "table": {
+      "severity": "Önem Derecesi",
+      "description": "Açıklama",
+      "status": "Durum",
+      "date": "Tarih"
+    },
+    "severity": {
+      "urgent": "Yüksek",
+      "needs_work": "Orta",
+      "monitor": "Düşük"
+    },
+    "status": {
+      "open": "Açık",
+      "quoted": "Teklif Verildi",
+      "resolved": "Çözüldü"
+    },
+    "edit": "Düzenle",
+    "resolve": "Çözüldü olarak işaretle",
+    "discoveredIn": "Keşfedildiği iş emri",
+    "resolvedIn": "Çözüldüğü iş emri",
+    "createWorkOrder": "İş emri oluştur ({count})",
+    "vehicleHasObservations": "Bu araçta {count} açık gözlem var",
+    "addToWorkOrder": "İş emrine ekle ({count})",
+    "dismiss": "Kapat",
+    "observationsAdded": "{count} gözlem iş emrine eklendi",
+    "delete": "Sil",
+    "findingCreated": "Gözlem eklendi",
+    "findingUpdated": "Gözlem güncellendi",
+    "findingResolved": "Gözlem çözüldü",
+    "findingDeleted": "Gözlem silindi",
+    "saveError": "Gözlem kaydedilemedi",
+    "deleteError": "Gözlem silinemedi",
+    "resolveError": "Gözlem çözülemedi",
+    "showing": "{start}-{end} / {total} gösteriliyor",
+    "perPage": "sayfa başına",
+    "page": "Sayfa {page} / {totalPages}"
+  },
   "form": {
     "editTitle": "Aracı Düzenle",
     "addTitle": "Yeni Araç Ekle",

+ 31 - 0
prisma/migrations/20260403135129_vehicle_outstanding_items/migration.sql

@@ -0,0 +1,31 @@
+-- CreateTable
+CREATE TABLE "vehicle_findings" (
+    "id" TEXT NOT NULL,
+    "description" TEXT NOT NULL,
+    "severity" TEXT NOT NULL DEFAULT 'needs_work',
+    "status" TEXT NOT NULL DEFAULT 'open',
+    "notes" TEXT,
+    "imageUrls" TEXT[] DEFAULT ARRAY[]::TEXT[],
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    "serviceRecordId" TEXT,
+    "resolvedServiceRecordId" TEXT,
+
+    CONSTRAINT "vehicle_findings_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "vehicle_findings_vehicleId_idx" ON "vehicle_findings"("vehicleId");
+
+-- CreateIndex
+CREATE INDEX "vehicle_findings_serviceRecordId_idx" ON "vehicle_findings"("serviceRecordId");
+
+-- AddForeignKey
+ALTER TABLE "vehicle_findings" ADD CONSTRAINT "vehicle_findings_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "vehicle_findings" ADD CONSTRAINT "vehicle_findings_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "vehicle_findings" ADD CONSTRAINT "vehicle_findings_resolvedServiceRecordId_fkey" FOREIGN KEY ("resolvedServiceRecordId") REFERENCES "service_records"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 28 - 0
prisma/schema.prisma

@@ -170,6 +170,7 @@ model Vehicle {
   recurringInvoices RecurringInvoice[]
   inspections       Inspection[]
   serviceRequests   ServiceRequest[]
+  findings          VehicleFinding[]
 
   @@index([organizationId])
   @@map("vehicles")
@@ -223,6 +224,9 @@ model ServiceRecord {
   payments      Payment[]
   statusReports StatusReport[]
 
+  discoveredFindings VehicleFinding[] @relation("FindingDiscoveredIn")
+  resolvedFindings   VehicleFinding[] @relation("FindingResolvedIn")
+
   @@index([vehicleId])
   @@index([technicianId])
   @@map("service_records")
@@ -280,6 +284,30 @@ model Reminder {
   @@map("reminders")
 }
 
+model VehicleFinding {
+  id          String   @id @default(cuid())
+  description String
+  severity    String   @default("needs_work")
+  status      String   @default("open")
+  notes       String?
+  imageUrls   String[] @default([])
+  createdAt   DateTime @default(now())
+  updatedAt   DateTime @updatedAt
+
+  vehicleId String
+  vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
+
+  serviceRecordId String?
+  serviceRecord   ServiceRecord? @relation("FindingDiscoveredIn", fields: [serviceRecordId], references: [id], onDelete: SetNull)
+
+  resolvedServiceRecordId String?
+  resolvedServiceRecord   ServiceRecord? @relation("FindingResolvedIn", fields: [resolvedServiceRecordId], references: [id], onDelete: SetNull)
+
+  @@index([vehicleId])
+  @@index([serviceRecordId])
+  @@map("vehicle_findings")
+}
+
 model Customer {
   id             String   @id @default(cuid())
   name           String

+ 358 - 0
src/app/(authenticated)/vehicles/[id]/findings-table.tsx

@@ -0,0 +1,358 @@
+"use client";
+
+import { useRouter, usePathname, useSearchParams } from "next/navigation";
+import Link from "next/link";
+import { useCallback, useState, useTransition } from "react";
+import { useFormatDate } from "@/lib/use-format-date";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+  ChevronLeft,
+  ChevronRight,
+  ChevronsLeft,
+  ChevronsRight,
+  Loader2,
+  MoreVertical,
+  Pencil,
+  Plus,
+  Trash2,
+  Wrench,
+  ExternalLink,
+} from "lucide-react";
+import { useTranslations } from "next-intl";
+
+interface FindingRow {
+  id: string;
+  description: string;
+  severity: string;
+  status: string;
+  notes: string | null;
+  imageUrls: string[];
+  createdAt: Date;
+  serviceRecord: { id: string; title: string } | null;
+  resolvedServiceRecord: { id: string; title: string } | null;
+}
+
+interface FindingsTableProps {
+  vehicleId: string;
+  records: FindingRow[];
+  total: number;
+  page: number;
+  pageSize: number;
+  totalPages: number;
+  onAddFinding: () => void;
+  onEditFinding: (finding: FindingRow) => void;
+  onDeleteFinding: (id: string) => void;
+  onCreateWorkOrder: (findingIds: string[]) => void;
+  isCreatingWorkOrder?: boolean;
+}
+
+const severityColors: Record<string, string> = {
+  urgent: "bg-red-500/10 text-red-500 border-red-500/20",
+  needs_work: "bg-amber-500/10 text-amber-600 border-amber-500/20",
+  monitor: "bg-blue-500/10 text-blue-500 border-blue-500/20",
+};
+
+
+export function FindingsTable({
+  vehicleId,
+  records,
+  total,
+  page,
+  pageSize,
+  totalPages,
+  onAddFinding,
+  onEditFinding,
+  onDeleteFinding,
+  onCreateWorkOrder,
+  isCreatingWorkOrder = false,
+}: FindingsTableProps) {
+  const router = useRouter();
+  const { formatDate } = useFormatDate();
+  const pathname = usePathname();
+  const searchParams = useSearchParams();
+  const [isPending, startTransition] = useTransition();
+  const t = useTranslations("vehicles.findings");
+  const [selected, setSelected] = useState<Set<string>>(new Set());
+
+  const toggleSelect = (id: string) => {
+    setSelected((prev) => {
+      const next = new Set(prev);
+      if (next.has(id)) next.delete(id);
+      else next.add(id);
+      return next;
+    });
+  };
+
+  const toggleAll = () => {
+    if (selected.size === records.length) {
+      setSelected(new Set());
+    } else {
+      setSelected(new Set(records.map((r) => r.id)));
+    }
+  };
+
+  const createUrl = useCallback(
+    (params: Record<string, string | number | undefined>) => {
+      const newParams = new URLSearchParams(searchParams.toString());
+      newParams.set("tab", "findings");
+      for (const [key, value] of Object.entries(params)) {
+        if (value === undefined || value === "") {
+          newParams.delete(key);
+        } else {
+          newParams.set(key, String(value));
+        }
+      }
+      if (!("findingsPage" in params) && "findingsPageSize" in params) {
+        newParams.delete("findingsPage");
+      }
+      return `${pathname}?${newParams.toString()}`;
+    },
+    [pathname, searchParams]
+  );
+
+  const navigate = useCallback(
+    (params: Record<string, string | number | undefined>) => {
+      startTransition(() => {
+        router.push(createUrl(params));
+      });
+    },
+    [router, createUrl]
+  );
+
+  const handlePageSizeChange = useCallback(
+    (newSize: string) => {
+      navigate({ findingsPageSize: newSize, findingsPage: 1 });
+    },
+    [navigate]
+  );
+
+  const startItem = (page - 1) * pageSize + 1;
+  const endItem = Math.min(page * pageSize, total);
+
+  return (
+    <div className="space-y-4">
+      {/* Toolbar */}
+      <div className="flex items-center justify-between gap-2">
+        <div className="flex items-center gap-2">
+          {isPending && (
+            <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+          )}
+          {selected.size > 0 && (
+            <Button
+              size="sm"
+              onClick={() => onCreateWorkOrder(Array.from(selected))}
+              disabled={isCreatingWorkOrder}
+            >
+              {isCreatingWorkOrder ? (
+                <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
+              ) : (
+                <Wrench className="mr-1 h-3.5 w-3.5" />
+              )}
+              {t("createWorkOrder", { count: selected.size })}
+            </Button>
+          )}
+        </div>
+        <Button size="sm" onClick={onAddFinding}>
+          <Plus className="mr-1 h-3.5 w-3.5" />
+          {t("addFinding")}
+        </Button>
+      </div>
+
+      {/* Table */}
+      <div className="rounded-lg border">
+        <Table>
+          <TableHeader>
+            <TableRow>
+              <TableHead className="w-10 px-2">
+                {records.length > 0 && (
+                  <Checkbox
+                    checked={selected.size === records.length && records.length > 0}
+                    onCheckedChange={toggleAll}
+                  />
+                )}
+              </TableHead>
+              <TableHead className="w-28">{t("table.severity")}</TableHead>
+              <TableHead>{t("table.description")}</TableHead>
+              <TableHead className="hidden w-30 sm:table-cell">
+                {t("table.date")}
+              </TableHead>
+              <TableHead className="w-12.5"></TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {records.length === 0 ? (
+              <TableRow>
+                <TableCell
+                  colSpan={5}
+                  className="h-32 text-center text-muted-foreground"
+                >
+                  {t("empty")}
+                </TableCell>
+              </TableRow>
+            ) : (
+              records.map((f) => (
+                <TableRow key={f.id}>
+                  <TableCell className="px-2">
+                    <Checkbox
+                      checked={selected.has(f.id)}
+                      onCheckedChange={() => toggleSelect(f.id)}
+                    />
+                  </TableCell>
+                  <TableCell>
+                    <Badge
+                      variant="outline"
+                      className={`text-xs ${severityColors[f.severity] || ""}`}
+                    >
+                      {t(`severity.${f.severity}` as "severity.urgent" | "severity.needs_work" | "severity.monitor")}
+                    </Badge>
+                  </TableCell>
+                  <TableCell>
+                    <div>
+                      <span className="font-medium">{f.description}</span>
+                      {f.notes && (
+                        <p className="mt-0.5 truncate text-xs text-muted-foreground">
+                          {f.notes}
+                        </p>
+                      )}
+                      <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs">
+                        {f.serviceRecord && (
+                          <Link
+                            href={`/vehicles/${vehicleId}/service/${f.serviceRecord.id}`}
+                            className="inline-flex items-center gap-1 text-blue-600 underline decoration-blue-600/30 hover:decoration-blue-600 dark:text-blue-400 dark:decoration-blue-400/30 dark:hover:decoration-blue-400"
+                            onClick={(e) => e.stopPropagation()}
+                          >
+                            <ExternalLink className="h-3 w-3" />
+                            {t("discoveredIn")}: {f.serviceRecord.title}
+                          </Link>
+                        )}
+                      </div>
+                    </div>
+                  </TableCell>
+                  <TableCell className="hidden font-mono text-xs text-muted-foreground sm:table-cell">
+                    {formatDate(new Date(f.createdAt))}
+                  </TableCell>
+                  <TableCell className="px-2">
+                    <DropdownMenu>
+                      <DropdownMenuTrigger asChild>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          className="h-7 w-7"
+                        >
+                          <MoreVertical className="h-3.5 w-3.5" />
+                        </Button>
+                      </DropdownMenuTrigger>
+                      <DropdownMenuContent align="end">
+                        <DropdownMenuItem onClick={() => onEditFinding(f)}>
+                          <Pencil className="mr-2 h-4 w-4" />
+                          {t("edit")}
+                        </DropdownMenuItem>
+                        <DropdownMenuItem
+                          className="text-destructive"
+                          onClick={() => onDeleteFinding(f.id)}
+                        >
+                          <Trash2 className="mr-2 h-4 w-4" />
+                          {t("delete")}
+                        </DropdownMenuItem>
+                      </DropdownMenuContent>
+                    </DropdownMenu>
+                  </TableCell>
+                </TableRow>
+              ))
+            )}
+          </TableBody>
+        </Table>
+      </div>
+
+      {/* Pagination */}
+      {total > 0 && (
+        <div className="flex flex-col items-center justify-between gap-3 sm:flex-row">
+          <div className="flex items-center gap-2 text-sm text-muted-foreground">
+            <span>
+              {t("showing", { start: startItem, end: endItem, total })}
+            </span>
+            <Select
+              value={String(pageSize)}
+              onValueChange={handlePageSizeChange}
+            >
+              <SelectTrigger className="h-8 w-[70px]">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="5">5</SelectItem>
+                <SelectItem value="10">10</SelectItem>
+                <SelectItem value="20">20</SelectItem>
+                <SelectItem value="50">50</SelectItem>
+              </SelectContent>
+            </Select>
+            <span>{t("perPage")}</span>
+          </div>
+          <div className="flex items-center gap-1">
+            <Button
+              variant="outline"
+              size="icon"
+              className="h-8 w-8"
+              disabled={page <= 1}
+              onClick={() => navigate({ findingsPage: 1 })}
+            >
+              <ChevronsLeft className="h-4 w-4" />
+            </Button>
+            <Button
+              variant="outline"
+              size="icon"
+              className="h-8 w-8"
+              disabled={page <= 1}
+              onClick={() => navigate({ findingsPage: page - 1 })}
+            >
+              <ChevronLeft className="h-4 w-4" />
+            </Button>
+            <span className="px-3 text-sm">
+              {t("page", { page, totalPages })}
+            </span>
+            <Button
+              variant="outline"
+              size="icon"
+              className="h-8 w-8"
+              disabled={page >= totalPages}
+              onClick={() => navigate({ findingsPage: page + 1 })}
+            >
+              <ChevronRight className="h-4 w-4" />
+            </Button>
+            <Button
+              variant="outline"
+              size="icon"
+              className="h-8 w-8"
+              disabled={page >= totalPages}
+              onClick={() => navigate({ findingsPage: totalPages })}
+            >
+              <ChevronsRight className="h-4 w-4" />
+            </Button>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}

+ 10 - 1
src/app/(authenticated)/vehicles/[id]/page.tsx

@@ -9,6 +9,7 @@ import { getVehiclePredictedMileage } from "@/features/vehicles/Actions/predicte
 import { getVehicleInspections } from "@/features/inspections/Actions/inspectionActions";
 import { getTemplates } from "@/features/inspections/Actions/templateActions";
 import { getVehicleQuotes } from "@/features/quotes/Actions/quoteActions";
+import { getVehicleFindings } from "@/features/vehicles/Actions/findingActions";
 import { getFeatures } from "@/lib/features";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { db } from "@/lib/db";
@@ -32,8 +33,10 @@ export default async function VehicleDetailPage({
 
   const notesPage = Number(sp.notesPage) || 1;
   const notesPageSize = Number(sp.notesPageSize) || 10;
+  const findingsPage = Number(sp.findingsPage) || 1;
+  const findingsPageSize = Number(sp.findingsPageSize) || 10;
 
-  const [result, customersResult, serviceResult, notesResult, settingsResult, maintenanceSettingsResult, inspectionsResult, templatesResult, quotesResult] = await Promise.all([
+  const [result, customersResult, serviceResult, notesResult, settingsResult, maintenanceSettingsResult, inspectionsResult, templatesResult, quotesResult, findingsResult] = await Promise.all([
     getVehicle(id),
     getCustomersList(),
     getServiceRecordsPaginated(id, { page, pageSize, search, type }),
@@ -47,6 +50,7 @@ export default async function VehicleDetailPage({
     getVehicleInspections(id),
     getTemplates(),
     getVehicleQuotes(id),
+    getVehicleFindings(id, { page: findingsPage, pageSize: findingsPageSize }),
   ]);
 
   if (!result.success || !result.data) {
@@ -70,6 +74,10 @@ export default async function VehicleDetailPage({
     ? notesResult.data
     : { records: [], total: 0, page: 1, pageSize: 10, totalPages: 0 };
 
+  const paginatedFindings = findingsResult.success && findingsResult.data
+    ? findingsResult.data
+    : { records: [], total: 0, page: 1, pageSize: 10, totalPages: 0 };
+
   const currencySettings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
   const currencyCode = currencySettings[SETTING_KEYS.CURRENCY_CODE] || "USD";
   const unitSystem = (currencySettings[SETTING_KEYS.UNIT_SYSTEM] || "imperial") as "metric" | "imperial";
@@ -153,6 +161,7 @@ export default async function VehicleDetailPage({
           inspectionTemplates={templatesResult.success && templatesResult.data ? templatesResult.data : []}
           quotes={quotesResult.success && quotesResult.data ? quotesResult.data : []}
           aiEnabled={aiEnabled}
+          paginatedFindings={paginatedFindings}
         />
       </div>
     </>

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

@@ -8,6 +8,7 @@ import { getTechnicians, getOrgMembers } from "@/features/workboard/Actions/tech
 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";
@@ -25,7 +26,7 @@ export default async function ServiceDetailPage({
   const sp = await searchParams;
   const initialTab = typeof sp.tab === "string" ? sp.tab : undefined;
 
-  const [result, settingsResult, inventoryResult, techniciansResult, presetsResult, authContext, session, orgMembersResult, statusReportsResult] =
+  const [result, settingsResult, inventoryResult, techniciansResult, presetsResult, authContext, session, orgMembersResult, statusReportsResult, findingsResult] =
     await Promise.all([
       getServiceRecord(serviceId),
       getSettings([
@@ -43,6 +44,7 @@ export default async function ServiceDetailPage({
       getCachedSession(),
       getOrgMembers(),
       getStatusReportsForService(serviceId),
+      getServiceFindings(serviceId),
     ]);
 
   if (!result.success || !result.data) {
@@ -175,6 +177,13 @@ export default async function ServiceDetailPage({
     .filter((a) => a.category === "document" || a.category === "diagnostic")
     .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }));
 
+  // 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 />
@@ -207,6 +216,8 @@ export default async function ServiceDetailPage({
         aiEnabled={aiEnabled}
         defaultDueDays={defaultDueDays}
         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}
       />
     </div>
   );

+ 111 - 2
src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx

@@ -22,11 +22,15 @@ import { useGlassModal } from '@/components/glass-modal'
 import { VehicleForm } from '@/features/vehicles/Components/VehicleForm'
 import { NoteForm } from '@/features/vehicles/Components/NoteForm'
 import { ReminderForm } from '@/features/vehicles/Components/ReminderForm'
+import { FindingForm } from '@/features/vehicles/Components/FindingForm'
 import { ServiceRecordsTable } from './service-records-table'
 import { NotesTable } from './notes-table'
+import { FindingsTable } from './findings-table'
 import { toast } from 'sonner'
 import { deleteNote, toggleNotePin } from '@/features/vehicles/Actions/noteActions'
 import { toggleReminder, deleteReminder } from '@/features/vehicles/Actions/reminderActions'
+import { deleteFinding } from '@/features/vehicles/Actions/findingActions'
+import { createServiceRecord } from '@/features/vehicles/Actions/serviceActions'
 import { unarchiveVehicle } from '@/features/vehicles/Actions/unarchiveVehicle'
 import { deleteVehicle } from '@/features/vehicles/Actions/deleteVehicle'
 import {
@@ -128,6 +132,24 @@ interface PaginatedNotes {
   totalPages: number
 }
 
+interface PaginatedFindings {
+  records: {
+    id: string
+    description: string
+    severity: string
+    status: string
+    notes: string | null
+    imageUrls: string[]
+    createdAt: Date
+    serviceRecord: { id: string; title: string } | null
+    resolvedServiceRecord: { id: string; title: string } | null
+  }[]
+  total: number
+  page: number
+  pageSize: number
+  totalPages: number
+}
+
 interface VehicleDetail {
   id: string
   make: string
@@ -172,6 +194,7 @@ interface VehicleDetail {
     serviceRecords: number
     notes: number
     reminders: number
+    findings: number
   }
 }
 
@@ -209,11 +232,13 @@ export function VehicleDetailClient({
   inspectionTemplates,
   quotes = [],
   aiEnabled = false,
+  paginatedFindings,
 }: {
   vehicle: VehicleDetail
   customers: CustomerOption[]
   paginatedServices: PaginatedServices
   paginatedNotes: PaginatedNotes
+  paginatedFindings: PaginatedFindings
   serviceSearch: string
   serviceRecordType: string
   currencyCode?: string
@@ -252,7 +277,8 @@ export function VehicleDetailClient({
   const tc = useTranslations('common.buttons')
 
   const tq = useTranslations('vehicles.quotes')
-  const validTabs = ['services', 'quotes', 'inspections', 'notes', 'reminders'] as const
+  const tf = useTranslations('vehicles.findings')
+  const validTabs = ['services', 'quotes', 'inspections', 'findings', 'notes', 'reminders'] as const
   const tabParam = searchParams.get('tab')
   const activeTab = validTabs.includes(tabParam as (typeof validTabs)[number])
     ? (tabParam as string)
@@ -279,6 +305,8 @@ export function VehicleDetailClient({
   const [editingReminder, setEditingReminder] = useState<
     VehicleDetail['reminders'][number] | undefined
   >()
+  const [showFindingForm, setShowFindingForm] = useState(false)
+  const [editingFinding, setEditingFinding] = useState<PaginatedFindings['records'][number] | undefined>()
   const [reminderFilter, setReminderFilter] = useState<'active' | 'completed' | 'all'>('active')
   const [showImage, setShowImage] = useState(false)
   const [selectedNote, setSelectedNote] = useState<PaginatedNotes['records'][number] | null>(null)
@@ -361,6 +389,49 @@ export function VehicleDetailClient({
     }
   }
 
+  const handleDeleteFinding = async (id: string) => {
+    const result = await deleteFinding(id)
+    if (result.success) {
+      toast.success(tf('findingDeleted'))
+      router.refresh()
+    } else {
+      modal.open('error', 'Error', result.error || tf('deleteError'))
+    }
+  }
+
+  const [creatingWorkOrder, setCreatingWorkOrder] = useState(false)
+  const handleCreateWorkOrderFromFindings = async (findingIds: string[]) => {
+    setCreatingWorkOrder(true)
+    const selectedFindings = paginatedFindings.records.filter((f) => findingIds.includes(f.id))
+    const vehicleName = `${vehicle.year} ${vehicle.make} ${vehicle.model}`
+    const result = await createServiceRecord({
+      vehicleId: vehicle.id,
+      title: `${vehicleName} - ${tf('sectionTitle')}`,
+      type: 'repair',
+      status: 'pending',
+      laborItems: selectedFindings.map((f) => ({
+        description: `${f.description}${f.notes ? ` - ${f.notes}` : ''}`,
+        hours: 0,
+        rate: 0,
+        total: 0,
+        pricingType: 'hourly' as const,
+      })),
+      subtotal: 0,
+      taxRate: 0,
+      taxAmount: 0,
+      totalAmount: 0,
+    })
+    if (result.success && result.data) {
+      const newServiceId = result.data.id
+      // Delete the observations that were added to the work order
+      await Promise.all(findingIds.map((id) => deleteFinding(id)))
+      router.push(`/vehicles/${vehicle.id}/service/${newServiceId}`)
+    } else {
+      modal.open('error', 'Error', result.error || 'Failed to create work order')
+      setCreatingWorkOrder(false)
+    }
+  }
+
   const handleUnarchive = async () => {
     const result = await unarchiveVehicle(vehicle.id)
     if (result.success) {
@@ -974,7 +1045,7 @@ export function VehicleDetailClient({
 
       {/* Tabs */}
       <Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-4">
-        <TabsList className="grid w-full grid-cols-5">
+        <TabsList className="grid w-full grid-cols-6">
           <TabsTrigger value="services" className="gap-1.5">
             <Wrench className="h-3.5 w-3.5 shrink-0" />
             <span className="hidden sm:inline">{t('tabs.services')}</span>
@@ -1002,6 +1073,15 @@ export function VehicleDetailClient({
               </Badge>
             )}
           </TabsTrigger>
+          <TabsTrigger value="findings" className="gap-1.5">
+            <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
+            <span className="hidden sm:inline">{t('tabs.findings')}</span>
+            {vehicle._count.findings > 0 && (
+              <Badge variant="secondary" className="ml-1 h-5 min-w-5 px-1 text-[10px]">
+                {vehicle._count.findings}
+              </Badge>
+            )}
+          </TabsTrigger>
           <TabsTrigger value="notes" className="gap-1.5">
             <StickyNote className="h-3.5 w-3.5 shrink-0" />
             <span className="hidden sm:inline">{t('tabs.notes')}</span>
@@ -1193,6 +1273,29 @@ export function VehicleDetailClient({
           )}
         </TabsContent>
 
+        {/* Findings Tab */}
+        <TabsContent value="findings" forceMount>
+          <FindingsTable
+            vehicleId={vehicle.id}
+            records={paginatedFindings.records}
+            total={paginatedFindings.total}
+            page={paginatedFindings.page}
+            pageSize={paginatedFindings.pageSize}
+            totalPages={paginatedFindings.totalPages}
+            onAddFinding={() => {
+              setEditingFinding(undefined)
+              setShowFindingForm(true)
+            }}
+            onEditFinding={(f) => {
+              setEditingFinding(f)
+              setShowFindingForm(true)
+            }}
+            onDeleteFinding={handleDeleteFinding}
+            onCreateWorkOrder={handleCreateWorkOrderFromFindings}
+            isCreatingWorkOrder={creatingWorkOrder}
+          />
+        </TabsContent>
+
         {/* Notes Tab */}
         <TabsContent value="notes" forceMount>
           <NotesTable
@@ -1402,6 +1505,12 @@ export function VehicleDetailClient({
         onOpenChange={setShowReminderForm}
         reminder={editingReminder}
       />
+      <FindingForm
+        vehicleId={vehicle.id}
+        open={showFindingForm}
+        onOpenChange={setShowFindingForm}
+        finding={editingFinding}
+      />
       <NewInspectionDialog
         open={showNewInspection}
         onOpenChange={setShowNewInspection}

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

@@ -132,7 +132,7 @@ function isFieldVisible(config: InvoiceLayoutConfig | undefined, sectionId: stri
 }
 
 function getSectionOrder(config: InvoiceLayoutConfig | undefined): string[] {
-  if (!config) return ['header', 'customer', 'vehicle', 'service', 'parts_table', 'labor_table', 'totals', 'general', 'notes', 'diagnostic_notes', 'bank_account', 'footer']
+  if (!config) return ['header', 'customer', 'vehicle', 'service', 'parts_table', 'labor_table', 'findings', 'totals', 'general', 'notes', 'diagnostic_notes', 'bank_account', 'footer']
   return [...config.sections].sort((a, b) => a.order - b.order).map(s => s.id)
 }
 
@@ -195,6 +195,7 @@ export function InvoiceView({
   portalUrl,
   layoutConfig,
   customFields = [],
+  findings = [],
   telegramBotLink,
   serviceType = "automotive",
 }: {
@@ -218,6 +219,7 @@ export function InvoiceView({
   portalUrl?: string
   layoutConfig?: InvoiceLayoutConfig
   customFields?: CustomField[]
+  findings?: Array<{ description: string; severity: string; notes: string | null }>
   telegramBotLink?: string
   serviceType?: "automotive" | "marine"
 }) {
@@ -242,7 +244,17 @@ export function InvoiceView({
   }, [token])
 
   const vehicleName = `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`
-  const displayTotal = record.totalAmount > 0 ? record.totalAmount : record.cost
+  const partsSubtotal = record.partItems.reduce((sum, p) => sum + p.total, 0)
+  const laborSubtotal = record.laborItems.reduce((sum, l) => sum + l.total, 0)
+  const computedSubtotal = partsSubtotal + laborSubtotal
+  const computedDiscount = record.discountType === 'percentage'
+    ? computedSubtotal * (record.discountValue / 100)
+    : record.discountType === 'fixed'
+      ? Math.min(record.discountValue, computedSubtotal)
+      : 0
+  const computedTax = (computedSubtotal - computedDiscount) * (record.taxRate / 100)
+  const computedTotal = computedSubtotal - computedDiscount + computedTax
+  const displayTotal = record.totalAmount > 0 ? record.totalAmount : computedTotal > 0 ? computedTotal : record.cost
   const invoiceNum = record.invoiceNumber || `INV-${record.id.slice(-8).toUpperCase()}`
   const df = dateFormat || DEFAULT_DATE_FORMAT
   const tz = timezone || undefined
@@ -856,11 +868,21 @@ export function InvoiceView({
               return (
                 <div key="totals">
                   <div className="mt-6 ml-auto max-w-xs space-y-2">
-                    {record.subtotal > 0 && (
-                      <div className="flex justify-between text-sm">
-                        <span className="text-gray-500">{t('subtotal')}</span>
-                        <span>{formatCurrency(record.subtotal, currencyCode)}</span>
-                      </div>
+                    {partsSubtotal > 0 && laborSubtotal > 0 && (
+                      <>
+                        <div className="flex justify-between text-sm">
+                          <span className="text-gray-500">{t('parts')}</span>
+                          <span>{formatCurrency(partsSubtotal, currencyCode)}</span>
+                        </div>
+                        <div className="flex justify-between text-sm">
+                          <span className="text-gray-500">{t('labor')}</span>
+                          <span>{formatCurrency(laborSubtotal, currencyCode)}</span>
+                        </div>
+                        <div className="flex justify-between text-sm">
+                          <span className="text-gray-500">{t('subtotal')}</span>
+                          <span>{formatCurrency(computedSubtotal, currencyCode)}</span>
+                        </div>
+                      </>
                     )}
                     {record.discountAmount > 0 && (
                       <div className="flex justify-between text-sm">
@@ -973,6 +995,41 @@ export function InvoiceView({
                 </div>
               )
 
+            case 'findings':
+              if (!findings || findings.length === 0) return null
+              return (
+                <div key="findings" className="mt-6">
+                  <h4 className="mb-1 font-semibold">{t('findings', { defaultValue: 'Findings' })}</h4>
+                  <p className="mb-3 text-sm text-gray-500 dark:text-gray-400">{t('findingsDescription', { defaultValue: 'The following items were observed during this service and may require attention.' })}</p>
+                  <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
+                    <table className="w-full text-sm">
+                      <thead>
+                        <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
+                          <th className="w-[15%] p-2 font-medium">{t('severity', { defaultValue: 'Severity' })}</th>
+                          <th className="w-[40%] p-2 font-medium">{t('description')}</th>
+                          <th className="p-2 font-medium">{t('findingNotes', { defaultValue: 'Notes' })}</th>
+                        </tr>
+                      </thead>
+                      <tbody className="divide-y">
+                        {findings.map((f, i) => {
+                          const severityColor = f.severity === 'urgent' ? 'text-red-600 dark:text-red-400' : f.severity === 'needs_work' ? 'text-amber-600 dark:text-amber-400' : 'text-blue-600 dark:text-blue-400'
+                          const severityLabel = f.severity === 'urgent' ? t('findingSeverityUrgent', { defaultValue: 'Urgent' }) : f.severity === 'needs_work' ? t('findingSeverityNeedsWork', { defaultValue: 'Needs Work' }) : t('findingSeverityMonitor', { defaultValue: 'Monitor' })
+                          return (
+                            <tr key={i}>
+                              <td className="p-2">
+                                <span className={`text-xs font-semibold uppercase ${severityColor}`}>{severityLabel}</span>
+                              </td>
+                              <td className="p-2">{f.description}</td>
+                              <td className="p-2 text-gray-500 dark:text-gray-400">{f.notes || '-'}</td>
+                            </tr>
+                          )
+                        })}
+                      </tbody>
+                    </table>
+                  </div>
+                </div>
+              )
+
             case 'bank_account': {
               const effectiveShowBankAccount = layoutConfig
                 ? isFieldVisible(layoutConfig, 'bank_account', 'bank_account')

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

@@ -112,6 +112,13 @@ export default async function PublicInvoicePage({
     getFeatures(orgId),
   ]);
 
+  // Fetch findings for this service record (open ones to show on invoice)
+  const findings = await db.vehicleFinding.findMany({
+    where: { serviceRecordId: record.id, status: { not: "resolved" } },
+    select: { description: true, severity: true, notes: true },
+    orderBy: { createdAt: "desc" },
+  });
+
   // Fetch custom field values for this service record
   const customFieldValues = await db.customFieldValue.findMany({
     where: { entityId: record.id, entityType: "service_record" },
@@ -210,6 +217,7 @@ export default async function PublicInvoicePage({
       portalUrl={portalUrl}
       layoutConfig={layoutConfig}
       customFields={customFields}
+      findings={findings}
       telegramBotLink={telegramBotLink}
       serviceType={(settingsMap["workshop.serviceType"] || "automotive") as "automotive" | "marine"}
     />

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

@@ -83,6 +83,13 @@ export async function GET(
       return NextResponse.json({ error: "Record not found" }, { status: 404 });
     }
 
+    // Fetch findings for this service record (open ones to show on invoice)
+    const findings = await db.vehicleFinding.findMany({
+      where: { serviceRecordId: record.id, status: { not: "resolved" } },
+      select: { description: true, severity: true, notes: true },
+      orderBy: { createdAt: "desc" },
+    });
+
     // Fetch custom field values for this service record
     const customFieldValues = await db.customFieldValue.findMany({
       where: { entityId: record.id, entityType: "service_record" },
@@ -247,7 +254,7 @@ export async function GET(
     }
 
     const element = React.createElement(InvoicePDF, {
-      data: { ...record, customFields },
+      data: { ...record, customFields, findings },
       workshop: {
         name: org?.name || "",
         address: settingsMap["workshop.address"] || "",

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

@@ -75,6 +75,13 @@ export async function GET(
       return NextResponse.json({ error: "Not found" }, { status: 404 });
     }
 
+    // Fetch findings for this service record (open ones to show on invoice)
+    const findings = await db.vehicleFinding.findMany({
+      where: { serviceRecordId: record.id, status: { not: "resolved" } },
+      select: { description: true, severity: true, notes: true },
+      orderBy: { createdAt: "desc" },
+    });
+
     // Fetch custom field values for this service record
     const customFieldValues = await db.customFieldValue.findMany({
       where: { entityId: record.id, entityType: "service_record" },
@@ -206,7 +213,7 @@ export async function GET(
     }
 
     const element = React.createElement(InvoicePDF, {
-      data: { ...record, customFields },
+      data: { ...record, customFields, findings },
       workshop: {
         name: org?.name || "",
         address: settingsMap["workshop.address"] || "",

+ 1 - 0
src/features/settings/Actions/invoiceLayoutActions.ts

@@ -54,6 +54,7 @@ export async function saveInvoiceLayoutConfig(config: InvoiceLayoutConfig) {
     });
 
     revalidatePath("/settings/templates");
+    revalidatePath("/settings/invoice");
     return validated;
   }, {
     requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }],

+ 4 - 0
src/features/settings/Components/InvoiceLayoutPreviewRenderer.tsx

@@ -46,6 +46,10 @@ const DUMMY_INVOICE_DATA = {
   mileage: 45230,
   diagnosticNotes:
     "<p>Brake wear at 15%. Recommended replacement within 5,000 miles.</p>",
+  findings: [
+    { description: "Rear brake pads worn to 15%", severity: "needs_work", notes: "Recommend replacement within 5,000 miles" },
+    { description: "Minor oil leak at valve cover gasket", severity: "monitor", notes: null },
+  ],
   invoiceNotes:
     "<p>Front brake pads replaced. Oil and filter changed with synthetic 5W-30. Next service recommended at 50,000 miles.</p>",
   subtotal: 314.5,

+ 24 - 5
src/features/settings/Schema/invoiceLayoutSchema.ts

@@ -12,7 +12,7 @@ export const invoiceFieldConfigSchema = z.object({
 export const invoiceSectionSchema = z.object({
   id: z.string(),
   visible: z.boolean(),
-  order: z.number().int().nonnegative(),
+  order: z.number().int(),
   /** When set, the section renders in a 2-column row alongside other column sections. */
   column: z.enum(["left", "right"]).optional(),
   /** Controls which fields are shown within this section. */
@@ -60,6 +60,7 @@ export const BUILTIN_SECTIONS = [
   { id: "service", name: "Service" },
   { id: "parts_table", name: "Parts Table" },
   { id: "labor_table", name: "Labor Table" },
+  { id: "findings", name: "Findings" },
   { id: "totals", name: "Totals" },
   { id: "notes", name: "Notes" },
   { id: "diagnostic_notes", name: "Diagnostic Notes" },
@@ -262,11 +263,29 @@ export function mergeWithDefaults(
   }
 
   // Append any new built-in sections that are missing from saved.
-  let nextOrder = Math.max(...migrated.map((s) => s.order), -1) + 1;
-
+  // Insert each after its natural predecessor from the default order,
+  // so e.g. "findings" lands after "labor_table" instead of at the end.
+  const defaultOrder = defaults.sections.map((s) => s.id);
+  const toInsert: { section: InvoiceSection; afterIdx: number }[] = [];
   for (const def of defaults.sections) {
-    if (!seen.has(def.id)) {
-      merged.push({ ...def, order: nextOrder++ });
+    if (seen.has(def.id)) continue;
+    const defaultIdx = defaultOrder.indexOf(def.id);
+    let insertAfterIdx = -1;
+    for (let i = defaultIdx - 1; i >= 0; i--) {
+      const idx = merged.findIndex((s) => s.id === defaultOrder[i]);
+      if (idx !== -1) { insertAfterIdx = idx; break; }
+    }
+    toInsert.push({ section: def, afterIdx: insertAfterIdx });
+  }
+  if (toInsert.length > 0) {
+    // Insert in reverse so indices stay stable
+    toInsert.sort((a, b) => b.afterIdx - a.afterIdx);
+    for (const { section, afterIdx } of toInsert) {
+      merged.splice(afterIdx + 1, 0, { ...section, order: 0 });
+    }
+    // Renumber all orders as clean integers
+    for (let i = 0; i < merged.length; i++) {
+      merged[i] = { ...merged[i], order: i };
     }
   }
 

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

@@ -0,0 +1,218 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import {
+  createFindingSchema,
+  updateFindingSchema,
+  resolveFindingSchema,
+} from "../Schema/findingSchema";
+import { revalidatePath } from "next/cache";
+
+export async function getServiceFindings(serviceRecordId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      return db.vehicleFinding.findMany({
+        where: {
+          serviceRecordId,
+          vehicle: { organizationId },
+        },
+        orderBy: { createdAt: "desc" },
+      });
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.VEHICLES },
+      ],
+    }
+  );
+}
+
+export async function getVehicleFindings(
+  vehicleId: string,
+  params: { page?: number; pageSize?: number }
+) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const vehicle = await db.vehicle.findFirst({
+        where: { id: vehicleId, organizationId },
+      });
+      if (!vehicle) throw new Error("Vehicle not found");
+
+      const page = params.page || 1;
+      const pageSize = params.pageSize || 10;
+      const skip = (page - 1) * pageSize;
+
+      const [records, total] = await Promise.all([
+        db.vehicleFinding.findMany({
+          where: { vehicleId },
+          include: {
+            serviceRecord: { select: { id: true, title: true } },
+            resolvedServiceRecord: { select: { id: true, title: true } },
+          },
+          orderBy: [
+            { status: "asc" },
+            { createdAt: "desc" },
+          ],
+          skip,
+          take: pageSize,
+        }),
+        db.vehicleFinding.count({ where: { vehicleId } }),
+      ]);
+
+      return {
+        records,
+        total,
+        page,
+        pageSize,
+        totalPages: Math.ceil(total / pageSize),
+      };
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.VEHICLES },
+      ],
+    }
+  );
+}
+
+export async function createFinding(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const data = createFindingSchema.parse(input);
+      const vehicle = await db.vehicle.findFirst({
+        where: { id: data.vehicleId, organizationId },
+      });
+      if (!vehicle) throw new Error("Vehicle not found");
+
+      const finding = await db.vehicleFinding.create({ data });
+      revalidatePath(`/vehicles/${data.vehicleId}`);
+      revalidatePath("/");
+      return finding;
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.UPDATE,
+          subject: PermissionSubject.VEHICLES,
+        },
+      ],
+      audit: ({ result }) => ({
+        action: "finding.create",
+        entity: "VehicleFinding",
+        entityId: result.id,
+        message: `Created finding "${result.description}" on vehicle ${result.vehicleId}`,
+        metadata: { findingId: result.id, vehicleId: result.vehicleId },
+      }),
+    }
+  );
+}
+
+export async function updateFinding(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const { id, ...data } = updateFindingSchema.parse(input);
+      const finding = await db.vehicleFinding.findFirst({
+        where: { id, vehicle: { organizationId } },
+      });
+      if (!finding) throw new Error("Finding not found");
+
+      const updated = await db.vehicleFinding.update({
+        where: { id },
+        data: {
+          ...data,
+          notes:
+            data.notes !== undefined ? data.notes || null : undefined,
+        },
+      });
+      revalidatePath(`/vehicles/${finding.vehicleId}`);
+      revalidatePath("/");
+      return updated;
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.UPDATE,
+          subject: PermissionSubject.VEHICLES,
+        },
+      ],
+      audit: ({ result }) => ({
+        action: "finding.update",
+        entity: "VehicleFinding",
+        entityId: result.id,
+        message: `Updated finding "${result.description}"`,
+        metadata: { findingId: result.id, vehicleId: result.vehicleId },
+      }),
+    }
+  );
+}
+
+export async function resolveFinding(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const data = resolveFindingSchema.parse(input);
+      const finding = await db.vehicleFinding.findFirst({
+        where: { id: data.id, vehicle: { organizationId } },
+      });
+      if (!finding) throw new Error("Finding not found");
+
+      const updated = await db.vehicleFinding.update({
+        where: { id: data.id },
+        data: {
+          status: "resolved",
+          resolvedServiceRecordId: data.resolvedServiceRecordId || null,
+        },
+      });
+      revalidatePath(`/vehicles/${finding.vehicleId}`);
+      revalidatePath("/");
+      return updated;
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.UPDATE,
+          subject: PermissionSubject.VEHICLES,
+        },
+      ],
+      audit: ({ result }) => ({
+        action: "finding.resolve",
+        entity: "VehicleFinding",
+        entityId: result.id,
+        message: `Resolved finding "${result.description}"`,
+        metadata: { findingId: result.id, vehicleId: result.vehicleId },
+      }),
+    }
+  );
+}
+
+export async function deleteFinding(findingId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const finding = await db.vehicleFinding.findFirst({
+        where: { id: findingId, vehicle: { organizationId } },
+      });
+      if (!finding) throw new Error("Finding not found");
+
+      await db.vehicleFinding.delete({ where: { id: findingId } });
+      revalidatePath(`/vehicles/${finding.vehicleId}`);
+      revalidatePath("/");
+      return { findingId };
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.UPDATE,
+          subject: PermissionSubject.VEHICLES,
+        },
+      ],
+      audit: ({ result }) => ({
+        action: "finding.delete",
+        entity: "VehicleFinding",
+        entityId: result.findingId,
+        message: `Deleted finding ${result.findingId}`,
+        metadata: { findingId: result.findingId },
+      }),
+    }
+  );
+}

+ 1 - 0
src/features/vehicles/Actions/vehicleActions.ts

@@ -57,6 +57,7 @@ export async function getVehicle(vehicleId: string) {
             serviceRecords: true,
             notes: true,
             reminders: { where: { isCompleted: false } },
+            findings: { where: { status: "open" } },
           },
         },
       },

+ 167 - 0
src/features/vehicles/Components/FindingForm.tsx

@@ -0,0 +1,167 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { useGlassModal } from "@/components/glass-modal";
+import { toast } from "sonner";
+import { createFinding, updateFinding } from "../Actions/findingActions";
+import { Loader2 } from "lucide-react";
+import { useTranslations } from "next-intl";
+
+interface FindingData {
+  id: string;
+  description: string;
+  severity: string;
+  status: string;
+  notes: string | null;
+  serviceRecordId?: string | null;
+}
+
+interface FindingFormProps {
+  vehicleId: string;
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  finding?: FindingData;
+  serviceRecordId?: string;
+}
+
+export function FindingForm({
+  vehicleId,
+  open,
+  onOpenChange,
+  finding,
+  serviceRecordId,
+}: FindingFormProps) {
+  const router = useRouter();
+  const modal = useGlassModal();
+  const t = useTranslations("vehicles.findings");
+  const tc = useTranslations("common.buttons");
+  const [loading, setLoading] = useState(false);
+  const [description, setDescription] = useState("");
+  const [severity, setSeverity] = useState("needs_work");
+  const [notes, setNotes] = useState("");
+
+  const isEdit = !!finding;
+
+  useEffect(() => {
+    if (open && finding) {
+      setDescription(finding.description);
+      setSeverity(finding.severity);
+      setNotes(finding.notes || "");
+    } else if (open) {
+      setDescription("");
+      setSeverity("needs_work");
+      setNotes("");
+    }
+  }, [open, finding]);
+
+  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    setLoading(true);
+
+    const payload = {
+      vehicleId,
+      description,
+      severity: severity as "needs_work" | "monitor" | "urgent",
+      notes: notes || undefined,
+      serviceRecordId: serviceRecordId || undefined,
+    };
+
+    const result = isEdit
+      ? await updateFinding({ ...payload, id: finding.id })
+      : await createFinding(payload);
+
+    if (result.success) {
+      toast.success(isEdit ? t("findingUpdated") : t("findingCreated"));
+      onOpenChange(false);
+      router.refresh();
+    } else {
+      modal.open(
+        "error",
+        "Error",
+        result.error || t("saveError", { action: isEdit ? "update" : "create" })
+      );
+    }
+
+    setLoading(false);
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-md">
+        <DialogHeader>
+          <DialogTitle>{isEdit ? t("editTitle") : t("addTitle")}</DialogTitle>
+        </DialogHeader>
+
+        <form onSubmit={handleSubmit} className="space-y-4">
+          <div className="space-y-2">
+            <Label htmlFor="finding-description">{t("descriptionLabel")}</Label>
+            <Input
+              id="finding-description"
+              value={description}
+              onChange={(e) => setDescription(e.target.value)}
+              placeholder={t("descriptionPlaceholder")}
+              required
+            />
+          </div>
+
+          <div className="space-y-2">
+            <Label htmlFor="finding-severity">{t("severityLabel")}</Label>
+            <Select value={severity} onValueChange={setSeverity}>
+              <SelectTrigger id="finding-severity">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="urgent">{t("severity.urgent")}</SelectItem>
+                <SelectItem value="needs_work">{t("severity.needs_work")}</SelectItem>
+                <SelectItem value="monitor">{t("severity.monitor")}</SelectItem>
+              </SelectContent>
+            </Select>
+          </div>
+
+          <div className="space-y-2">
+            <Label htmlFor="finding-notes">{t("notesLabel")}</Label>
+            <Textarea
+              id="finding-notes"
+              value={notes}
+              onChange={(e) => setNotes(e.target.value)}
+              placeholder={t("notesPlaceholder")}
+              rows={3}
+            />
+          </div>
+
+          <div className="flex justify-end gap-3 pt-2">
+            <Button
+              type="button"
+              variant="outline"
+              onClick={() => onOpenChange(false)}
+            >
+              {tc("cancel")}
+            </Button>
+            <Button type="submit" disabled={loading}>
+              {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              {isEdit ? tc("saveChanges") : t("addTitle")}
+            </Button>
+          </div>
+        </form>
+      </DialogContent>
+    </Dialog>
+  );
+}

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

@@ -4,7 +4,7 @@ import { formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { createStyles, gray, getFontBold } from './styles'
 import { Header } from './Header'
 import { CustomerSection, VehicleSection, ServiceSection } from './InfoSection'
-import { PartsTable, LaborTable } from './Tables'
+import { PartsTable, LaborTable, FindingsPdfSection } from './Tables'
 import { Totals } from './Totals'
 import { NotesOnly, BankAccountSection, DiagnosticNotesSection } from './Notes'
 import { CustomFields } from './CustomFields'
@@ -84,7 +84,15 @@ export function InvoicePDF({
   const vehicleName = `${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 displayTotal = data.totalAmount > 0 ? data.totalAmount : data.cost
+  const computedSubtotal = partsSubtotal + laborSubtotal
+  const computedDiscount = data.discountType === 'percentage'
+    ? computedSubtotal * ((data.discountValue || 0) / 100)
+    : data.discountType === 'fixed'
+      ? Math.min(data.discountValue || 0, computedSubtotal)
+      : 0
+  const computedTax = (computedSubtotal - computedDiscount) * (data.taxRate / 100)
+  const computedTotal = computedSubtotal - computedDiscount + computedTax
+  const displayTotal = data.totalAmount > 0 ? data.totalAmount : computedTotal > 0 ? computedTotal : data.cost
   const invoiceNum = data.invoiceNumber || `INV-${data.id.slice(-8).toUpperCase()}`
   const df = invoiceSettings?.dateFormat || DEFAULT_DATE_FORMAT
   const tz = invoiceSettings?.timezone || undefined
@@ -249,6 +257,15 @@ export function InvoicePDF({
       />
     ),
 
+    findings: (
+      <FindingsPdfSection
+        findings={data.findings || []}
+        fontFamily={fontFamily}
+        styles={styles}
+        labels={labels}
+      />
+    ),
+
     bank_account: (
       <BankAccountSection
         invoiceSettings={invoiceSettings}

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

@@ -357,6 +357,7 @@ export function DiagnosticNotesSection({
   )
 }
 
+
 // ---------------------------------------------------------------------------
 // Composite Notes component – kept for backward compatibility
 // ---------------------------------------------------------------------------

+ 58 - 0
src/features/vehicles/Components/invoice-pdf/Tables.tsx

@@ -1,5 +1,6 @@
 import { Text, View } from '@react-pdf/renderer'
 import { formatCurrency } from '@/lib/format'
+import { getFontBold } from './styles'
 import type { InvoiceData } from './types'
 import type { Style } from '@react-pdf/types'
 
@@ -93,3 +94,60 @@ export function LaborTable({ data, currencyCode, styles, labels }: TablesProps)
     </View>
   )
 }
+
+export function FindingsPdfSection({
+  findings,
+  fontFamily,
+  styles,
+  labels,
+}: {
+  findings: Array<{ description: string; severity: string; notes: string | null }>
+  fontFamily: string
+  styles: Record<string, Style>
+  labels: Record<string, string>
+}) {
+  if (!findings || findings.length === 0) return null
+  const fontBold = getFontBold(fontFamily)
+
+  const severityLabels: Record<string, string> = {
+    urgent: labels.findingSeverityUrgent || 'Urgent',
+    needs_work: labels.findingSeverityNeedsWork || 'Needs Work',
+    monitor: labels.findingSeverityMonitor || 'Monitor',
+  }
+
+  const severityColors: Record<string, string> = {
+    urgent: '#ef4444',
+    needs_work: '#f59e0b',
+    monitor: '#3b82f6',
+  }
+
+  return (
+    <View>
+      <Text style={styles.sectionTitle}>{labels.findings || 'Findings'}</Text>
+      <Text style={{ fontSize: 8, color: '#666', marginBottom: 4, lineHeight: 1.4 }}>{labels.findingsDescription || 'The following items were observed during this service and may require attention.'}</Text>
+      <View style={styles.table}>
+        <View style={styles.tableHeader}>
+          <Text style={{ ...styles.tableHeaderCell, width: '15%' }}>{labels.findingSeverityLabel || 'Severity'}</Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '40%' }}>{labels.description || 'Description'}</Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '45%' }}>{labels.findingNotesLabel || 'Notes'}</Text>
+        </View>
+        {findings.map((f, i) => (
+          <View key={i} style={styles.tableRow}>
+            <Text style={{
+              ...styles.tableCell,
+              width: '15%',
+              fontSize: 8,
+              color: severityColors[f.severity] || '#666',
+              fontFamily: fontBold,
+              textTransform: 'uppercase',
+            }}>
+              {severityLabels[f.severity] || f.severity}
+            </Text>
+            <Text style={{ ...styles.tableCell, width: '40%' }}>{f.description}</Text>
+            <Text style={{ ...styles.tableCell, width: '45%', color: '#666' }}>{f.notes || '-'}</Text>
+          </View>
+        ))}
+      </View>
+    </View>
+  )
+}

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

@@ -48,6 +48,7 @@ export interface InvoiceData {
     pricingType?: string
   }[]
   customFields?: Array<{ fieldId: string; label: string; value: string; fieldType: string }>;
+  findings?: Array<{ description: string; severity: string; notes: string | null }>;
   vehicle: {
     make: string
     model: string

+ 1 - 1
src/features/vehicles/Components/service-detail/InvoiceSummary.tsx

@@ -56,7 +56,7 @@ export function InvoiceSummary({
             <span>{formatCurrency(laborSubtotal, currencyCode)}</span>
           </div>
         )}
-        {subtotal > 0 && (
+        {subtotal > 0 && ((hasPartItems && hasLaborItems) || discountAmount > 0 || taxRate > 0) && (
           <div className="flex justify-between text-sm">
             <span className="text-muted-foreground">{t("subtotal")}</span>
             <span>{formatCurrency(subtotal, currencyCode)}</span>

+ 129 - 0
src/features/vehicles/Components/service-detail/ServiceFindingsSection.tsx

@@ -0,0 +1,129 @@
+"use client";
+
+import React, { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+  AlertTriangle,
+  Pencil,
+  Plus,
+  Trash2,
+} from "lucide-react";
+import { toast } from "sonner";
+import { deleteFinding } from "../../Actions/findingActions";
+
+interface Finding {
+  id: string;
+  description: string;
+  severity: string;
+  status: string;
+  notes: string | null;
+}
+
+interface ServiceFindingsSectionProps {
+  vehicleId: string;
+  serviceRecordId: string;
+  findings: Finding[];
+  onAddFinding?: () => void;
+  onEditFinding?: (finding: Finding) => void;
+}
+
+const severityColors: Record<string, string> = {
+  urgent: "bg-red-500/10 text-red-500 border-red-500/20",
+  needs_work: "bg-amber-500/10 text-amber-600 border-amber-500/20",
+  monitor: "bg-blue-500/10 text-blue-500 border-blue-500/20",
+};
+
+export const ServiceFindingsSection = React.memo(function ServiceFindingsSection({
+  findings,
+  onAddFinding,
+  onEditFinding,
+}: ServiceFindingsSectionProps) {
+  const router = useRouter();
+  const t = useTranslations("vehicles.findings");
+  const [loading, setLoading] = useState<string | null>(null);
+
+  if (findings.length === 0) return null;
+
+  const handleDelete = async (id: string) => {
+    setLoading(id);
+    const result = await deleteFinding(id);
+    if (result.success) {
+      toast.success(t("findingDeleted"));
+      router.refresh();
+    }
+    setLoading(null);
+  };
+
+  const openCount = findings.filter((f) => f.status === "open").length;
+
+  return (
+    <div className="rounded-lg border bg-card p-3">
+      <div className="mb-2 flex items-center justify-between">
+        <div className="flex items-center gap-2">
+          <AlertTriangle className="h-4 w-4 text-amber-500" />
+          <h3 className="text-sm font-semibold">{t("sectionTitle")}</h3>
+          {openCount > 0 && (
+            <Badge variant="destructive" className="h-5 min-w-5 px-1 text-[10px]">
+              {openCount}
+            </Badge>
+          )}
+        </div>
+        <Button
+          size="sm"
+          variant="outline"
+          className="h-7 text-xs"
+          onClick={() => onAddFinding?.()}
+        >
+          <Plus className="mr-1 h-3 w-3" />
+          {t("addFinding")}
+        </Button>
+      </div>
+
+      <div className="space-y-1.5">
+        {findings.map((f) => (
+          <div
+            key={f.id}
+            className="flex items-start justify-between gap-2 rounded-md border px-2.5 py-1.5"
+          >
+            <div className="min-w-0 flex-1">
+              <div className="flex items-center gap-1.5">
+                <Badge
+                  variant="outline"
+                  className={`text-[10px] px-1.5 py-0 ${severityColors[f.severity] || ""}`}
+                >
+                  {t(`severity.${f.severity}` as "severity.urgent" | "severity.needs_work" | "severity.monitor")}
+                </Badge>
+              </div>
+              <p className="mt-0.5 text-sm">{f.description}</p>
+              {f.notes && (
+                <p className="mt-0.5 text-xs text-muted-foreground">{f.notes}</p>
+              )}
+            </div>
+            <div className="flex shrink-0 items-center gap-0.5">
+              <Button
+                size="icon"
+                variant="ghost"
+                className="h-6 w-6"
+                onClick={() => onEditFinding?.(f)}
+              >
+                <Pencil className="h-3 w-3" />
+              </Button>
+              <Button
+                size="icon"
+                variant="ghost"
+                className="h-6 w-6 text-destructive"
+                disabled={loading === f.id}
+                onClick={() => handleDelete(f.id)}
+              >
+                <Trash2 className="h-3 w-3" />
+              </Button>
+            </div>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+});

+ 50 - 1
src/features/vehicles/Components/service-edit/LaborEditor.tsx

@@ -4,7 +4,13 @@ import { useRef, useCallback, useState, useEffect } from 'react'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Textarea } from '@/components/ui/textarea'
-import { GripVertical, Layers, Plus, Trash2, Wrench } from 'lucide-react'
+import { AlertTriangle, Eye, GripVertical, Layers, Plus, Trash2, Wrench } from 'lucide-react'
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu'
 import { formatCurrency, getCurrencySymbol } from '@/lib/format'
 import { useTranslations } from 'next-intl'
 import type { ServiceLaborInput } from '@/features/vehicles/Schema/serviceSchema'
@@ -36,6 +42,9 @@ interface LaborEditorProps {
   defaultLaborRate: number
   hasPresets?: boolean
   onOpenPresets?: () => void
+  onAddFinding?: () => void
+  openObservationsCount?: number
+  onShowExistingObservations?: () => void
 }
 
 function SortableLaborRow({
@@ -149,6 +158,9 @@ export function LaborEditor({
   defaultLaborRate,
   hasPresets,
   onOpenPresets,
+  onAddFinding,
+  openObservationsCount = 0,
+  onShowExistingObservations,
 }: LaborEditorProps) {
   const t = useTranslations('service.labor')
   const cs = getCurrencySymbol(currencyCode)
@@ -238,6 +250,43 @@ export function LaborEditor({
             <Wrench className="mr-1 h-3.5 w-3.5" />
             <span className="hidden sm:inline">{t('addService')}</span>
           </Button>
+          {onAddFinding && openObservationsCount > 0 && onShowExistingObservations ? (
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <Button type="button" variant="outline" size="sm">
+                  <AlertTriangle className="mr-1 h-3.5 w-3.5" />
+                  <span className="hidden sm:inline">{t('addFinding')}</span>
+                  <span className="ml-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-amber-500 px-1 text-[10px] font-medium text-white">
+                    {openObservationsCount}
+                  </span>
+                </Button>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent align="end">
+                <DropdownMenuItem
+                  onClick={(e) => { e.preventDefault(); onAddFinding() }}
+                >
+                  <Plus className="mr-2 h-4 w-4" />
+                  {t('newObservation')}
+                </DropdownMenuItem>
+                <DropdownMenuItem
+                  onClick={(e) => { e.preventDefault(); onShowExistingObservations() }}
+                >
+                  <Eye className="mr-2 h-4 w-4" />
+                  {t('addExisting', { count: openObservationsCount })}
+                </DropdownMenuItem>
+              </DropdownMenuContent>
+            </DropdownMenu>
+          ) : onAddFinding ? (
+            <Button
+              type="button"
+              variant="outline"
+              size="sm"
+              onClick={onAddFinding}
+            >
+              <AlertTriangle className="mr-1 h-3.5 w-3.5" />
+              <span className="hidden sm:inline">{t('addFinding')}</span>
+            </Button>
+          ) : null}
         </div>
       </div>
 

+ 23 - 0
src/features/vehicles/Components/service-page/DetailsLeftColumn.tsx

@@ -3,6 +3,7 @@ import { LaborEditor } from '../service-edit/LaborEditor'
 import { NotesSection } from '../service-edit/NotesSection'
 import { PaymentsSection } from '../service-detail/PaymentsSection'
 import { InvoiceSummary } from '../service-detail/InvoiceSummary'
+import { ServiceFindingsSection } from '../service-detail/ServiceFindingsSection'
 import type { useServiceFormState } from './useServiceFormState'
 import type { useServiceActions } from './useServiceActions'
 import type { ServiceDetail } from '../service-detail/types'
@@ -19,6 +20,12 @@ interface DetailsLeftColumnProps {
   onOpenPresets?: () => void
   onScanBarcode?: () => void
   aiEnabled?: boolean
+  vehicleId: string
+  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
+  openObservationsCount?: number
+  onShowExistingObservations?: () => void
 }
 
 export function DetailsLeftColumn({
@@ -32,6 +39,12 @@ export function DetailsLeftColumn({
   onOpenPresets,
   onScanBarcode,
   aiEnabled,
+  vehicleId,
+  findings = [],
+  onAddFinding,
+  onEditFinding,
+  openObservationsCount = 0,
+  onShowExistingObservations,
 }: DetailsLeftColumnProps) {
   return (
     <div className="space-y-3">
@@ -54,6 +67,9 @@ export function DetailsLeftColumn({
         defaultLaborRate={defaultLaborRate}
         hasPresets={hasPresets}
         onOpenPresets={onOpenPresets}
+        onAddFinding={onAddFinding}
+        openObservationsCount={openObservationsCount}
+        onShowExistingObservations={onShowExistingObservations}
       />
       <NotesSection
         initialData={formState.initialData}
@@ -61,6 +77,13 @@ export function DetailsLeftColumn({
         serviceRecordId={record.id}
         aiEnabled={aiEnabled}
       />
+      <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 || []}

+ 128 - 0
src/features/vehicles/Components/service-page/ObservationsManager.tsx

@@ -0,0 +1,128 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { useTranslations } from 'next-intl'
+import { Button } from '@/components/ui/button'
+import { Checkbox } from '@/components/ui/checkbox'
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
+import { FindingForm } from '../FindingForm'
+
+interface Observation {
+  id: string
+  description: string
+  severity: string
+  notes: string | null
+  serviceRecordId: string | null
+}
+
+interface Finding {
+  id: string
+  description: string
+  severity: string
+  status: string
+  notes: string | null
+}
+
+export interface ObservationsControls {
+  onAddFinding: () => void
+  onEditFinding: (finding: Finding) => void
+  openObservationsCount: number
+  onShowExistingObservations: () => void
+}
+
+interface ObservationsManagerProps {
+  vehicleId: string
+  serviceRecordId: string
+  openObservations: Observation[]
+  onAddObservations: (selectedIds: string[]) => Promise<void>
+  addingObservations: boolean
+  onControlsReady: (controls: ObservationsControls) => void
+}
+
+export function ObservationsManager({
+  vehicleId,
+  serviceRecordId,
+  openObservations,
+  onAddObservations,
+  addingObservations,
+  onControlsReady,
+}: ObservationsManagerProps) {
+  const tf = useTranslations('vehicles.findings')
+  const [openFindingForm, setOpenFindingForm] = useState(false)
+  const [editingFinding, setEditingFinding] = useState<Finding | undefined>()
+  const [showExistingDialog, setShowExistingDialog] = useState(false)
+
+  const otherObservations = openObservations.filter((o) => o.serviceRecordId !== serviceRecordId)
+  const [selectedObs, setSelectedObs] = useState<Set<string>>(() => new Set(otherObservations.map((o) => o.id)))
+
+  const toggleObs = (id: string) => {
+    setSelectedObs((prev) => {
+      const next = new Set(prev)
+      if (next.has(id)) next.delete(id)
+      else next.add(id)
+      return next
+    })
+  }
+
+  // Expose controls to parent via callback
+  useEffect(() => {
+    onControlsReady({
+      onAddFinding: () => { setEditingFinding(undefined); setOpenFindingForm(true) },
+      onEditFinding: (f: Finding) => { setEditingFinding(f); setOpenFindingForm(true) },
+      openObservationsCount: otherObservations.length,
+      onShowExistingObservations: () => setShowExistingDialog(true),
+    })
+  }, [otherObservations.length]) // eslint-disable-line react-hooks/exhaustive-deps
+
+  return (
+    <>
+      <FindingForm
+        vehicleId={vehicleId}
+        serviceRecordId={serviceRecordId}
+        open={openFindingForm}
+        onOpenChange={setOpenFindingForm}
+        finding={editingFinding}
+      />
+
+      <Dialog open={showExistingDialog} onOpenChange={setShowExistingDialog}>
+        <DialogContent className="sm:max-w-md" aria-describedby={undefined}>
+          <DialogHeader>
+            <DialogTitle>{tf('vehicleHasObservations', { count: otherObservations.length })}</DialogTitle>
+          </DialogHeader>
+          <div className="space-y-1.5">
+            {otherObservations.map((o) => (
+              <label key={o.id} className="flex items-start gap-2 rounded-md border px-3 py-2 text-sm hover:bg-muted/50 cursor-pointer">
+                <Checkbox
+                  checked={selectedObs.has(o.id)}
+                  onCheckedChange={() => toggleObs(o.id)}
+                  className="mt-0.5"
+                />
+                <span>{o.description}{o.notes ? <span className="text-muted-foreground"> — {o.notes}</span> : null}</span>
+              </label>
+            ))}
+          </div>
+          <div className="flex justify-end gap-2 pt-2">
+            <Button variant="outline" onClick={() => setShowExistingDialog(false)}>
+              {tf('dismiss')}
+            </Button>
+            <Button
+              disabled={selectedObs.size === 0 || addingObservations}
+              onClick={async () => {
+                await onAddObservations(Array.from(selectedObs))
+                setShowExistingDialog(false)
+              }}
+            >
+              {addingObservations ? <span className="mr-1 h-3.5 w-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" /> : null}
+              {tf('addToWorkOrder', { count: selectedObs.size })}
+            </Button>
+          </div>
+        </DialogContent>
+      </Dialog>
+    </>
+  )
+}

+ 76 - 31
src/features/vehicles/Components/service-page/ServicePageClient.tsx

@@ -27,6 +27,7 @@ import {
   DialogDescription,
 } from '@/components/ui/dialog'
 import { CalendarIcon, AlertTriangle } from 'lucide-react'
+import { deleteFinding } from '@/features/vehicles/Actions/findingActions'
 import { updateServiceRecord } from '@/features/vehicles/Actions/serviceActions'
 import { LaborPresetPickerDialog } from '@/features/labor-presets/Components/LaborPresetPickerDialog'
 import type { LaborPresetOption } from './service-page-types'
@@ -40,6 +41,7 @@ import { useServiceFormState } from './useServiceFormState'
 import { useServiceActions } from './useServiceActions'
 import { DetailsLeftColumn } from './DetailsLeftColumn'
 import { DetailsRightColumn } from './DetailsRightColumn'
+import { ObservationsManager, type ObservationsControls } from './ObservationsManager'
 
 export type { ServicePageClientProps, BoardTechnicianOption } from './service-page-types'
 import type { ServicePageClientProps } from './service-page-types'
@@ -73,6 +75,8 @@ export function ServicePageClient({
   defaultDueDays = 0,
   statusReports = [],
   initialTab,
+  findings = [],
+  openObservations = [],
 }: ServicePageClientProps) {
   const t = useTranslations('service')
   const router = useRouter()
@@ -152,6 +156,31 @@ export function ServicePageClient({
 
   useHardwareScanner({ onScan: handleBarcodeScan, enabled: activeTab === 'details' })
 
+  // Observations state
+  const tf = useTranslations('vehicles.findings')
+  const [addingObservations, setAddingObservations] = useState(false)
+  const otherObsCount = openObservations.filter((o) => o.serviceRecordId !== record.id).length
+  const obsControlsRef = useRef<ObservationsControls | null>(null)
+
+  const handleAddObservationsToWorkOrder = async (selectedIds: string[]) => {
+    const selected = openObservations.filter((o) => selectedIds.includes(o.id))
+    if (selected.length === 0) return
+    setAddingObservations(true)
+    const newItems = selected.map((o) => ({
+      description: `${o.description}${o.notes ? ` - ${o.notes}` : ''}`,
+      hours: 0,
+      rate: 0,
+      total: 0,
+      pricingType: 'hourly' as const,
+    }))
+    formState.dirtySetLaborItems((prev) => [...newItems, ...prev])
+    // Delete the observations that were added to the work order
+    await Promise.all(selected.map((o) => deleteFinding(o.id)))
+    setAddingObservations(false)
+    toast.success(tf('observationsAdded', { count: selected.length }))
+    router.refresh()
+  }
+
   const handleSelectPreset = (preset: LaborPresetOption) => {
     const newItems = preset.items.map((item) => ({
       description: item.description,
@@ -197,38 +226,54 @@ export function ServicePageClient({
       />
 
       {activeTab === 'details' && (
-        <form id="service-record-form" ref={formState.formRef} onSubmit={actions.handleSubmit} onInput={formState.markDirty} className="flex min-h-0 flex-1 flex-col">
-          <ServiceDetailContent
-            leftColumn={
-              <DetailsLeftColumn
-                formState={formState}
-                actions={actions}
-                record={record}
-                currencyCode={currencyCode}
-                defaultLaborRate={defaultLaborRate}
-                inventoryParts={inventoryParts}
-                hasPresets={laborPresets.length > 0}
-                onOpenPresets={() => formState.setShowPresetPicker(true)}
-                onScanBarcode={() => formState.setShowBarcodeScanner(true)}
-                aiEnabled={aiEnabled}
-              />
-            }
-            rightColumn={
-              <DetailsRightColumn
-                formState={formState}
-                actions={actions}
-                record={record}
-                vehicleId={vehicleId}
-                organizationId={organizationId}
-                currencyCode={currencyCode}
-                taxEnabled={taxEnabled}
-                initialVehicle={initialVehicle}
-                boardTechnicians={boardTechnicians}
-                orgMembers={orgMembers}
-              />
-            }
+        <>
+          <form id="service-record-form" ref={formState.formRef} onSubmit={actions.handleSubmit} onInput={formState.markDirty} className="flex min-h-0 flex-1 flex-col">
+            <ServiceDetailContent
+              leftColumn={
+                <DetailsLeftColumn
+                  formState={formState}
+                  actions={actions}
+                  record={record}
+                  currencyCode={currencyCode}
+                  defaultLaborRate={defaultLaborRate}
+                  inventoryParts={inventoryParts}
+                  hasPresets={laborPresets.length > 0}
+                  onOpenPresets={() => formState.setShowPresetPicker(true)}
+                  onScanBarcode={() => formState.setShowBarcodeScanner(true)}
+                  aiEnabled={aiEnabled}
+                  vehicleId={vehicleId}
+                  findings={findings}
+                  onAddFinding={() => obsControlsRef.current?.onAddFinding()}
+                  onEditFinding={(f) => obsControlsRef.current?.onEditFinding(f)}
+                  openObservationsCount={otherObsCount}
+                  onShowExistingObservations={() => obsControlsRef.current?.onShowExistingObservations()}
+                />
+              }
+              rightColumn={
+                <DetailsRightColumn
+                  formState={formState}
+                  actions={actions}
+                  record={record}
+                  vehicleId={vehicleId}
+                  organizationId={organizationId}
+                  currencyCode={currencyCode}
+                  taxEnabled={taxEnabled}
+                  initialVehicle={initialVehicle}
+                  boardTechnicians={boardTechnicians}
+                  orgMembers={orgMembers}
+                />
+              }
+            />
+          </form>
+          <ObservationsManager
+            vehicleId={vehicleId}
+            serviceRecordId={record.id}
+            openObservations={openObservations}
+            onAddObservations={handleAddObservationsToWorkOrder}
+            addingObservations={addingObservations}
+            onControlsReady={(c) => { obsControlsRef.current = c }}
           />
-        </form>
+        </>
       )}
 
       {activeTab === 'images' && (

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

@@ -56,6 +56,8 @@ export interface ServicePageClientProps {
   defaultDueDays?: number
   statusReports?: { id: string; title: string | null; message: string | null; status: string; videoUrl: string | null; createdAt: string; publicToken: string; expiresAt: string | null; customerFeedback: string | null; feedbackAt: string | null; sentVia: string | null; sentAt: string | null }[]
   initialTab?: string
+  findings?: { id: string; description: string; severity: string; status: string; notes: string | null }[]
+  openObservations?: { id: string; description: string; severity: string; notes: string | null; serviceRecordId: string | null }[]
 }
 
 export type { ServicePartInput, ServiceLaborInput, ServiceDetail, InitialData, InventoryPartOption, LaborPresetOption }

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

@@ -111,9 +111,9 @@ export function useServiceActions({
       taxRate,
       taxAmount,
       totalAmount,
-      discountType: discountType === 'none' ? undefined : discountType,
-      discountValue,
-      discountAmount,
+      discountType: discountType === 'none' ? 'none' : discountType,
+      discountValue: discountType === 'none' ? 0 : discountValue,
+      discountAmount: discountType === 'none' ? 0 : discountAmount,
     }
 
     const result = await updateServiceRecord(payload)

+ 24 - 0
src/features/vehicles/Schema/findingSchema.ts

@@ -0,0 +1,24 @@
+import { z } from "zod";
+
+export const createFindingSchema = z.object({
+  vehicleId: z.string().min(1),
+  description: z.string().min(1, "Description is required"),
+  severity: z.enum(["needs_work", "monitor", "urgent"]).default("needs_work"),
+  notes: z.string().optional(),
+  serviceRecordId: z.string().optional(),
+});
+
+export type CreateFindingInput = z.infer<typeof createFindingSchema>;
+
+export const updateFindingSchema = createFindingSchema.partial().extend({
+  id: z.string().min(1),
+});
+
+export type UpdateFindingInput = z.infer<typeof updateFindingSchema>;
+
+export const resolveFindingSchema = z.object({
+  id: z.string().min(1),
+  resolvedServiceRecordId: z.string().optional(),
+});
+
+export type ResolveFindingInput = z.infer<typeof resolveFindingSchema>;