4
0
Эх сурвалжийг харах

Add warranty tracking to service records (#148)

* added warranty implementation

* warranty tracker

* lint
Bernt Christian Egeland 5 сар өмнө
parent
commit
6e5a2b56bc
59 өөрчлөгдсөн 984 нэмэгдсэн , 21 устгасан
  1. 27 0
      messages/de/service.json
  2. 5 0
      messages/de/share.json
  3. 10 1
      messages/de/vehicles.json
  4. 6 0
      messages/en/pdf.json
  5. 27 0
      messages/en/service.json
  6. 5 0
      messages/en/share.json
  7. 10 1
      messages/en/vehicles.json
  8. 27 0
      messages/es/service.json
  9. 5 0
      messages/es/share.json
  10. 10 1
      messages/es/vehicles.json
  11. 27 0
      messages/fr/service.json
  12. 5 0
      messages/fr/share.json
  13. 10 1
      messages/fr/vehicles.json
  14. 27 0
      messages/it/service.json
  15. 5 0
      messages/it/share.json
  16. 10 1
      messages/it/vehicles.json
  17. 27 0
      messages/lt/service.json
  18. 5 0
      messages/lt/share.json
  19. 10 1
      messages/lt/vehicles.json
  20. 27 0
      messages/nb/service.json
  21. 5 0
      messages/nb/share.json
  22. 10 1
      messages/nb/vehicles.json
  23. 27 0
      messages/nl/service.json
  24. 5 0
      messages/nl/share.json
  25. 10 1
      messages/nl/vehicles.json
  26. 27 0
      messages/pl/service.json
  27. 5 0
      messages/pl/share.json
  28. 10 1
      messages/pl/vehicles.json
  29. 27 0
      messages/pt-BR/service.json
  30. 5 0
      messages/pt-BR/share.json
  31. 10 1
      messages/pt-BR/vehicles.json
  32. 27 0
      messages/ru/service.json
  33. 5 0
      messages/ru/share.json
  34. 10 1
      messages/ru/vehicles.json
  35. 27 0
      messages/tr/service.json
  36. 5 0
      messages/tr/share.json
  37. 10 1
      messages/tr/vehicles.json
  38. 5 0
      prisma/migrations/20260412143333_warranty_tracking/migration.sql
  39. 13 6
      prisma/schema.prisma
  40. 4 0
      src/__tests__/features/public/invoice-view.test.tsx
  41. 35 1
      src/app/(authenticated)/vehicles/[id]/service-records-table.tsx
  42. 3 0
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  43. 5 0
      src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx
  44. 25 0
      src/app/(public)/portal/[orgId]/vehicles/[vehicleId]/page.tsx
  45. 26 0
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  46. 5 0
      src/features/portal/Actions/portalActions.ts
  47. 1 0
      src/features/settings/Schema/invoiceLayoutSchema.ts
  48. 3 0
      src/features/settings/Schema/settingsSchema.ts
  49. 25 2
      src/features/vehicles/Actions/serviceActions.ts
  50. 15 0
      src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx
  51. 86 0
      src/features/vehicles/Components/invoice-pdf/Warranty.tsx
  52. 4 0
      src/features/vehicles/Components/invoice-pdf/types.ts
  53. 3 0
      src/features/vehicles/Components/service-edit/form-types.ts
  54. 10 0
      src/features/vehicles/Components/service-page/DetailsRightColumn.tsx
  55. 161 0
      src/features/vehicles/Components/service-page/WarrantySection.tsx
  56. 6 0
      src/features/vehicles/Components/service-page/useServiceActions.ts
  57. 9 0
      src/features/vehicles/Components/service-page/useServiceFormState.ts
  58. 3 0
      src/features/vehicles/Schema/serviceSchema.ts
  59. 27 0
      src/lib/warranty.ts

+ 27 - 0
messages/de/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "vor {count}m",
     "hoursAgo": "vor {count}h",
     "daysAgo": "vor {count}d"
+  },
+  "table": {
+    "warranty": "Garantie"
+  },
+  "warranty": {
+    "title": "Garantie",
+    "months": "Dauer (Monate)",
+    "mileage": "Kilometergrenze",
+    "notes": "Garantiebedingungen",
+    "notesPlaceholder": "z.B. 12 Monate Garantie auf alle Teile und Arbeit...",
+    "expiresAt": "Läuft ab",
+    "presets": {
+      "3months": "3 Mo.",
+      "6months": "6 Mo.",
+      "12months": "12 Mo.",
+      "24months": "24 Mo."
+    },
+    "status": {
+      "active": "Aktiv",
+      "expiring": "Läuft bald ab",
+      "expired": "Abgelaufen",
+      "none": "Keine"
+    },
+    "defaults": {
+      "title": "Standard-Garantieeinstellungen",
+      "description": "Standardgarantiebedingungen für neue Serviceeinträge festlegen"
+    }
   }
 }

+ 5 - 0
messages/de/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Diagnoseberichte ({count})",
     "notes": "Hinweise",
     "diagnosticNotes": "Diagnosehinweise",
+    "warrantyTitle": "Garantie",
+    "warrantyExpires": "Läuft ab",
+    "warrantyMonth": "Monat",
+    "warrantyMonths": "Monate",
+    "warrantyKm": "km",
     "findings": "Beobachtungen",
     "findingsDescription": "Die folgenden Punkte wurden bei diesem Service festgestellt und erfordern möglicherweise Aufmerksamkeit.",
     "severity": "Schweregrad",

+ 10 - 1
messages/de/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Betriebsstunden",
       "technician": "Techniker",
       "files": "Dateien",
-      "total": "Gesamt"
+      "total": "Gesamt",
+      "warranty": "Garantie"
+    },
+    "warranty": {
+      "status": {
+        "active": "Aktiv",
+        "expiring": "Auslaufend",
+        "expired": "Abgelaufen",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Keine Serviceeintr\u00e4ge entsprechen Ihren Filtern.",
     "empty": "Noch keine Serviceeintr\u00e4ge.",

+ 6 - 0
messages/en/pdf.json

@@ -42,6 +42,12 @@
     "orgNumberLabel": "Org. Number",
     "paymentTermsLabel": "Payment Terms",
     "dueDateLabel": "Due Date",
+    "warrantyTitle": "Warranty",
+    "warrantyDuration": "Duration",
+    "warrantyMileage": "Mileage Limit",
+    "warrantyExpires": "Expires",
+    "warrantyNotes": "Terms",
+    "warrantyMonthsUnit": "months",
     "diagnosticNotes": "Diagnostic Notes",
     "findings": "Observations",
     "findingsDescription": "The following items were observed during this service and may require attention.",

+ 27 - 0
messages/en/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "{count}m ago",
     "hoursAgo": "{count}h ago",
     "daysAgo": "{count}d ago"
+  },
+  "table": {
+    "warranty": "Warranty"
+  },
+  "warranty": {
+    "title": "Warranty",
+    "months": "Duration (months)",
+    "mileage": "Mileage Limit",
+    "notes": "Warranty Terms",
+    "notesPlaceholder": "e.g. 12-month warranty on all parts and labor...",
+    "expiresAt": "Expires",
+    "presets": {
+      "3months": "3 mo",
+      "6months": "6 mo",
+      "12months": "12 mo",
+      "24months": "24 mo"
+    },
+    "status": {
+      "active": "Active",
+      "expiring": "Expiring",
+      "expired": "Expired",
+      "none": "N/A"
+    },
+    "defaults": {
+      "title": "Default Warranty Settings",
+      "description": "Set default warranty terms for new service records"
+    }
   }
 }

+ 5 - 0
messages/en/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Diagnostic Reports ({count})",
     "notes": "Notes",
     "diagnosticNotes": "Diagnostic Notes",
+    "warrantyTitle": "Warranty",
+    "warrantyExpires": "Expires",
+    "warrantyMonth": "month",
+    "warrantyMonths": "months",
+    "warrantyKm": "km",
     "findings": "Observations",
     "findingsDescription": "The following items were observed during this service and may require attention.",
     "severity": "Severity",

+ 10 - 1
messages/en/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Engine Hours",
       "technician": "Technician",
       "files": "Files",
-      "total": "Total"
+      "total": "Total",
+      "warranty": "Warranty"
+    },
+    "warranty": {
+      "status": {
+        "active": "Active",
+        "expiring": "Expiring",
+        "expired": "Expired",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "No service records match your filters.",
     "empty": "No service records yet.",

+ 27 - 0
messages/es/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "hace {count}m",
     "hoursAgo": "hace {count}h",
     "daysAgo": "hace {count}d"
+  },
+  "table": {
+    "warranty": "Garantía"
+  },
+  "warranty": {
+    "title": "Garantía",
+    "months": "Duración (meses)",
+    "mileage": "Límite de kilometraje",
+    "notes": "Términos de garantía",
+    "notesPlaceholder": "ej. Garantía de 12 meses en todas las piezas y mano de obra...",
+    "expiresAt": "Vence",
+    "presets": {
+      "3months": "3 m",
+      "6months": "6 m",
+      "12months": "12 m",
+      "24months": "24 m"
+    },
+    "status": {
+      "active": "Activa",
+      "expiring": "Por vencer",
+      "expired": "Vencida",
+      "none": "N/A"
+    },
+    "defaults": {
+      "title": "Configuración de garantía predeterminada",
+      "description": "Establecer términos de garantía predeterminados para nuevos registros de servicio"
+    }
   }
 }

+ 5 - 0
messages/es/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Informes de diagnóstico ({count})",
     "notes": "Notas",
     "diagnosticNotes": "Notas de diagnóstico",
+    "warrantyTitle": "Garantía",
+    "warrantyExpires": "Vence",
+    "warrantyMonth": "mes",
+    "warrantyMonths": "meses",
+    "warrantyKm": "km",
     "findings": "Observaciones",
     "findingsDescription": "Los siguientes elementos fueron observados durante este servicio y pueden requerir atención.",
     "severity": "Gravedad",

+ 10 - 1
messages/es/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Horas de Motor",
       "technician": "Tecnico",
       "files": "Archivos",
-      "total": "Total"
+      "total": "Total",
+      "warranty": "Garantia"
+    },
+    "warranty": {
+      "status": {
+        "active": "Activa",
+        "expiring": "Por vencer",
+        "expired": "Vencida",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Ningun registro de servicio coincide con sus filtros.",
     "empty": "Aun no hay registros de servicio.",

+ 27 - 0
messages/fr/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "il y a {count}m",
     "hoursAgo": "il y a {count}h",
     "daysAgo": "il y a {count}j"
+  },
+  "table": {
+    "warranty": "Garantie"
+  },
+  "warranty": {
+    "title": "Garantie",
+    "months": "Durée (mois)",
+    "mileage": "Limite de kilométrage",
+    "notes": "Conditions de garantie",
+    "notesPlaceholder": "ex. Garantie de 12 mois sur toutes les pièces et la main-d'œuvre...",
+    "expiresAt": "Expire",
+    "presets": {
+      "3months": "3 m",
+      "6months": "6 m",
+      "12months": "12 m",
+      "24months": "24 m"
+    },
+    "status": {
+      "active": "Active",
+      "expiring": "Expire bientôt",
+      "expired": "Expirée",
+      "none": "Aucune"
+    },
+    "defaults": {
+      "title": "Paramètres de garantie par défaut",
+      "description": "Définir les conditions de garantie par défaut pour les nouveaux dossiers de service"
+    }
   }
 }

+ 5 - 0
messages/fr/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Rapports de diagnostic ({count})",
     "notes": "Notes",
     "diagnosticNotes": "Notes de diagnostic",
+    "warrantyTitle": "Garantie",
+    "warrantyExpires": "Expire",
+    "warrantyMonth": "mois",
+    "warrantyMonths": "mois",
+    "warrantyKm": "km",
     "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é",

+ 10 - 1
messages/fr/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Heures Moteur",
       "technician": "Technicien",
       "files": "Fichiers",
-      "total": "Total"
+      "total": "Total",
+      "warranty": "Garantie"
+    },
+    "warranty": {
+      "status": {
+        "active": "Active",
+        "expiring": "Expiration proche",
+        "expired": "Expirée",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Aucun dossier de service ne correspond \u00e0 vos filtres.",
     "empty": "Aucun dossier de service pour le moment.",

+ 27 - 0
messages/it/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "{count}m fa",
     "hoursAgo": "{count}h fa",
     "daysAgo": "{count}g fa"
+  },
+  "table": {
+    "warranty": "Garanzia"
+  },
+  "warranty": {
+    "title": "Garanzia",
+    "months": "Durata (mesi)",
+    "mileage": "Limite chilometrico",
+    "notes": "Termini di garanzia",
+    "notesPlaceholder": "es. Garanzia di 12 mesi su tutti i ricambi e la manodopera...",
+    "expiresAt": "Scadenza",
+    "presets": {
+      "3months": "3 m",
+      "6months": "6 m",
+      "12months": "12 m",
+      "24months": "24 m"
+    },
+    "status": {
+      "active": "Attiva",
+      "expiring": "In scadenza",
+      "expired": "Scaduta",
+      "none": "Nessuna"
+    },
+    "defaults": {
+      "title": "Impostazioni garanzia predefinite",
+      "description": "Imposta i termini di garanzia predefiniti per i nuovi interventi"
+    }
   }
 }

+ 5 - 0
messages/it/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Rapporti Diagnostici ({count})",
     "notes": "Note",
     "diagnosticNotes": "Note Diagnostiche",
+    "warrantyTitle": "Garanzia",
+    "warrantyExpires": "Scadenza",
+    "warrantyMonth": "mese",
+    "warrantyMonths": "mesi",
+    "warrantyKm": "km",
     "findings": "Osservazioni",
     "findingsDescription": "I seguenti elementi sono stati osservati durante questo servizio e potrebbero richiedere attenzione.",
     "severity": "Gravità",

+ 10 - 1
messages/it/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Ore Motore",
       "technician": "Tecnico",
       "files": "File",
-      "total": "Totale"
+      "total": "Totale",
+      "warranty": "Garanzia"
+    },
+    "warranty": {
+      "status": {
+        "active": "Attiva",
+        "expiring": "In scadenza",
+        "expired": "Scaduta",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Nessun intervento corrisponde ai filtri.",
     "empty": "Nessun intervento ancora.",

+ 27 - 0
messages/lt/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "prieš {count} min.",
     "hoursAgo": "prieš {count} val.",
     "daysAgo": "prieš {count} d."
+  },
+  "table": {
+    "warranty": "Garantija"
+  },
+  "warranty": {
+    "title": "Garantija",
+    "months": "Trukmė (mėnesiai)",
+    "mileage": "Ridos limitas",
+    "notes": "Garantijos sąlygos",
+    "notesPlaceholder": "pvz. 12 mėnesių garantija visoms dalims ir darbui...",
+    "expiresAt": "Galioja iki",
+    "presets": {
+      "3months": "3 mėn.",
+      "6months": "6 mėn.",
+      "12months": "12 mėn.",
+      "24months": "24 mėn."
+    },
+    "status": {
+      "active": "Aktyvi",
+      "expiring": "Baigiasi",
+      "expired": "Pasibaigusi",
+      "none": "Nėra"
+    },
+    "defaults": {
+      "title": "Numatytieji garantijos nustatymai",
+      "description": "Nustatyti numatytas garantijos sąlygas naujiems aptarnavimo įrašams"
+    }
   }
 }

+ 5 - 0
messages/lt/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Diagnostikos ataskaitos ({count})",
     "notes": "Pastabos",
     "diagnosticNotes": "Diagnostikos pastabos",
+    "warrantyTitle": "Garantija",
+    "warrantyExpires": "Galioja iki",
+    "warrantyMonth": "mėnuo",
+    "warrantyMonths": "mėnesių",
+    "warrantyKm": "km",
     "findings": "Pastebėjimai",
     "findingsDescription": "Šie elementai buvo pastebėti aptarnavimo metu ir gali reikalauti dėmesio.",
     "severity": "Svarba",

+ 10 - 1
messages/lt/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Variklio valandos",
       "technician": "Technikas",
       "files": "Failai",
-      "total": "Suma"
+      "total": "Suma",
+      "warranty": "Garantija"
+    },
+    "warranty": {
+      "status": {
+        "active": "Aktyvi",
+        "expiring": "Baigiasi",
+        "expired": "Pasibaigusi",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Nėra paslaugų įrašų, atitinkančių filtrus.",
     "empty": "Paslaugų įrašų dar nėra.",

+ 27 - 0
messages/nb/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "{count}m siden",
     "hoursAgo": "{count}t siden",
     "daysAgo": "{count}d siden"
+  },
+  "table": {
+    "warranty": "Garanti"
+  },
+  "warranty": {
+    "title": "Garanti",
+    "months": "Varighet (måneder)",
+    "mileage": "Kilometergrense",
+    "notes": "Garantivilkår",
+    "notesPlaceholder": "f.eks. 12 måneders garanti på alle deler og arbeid...",
+    "expiresAt": "Utløper",
+    "presets": {
+      "3months": "3 md",
+      "6months": "6 md",
+      "12months": "12 md",
+      "24months": "24 md"
+    },
+    "status": {
+      "active": "Aktiv",
+      "expiring": "Utløper snart",
+      "expired": "Utløpt",
+      "none": "Ingen"
+    },
+    "defaults": {
+      "title": "Standard garantiinnstillinger",
+      "description": "Angi standard garantivilkår for nye serviceoppføringer"
+    }
   }
 }

+ 5 - 0
messages/nb/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Diagnoserapporter ({count})",
     "notes": "Notater",
     "diagnosticNotes": "Diagnosenotater",
+    "warrantyTitle": "Garanti",
+    "warrantyExpires": "Utløper",
+    "warrantyMonth": "måned",
+    "warrantyMonths": "måneder",
+    "warrantyKm": "km",
     "findings": "Observasjoner",
     "findingsDescription": "Følgende punkter ble observert under denne servicen og kan kreve oppmerksomhet.",
     "severity": "Alvorlighet",

+ 10 - 1
messages/nb/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Motortimer",
       "technician": "Mekaniker",
       "files": "Filer",
-      "total": "Totalt"
+      "total": "Totalt",
+      "warranty": "Garanti"
+    },
+    "warranty": {
+      "status": {
+        "active": "Aktiv",
+        "expiring": "Utloper snart",
+        "expired": "Utlopt",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Ingen serviceoppforinger samsvarer med filtrene dine.",
     "empty": "Ingen serviceoppforinger enna.",

+ 27 - 0
messages/nl/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "{count}m geleden",
     "hoursAgo": "{count}u geleden",
     "daysAgo": "{count}d geleden"
+  },
+  "table": {
+    "warranty": "Garantie"
+  },
+  "warranty": {
+    "title": "Garantie",
+    "months": "Duur (maanden)",
+    "mileage": "Kilometerlimiet",
+    "notes": "Garantievoorwaarden",
+    "notesPlaceholder": "bijv. 12 maanden garantie op alle onderdelen en arbeid...",
+    "expiresAt": "Verloopt",
+    "presets": {
+      "3months": "3 md",
+      "6months": "6 md",
+      "12months": "12 md",
+      "24months": "24 md"
+    },
+    "status": {
+      "active": "Actief",
+      "expiring": "Verloopt binnenkort",
+      "expired": "Verlopen",
+      "none": "Geen"
+    },
+    "defaults": {
+      "title": "Standaard garantie-instellingen",
+      "description": "Stel standaard garantievoorwaarden in voor nieuwe serviceregistraties"
+    }
   }
 }

+ 5 - 0
messages/nl/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Diagnoserapporten ({count})",
     "notes": "Notities",
     "diagnosticNotes": "Diagnosenotities",
+    "warrantyTitle": "Garantie",
+    "warrantyExpires": "Verloopt",
+    "warrantyMonth": "maand",
+    "warrantyMonths": "maanden",
+    "warrantyKm": "km",
     "findings": "Observaties",
     "findingsDescription": "De volgende punten zijn waargenomen tijdens deze service en vereisen mogelijk aandacht.",
     "severity": "Ernst",

+ 10 - 1
messages/nl/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Motoruren",
       "technician": "Monteur",
       "files": "Bestanden",
-      "total": "Totaal"
+      "total": "Totaal",
+      "warranty": "Garantie"
+    },
+    "warranty": {
+      "status": {
+        "active": "Actief",
+        "expiring": "Verloopt binnenkort",
+        "expired": "Verlopen",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Geen servicegegevens gevonden voor uw filters.",
     "empty": "Nog geen servicegegevens.",

+ 27 - 0
messages/pl/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "{count}m temu",
     "hoursAgo": "{count}g temu",
     "daysAgo": "{count}d temu"
+  },
+  "table": {
+    "warranty": "Gwarancja"
+  },
+  "warranty": {
+    "title": "Gwarancja",
+    "months": "Czas trwania (miesiące)",
+    "mileage": "Limit przebiegu",
+    "notes": "Warunki gwarancji",
+    "notesPlaceholder": "np. 12-miesięczna gwarancja na wszystkie części i robociznę...",
+    "expiresAt": "Wygasa",
+    "presets": {
+      "3months": "3 m-ce",
+      "6months": "6 m-cy",
+      "12months": "12 m-cy",
+      "24months": "24 m-ce"
+    },
+    "status": {
+      "active": "Aktywna",
+      "expiring": "Wygasa wkrótce",
+      "expired": "Wygasła",
+      "none": "Brak"
+    },
+    "defaults": {
+      "title": "Domyślne ustawienia gwarancji",
+      "description": "Ustaw domyślne warunki gwarancji dla nowych wpisów serwisowych"
+    }
   }
 }

+ 5 - 0
messages/pl/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Raporty diagnostyczne ({count})",
     "notes": "Uwagi",
     "diagnosticNotes": "Uwagi diagnostyczne",
+    "warrantyTitle": "Gwarancja",
+    "warrantyExpires": "Wygasa",
+    "warrantyMonth": "miesiąc",
+    "warrantyMonths": "miesięcy",
+    "warrantyKm": "km",
     "findings": "Obserwacje",
     "findingsDescription": "Następujące elementy zostały zaobserwowane podczas tego serwisu i mogą wymagać uwagi.",
     "severity": "Priorytet",

+ 10 - 1
messages/pl/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Godziny Pracy",
       "technician": "Technik",
       "files": "Pliki",
-      "total": "Suma"
+      "total": "Suma",
+      "warranty": "Gwarancja"
+    },
+    "warranty": {
+      "status": {
+        "active": "Aktywna",
+        "expiring": "Wygasa",
+        "expired": "Wygasla",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Brak rekordów serwisowych pasujących do filtrów.",
     "empty": "Brak rekordów serwisowych.",

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

@@ -333,5 +333,32 @@
     "minutesAgo": "{count}m atrás",
     "hoursAgo": "{count}h atrás",
     "daysAgo": "{count}d atrás"
+  },
+  "table": {
+    "warranty": "Garantia"
+  },
+  "warranty": {
+    "title": "Garantia",
+    "months": "Duração (meses)",
+    "mileage": "Limite de quilometragem",
+    "notes": "Termos de garantia",
+    "notesPlaceholder": "ex. Garantia de 12 meses em todas as peças e mão de obra...",
+    "expiresAt": "Expira",
+    "presets": {
+      "3months": "3 m",
+      "6months": "6 m",
+      "12months": "12 m",
+      "24months": "24 m"
+    },
+    "status": {
+      "active": "Ativa",
+      "expiring": "Expirando",
+      "expired": "Expirada",
+      "none": "N/A"
+    },
+    "defaults": {
+      "title": "Configurações padrão de garantia",
+      "description": "Definir termos de garantia padrão para novos registros de serviço"
+    }
   }
 }

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

@@ -58,6 +58,11 @@
     "diagnosticReports": "Relatórios de Diagnóstico ({count})",
     "notes": "Observações",
     "diagnosticNotes": "Notas de Diagnóstico",
+    "warrantyTitle": "Garantia",
+    "warrantyExpires": "Expira",
+    "warrantyMonth": "mês",
+    "warrantyMonths": "meses",
+    "warrantyKm": "km",
     "findings": "Observações",
     "findingsDescription": "Os seguintes itens foram observados durante este serviço e podem necessitar de atenção.",
     "severity": "Gravidade",

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

@@ -148,7 +148,16 @@
       "mileageMarine": "Horas de Motor",
       "technician": "Técnico",
       "files": "Arquivos",
-      "total": "Total"
+      "total": "Total",
+      "warranty": "Garantia"
+    },
+    "warranty": {
+      "status": {
+        "active": "Ativa",
+        "expiring": "Expirando",
+        "expired": "Expirada",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Nenhum registro de serviço corresponde aos seus filtros.",
     "empty": "Nenhum registro de serviço ainda.",

+ 27 - 0
messages/ru/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "{count}м назад",
     "hoursAgo": "{count}ч назад",
     "daysAgo": "{count}д назад"
+  },
+  "table": {
+    "warranty": "Гарантия"
+  },
+  "warranty": {
+    "title": "Гарантия",
+    "months": "Срок (месяцев)",
+    "mileage": "Лимит пробега",
+    "notes": "Условия гарантии",
+    "notesPlaceholder": "напр. 12-месячная гарантия на все запчасти и работу...",
+    "expiresAt": "Истекает",
+    "presets": {
+      "3months": "3 мес",
+      "6months": "6 мес",
+      "12months": "12 мес",
+      "24months": "24 мес"
+    },
+    "status": {
+      "active": "Активна",
+      "expiring": "Истекает",
+      "expired": "Истекла",
+      "none": "Нет"
+    },
+    "defaults": {
+      "title": "Настройки гарантии по умолчанию",
+      "description": "Установить условия гарантии по умолчанию для новых записей обслуживания"
+    }
   }
 }

+ 5 - 0
messages/ru/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Диагностические отчёты ({count})",
     "notes": "Заметки",
     "diagnosticNotes": "Диагностические заметки",
+    "warrantyTitle": "Гарантия",
+    "warrantyExpires": "Истекает",
+    "warrantyMonth": "месяц",
+    "warrantyMonths": "месяцев",
+    "warrantyKm": "км",
     "findings": "Наблюдения",
     "findingsDescription": "Следующие элементы были обнаружены во время данного обслуживания и могут потребовать внимания.",
     "severity": "Приоритет",

+ 10 - 1
messages/ru/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Моточасы",
       "technician": "Механик",
       "files": "Файлы",
-      "total": "Итого"
+      "total": "Итого",
+      "warranty": "Гарантия"
+    },
+    "warranty": {
+      "status": {
+        "active": "Активна",
+        "expiring": "Истекает",
+        "expired": "Истекла",
+        "none": "Н/Д"
+      }
     },
     "emptyFiltered": "Нет записей обслуживания, соответствующих фильтрам.",
     "empty": "Записей обслуживания пока нет.",

+ 27 - 0
messages/tr/service.json

@@ -333,5 +333,32 @@
     "minutesAgo": "{count}dk önce",
     "hoursAgo": "{count}sa önce",
     "daysAgo": "{count}g önce"
+  },
+  "table": {
+    "warranty": "Garanti"
+  },
+  "warranty": {
+    "title": "Garanti",
+    "months": "Süre (ay)",
+    "mileage": "Kilometre limiti",
+    "notes": "Garanti koşulları",
+    "notesPlaceholder": "ör. Tüm parça ve işçilik için 12 ay garanti...",
+    "expiresAt": "Bitiş",
+    "presets": {
+      "3months": "3 ay",
+      "6months": "6 ay",
+      "12months": "12 ay",
+      "24months": "24 ay"
+    },
+    "status": {
+      "active": "Aktif",
+      "expiring": "Sona eriyor",
+      "expired": "Süresi doldu",
+      "none": "Yok"
+    },
+    "defaults": {
+      "title": "Varsayılan garanti ayarları",
+      "description": "Yeni servis kayıtları için varsayılan garanti koşullarını ayarlayın"
+    }
   }
 }

+ 5 - 0
messages/tr/share.json

@@ -58,6 +58,11 @@
     "diagnosticReports": "Tanı Raporları ({count})",
     "notes": "Notlar",
     "diagnosticNotes": "Tanı Notları",
+    "warrantyTitle": "Garanti",
+    "warrantyExpires": "Bitiş",
+    "warrantyMonth": "ay",
+    "warrantyMonths": "ay",
+    "warrantyKm": "km",
     "findings": "Gözlemler",
     "findingsDescription": "Aşağıdaki maddeler bu servis sırasında gözlemlenmiştir ve ilgi gerektirebilir.",
     "severity": "Önem",

+ 10 - 1
messages/tr/vehicles.json

@@ -148,7 +148,16 @@
       "mileageMarine": "Motor Saatleri",
       "technician": "Teknisyen",
       "files": "Dosyalar",
-      "total": "Toplam"
+      "total": "Toplam",
+      "warranty": "Garanti"
+    },
+    "warranty": {
+      "status": {
+        "active": "Aktif",
+        "expiring": "Sona eriyor",
+        "expired": "Sona erdi",
+        "none": "N/A"
+      }
     },
     "emptyFiltered": "Filtrelerinizle eşleşen servis kaydı bulunamadı.",
     "empty": "Henüz servis kaydı yok.",

+ 5 - 0
prisma/migrations/20260412143333_warranty_tracking/migration.sql

@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE "service_records" ADD COLUMN     "warrantyExpiresAt" TIMESTAMP(3),
+ADD COLUMN     "warrantyMileage" INTEGER,
+ADD COLUMN     "warrantyMonths" INTEGER,
+ADD COLUMN     "warrantyNotes" TEXT;

+ 13 - 6
prisma/schema.prisma

@@ -205,12 +205,19 @@ model ServiceRecord {
   discountValue   Float     @default(0)
   discountAmount  Float     @default(0)
   manuallyPaid    Boolean   @default(false)
-  publicToken     String?   @unique
-  sharedAt        DateTime?
-  viewCount       Int       @default(0)
-  lastViewedAt    DateTime?
-  createdAt       DateTime  @default(now())
-  updatedAt       DateTime  @updatedAt
+
+  // Warranty tracking
+  warrantyMonths    Int?
+  warrantyMileage   Int?
+  warrantyExpiresAt DateTime?
+  warrantyNotes     String?
+
+  publicToken  String?   @unique
+  sharedAt     DateTime?
+  viewCount    Int       @default(0)
+  lastViewedAt DateTime?
+  createdAt    DateTime  @default(now())
+  updatedAt    DateTime  @updatedAt
 
   vehicleId String
   vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)

+ 4 - 0
src/__tests__/features/public/invoice-view.test.tsx

@@ -65,6 +65,10 @@ const BASE_RECORD = {
   discountType: null,
   discountValue: 0,
   discountAmount: 0,
+  warrantyMonths: null,
+  warrantyMileage: null,
+  warrantyExpiresAt: null,
+  warrantyNotes: null,
   partItems: [],
   laborItems: [],
   payments: [],

+ 35 - 1
src/app/(authenticated)/vehicles/[id]/service-records-table.tsx

@@ -24,6 +24,7 @@ import {
 } from "@/components/ui/table";
 import { typeColors, statusColors } from "@/lib/table-utils";
 import { formatCurrency } from "@/lib/format";
+import { getWarrantyStatus, type WarrantyStatus } from "@/lib/warranty";
 import {
   ChevronLeft,
   ChevronRight,
@@ -39,6 +40,21 @@ import { toast } from "sonner";
 import { useTranslations } from "next-intl";
 import { useServiceType } from "@/components/service-type-context";
 
+const warrantyBadgeStyles: Record<WarrantyStatus, string> = {
+  active: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
+  expiring: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
+  expired: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
+  none: "bg-muted text-muted-foreground",
+};
+
+function WarrantyBadge({ status, t }: { status: WarrantyStatus; t: (key: string) => string }) {
+  return (
+    <Badge variant="outline" className={`text-xs ${warrantyBadgeStyles[status]}`}>
+      {t(`warranty.status.${status}`)}
+    </Badge>
+  );
+}
+
 interface ServiceRecordRow {
   id: string;
   title: string;
@@ -53,6 +69,10 @@ interface ServiceRecordRow {
   techName: string | null;
   totalAmount: number;
   invoiceNumber: string | null;
+  warrantyMonths: number | null;
+  warrantyMileage: number | null;
+  warrantyExpiresAt: Date | null;
+  warrantyNotes: string | null;
   _count: { partItems: number; laborItems: number; attachments: number };
   laborItems?: { description: string }[];
 }
@@ -67,6 +87,7 @@ interface ServiceRecordsTableProps {
   search: string;
   type: string;
   currencyCode?: string;
+  vehicleMileage?: number;
 }
 
 export function ServiceRecordsTable({
@@ -79,6 +100,7 @@ export function ServiceRecordsTable({
   search,
   type,
   currencyCode = "USD",
+  vehicleMileage,
 }: ServiceRecordsTableProps) {
   const router = useRouter();
   const { formatDate } = useFormatDate();
@@ -227,12 +249,13 @@ export function ServiceRecordsTable({
               <TableHead className="hidden w-[120px] sm:table-cell">{t("table.technician")}</TableHead>
               <TableHead className="w-[50px] text-center">{t("table.files")}</TableHead>
               <TableHead className="w-[100px] text-right">{t("table.total")}</TableHead>
+              <TableHead className="w-[90px] text-right">{t("table.warranty")}</TableHead>
             </TableRow>
           </TableHeader>
           <TableBody>
             {records.length === 0 ? (
               <TableRow>
-                <TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
+                <TableCell colSpan={9} className="h-32 text-center text-muted-foreground">
                   {search || type !== "all"
                     ? t("emptyFiltered")
                     : t("empty")}
@@ -300,6 +323,17 @@ export function ServiceRecordsTable({
                         {formatCurrency(displayTotal, currencyCode)}
                       </span>
                     </TableCell>
+                    <TableCell className="text-right">
+                      <WarrantyBadge
+                        status={getWarrantyStatus(
+                          record.warrantyExpiresAt,
+                          record.warrantyMileage,
+                          record.mileage,
+                          vehicleMileage,
+                        )}
+                        t={t}
+                      />
+                    </TableCell>
                   </TableRow>
                 );
               })

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

@@ -163,6 +163,9 @@ export default async function ServiceDetailPage({
     discountType: record.discountType || undefined,
     discountValue: record.discountValue,
     discountAmount: record.discountAmount,
+    warrantyMonths: record.warrantyMonths ?? null,
+    warrantyMileage: record.warrantyMileage ?? null,
+    warrantyNotes: record.warrantyNotes ?? null,
   };
 
   // Prepare media attachments for managers

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

@@ -110,6 +110,10 @@ interface PaginatedServices {
     techName: string | null
     totalAmount: number
     invoiceNumber: string | null
+    warrantyMonths: number | null
+    warrantyMileage: number | null
+    warrantyExpiresAt: Date | null
+    warrantyNotes: string | null
     _count: { partItems: number; laborItems: number; attachments: number }
   }[]
   total: number
@@ -1118,6 +1122,7 @@ export function VehicleDetailClient({
             search={serviceSearch}
             type={serviceRecordType}
             currencyCode={currencyCode}
+            vehicleMileage={vehicle.mileage}
           />
         </TabsContent>
 

+ 25 - 0
src/app/(public)/portal/[orgId]/vehicles/[vehicleId]/page.tsx

@@ -7,6 +7,14 @@ import { Download } from 'lucide-react'
 import Link from 'next/link'
 import { getTranslations } from 'next-intl/server'
 import { db } from '@/lib/db'
+import { getWarrantyStatus, type WarrantyStatus } from '@/lib/warranty'
+
+const warrantyBadgeStyles: Record<WarrantyStatus, string> = {
+  active: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
+  expiring: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
+  expired: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
+  none: 'bg-muted text-muted-foreground',
+}
 
 export default async function PortalVehicleDetailPage({
   params,
@@ -16,6 +24,7 @@ export default async function PortalVehicleDetailPage({
   const { orgId, vehicleId } = await params
   const t = await getTranslations('portal.vehicles')
   const tInvoices = await getTranslations('portal.invoices')
+  const tWarranty = await getTranslations('vehicles.services.warranty.status')
   const [result, serviceTypeSetting] = await Promise.all([
     getPortalVehicleDetail(vehicleId),
     db.appSetting.findUnique({
@@ -105,6 +114,22 @@ export default async function PortalVehicleDetailPage({
                       </div>
                       <div className="flex items-center gap-3">
                         <Badge variant="outline">{sr.status}</Badge>
+                        {(() => {
+                          const ws = getWarrantyStatus(
+                            sr.warrantyExpiresAt,
+                            sr.warrantyMileage,
+                            sr.mileage,
+                            v.mileage,
+                          )
+                          return ws !== 'none' ? (
+                            <Badge
+                              variant="outline"
+                              className={`text-xs ${warrantyBadgeStyles[ws]}`}
+                            >
+                              {tWarranty(ws)}
+                            </Badge>
+                          ) : null
+                        })()}
                         <span className="text-sm font-medium">${sr.totalAmount.toFixed(2)}</span>
                         {sr.publicToken && (
                           <Link

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

@@ -46,6 +46,10 @@ interface InvoiceRecord {
   discountType: string | null
   discountValue: number
   discountAmount: number
+  warrantyMonths: number | null
+  warrantyMileage: number | null
+  warrantyExpiresAt: Date | string | null
+  warrantyNotes: string | null
   partItems: {
     partNumber: string | null
     name: string
@@ -1027,6 +1031,28 @@ export function InvoiceView({
                 </div>
               )
 
+            case 'warranty': {
+              if (!record.warrantyMonths && !record.warrantyNotes) return null
+              const parts: string[] = []
+              if (record.warrantyMonths) parts.push(`${record.warrantyMonths} ${record.warrantyMonths === 1 ? t('warrantyMonth', { defaultValue: 'month' }) : t('warrantyMonths', { defaultValue: 'months' })}`)
+              if (record.warrantyMileage) parts.push(`${record.warrantyMileage.toLocaleString()} ${t('warrantyKm', { defaultValue: 'km' })}`)
+              const expiresAt = record.warrantyExpiresAt ? fmtDate(new Date(record.warrantyExpiresAt), df) : null
+              return (
+                <div key="warranty" className="mt-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                  <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('warrantyTitle', { defaultValue: 'Warranty' })}</p>
+                  {parts.length > 0 && (
+                    <p className="text-sm text-gray-600 dark:text-gray-400">{parts.join(' / ')}</p>
+                  )}
+                  {expiresAt && (
+                    <p className="text-sm text-gray-500 dark:text-gray-400">{t('warrantyExpires', { defaultValue: 'Expires' })}: {expiresAt}</p>
+                  )}
+                  {record.warrantyNotes && (
+                    <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{record.warrantyNotes}</p>
+                  )}
+                </div>
+              )
+            }
+
             case 'findings':
               if (!findings || findings.length === 0) return null
               return (

+ 5 - 0
src/features/portal/Actions/portalActions.ts

@@ -200,6 +200,11 @@ export async function getPortalVehicleDetail(vehicleId: string) {
             serviceDate: true,
             startDateTime: true,
             publicToken: true,
+            mileage: true,
+            warrantyMonths: true,
+            warrantyMileage: true,
+            warrantyExpiresAt: true,
+            warrantyNotes: true,
           },
           orderBy: [{ startDateTime: { sort: 'desc', nulls: 'last' } }, { serviceDate: 'desc' }],
         },

+ 1 - 0
src/features/settings/Schema/invoiceLayoutSchema.ts

@@ -63,6 +63,7 @@ export const BUILTIN_SECTIONS = [
   { id: "findings", name: "Findings" },
   { id: "totals", name: "Totals" },
   { id: "notes", name: "Notes" },
+  { id: "warranty", name: "Warranty" },
   { id: "diagnostic_notes", name: "Diagnostic Notes" },
   { id: "bank_account", name: "Bank Account" },
   { id: "footer", name: "Footer" },

+ 3 - 0
src/features/settings/Schema/settingsSchema.ts

@@ -99,6 +99,9 @@ export const SETTING_KEYS = {
   WORKSHOP_LOCALE: 'workshop.locale',
   WORKSHOP_DEFAULT_COUNTRY_CODE: 'workshop.defaultCountryCode',
   FORCE_CUSTOMER_LOCALE: 'workshop.forceCustomerLocale',
+  DEFAULT_WARRANTY_MONTHS: 'defaultWarrantyMonths',
+  DEFAULT_WARRANTY_MILEAGE: 'defaultWarrantyMileage',
+  DEFAULT_WARRANTY_NOTES: 'defaultWarrantyNotes',
 } as const
 
 export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS]

+ 25 - 2
src/features/vehicles/Actions/serviceActions.ts

@@ -274,7 +274,7 @@ export async function createServiceRecord(input: unknown) {
       });
     }
 
-    const { partItems, laborItems, attachments, serviceDate, invoiceDate, invoiceDueDate, ...recordData } = data;
+    const { partItems, laborItems, attachments, serviceDate, invoiceDate, invoiceDueDate, warrantyMonths, warrantyMileage, warrantyNotes, ...recordData } = data;
 
     const record = await db.$transaction(async (tx) => {
       const created = await tx.serviceRecord.create({
@@ -286,6 +286,16 @@ export async function createServiceRecord(input: unknown) {
           serviceDate: new Date(serviceDate),
           invoiceDate: invoiceDate ? new Date(invoiceDate) : new Date(serviceDate),
           invoiceDueDate: invoiceDueDate ? new Date(invoiceDueDate) : undefined,
+          warrantyMonths: warrantyMonths || null,
+          warrantyMileage: warrantyMileage || null,
+          warrantyNotes: warrantyNotes || null,
+          warrantyExpiresAt: warrantyMonths
+            ? (() => {
+                const base = new Date(serviceDate || Date.now());
+                base.setMonth(base.getMonth() + warrantyMonths);
+                return base;
+              })()
+            : null,
         },
       });
 
@@ -390,7 +400,7 @@ export async function updateServiceRecord(input: unknown) {
     });
     if (!existing) throw new Error("Service record not found");
 
-    const { id, partItems, laborItems, attachments, serviceDate: _sd, invoiceDate: _id, invoiceDueDate: _idd, ...recordData } = data;
+    const { id, partItems, laborItems, attachments, serviceDate: _sd, invoiceDate: _id, invoiceDueDate: _idd, warrantyMonths: _wm, warrantyMileage: _wmil, warrantyNotes: _wn, ...recordData } = data;
 
     // Determine which categories are being replaced and which files were removed
     let removedFileUrls: string[] = [];
@@ -420,6 +430,19 @@ export async function updateServiceRecord(input: unknown) {
           serviceDate: data.serviceDate ? new Date(data.serviceDate) : undefined,
           invoiceDate: data.invoiceDate ? new Date(data.invoiceDate) : undefined,
           invoiceDueDate: data.invoiceDueDate ? new Date(data.invoiceDueDate) : undefined,
+          warrantyMonths: data.warrantyMonths !== undefined ? (data.warrantyMonths || null) : undefined,
+          warrantyMileage: data.warrantyMileage !== undefined ? (data.warrantyMileage || null) : undefined,
+          warrantyNotes: data.warrantyNotes !== undefined ? (data.warrantyNotes || null) : undefined,
+          warrantyExpiresAt: data.warrantyMonths !== undefined
+            ? (data.warrantyMonths
+              ? (() => {
+                  const serviceDate = data.serviceDate || existing.serviceDate;
+                  const base = new Date(serviceDate);
+                  base.setMonth(base.getMonth() + data.warrantyMonths);
+                  return base;
+                })()
+              : null)
+            : undefined,
         },
       });
 

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

@@ -8,6 +8,7 @@ import { CustomerSection, VehicleSection, ServiceSection } from './InfoSection'
 import { PartsTable, LaborTable, FindingsPdfSection } from './Tables'
 import { Totals } from './Totals'
 import { NotesOnly, BankAccountSection, DiagnosticNotesSection } from './Notes'
+import { WarrantySection } from './Warranty'
 import { CustomFields } from './CustomFields'
 import { Footer, AttachmentsFooter } from './Footer'
 import type { InvoiceLayoutConfig } from '@/features/settings/Schema/invoiceLayoutSchema'
@@ -259,6 +260,20 @@ export function InvoicePDF({
       />
     ),
 
+    warranty: (
+      <WarrantySection
+        warrantyMonths={data.warrantyMonths}
+        warrantyMileage={data.warrantyMileage}
+        warrantyExpiresAt={data.warrantyExpiresAt}
+        warrantyNotes={data.warrantyNotes}
+        fontFamily={fontFamily}
+        styles={styles}
+        labels={labels}
+        dateFormat={invoiceSettings?.dateFormat}
+        timezone={invoiceSettings?.timezone}
+      />
+    ),
+
     diagnostic_notes: (
       <DiagnosticNotesSection
         diagnosticNotes={data.diagnosticNotes}

+ 86 - 0
src/features/vehicles/Components/invoice-pdf/Warranty.tsx

@@ -0,0 +1,86 @@
+import { Text, View } from '@react-pdf/renderer'
+import type { Style } from '@react-pdf/types'
+import { getFontBold } from './styles'
+import { formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
+
+interface WarrantyProps {
+  warrantyMonths: number | null | undefined
+  warrantyMileage: number | null | undefined
+  warrantyExpiresAt: Date | string | null | undefined
+  warrantyNotes: string | null | undefined
+  fontFamily: string
+  styles: Record<string, Style>
+  labels: Record<string, string>
+  dateFormat?: string
+  timezone?: string
+}
+
+export function WarrantySection({
+  warrantyMonths,
+  warrantyMileage,
+  warrantyExpiresAt,
+  warrantyNotes,
+  fontFamily,
+  styles,
+  labels,
+  dateFormat,
+  timezone,
+}: WarrantyProps) {
+  // Only render if there is warranty data
+  if (!warrantyMonths && !warrantyNotes) return null
+
+  const fontBold = getFontBold(fontFamily)
+
+  // Build the duration/mileage line, e.g. "12 months / 20,000 km"
+  const parts: string[] = []
+  if (warrantyMonths) {
+    parts.push(
+      `${warrantyMonths} ${labels.warrantyMonthsUnit || (warrantyMonths === 1 ? 'month' : 'months')}`
+    )
+  }
+  if (warrantyMileage) {
+    parts.push(`${warrantyMileage.toLocaleString()} ${labels.km || 'km'}`)
+  }
+  const durationLine = parts.length > 0 ? parts.join(' / ') : null
+
+  // Format the expiry date
+  let expiresLine: string | null = null
+  if (warrantyExpiresAt) {
+    const formatted = formatDateForPdf(warrantyExpiresAt, dateFormat || DEFAULT_DATE_FORMAT, timezone)
+    expiresLine = `${labels.warrantyExpires || 'Expires'}: ${formatted}`
+  }
+
+  return (
+    <View wrap={false} style={styles.notesSection}>
+      <Text style={styles.notesLabel}>
+        {labels.warrantyTitle || 'Warranty'}
+      </Text>
+
+      {durationLine && (
+        <View style={{ flexDirection: 'row', marginBottom: 2 }}>
+          <Text style={{ fontSize: 9, fontFamily: fontBold }}>
+            {labels.warrantyDuration || 'Duration'}:{' '}
+          </Text>
+          <Text style={{ fontSize: 9, color: '#6b7280' }}>{durationLine}</Text>
+        </View>
+      )}
+
+      {expiresLine && (
+        <Text style={{ fontSize: 9, color: '#6b7280', marginBottom: 2 }}>
+          {expiresLine}
+        </Text>
+      )}
+
+      {warrantyNotes && (
+        <View style={{ marginTop: 2 }}>
+          <Text style={{ fontSize: 8, fontFamily: fontBold, color: '#6b7280', marginBottom: 1 }}>
+            {labels.warrantyNotes || 'Terms'}
+          </Text>
+          <Text style={{ fontSize: 9, color: '#6b7280', lineHeight: 1.5 }}>
+            {warrantyNotes}
+          </Text>
+        </View>
+      )}
+    </View>
+  )
+}

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

@@ -51,6 +51,10 @@ export interface InvoiceData {
   }[]
   customFields?: Array<{ fieldId: string; label: string; value: string; fieldType: string }>;
   findings?: Array<{ description: string; severity: string; notes: string | null }>;
+  warrantyMonths?: number | null;
+  warrantyMileage?: number | null;
+  warrantyExpiresAt?: Date | string | null;
+  warrantyNotes?: string | null;
   vehicle: {
     make: string
     model: string

+ 3 - 0
src/features/vehicles/Components/service-edit/form-types.ts

@@ -43,6 +43,9 @@ export interface InitialData {
   discountType?: string
   discountValue?: number
   discountAmount?: number
+  warrantyMonths?: number | null
+  warrantyMileage?: number | null
+  warrantyNotes?: string | null
 }
 
 export interface VehicleOption {

+ 10 - 0
src/features/vehicles/Components/service-page/DetailsRightColumn.tsx

@@ -8,6 +8,7 @@ import { ScheduleTimesSection } from '../service-edit/ScheduleTimesSection'
 import { TotalsSection } from '../service-edit/TotalsSection'
 import { ServiceAttachments } from '../service-detail/ServiceAttachments'
 import { CustomFieldsForm } from '@/features/custom-fields/Components/CustomFieldsForm'
+import { WarrantySection } from './WarrantySection'
 import { revokePublicLink } from '@/features/vehicles/Actions/serviceActions'
 import type { useServiceFormState } from './useServiceFormState'
 import type { useServiceActions } from './useServiceActions'
@@ -106,6 +107,15 @@ export function DetailsRightColumn({
         totalAmount={formState.totalAmount}
         currencyCode={currencyCode}
       />
+      <WarrantySection
+        warrantyMonths={formState.warrantyMonths}
+        warrantyMileage={formState.warrantyMileage}
+        warrantyNotes={formState.warrantyNotes}
+        serviceDate={formState.initialData.serviceDate}
+        onWarrantyMonthsChange={formState.dirtySetWarrantyMonths}
+        onWarrantyMileageChange={formState.dirtySetWarrantyMileage}
+        onWarrantyNotesChange={formState.dirtySetWarrantyNotes}
+      />
       <ServiceAttachments
         attachments={record.attachments || []}
         imageAttachments={formState.imageAttachments}

+ 161 - 0
src/features/vehicles/Components/service-page/WarrantySection.tsx

@@ -0,0 +1,161 @@
+'use client'
+
+import { useState, useMemo } from 'react'
+import { useTranslations } from 'next-intl'
+import { ChevronDown, ChevronRight, Shield, X } from 'lucide-react'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Textarea } from '@/components/ui/textarea'
+import { Button } from '@/components/ui/button'
+import { useFormatDate } from '@/lib/use-format-date'
+
+interface WarrantySectionProps {
+  warrantyMonths: number | null
+  warrantyMileage: number | null
+  warrantyNotes: string | null
+  serviceDate: string | null
+  onWarrantyMonthsChange: (v: number | null) => void
+  onWarrantyMileageChange: (v: number | null) => void
+  onWarrantyNotesChange: (v: string | null) => void
+}
+
+const PRESETS = [3, 6, 12, 24] as const
+
+export function WarrantySection({
+  warrantyMonths,
+  warrantyMileage,
+  warrantyNotes,
+  serviceDate,
+  onWarrantyMonthsChange,
+  onWarrantyMileageChange,
+  onWarrantyNotesChange,
+}: WarrantySectionProps) {
+  const t = useTranslations('service.warranty')
+  const { formatDate } = useFormatDate()
+
+  const hasValues = !!(warrantyMonths || warrantyMileage || warrantyNotes)
+  const [open, setOpen] = useState(hasValues)
+
+  const expiresAt = useMemo(() => {
+    if (!warrantyMonths || !serviceDate) return null
+    const d = new Date(serviceDate)
+    d.setMonth(d.getMonth() + warrantyMonths)
+    return d
+  }, [warrantyMonths, serviceDate])
+
+  const handlePresetClick = (months: number) => {
+    onWarrantyMonthsChange(warrantyMonths === months ? null : months)
+  }
+
+  return (
+    <div className="rounded-lg border p-3">
+      <div className="flex items-center gap-2">
+        <button
+          type="button"
+          className="flex flex-1 items-center gap-2 text-left"
+          onClick={() => setOpen((prev) => !prev)}
+        >
+          {open ? (
+            <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
+          ) : (
+            <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
+          )}
+          <Shield className="h-3.5 w-3.5 text-muted-foreground" />
+          <h3 className="text-sm font-semibold">{t('title')}</h3>
+          {!open && hasValues && (
+            <span className="text-xs text-muted-foreground">{warrantyMonths ? `${warrantyMonths} mo` : ''}</span>
+          )}
+        </button>
+        {hasValues && (
+          <Button
+            type="button"
+            variant="ghost"
+            size="sm"
+            className="h-6 px-2 text-xs text-muted-foreground hover:text-destructive"
+            onClick={() => {
+              onWarrantyMonthsChange(null)
+              onWarrantyMileageChange(null)
+              onWarrantyNotesChange(null)
+            }}
+          >
+            <X className="h-3 w-3" />
+          </Button>
+        )}
+      </div>
+
+      {open && (
+        <div className="mt-3 space-y-3">
+          {/* Duration (months) */}
+          <div className="space-y-1.5">
+            <Label className="text-xs">{t('months')}</Label>
+            <div className="flex items-center gap-2">
+              <Input
+                type="number"
+                min={0}
+                className="h-8 w-24 text-sm"
+                value={warrantyMonths ?? ''}
+                onChange={(e) => {
+                  const v = e.target.value
+                  onWarrantyMonthsChange(v === '' ? null : Number(v))
+                }}
+              />
+              <div className="flex gap-1">
+                {PRESETS.map((m) => (
+                  <Button
+                    key={m}
+                    type="button"
+                    variant={warrantyMonths === m ? 'default' : 'outline'}
+                    size="sm"
+                    className="h-7 rounded-full px-2.5 text-xs"
+                    onClick={() => handlePresetClick(m)}
+                  >
+                    {t(`presets.${m}months` as 'presets.3months')}
+                  </Button>
+                ))}
+              </div>
+            </div>
+          </div>
+
+          {/* Calculated expiration */}
+          {expiresAt && (
+            <div className="space-y-1">
+              <Label className="text-xs">{t('expiresAt')}</Label>
+              <p className="text-sm text-muted-foreground" suppressHydrationWarning>
+                {formatDate(expiresAt)}
+              </p>
+            </div>
+          )}
+
+          {/* Mileage limit */}
+          <div className="space-y-1.5">
+            <Label className="text-xs">{t('mileage')}</Label>
+            <Input
+              type="number"
+              min={0}
+              className="h-8 w-40 text-sm"
+              value={warrantyMileage ?? ''}
+              onChange={(e) => {
+                const v = e.target.value
+                onWarrantyMileageChange(v === '' ? null : Number(v))
+              }}
+            />
+          </div>
+
+          {/* Warranty terms */}
+          <div className="space-y-1.5">
+            <Label className="text-xs">{t('notes')}</Label>
+            <Textarea
+              className="min-h-[60px] text-sm"
+              placeholder={t('notesPlaceholder')}
+              value={warrantyNotes ?? ''}
+              onChange={(e) => {
+                const v = e.target.value
+                onWarrantyNotesChange(v === '' ? null : v)
+              }}
+            />
+          </div>
+        </div>
+      )}
+    </div>
+  )
+}

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

@@ -61,6 +61,9 @@ export function useServiceActions({
     discountType,
     discountValue,
     discountAmount,
+    warrantyMonths,
+    warrantyMileage,
+    warrantyNotes,
     isSavingRef,
     autosaveTimer,
     setLoading,
@@ -138,6 +141,9 @@ export function useServiceActions({
       discountType: discountType === 'none' ? 'none' : discountType,
       discountValue: discountType === 'none' ? 0 : discountValue,
       discountAmount: discountType === 'none' ? 0 : discountAmount,
+      warrantyMonths: warrantyMonths ?? 0,
+      warrantyMileage: warrantyMileage ?? 0,
+      warrantyNotes: warrantyNotes ?? '',
     }
 
     const result = await updateServiceRecord(payload)

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

@@ -32,6 +32,9 @@ export function useServiceFormState({
   const [showInventoryPicker, setShowInventoryPicker] = useState(false)
   const [showBarcodeScanner, setShowBarcodeScanner] = useState(false)
   const [showPresetPicker, setShowPresetPicker] = useState(false)
+  const [warrantyMonths, setWarrantyMonths] = useState<number | null>(initialData.warrantyMonths ?? null)
+  const [warrantyMileage, setWarrantyMileage] = useState<number | null>(initialData.warrantyMileage ?? null)
+  const [warrantyNotes, setWarrantyNotes] = useState<string | null>(initialData.warrantyNotes ?? null)
 
   // Autosave state
   const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
@@ -181,6 +184,9 @@ export function useServiceFormState({
   const dirtySetType = useCallback((v: string) => { setType(v); markDirty() }, [markDirty])
   const dirtySetStatus = useCallback((v: string) => { setStatus(v); markDirty() }, [markDirty])
   const dirtySetSelectedVehicleId = useCallback((v: string) => { setSelectedVehicleId(v); markDirty() }, [markDirty])
+  const dirtySetWarrantyMonths = useCallback((v: number | null) => { setWarrantyMonths(v); markDirty() }, [markDirty])
+  const dirtySetWarrantyMileage = useCallback((v: number | null) => { setWarrantyMileage(v); markDirty() }, [markDirty])
+  const dirtySetWarrantyNotes = useCallback((v: string | null) => { setWarrantyNotes(v); markDirty() }, [markDirty])
 
   return {
     // State
@@ -210,6 +216,9 @@ export function useServiceFormState({
     dirtySetPartItems, dirtySetLaborItems,
     dirtySetDiscountType, dirtySetDiscountValue, dirtySetTaxRate,
     dirtySetType, dirtySetStatus, dirtySetSelectedVehicleId,
+    // Warranty
+    warrantyMonths, warrantyMileage, warrantyNotes,
+    dirtySetWarrantyMonths, dirtySetWarrantyMileage, dirtySetWarrantyNotes,
     initialData,
   }
 }

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

@@ -57,6 +57,9 @@ export const createServiceSchema = z.object({
   invoiceNumber: z.string().optional(),
   invoiceDate: z.string().optional(),
   invoiceDueDate: z.string().optional(),
+  warrantyMonths: z.coerce.number().int().min(0).optional(),
+  warrantyMileage: z.coerce.number().int().min(0).optional(),
+  warrantyNotes: z.string().optional(),
 });
 
 export const updateServiceSchema = createServiceSchema.partial().extend({

+ 27 - 0
src/lib/warranty.ts

@@ -0,0 +1,27 @@
+export type WarrantyStatus = "active" | "expiring" | "expired" | "none";
+
+export function getWarrantyStatus(
+  warrantyExpiresAt: Date | string | null | undefined,
+  warrantyMileage: number | null | undefined,
+  serviceMileage: number | null | undefined,
+  currentVehicleMileage: number | null | undefined,
+): WarrantyStatus {
+  if (!warrantyExpiresAt) return "none";
+
+  const now = new Date();
+  const expires = new Date(warrantyExpiresAt);
+  const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
+
+  // Check if date-expired
+  if (expires < now) return "expired";
+
+  // Check if mileage-expired
+  if (warrantyMileage && serviceMileage != null && currentVehicleMileage != null) {
+    if (currentVehicleMileage > serviceMileage + warrantyMileage) return "expired";
+  }
+
+  // Check if expiring soon (within 30 days)
+  if (expires < thirtyDaysFromNow) return "expiring";
+
+  return "active";
+}