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

Add individual vehicle reports (#135)

* new vehicle tab with detailed info

* pdf report

* pdf layput

* tab menu position

* skelleton loading

* table pagination
Bernt Christian Egeland 5 месяцев назад
Родитель
Сommit
9632875db2

+ 7 - 0
README.md

@@ -54,3 +54,10 @@ See the [Quick Start guide](https://torqvoice.com/docs/installation#quick-start)
 ## License
 
 [Elastic License 2.0 (ELv2)](LICENSE)
+
+- Add a dedicated Vehicles report tab with per-vehicle cost breakdown by service type (repair vs maintenance vs 
+upgrade), monthly cost trends, top parts used, and full service history
+- Vehicle selector with searchable dropdown and infinite scroll to pick any vehicle in the organization         
+- CSV, PDF export support for vehicle reports                                                                   
+- Add date range presets (Grafana-style) to the report date picker: Today, Last 7/30/90 days, This month, This  
+year, Last year, etc.

+ 64 - 5
messages/de/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Techniker",
     "parts": "Teile",
     "jobAnalytics": "Auftragsanalyse",
-    "retention": "Kundenbindung"
+    "retention": "Kundenbindung",
+    "vehicles": "Fahrzeuge"
   },
   "dateRange": {
     "to": "bis",
     "pickDate": "Datumsbereich wählen",
-    "apply": "Anwenden"
+    "apply": "Anwenden",
+    "presets": {
+      "today": "Heute",
+      "yesterday": "Gestern",
+      "last7": "Letzte 7 Tage",
+      "last30": "Letzte 30 Tage",
+      "last90": "Letzte 90 Tage",
+      "thisMonth": "Dieser Monat",
+      "lastMonth": "Letzter Monat",
+      "thisQuarter": "Dieses Quartal",
+      "thisYear": "Dieses Jahr",
+      "lastYear": "Letztes Jahr",
+      "allTime": "Gesamter Zeitraum"
+    }
   },
   "actions": {
     "refresh": "Aktualisieren",
@@ -187,6 +201,36 @@
       "taxableAmount": "Steuerpflichtiger Betrag"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Fahrzeug auswählen...",
+    "selectVehiclePrompt": "Wählen Sie oben ein Fahrzeug aus, um den Bericht anzuzeigen.",
+    "totalServices": "Gesamtleistungen",
+    "totalCost": "Gesamtkosten",
+    "repairs": "Reparaturen",
+    "maintenance": "Wartung",
+    "upgrades": "Upgrades",
+    "inspections": "Inspektionen",
+    "totalPartsUsed": "Verwendete Teile",
+    "totalLaborHours": "Arbeitsstunden",
+    "monthlyCosts": "Monatliche Kosten",
+    "serviceTypeBreakdown": "Dienstleistungstyp-Aufschlüsselung",
+    "topParts": "Meistverwendete Teile",
+    "serviceHistory": "Servicehistorie",
+    "tableHeaders": {
+      "date": "Datum",
+      "title": "Titel",
+      "type": "Typ",
+      "status": "Status",
+      "totalAmount": "Gesamt",
+      "partsCount": "Teile",
+      "laborHours": "Arbeitsstunden",
+      "techName": "Techniker",
+      "partName": "Teilename",
+      "partNumber": "Teilenr.",
+      "quantity": "Menge",
+      "cost": "Kosten"
+    }
+  },
   "charts": {
     "revenue": "Umsatz",
     "collected": "Eingezogen",
@@ -194,7 +238,9 @@
     "totalSpent": "Gesamtausgaben",
     "jobs": "Aufträge",
     "visits": "Besuche",
-    "taxCollected": "Eingezogene Steuer"
+    "taxCollected": "Eingezogene Steuer",
+    "partsCost": "Teilekosten",
+    "laborCost": "Arbeitskosten"
   },
   "days": {
     "sunday": "Sonntag",
@@ -255,7 +301,13 @@
     "partsCost": "Teilekosten",
     "partsNetProfit": "Teile-Nettogewinn",
     "laborRevenue": "Arbeit",
-    "netProfit": "Nettogewinn"
+    "netProfit": "Nettogewinn",
+    "date": "Datum",
+    "title": "Titel",
+    "type": "Typ",
+    "status": "Status",
+    "partsUsed": "Teile",
+    "laborHours": "Arbeitsstunden"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Fahrzeugbericht",
+    "vehicleLabel": "Fahrzeug",
+    "totalCost": "Gesamtkosten",
+    "partsCostLabel": "Teilekosten",
+    "laborCostLabel": "Arbeitskosten",
+    "date": "Datum",
+    "title": "Titel"
   },
   "empty": "Noch keine Daten geladen. Klicken Sie auf Aktualisieren, um den Bericht zu laden."
 }

+ 65 - 5
messages/en/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Technicians",
     "parts": "Parts",
     "jobAnalytics": "Job Analytics",
-    "retention": "Retention"
+    "retention": "Retention",
+    "vehicles": "Vehicles"
   },
   "dateRange": {
     "to": "to",
     "pickDate": "Pick a date range",
-    "apply": "Apply"
+    "apply": "Apply",
+    "presets": {
+      "today": "Today",
+      "yesterday": "Yesterday",
+      "last7": "Last 7 days",
+      "last30": "Last 30 days",
+      "last90": "Last 90 days",
+      "thisMonth": "This month",
+      "lastMonth": "Last month",
+      "thisQuarter": "This quarter",
+      "thisYear": "This year",
+      "lastYear": "Last year",
+      "allTime": "All time"
+    }
   },
   "actions": {
     "refresh": "Refresh",
@@ -187,6 +201,36 @@
       "taxableAmount": "Taxable Amount"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Select a vehicle...",
+    "selectVehiclePrompt": "Select a vehicle above to view its report.",
+    "totalServices": "Total Services",
+    "totalCost": "Total Cost",
+    "repairs": "Repairs",
+    "maintenance": "Maintenance",
+    "upgrades": "Upgrades",
+    "inspections": "Inspections",
+    "totalPartsUsed": "Parts Used",
+    "totalLaborHours": "Labor Hours",
+    "monthlyCosts": "Monthly Costs",
+    "serviceTypeBreakdown": "Service Type Breakdown",
+    "topParts": "Top Parts Used",
+    "serviceHistory": "Service History",
+    "tableHeaders": {
+      "date": "Date",
+      "title": "Title",
+      "type": "Type",
+      "status": "Status",
+      "totalAmount": "Total",
+      "partsCount": "Parts",
+      "laborHours": "Labor Hours",
+      "techName": "Technician",
+      "partName": "Part Name",
+      "partNumber": "Part #",
+      "quantity": "Qty",
+      "cost": "Cost"
+    }
+  },
   "charts": {
     "revenue": "Revenue",
     "collected": "Collected",
@@ -194,7 +238,9 @@
     "totalSpent": "Total Spent",
     "jobs": "Jobs",
     "visits": "Visits",
-    "taxCollected": "Tax Collected"
+    "taxCollected": "Tax Collected",
+    "partsCost": "Parts Cost",
+    "laborCost": "Labor Cost"
   },
   "days": {
     "sunday": "Sunday",
@@ -255,7 +301,13 @@
     "partsCost": "Parts Cost",
     "partsNetProfit": "Parts Net Profit",
     "laborRevenue": "Labor",
-    "netProfit": "Net Profit"
+    "netProfit": "Net Profit",
+    "date": "Date",
+    "title": "Title",
+    "type": "Type",
+    "status": "Status",
+    "partsUsed": "Parts",
+    "laborHours": "Labor Hours"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,15 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Vehicle Report",
+    "vehicleLabel": "Vehicle",
+    "customer": "Customer",
+    "totalCost": "Total Cost",
+    "partsCostLabel": "Parts Cost",
+    "laborCostLabel": "Labor Cost",
+    "date": "Date",
+    "title": "Title"
   },
   "empty": "No data loaded yet. Click Refresh to load the report."
 }

+ 64 - 5
messages/es/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Técnicos",
     "parts": "Piezas",
     "jobAnalytics": "Análisis de Trabajos",
-    "retention": "Retención"
+    "retention": "Retención",
+    "vehicles": "Vehículos"
   },
   "dateRange": {
     "to": "a",
     "pickDate": "Seleccionar rango de fechas",
-    "apply": "Aplicar"
+    "apply": "Aplicar",
+    "presets": {
+      "today": "Hoy",
+      "yesterday": "Ayer",
+      "last7": "Últimos 7 días",
+      "last30": "Últimos 30 días",
+      "last90": "Últimos 90 días",
+      "thisMonth": "Este mes",
+      "lastMonth": "Mes pasado",
+      "thisQuarter": "Este trimestre",
+      "thisYear": "Este año",
+      "lastYear": "Año pasado",
+      "allTime": "Todo el período"
+    }
   },
   "actions": {
     "refresh": "Actualizar",
@@ -187,6 +201,36 @@
       "taxableAmount": "Monto Gravable"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Seleccionar un vehículo...",
+    "selectVehiclePrompt": "Seleccione un vehículo arriba para ver su informe.",
+    "totalServices": "Total de Servicios",
+    "totalCost": "Costo Total",
+    "repairs": "Reparaciones",
+    "maintenance": "Mantenimiento",
+    "upgrades": "Mejoras",
+    "inspections": "Inspecciones",
+    "totalPartsUsed": "Piezas Utilizadas",
+    "totalLaborHours": "Horas de Mano de Obra",
+    "monthlyCosts": "Costos Mensuales",
+    "serviceTypeBreakdown": "Desglose por Tipo de Servicio",
+    "topParts": "Piezas Más Utilizadas",
+    "serviceHistory": "Historial de Servicios",
+    "tableHeaders": {
+      "date": "Fecha",
+      "title": "Título",
+      "type": "Tipo",
+      "status": "Estado",
+      "totalAmount": "Total",
+      "partsCount": "Piezas",
+      "laborHours": "Horas de Mano de Obra",
+      "techName": "Técnico",
+      "partName": "Nombre de Pieza",
+      "partNumber": "N.º Pieza",
+      "quantity": "Cant.",
+      "cost": "Costo"
+    }
+  },
   "charts": {
     "revenue": "Ingresos",
     "collected": "Cobrado",
@@ -194,7 +238,9 @@
     "totalSpent": "Gasto Total",
     "jobs": "Trabajos",
     "visits": "Visitas",
-    "taxCollected": "Impuesto Recaudado"
+    "taxCollected": "Impuesto Recaudado",
+    "partsCost": "Coste de Piezas",
+    "laborCost": "Costo de Mano de Obra"
   },
   "days": {
     "sunday": "Domingo",
@@ -255,7 +301,13 @@
     "partsCost": "Coste de piezas",
     "partsNetProfit": "Beneficio Neto Repuestos",
     "laborRevenue": "Mano de Obra",
-    "netProfit": "Beneficio neto"
+    "netProfit": "Beneficio neto",
+    "date": "Fecha",
+    "title": "Título",
+    "type": "Tipo",
+    "status": "Estado",
+    "partsUsed": "Piezas",
+    "laborHours": "Horas de Mano de Obra"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Informe de Vehículo",
+    "vehicleLabel": "Vehículo",
+    "totalCost": "Costo Total",
+    "partsCostLabel": "Coste de Piezas",
+    "laborCostLabel": "Costo de Mano de Obra",
+    "date": "Fecha",
+    "title": "Título"
   },
   "empty": "Aún no se han cargado datos. Haga clic en Actualizar para cargar el informe."
 }

+ 64 - 5
messages/fr/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Techniciens",
     "parts": "Pièces",
     "jobAnalytics": "Analyse des Travaux",
-    "retention": "Fidélisation"
+    "retention": "Fidélisation",
+    "vehicles": "Véhicules"
   },
   "dateRange": {
     "to": "au",
     "pickDate": "Choisir une plage de dates",
-    "apply": "Appliquer"
+    "apply": "Appliquer",
+    "presets": {
+      "today": "Aujourd'hui",
+      "yesterday": "Hier",
+      "last7": "7 derniers jours",
+      "last30": "30 derniers jours",
+      "last90": "90 derniers jours",
+      "thisMonth": "Ce mois-ci",
+      "lastMonth": "Mois dernier",
+      "thisQuarter": "Ce trimestre",
+      "thisYear": "Cette année",
+      "lastYear": "Année dernière",
+      "allTime": "Toute la période"
+    }
   },
   "actions": {
     "refresh": "Actualiser",
@@ -187,6 +201,36 @@
       "taxableAmount": "Montant Taxable"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Sélectionner un véhicule...",
+    "selectVehiclePrompt": "Sélectionnez un véhicule ci-dessus pour afficher son rapport.",
+    "totalServices": "Total des Services",
+    "totalCost": "Coût Total",
+    "repairs": "Réparations",
+    "maintenance": "Entretien",
+    "upgrades": "Améliorations",
+    "inspections": "Inspections",
+    "totalPartsUsed": "Pièces Utilisées",
+    "totalLaborHours": "Heures de Main-d'œuvre",
+    "monthlyCosts": "Coûts Mensuels",
+    "serviceTypeBreakdown": "Répartition par Type de Service",
+    "topParts": "Pièces les Plus Utilisées",
+    "serviceHistory": "Historique des Services",
+    "tableHeaders": {
+      "date": "Date",
+      "title": "Titre",
+      "type": "Type",
+      "status": "Statut",
+      "totalAmount": "Total",
+      "partsCount": "Pièces",
+      "laborHours": "Heures de Main-d'œuvre",
+      "techName": "Technicien",
+      "partName": "Nom de Pièce",
+      "partNumber": "N° Pièce",
+      "quantity": "Qté",
+      "cost": "Coût"
+    }
+  },
   "charts": {
     "revenue": "Revenus",
     "collected": "Encaissé",
@@ -194,7 +238,9 @@
     "totalSpent": "Total Dépensé",
     "jobs": "Travaux",
     "visits": "Visites",
-    "taxCollected": "Taxe Perçue"
+    "taxCollected": "Taxe Perçue",
+    "partsCost": "Coût des Pièces",
+    "laborCost": "Coût de Main-d'œuvre"
   },
   "days": {
     "sunday": "Dimanche",
@@ -255,7 +301,13 @@
     "partsCost": "Coût des pièces",
     "partsNetProfit": "Bénéfice Net Pièces",
     "laborRevenue": "Main-d'œuvre",
-    "netProfit": "Bénéfice net"
+    "netProfit": "Bénéfice net",
+    "date": "Date",
+    "title": "Titre",
+    "type": "Type",
+    "status": "Statut",
+    "partsUsed": "Pièces",
+    "laborHours": "Heures de Main-d'œuvre"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Rapport Véhicule",
+    "vehicleLabel": "Véhicule",
+    "totalCost": "Coût Total",
+    "partsCostLabel": "Coût des Pièces",
+    "laborCostLabel": "Coût de Main-d'œuvre",
+    "date": "Date",
+    "title": "Titre"
   },
   "empty": "Aucune donnée chargée. Cliquez sur Actualiser pour charger le rapport."
 }

+ 64 - 5
messages/it/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Tecnici",
     "parts": "Ricambi",
     "jobAnalytics": "Analisi Lavori",
-    "retention": "Fidelizzazione"
+    "retention": "Fidelizzazione",
+    "vehicles": "Veicoli"
   },
   "dateRange": {
     "to": "a",
     "pickDate": "Seleziona intervallo date",
-    "apply": "Applica"
+    "apply": "Applica",
+    "presets": {
+      "today": "Oggi",
+      "yesterday": "Ieri",
+      "last7": "Ultimi 7 giorni",
+      "last30": "Ultimi 30 giorni",
+      "last90": "Ultimi 90 giorni",
+      "thisMonth": "Questo mese",
+      "lastMonth": "Mese scorso",
+      "thisQuarter": "Questo trimestre",
+      "thisYear": "Quest'anno",
+      "lastYear": "Anno scorso",
+      "allTime": "Tutto il periodo"
+    }
   },
   "actions": {
     "refresh": "Aggiorna",
@@ -187,6 +201,36 @@
       "taxableAmount": "Importo Tassabile"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Seleziona un veicolo...",
+    "selectVehiclePrompt": "Seleziona un veicolo sopra per visualizzare il report.",
+    "totalServices": "Totale Servizi",
+    "totalCost": "Costo Totale",
+    "repairs": "Riparazioni",
+    "maintenance": "Manutenzione",
+    "upgrades": "Upgrade",
+    "inspections": "Ispezioni",
+    "totalPartsUsed": "Ricambi Utilizzati",
+    "totalLaborHours": "Ore di Manodopera",
+    "monthlyCosts": "Costi Mensili",
+    "serviceTypeBreakdown": "Riepilogo Tipi di Servizio",
+    "topParts": "Ricambi Più Utilizzati",
+    "serviceHistory": "Storico Servizi",
+    "tableHeaders": {
+      "date": "Data",
+      "title": "Titolo",
+      "type": "Tipo",
+      "status": "Stato",
+      "totalAmount": "Totale",
+      "partsCount": "Ricambi",
+      "laborHours": "Ore di Manodopera",
+      "techName": "Tecnico",
+      "partName": "Nome Ricambio",
+      "partNumber": "N. Ricambio",
+      "quantity": "Qtà",
+      "cost": "Costo"
+    }
+  },
   "charts": {
     "revenue": "Ricavi",
     "collected": "Incassato",
@@ -194,7 +238,9 @@
     "totalSpent": "Totale Speso",
     "jobs": "Lavori",
     "visits": "Visite",
-    "taxCollected": "Tassa Riscossa"
+    "taxCollected": "Tassa Riscossa",
+    "partsCost": "Costo Ricambi",
+    "laborCost": "Costo Manodopera"
   },
   "days": {
     "sunday": "Domenica",
@@ -255,7 +301,13 @@
     "partsCost": "Costo ricambi",
     "partsNetProfit": "Profitto Netto Ricambi",
     "laborRevenue": "Manodopera",
-    "netProfit": "Utile netto"
+    "netProfit": "Utile netto",
+    "date": "Data",
+    "title": "Titolo",
+    "type": "Tipo",
+    "status": "Stato",
+    "partsUsed": "Ricambi",
+    "laborHours": "Ore di Manodopera"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Report Veicolo",
+    "vehicleLabel": "Veicolo",
+    "totalCost": "Costo Totale",
+    "partsCostLabel": "Costo Ricambi",
+    "laborCostLabel": "Costo Manodopera",
+    "date": "Data",
+    "title": "Titolo"
   },
   "empty": "Nessun dato caricato. Clicca su Aggiorna per caricare il report."
 }

+ 64 - 5
messages/lt/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Technikai",
     "parts": "Dalys",
     "jobAnalytics": "Darbų analitika",
-    "retention": "Grįžtamumas"
+    "retention": "Grįžtamumas",
+    "vehicles": "Transporto priemonės"
   },
   "dateRange": {
     "to": "iki",
     "pickDate": "Pasirinkite datų intervalą",
-    "apply": "Taikyti"
+    "apply": "Taikyti",
+    "presets": {
+      "today": "Šiandien",
+      "yesterday": "Vakar",
+      "last7": "Paskutinės 7 dienos",
+      "last30": "Paskutinės 30 dienų",
+      "last90": "Paskutinės 90 dienų",
+      "thisMonth": "Šis mėnuo",
+      "lastMonth": "Praėjęs mėnuo",
+      "thisQuarter": "Šis ketvirtis",
+      "thisYear": "Šie metai",
+      "lastYear": "Praėję metai",
+      "allTime": "Visas laikotarpis"
+    }
   },
   "actions": {
     "refresh": "Atnaujinti",
@@ -187,6 +201,36 @@
       "taxableAmount": "Apmokestinama suma"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Pasirinkite transporto priemonę...",
+    "selectVehiclePrompt": "Pasirinkite transporto priemonę aukščiau, kad peržiūrėtumėte ataskaitą.",
+    "totalServices": "Iš viso paslaugų",
+    "totalCost": "Bendra savikaina",
+    "repairs": "Remontai",
+    "maintenance": "Priežiūra",
+    "upgrades": "Patobulinimai",
+    "inspections": "Apžiūros",
+    "totalPartsUsed": "Panaudotos dalys",
+    "totalLaborHours": "Darbo valandos",
+    "monthlyCosts": "Mėnesio išlaidos",
+    "serviceTypeBreakdown": "Paslaugų tipų suskirstymas",
+    "topParts": "Populiariausios dalys",
+    "serviceHistory": "Paslaugų istorija",
+    "tableHeaders": {
+      "date": "Data",
+      "title": "Pavadinimas",
+      "type": "Tipas",
+      "status": "Būsena",
+      "totalAmount": "Suma",
+      "partsCount": "Dalys",
+      "laborHours": "Darbo valandos",
+      "techName": "Technikas",
+      "partName": "Dalies pavadinimas",
+      "partNumber": "Dalies Nr.",
+      "quantity": "Kiekis",
+      "cost": "Savikaina"
+    }
+  },
   "charts": {
     "revenue": "Pajamos",
     "collected": "Surinkta",
@@ -194,7 +238,9 @@
     "totalSpent": "Iš viso išleista",
     "jobs": "Darbai",
     "visits": "Apsilankymai",
-    "taxCollected": "Surinkti mokesčiai"
+    "taxCollected": "Surinkti mokesčiai",
+    "partsCost": "Dalių savikaina",
+    "laborCost": "Darbo savikaina"
   },
   "days": {
     "sunday": "Sekmadienis",
@@ -255,7 +301,13 @@
     "partsCost": "Dalių savikaina",
     "partsNetProfit": "Dalių grynasis pelnas",
     "laborRevenue": "Darbai",
-    "netProfit": "Grynasis pelnas"
+    "netProfit": "Grynasis pelnas",
+    "date": "Data",
+    "title": "Pavadinimas",
+    "type": "Tipas",
+    "status": "Būsena",
+    "partsUsed": "Dalys",
+    "laborHours": "Darbo valandos"
   },
   "pdf": {
     "reportTitle": "Verslo ataskaita",
@@ -345,7 +397,14 @@
     "totalSellValue": "Pardavimo vertė",
     "quantity": "Kiekis",
     "minQuantity": "Min. kiekis",
-    "unitCost": "Vnt. savikaina"
+    "unitCost": "Vnt. savikaina",
+    "vehiclesSection": "Transporto priemonės ataskaita",
+    "vehicleLabel": "Transporto priemonė",
+    "totalCost": "Bendra savikaina",
+    "partsCostLabel": "Dalių savikaina",
+    "laborCostLabel": "Darbo savikaina",
+    "date": "Data",
+    "title": "Pavadinimas"
   },
   "empty": "Duomenys dar neįkelti. Spustelėkite \"Atnaujinti\", kad įkeltumėte ataskaitą."
 }

+ 64 - 5
messages/nb/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Teknikere",
     "parts": "Deler",
     "jobAnalytics": "Jobbanalyse",
-    "retention": "Kundelojalitet"
+    "retention": "Kundelojalitet",
+    "vehicles": "Kjøretøy"
   },
   "dateRange": {
     "to": "til",
     "pickDate": "Velg datoperiode",
-    "apply": "Bruk"
+    "apply": "Bruk",
+    "presets": {
+      "today": "I dag",
+      "yesterday": "I går",
+      "last7": "Siste 7 dager",
+      "last30": "Siste 30 dager",
+      "last90": "Siste 90 dager",
+      "thisMonth": "Denne måneden",
+      "lastMonth": "Forrige måned",
+      "thisQuarter": "Dette kvartalet",
+      "thisYear": "I år",
+      "lastYear": "I fjor",
+      "allTime": "Hele perioden"
+    }
   },
   "actions": {
     "refresh": "Oppdater",
@@ -187,6 +201,36 @@
       "taxableAmount": "Skattepliktig Beløp"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Velg et kjøretøy...",
+    "selectVehiclePrompt": "Velg et kjøretøy ovenfor for å se rapporten.",
+    "totalServices": "Totalt Tjenester",
+    "totalCost": "Total Kostnad",
+    "repairs": "Reparasjoner",
+    "maintenance": "Vedlikehold",
+    "upgrades": "Oppgraderinger",
+    "inspections": "Inspeksjoner",
+    "totalPartsUsed": "Deler Brukt",
+    "totalLaborHours": "Arbeidstimer",
+    "monthlyCosts": "Månedlige Kostnader",
+    "serviceTypeBreakdown": "Tjenestetype-oversikt",
+    "topParts": "Mest Brukte Deler",
+    "serviceHistory": "Tjenestehistorikk",
+    "tableHeaders": {
+      "date": "Dato",
+      "title": "Tittel",
+      "type": "Type",
+      "status": "Status",
+      "totalAmount": "Totalt",
+      "partsCount": "Deler",
+      "laborHours": "Arbeidstimer",
+      "techName": "Tekniker",
+      "partName": "Delenavn",
+      "partNumber": "Delenr.",
+      "quantity": "Ant.",
+      "cost": "Kostnad"
+    }
+  },
   "charts": {
     "revenue": "Inntekter",
     "collected": "Innkrevd",
@@ -194,7 +238,9 @@
     "totalSpent": "Totalt Brukt",
     "jobs": "Jobber",
     "visits": "Besøk",
-    "taxCollected": "Skatt Innkrevd"
+    "taxCollected": "Skatt Innkrevd",
+    "partsCost": "Delekostnad",
+    "laborCost": "Arbeidskostnad"
   },
   "days": {
     "sunday": "Søndag",
@@ -255,7 +301,13 @@
     "partsCost": "Delekostnad",
     "partsNetProfit": "Netto fortjeneste deler",
     "laborRevenue": "Arbeid",
-    "netProfit": "Nettoresultat"
+    "netProfit": "Nettoresultat",
+    "date": "Dato",
+    "title": "Tittel",
+    "type": "Type",
+    "status": "Status",
+    "partsUsed": "Deler",
+    "laborHours": "Arbeidstimer"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Kjøretøyrapport",
+    "vehicleLabel": "Kjøretøy",
+    "totalCost": "Total Kostnad",
+    "partsCostLabel": "Delekostnad",
+    "laborCostLabel": "Arbeidskostnad",
+    "date": "Dato",
+    "title": "Tittel"
   },
   "empty": "Ingen data lastet ennå. Klikk Oppdater for å laste rapporten."
 }

+ 64 - 5
messages/nl/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Technici",
     "parts": "Onderdelen",
     "jobAnalytics": "Opdrachtanalyse",
-    "retention": "Klantbehoud"
+    "retention": "Klantbehoud",
+    "vehicles": "Voertuigen"
   },
   "dateRange": {
     "to": "tot",
     "pickDate": "Kies een datumbereik",
-    "apply": "Toepassen"
+    "apply": "Toepassen",
+    "presets": {
+      "today": "Vandaag",
+      "yesterday": "Gisteren",
+      "last7": "Laatste 7 dagen",
+      "last30": "Laatste 30 dagen",
+      "last90": "Laatste 90 dagen",
+      "thisMonth": "Deze maand",
+      "lastMonth": "Vorige maand",
+      "thisQuarter": "Dit kwartaal",
+      "thisYear": "Dit jaar",
+      "lastYear": "Vorig jaar",
+      "allTime": "Gehele periode"
+    }
   },
   "actions": {
     "refresh": "Vernieuwen",
@@ -187,6 +201,36 @@
       "taxableAmount": "Belastbaar Bedrag"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Selecteer een voertuig...",
+    "selectVehiclePrompt": "Selecteer hierboven een voertuig om het rapport te bekijken.",
+    "totalServices": "Totaal Diensten",
+    "totalCost": "Totale Kosten",
+    "repairs": "Reparaties",
+    "maintenance": "Onderhoud",
+    "upgrades": "Upgrades",
+    "inspections": "Inspecties",
+    "totalPartsUsed": "Gebruikte Onderdelen",
+    "totalLaborHours": "Arbeidsuren",
+    "monthlyCosts": "Maandelijkse Kosten",
+    "serviceTypeBreakdown": "Diensttype Overzicht",
+    "topParts": "Meest Gebruikte Onderdelen",
+    "serviceHistory": "Servicegeschiedenis",
+    "tableHeaders": {
+      "date": "Datum",
+      "title": "Titel",
+      "type": "Type",
+      "status": "Status",
+      "totalAmount": "Totaal",
+      "partsCount": "Onderdelen",
+      "laborHours": "Arbeidsuren",
+      "techName": "Technicus",
+      "partName": "Onderdeelnaam",
+      "partNumber": "Onderdeelnr.",
+      "quantity": "Aant.",
+      "cost": "Kosten"
+    }
+  },
   "charts": {
     "revenue": "Omzet",
     "collected": "Geïnd",
@@ -194,7 +238,9 @@
     "totalSpent": "Totaal Besteed",
     "jobs": "Opdrachten",
     "visits": "Bezoeken",
-    "taxCollected": "Geïnde Belasting"
+    "taxCollected": "Geïnde Belasting",
+    "partsCost": "Onderdeelkosten",
+    "laborCost": "Arbeidskosten"
   },
   "days": {
     "sunday": "Zondag",
@@ -255,7 +301,13 @@
     "partsCost": "Onderdeelkosten",
     "partsNetProfit": "Nettowinst Onderdelen",
     "laborRevenue": "Arbeid",
-    "netProfit": "Nettowinst"
+    "netProfit": "Nettowinst",
+    "date": "Datum",
+    "title": "Titel",
+    "type": "Type",
+    "status": "Status",
+    "partsUsed": "Onderdelen",
+    "laborHours": "Arbeidsuren"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Voertuigrapport",
+    "vehicleLabel": "Voertuig",
+    "totalCost": "Totale Kosten",
+    "partsCostLabel": "Onderdeelkosten",
+    "laborCostLabel": "Arbeidskosten",
+    "date": "Datum",
+    "title": "Titel"
   },
   "empty": "Nog geen gegevens geladen. Klik op Vernieuwen om het rapport te laden."
 }

+ 64 - 5
messages/pl/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Technicy",
     "parts": "Części",
     "jobAnalytics": "Analiza Zleceń",
-    "retention": "Utrzymanie Klientów"
+    "retention": "Utrzymanie Klientów",
+    "vehicles": "Pojazdy"
   },
   "dateRange": {
     "to": "do",
     "pickDate": "Wybierz zakres dat",
-    "apply": "Zastosuj"
+    "apply": "Zastosuj",
+    "presets": {
+      "today": "Dzisiaj",
+      "yesterday": "Wczoraj",
+      "last7": "Ostatnie 7 dni",
+      "last30": "Ostatnie 30 dni",
+      "last90": "Ostatnie 90 dni",
+      "thisMonth": "Ten miesiąc",
+      "lastMonth": "Poprzedni miesiąc",
+      "thisQuarter": "Ten kwartał",
+      "thisYear": "Ten rok",
+      "lastYear": "Poprzedni rok",
+      "allTime": "Cały okres"
+    }
   },
   "actions": {
     "refresh": "Odśwież",
@@ -187,6 +201,36 @@
       "taxableAmount": "Kwota Opodatkowana"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Wybierz pojazd...",
+    "selectVehiclePrompt": "Wybierz pojazd powyżej, aby zobaczyć raport.",
+    "totalServices": "Łączna Liczba Usług",
+    "totalCost": "Łączny Koszt",
+    "repairs": "Naprawy",
+    "maintenance": "Konserwacja",
+    "upgrades": "Ulepszenia",
+    "inspections": "Przeglądy",
+    "totalPartsUsed": "Użyte Części",
+    "totalLaborHours": "Godziny Robocizny",
+    "monthlyCosts": "Koszty Miesięczne",
+    "serviceTypeBreakdown": "Zestawienie Typów Usług",
+    "topParts": "Najczęściej Używane Części",
+    "serviceHistory": "Historia Usług",
+    "tableHeaders": {
+      "date": "Data",
+      "title": "Tytuł",
+      "type": "Typ",
+      "status": "Status",
+      "totalAmount": "Suma",
+      "partsCount": "Części",
+      "laborHours": "Godziny Robocizny",
+      "techName": "Technik",
+      "partName": "Nazwa Części",
+      "partNumber": "Nr Części",
+      "quantity": "Ilość",
+      "cost": "Koszt"
+    }
+  },
   "charts": {
     "revenue": "Przychody",
     "collected": "Zebrano",
@@ -194,7 +238,9 @@
     "totalSpent": "Suma Wydatków",
     "jobs": "Zlecenia",
     "visits": "Wizyty",
-    "taxCollected": "Zebrany Podatek"
+    "taxCollected": "Zebrany Podatek",
+    "partsCost": "Koszt Części",
+    "laborCost": "Koszt Robocizny"
   },
   "days": {
     "sunday": "Niedziela",
@@ -255,7 +301,13 @@
     "partsCost": "Koszt części",
     "partsNetProfit": "Zysk Netto z Części",
     "laborRevenue": "Robocizna",
-    "netProfit": "Zysk netto"
+    "netProfit": "Zysk netto",
+    "date": "Data",
+    "title": "Tytuł",
+    "type": "Typ",
+    "status": "Status",
+    "partsUsed": "Części",
+    "laborHours": "Godziny Robocizny"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Raport Pojazdu",
+    "vehicleLabel": "Pojazd",
+    "totalCost": "Łączny Koszt",
+    "partsCostLabel": "Koszt Części",
+    "laborCostLabel": "Koszt Robocizny",
+    "date": "Data",
+    "title": "Tytuł"
   },
   "empty": "Nie załadowano jeszcze danych. Kliknij Odśwież, aby załadować raport."
 }

+ 64 - 5
messages/pt-BR/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Técnicos",
     "parts": "Peças",
     "jobAnalytics": "Análise de Trabalhos",
-    "retention": "Retenção"
+    "retention": "Retenção",
+    "vehicles": "Veículos"
   },
   "dateRange": {
     "to": "até",
     "pickDate": "Selecionar período",
-    "apply": "Aplicar"
+    "apply": "Aplicar",
+    "presets": {
+      "today": "Hoje",
+      "yesterday": "Ontem",
+      "last7": "Últimos 7 dias",
+      "last30": "Últimos 30 dias",
+      "last90": "Últimos 90 dias",
+      "thisMonth": "Este mês",
+      "lastMonth": "Mês passado",
+      "thisQuarter": "Este trimestre",
+      "thisYear": "Este ano",
+      "lastYear": "Ano passado",
+      "allTime": "Todo o período"
+    }
   },
   "actions": {
     "refresh": "Atualizar",
@@ -187,6 +201,36 @@
       "taxableAmount": "Valor Tributável"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Selecionar um veículo...",
+    "selectVehiclePrompt": "Selecione um veículo acima para ver o relatório.",
+    "totalServices": "Total de Serviços",
+    "totalCost": "Custo Total",
+    "repairs": "Reparos",
+    "maintenance": "Manutenção",
+    "upgrades": "Melhorias",
+    "inspections": "Inspeções",
+    "totalPartsUsed": "Peças Usadas",
+    "totalLaborHours": "Horas de Mão de Obra",
+    "monthlyCosts": "Custos Mensais",
+    "serviceTypeBreakdown": "Detalhamento por Tipo de Serviço",
+    "topParts": "Peças Mais Usadas",
+    "serviceHistory": "Histórico de Serviços",
+    "tableHeaders": {
+      "date": "Data",
+      "title": "Título",
+      "type": "Tipo",
+      "status": "Status",
+      "totalAmount": "Total",
+      "partsCount": "Peças",
+      "laborHours": "Horas de Mão de Obra",
+      "techName": "Técnico",
+      "partName": "Nome da Peça",
+      "partNumber": "N.º Peça",
+      "quantity": "Qtd.",
+      "cost": "Custo"
+    }
+  },
   "charts": {
     "revenue": "Receita",
     "collected": "Recebido",
@@ -194,7 +238,9 @@
     "totalSpent": "Total Gasto",
     "jobs": "Trabalhos",
     "visits": "Visitas",
-    "taxCollected": "Imposto Arrecadado"
+    "taxCollected": "Imposto Arrecadado",
+    "partsCost": "Custo de Peças",
+    "laborCost": "Custo de Mão de Obra"
   },
   "days": {
     "sunday": "Domingo",
@@ -255,7 +301,13 @@
     "partsCost": "Custo de peças",
     "partsNetProfit": "Lucro Líquido Peças",
     "laborRevenue": "Mão de Obra",
-    "netProfit": "Lucro líquido"
+    "netProfit": "Lucro líquido",
+    "date": "Data",
+    "title": "Título",
+    "type": "Tipo",
+    "status": "Status",
+    "partsUsed": "Peças",
+    "laborHours": "Horas de Mão de Obra"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Relatório de Veículo",
+    "vehicleLabel": "Veículo",
+    "totalCost": "Custo Total",
+    "partsCostLabel": "Custo de Peças",
+    "laborCostLabel": "Custo de Mão de Obra",
+    "date": "Data",
+    "title": "Título"
   },
   "empty": "Nenhum dado carregado ainda. Clique em Atualizar para carregar o relatório."
 }

+ 64 - 5
messages/ru/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Механики",
     "parts": "Запчасти",
     "jobAnalytics": "Аналитика работ",
-    "retention": "Удержание"
+    "retention": "Удержание",
+    "vehicles": "Транспорт"
   },
   "dateRange": {
     "to": "до",
     "pickDate": "Выбрать диапазон дат",
-    "apply": "Применить"
+    "apply": "Применить",
+    "presets": {
+      "today": "Сегодня",
+      "yesterday": "Вчера",
+      "last7": "Последние 7 дней",
+      "last30": "Последние 30 дней",
+      "last90": "Последние 90 дней",
+      "thisMonth": "Этот месяц",
+      "lastMonth": "Прошлый месяц",
+      "thisQuarter": "Этот квартал",
+      "thisYear": "Этот год",
+      "lastYear": "Прошлый год",
+      "allTime": "За всё время"
+    }
   },
   "actions": {
     "refresh": "Обновить",
@@ -187,6 +201,36 @@
       "taxableAmount": "Налогооблагаемая сумма"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Выберите транспорт...",
+    "selectVehiclePrompt": "Выберите транспорт выше для просмотра отчёта.",
+    "totalServices": "Всего обслуживаний",
+    "totalCost": "Общая стоимость",
+    "repairs": "Ремонт",
+    "maintenance": "Техобслуживание",
+    "upgrades": "Модернизация",
+    "inspections": "Осмотры",
+    "totalPartsUsed": "Использовано запчастей",
+    "totalLaborHours": "Часы работы",
+    "monthlyCosts": "Ежемесячные расходы",
+    "serviceTypeBreakdown": "Разбивка по типам обслуживания",
+    "topParts": "Топ запчастей по использованию",
+    "serviceHistory": "История обслуживания",
+    "tableHeaders": {
+      "date": "Дата",
+      "title": "Название",
+      "type": "Тип",
+      "status": "Статус",
+      "totalAmount": "Итого",
+      "partsCount": "Запчасти",
+      "laborHours": "Часы работы",
+      "techName": "Механик",
+      "partName": "Название запчасти",
+      "partNumber": "Арт. №",
+      "quantity": "Кол-во",
+      "cost": "Стоимость"
+    }
+  },
   "charts": {
     "revenue": "Доход",
     "collected": "Собрано",
@@ -194,7 +238,9 @@
     "totalSpent": "Всего потрачено",
     "jobs": "Работы",
     "visits": "Визиты",
-    "taxCollected": "Собрано налога"
+    "taxCollected": "Собрано налога",
+    "partsCost": "Стоимость запчастей",
+    "laborCost": "Стоимость работ"
   },
   "days": {
     "monday": "Понедельник",
@@ -255,7 +301,13 @@
     "partsCost": "Стоимость запчастей",
     "partsNetProfit": "Чистая прибыль запчасти",
     "laborRevenue": "Работа",
-    "netProfit": "Чистая прибыль"
+    "netProfit": "Чистая прибыль",
+    "date": "Дата",
+    "title": "Название",
+    "type": "Тип",
+    "status": "Статус",
+    "partsUsed": "Запчасти",
+    "laborHours": "Часы работы"
   },
   "pdf": {
     "reportTitle": "Бизнес-отчёт",
@@ -345,7 +397,14 @@
     "totalSellValue": "Цена продажи",
     "quantity": "Кол-во",
     "minQuantity": "Мин. кол-во",
-    "unitCost": "Цена за ед."
+    "unitCost": "Цена за ед.",
+    "vehiclesSection": "Отчёт по транспорту",
+    "vehicleLabel": "Транспорт",
+    "totalCost": "Общая стоимость",
+    "partsCostLabel": "Стоимость запчастей",
+    "laborCostLabel": "Стоимость работ",
+    "date": "Дата",
+    "title": "Название"
   },
   "empty": "Данные ещё не загружены. Нажмите «Обновить» для загрузки отчёта."
 }

+ 64 - 5
messages/tr/reports.json

@@ -11,12 +11,26 @@
     "technicians": "Teknisyenler",
     "parts": "Parçalar",
     "jobAnalytics": "İş Analizi",
-    "retention": "Müşteri Sadakati"
+    "retention": "Müşteri Sadakati",
+    "vehicles": "Araçlar"
   },
   "dateRange": {
     "to": "-",
     "pickDate": "Tarih aralığı seçin",
-    "apply": "Uygula"
+    "apply": "Uygula",
+    "presets": {
+      "today": "Bugün",
+      "yesterday": "Dün",
+      "last7": "Son 7 gün",
+      "last30": "Son 30 gün",
+      "last90": "Son 90 gün",
+      "thisMonth": "Bu ay",
+      "lastMonth": "Geçen ay",
+      "thisQuarter": "Bu çeyrek",
+      "thisYear": "Bu yıl",
+      "lastYear": "Geçen yıl",
+      "allTime": "Tüm zamanlar"
+    }
   },
   "actions": {
     "refresh": "Yenile",
@@ -187,6 +201,36 @@
       "taxableAmount": "Vergilendirilebilir Tutar"
     }
   },
+  "vehicles": {
+    "selectVehicle": "Bir araç seçin...",
+    "selectVehiclePrompt": "Raporunu görüntülemek için yukarıdan bir araç seçin.",
+    "totalServices": "Toplam Hizmetler",
+    "totalCost": "Toplam Maliyet",
+    "repairs": "Onarımlar",
+    "maintenance": "Bakım",
+    "upgrades": "Yükseltmeler",
+    "inspections": "Muayeneler",
+    "totalPartsUsed": "Kullanılan Parçalar",
+    "totalLaborHours": "İşçilik Saatleri",
+    "monthlyCosts": "Aylık Maliyetler",
+    "serviceTypeBreakdown": "Hizmet Türü Dökümü",
+    "topParts": "En Çok Kullanılan Parçalar",
+    "serviceHistory": "Servis Geçmişi",
+    "tableHeaders": {
+      "date": "Tarih",
+      "title": "Başlık",
+      "type": "Tür",
+      "status": "Durum",
+      "totalAmount": "Toplam",
+      "partsCount": "Parçalar",
+      "laborHours": "İşçilik Saatleri",
+      "techName": "Teknisyen",
+      "partName": "Parça Adı",
+      "partNumber": "Parça No",
+      "quantity": "Adet",
+      "cost": "Maliyet"
+    }
+  },
   "charts": {
     "revenue": "Gelir",
     "collected": "Tahsil Edilen",
@@ -194,7 +238,9 @@
     "totalSpent": "Toplam Harcama",
     "jobs": "İşler",
     "visits": "Ziyaretler",
-    "taxCollected": "Tahsil Edilen Vergi"
+    "taxCollected": "Tahsil Edilen Vergi",
+    "partsCost": "Parça Maliyeti",
+    "laborCost": "İşçilik Maliyeti"
   },
   "days": {
     "sunday": "Pazar",
@@ -255,7 +301,13 @@
     "partsCost": "Parça maliyeti",
     "partsNetProfit": "Parça Net Kârı",
     "laborRevenue": "İşçilik",
-    "netProfit": "Net kâr"
+    "netProfit": "Net kâr",
+    "date": "Tarih",
+    "title": "Başlık",
+    "type": "Tür",
+    "status": "Durum",
+    "partsUsed": "Parçalar",
+    "laborHours": "İşçilik Saatleri"
   },
   "pdf": {
     "reportTitle": "Business Report",
@@ -345,7 +397,14 @@
     "totalSellValue": "Sell Value",
     "quantity": "Qty",
     "minQuantity": "Min Qty",
-    "unitCost": "Unit Cost"
+    "unitCost": "Unit Cost",
+    "vehiclesSection": "Araç Raporu",
+    "vehicleLabel": "Araç",
+    "totalCost": "Toplam Maliyet",
+    "partsCostLabel": "Parça Maliyeti",
+    "laborCostLabel": "İşçilik Maliyeti",
+    "date": "Tarih",
+    "title": "Başlık"
   },
   "empty": "Henüz veri yüklenmedi. Raporu yüklemek için Yenile'ye tıklayın."
 }

+ 2 - 1
src/app/(authenticated)/reports/page.tsx

@@ -47,12 +47,13 @@ export default async function ReportsPage() {
   const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
   const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
   const primaryColor = settings[SETTING_KEYS.INVOICE_PRIMARY_COLOR] || "#d97706";
+  const organizationName = data.organizations.find((o) => o.id === data.organizationId)?.name ?? "";
 
   return (
     <>
       <PageHeader />
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
-        <ReportsClient currencyCode={currencyCode} primaryColor={primaryColor} />
+        <ReportsClient currencyCode={currencyCode} primaryColor={primaryColor} organizationName={organizationName} />
       </div>
     </>
   );

+ 338 - 20
src/app/(authenticated)/reports/reports-client.tsx

@@ -29,6 +29,7 @@ import {
 } from "@/components/ui/table";
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { Badge } from "@/components/ui/badge";
+import { Skeleton } from "@/components/ui/skeleton";
 import {
   Select,
   SelectContent,
@@ -55,6 +56,9 @@ import {
   Clock,
   ArrowUpDown,
   Landmark,
+  Car,
+  ChevronLeft,
+  ChevronRight,
 } from "lucide-react";
 import {
   getRevenueReport,
@@ -67,6 +71,7 @@ import {
   getCustomerRetentionReport,
   getTaxReport,
   getPastDueInvoicesReport,
+  getVehicleReport,
 } from "@/features/reports/Actions/reportActions";
 import { formatCurrency } from "@/lib/format";
 import type {
@@ -80,6 +85,7 @@ import type {
   CustomerRetentionReport,
   TaxReport,
   PastDueInvoicesReport,
+  VehicleReportData,
 } from "@/features/reports/Schema/reportTypes";
 import { RevenueBarChart, RevenueTypeDonut } from "@/features/reports/Components/RevenueCharts";
 import { ServiceStatusChart, ServiceTypeDonut } from "@/features/reports/Components/ServiceCharts";
@@ -89,6 +95,8 @@ import { PartsDonut } from "@/features/reports/Components/PartsCharts";
 import { DayOfWeekChart, ServiceTypeAnalyticsDonut, MonthlyTrendChart } from "@/features/reports/Components/JobAnalyticsCharts";
 import { RetentionBarChart } from "@/features/reports/Components/RetentionCharts";
 import { TaxBarChart } from "@/features/reports/Components/TaxCharts";
+import { VehicleCostBarChart } from "@/features/reports/Components/VehicleCharts";
+import { VehicleCombobox } from "@/features/quotes/Components/VehicleCombobox";
 import {
   exportRevenueCsv,
   exportServicesCsv,
@@ -100,11 +108,12 @@ import {
   exportRetentionCsv,
   exportTaxCsv,
   exportPastDueInvoicesCsv,
+  exportVehicleReportCsv,
 } from "@/features/reports/Components/csv-export";
 import { ReportPDF } from "@/features/reports/Components/ReportPDF";
 import { pdf } from "@react-pdf/renderer";
 
-type ReportTab = "financial" | "services" | "customers" | "inventory" | "technicians" | "parts" | "job-analytics" | "retention";
+type ReportTab = "financial" | "services" | "customers" | "inventory" | "technicians" | "parts" | "job-analytics" | "retention" | "vehicles";
 type FinancialSubTab = "revenue" | "past-due-invoices" | "tax";
 type PastDueSortKey = "customerName" | "amountDue" | "daysPastDue";
 type PastDueSortDir = "asc" | "desc";
@@ -112,12 +121,13 @@ type PastDueSortDir = "asc" | "desc";
 interface ReportsClientProps {
   currencyCode: string;
   primaryColor: string;
+  organizationName: string;
 }
 
-const VALID_TABS: ReportTab[] = ["financial", "services", "customers", "inventory", "technicians", "parts", "job-analytics", "retention"];
+const VALID_TABS: ReportTab[] = ["financial", "services", "customers", "inventory", "technicians", "parts", "job-analytics", "retention", "vehicles"];
 const VALID_SUB_TABS: FinancialSubTab[] = ["revenue", "past-due-invoices", "tax"];
 
-export default function ReportsClient({ currencyCode, primaryColor }: ReportsClientProps) {
+export default function ReportsClient({ currencyCode, primaryColor, organizationName }: ReportsClientProps) {
   const t = useTranslations("reports");
   const searchParams = useSearchParams();
   const router = useRouter();
@@ -163,6 +173,9 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
   const [retentionData, setRetentionData] = useState<CustomerRetentionReport | null>(null);
   const [taxData, setTaxData] = useState<TaxReport | null>(null);
   const [pastDueData, setPastDueData] = useState<PastDueInvoicesReport | null>(null);
+  const [vehicleData, setVehicleData] = useState<VehicleReportData | null>(null);
+  const [selectedVehicleId, setSelectedVehicleId] = useState<string>("");
+  const [historyPage, setHistoryPage] = useState(0);
 
   const fmtCurrency = useCallback(
     (value: number) => formatCurrency(value, currencyCode),
@@ -172,7 +185,7 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
   type FetchableReport = ReportTab | FinancialSubTab;
 
   const fetchReport = useCallback(
-    async (type: FetchableReport, overrideDateRange?: DateRange) => {
+    async (type: FetchableReport, overrideDateRange?: DateRange, overrideVehicleId?: string) => {
       const range = overrideDateRange ?? dateRange;
       setLoading(true);
       try {
@@ -231,6 +244,13 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
             if (result.success && result.data) setRetentionData(result.data);
             break;
           }
+          case "vehicles": {
+            const vid = overrideVehicleId ?? selectedVehicleId;
+            if (!vid) break;
+            const result = await getVehicleReport({ ...dateParams, vehicleId: vid });
+            if (result.success && result.data) setVehicleData(result.data);
+            break;
+          }
         }
       } catch (error) {
         console.error("Failed to fetch report:", error);
@@ -238,7 +258,7 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
         setLoading(false);
       }
     },
-    [dateRange],
+    [dateRange, selectedVehicleId],
   );
 
   // Auto-fetch current tab on mount
@@ -272,9 +292,14 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
         parts: partsData,
         "job-analytics": jobAnalyticsData,
         retention: retentionData,
+        vehicles: vehicleData,
       };
       if (!dataMap[tab]) {
-        fetchReport(tab);
+        if (tab === "vehicles") {
+          if (selectedVehicleId) fetchReport(tab);
+        } else {
+          fetchReport(tab);
+        }
       }
     }
   };
@@ -296,6 +321,8 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
   const handleRefresh = () => {
     if (activeTab === "financial") {
       fetchReport(financialSubTab);
+    } else if (activeTab === "vehicles") {
+      if (selectedVehicleId) fetchReport("vehicles");
     } else {
       fetchReport(activeTab);
     }
@@ -338,6 +365,9 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
         case "retention":
           if (retentionData) exportRetentionCsv(retentionData, currencyCode, [h("customer"), h("company"), h("visits"), h("totalSpent"), h("avgDaysBetweenVisits")]);
           break;
+        case "vehicles":
+          if (vehicleData) exportVehicleReportCsv(vehicleData, currencyCode, [h("date"), h("title"), h("type"), h("status"), h("totalAmount"), h("partsUsed"), h("laborHours")]);
+          break;
       }
     }
   };
@@ -441,6 +471,19 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
         quantity: t("pdf.quantity"),
         minQuantity: t("pdf.minQuantity"),
         unitCost: t("pdf.unitCost"),
+        vehiclesSection: t("pdf.vehiclesSection"),
+        vehicleLabel: t("pdf.vehicleLabel"),
+        totalCost: t("pdf.totalCost"),
+        partsCostLabel: t("pdf.partsCostLabel"),
+        laborCostLabel: t("pdf.laborCostLabel"),
+        date: t("pdf.date"),
+        repairs: t("vehicles.repairs"),
+        maintenance: t("vehicles.maintenance"),
+        upgrades: t("vehicles.upgrades"),
+        inspections: t("vehicles.inspections"),
+        totalLaborHours: t("vehicles.totalLaborHours"),
+        serviceHistory: t("vehicles.serviceHistory"),
+        serviceTypeBreakdown: t("vehicles.serviceTypeBreakdown"),
       };
 
       // Only include data for the current tab
@@ -460,6 +503,7 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
           parts: ["partsData", partsData],
           "job-analytics": ["jobAnalyticsData", jobAnalyticsData],
           retention: ["retentionData", retentionData],
+          vehicles: ["vehicleData", vehicleData],
         };
         const entry = dataMap[activeTab];
         if (entry) pdfProps[entry[0] as string] = entry[1];
@@ -470,6 +514,7 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
           dateRange={dateRangeStr}
           currencyCode={currencyCode}
           primaryColor={primaryColor}
+          organizationName={organizationName}
           labels={labels}
           {...pdfProps}
         />,
@@ -506,6 +551,7 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
       parts: partsData,
       "job-analytics": jobAnalyticsData,
       retention: retentionData,
+      vehicles: vehicleData,
     };
     return !!dataMap[activeTab];
   })();
@@ -539,8 +585,8 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
 
   return (
     <Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-4">
-      {/* Tab bar with date range and actions */}
-      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+      {/* Tab bar */}
+      <div className="space-y-3">
         <TabsList className="flex-wrap h-auto">
           <TabsTrigger value="financial" className="gap-1.5">
             <Landmark className="h-4 w-4" />
@@ -550,6 +596,10 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
             <BarChart3 className="h-4 w-4" />
             {t("tabs.services")}
           </TabsTrigger>
+          <TabsTrigger value="vehicles" className="gap-1.5">
+            <Car className="h-4 w-4" />
+            {t("tabs.vehicles")}
+          </TabsTrigger>
           <TabsTrigger value="customers" className="gap-1.5">
             <Users className="h-4 w-4" />
             {t("tabs.customers")}
@@ -576,7 +626,8 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
           </TabsTrigger>
         </TabsList>
 
-        <div className="flex items-center gap-2">
+        {/* Date range and actions */}
+        <div className="flex items-center gap-2 flex-wrap justify-end">
           {showDateRange && (
             <Popover open={datePickerOpen} onOpenChange={(open) => {
               setDatePickerOpen(open);
@@ -607,13 +658,54 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
                 </Button>
               </PopoverTrigger>
               <PopoverContent className="w-auto p-0" align="end">
-                <Calendar
-                  mode="range"
-                  defaultMonth={dateRange.to ? new Date(dateRange.to.getFullYear(), dateRange.to.getMonth() - 1) : dateRange.from}
-                  selected={pendingDateRange}
-                  onSelect={(range) => range && setPendingDateRange(range)}
-                  numberOfMonths={2}
-                />
+                <div className="flex">
+                  <Calendar
+                    mode="range"
+                    defaultMonth={dateRange.to ? new Date(dateRange.to.getFullYear(), dateRange.to.getMonth() - 1) : dateRange.from}
+                    selected={pendingDateRange}
+                    onSelect={(range) => range && setPendingDateRange(range)}
+                    numberOfMonths={2}
+                  />
+                  <div className="border-l p-2 flex flex-col gap-0.5 min-w-[130px]">
+                    {(() => {
+                      const now = new Date();
+                      const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+                      const presets: { label: string; from: Date; to: Date }[] = [
+                        { label: t("dateRange.presets.today"), from: today, to: today },
+                        { label: t("dateRange.presets.yesterday"), from: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1), to: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1) },
+                        { label: t("dateRange.presets.last7"), from: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6), to: today },
+                        { label: t("dateRange.presets.last30"), from: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29), to: today },
+                        { label: t("dateRange.presets.last90"), from: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 89), to: today },
+                        { label: t("dateRange.presets.thisMonth"), from: new Date(now.getFullYear(), now.getMonth(), 1), to: today },
+                        { label: t("dateRange.presets.lastMonth"), from: new Date(now.getFullYear(), now.getMonth() - 1, 1), to: new Date(now.getFullYear(), now.getMonth(), 0) },
+                        { label: t("dateRange.presets.thisQuarter"), from: new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1), to: today },
+                        { label: t("dateRange.presets.thisYear"), from: new Date(now.getFullYear(), 0, 1), to: today },
+                        { label: t("dateRange.presets.lastYear"), from: new Date(now.getFullYear() - 1, 0, 1), to: new Date(now.getFullYear() - 1, 11, 31) },
+                        { label: t("dateRange.presets.allTime"), from: new Date(2000, 0, 1), to: today },
+                      ];
+                      return presets.map((p) => (
+                        <Button
+                          key={p.label}
+                          variant="ghost"
+                          size="sm"
+                          className="justify-start text-xs h-7 px-2"
+                          onClick={() => {
+                            const range = { from: p.from, to: p.to };
+                            setDateRange(range);
+                            setDatePickerOpen(false);
+                            if (activeTab === "financial") {
+                              fetchReport(financialSubTab, range);
+                            } else {
+                              fetchReport(activeTab, range);
+                            }
+                          }}
+                        >
+                          {p.label}
+                        </Button>
+                      ));
+                    })()}
+                  </div>
+                </div>
                 <div className="border-t p-3 flex justify-end">
                   <Button
                     size="sm"
@@ -656,10 +748,19 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
         </div>
       </div>
 
-      {/* Loading overlay */}
-      {loading && (
-        <div className="flex items-center justify-center py-12">
-          <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
+      {/* Loading skeleton (hidden on vehicles tab — it handles its own) */}
+      {loading && activeTab !== "vehicles" && (
+        <div className="space-y-4">
+          <div className="grid gap-2 grid-cols-3">
+            {Array.from({ length: 3 }).map((_, i) => (
+              <Skeleton key={i} className="h-16 rounded-lg" />
+            ))}
+          </div>
+          <div className="grid gap-4 lg:grid-cols-5">
+            <Skeleton className="h-[300px] rounded-lg lg:col-span-3" />
+            <Skeleton className="h-[300px] rounded-lg lg:col-span-2" />
+          </div>
+          <Skeleton className="h-48 rounded-lg" />
         </div>
       )}
 
@@ -1727,6 +1828,223 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
         )}
         {!loading && !retentionData && <EmptyState message={t("empty")} />}
       </TabsContent>
+
+      {/* Vehicle Reports Tab */}
+      <TabsContent value="vehicles">
+        <div className="mb-4 max-w-md">
+          <VehicleCombobox
+            value={selectedVehicleId}
+            onChange={(id) => {
+              setSelectedVehicleId(id);
+              setVehicleData(null);
+              setHistoryPage(0);
+              if (id) {
+                fetchReport("vehicles", undefined, id);
+              }
+            }}
+            placeholder={t("vehicles.selectVehicle")}
+            noneLabel="—"
+          />
+        </div>
+
+        {loading && activeTab === "vehicles" && (
+          <div className="space-y-4">
+            <Skeleton className="h-[72px] rounded-lg" />
+            <div className="grid gap-4 lg:grid-cols-5">
+              <Skeleton className="h-[300px] rounded-lg lg:col-span-3" />
+              <Skeleton className="h-[300px] rounded-lg lg:col-span-2" />
+            </div>
+            <div className="grid gap-4 lg:grid-cols-5">
+              <Skeleton className="h-48 rounded-lg lg:col-span-2" />
+              <Skeleton className="h-48 rounded-lg lg:col-span-3" />
+            </div>
+          </div>
+        )}
+        {!loading && vehicleData && (
+          <div className="space-y-4">
+            {/* Vehicle header with inline stats */}
+            <Card className="border-0 shadow-sm">
+              <CardContent className="p-4">
+                <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+                  <div className="flex items-center gap-3 min-w-0">
+                    <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
+                      <Car className="h-4 w-4 text-blue-500" />
+                    </div>
+                    <div className="min-w-0">
+                      <p className="text-base font-semibold">
+                        {vehicleData.vehicleInfo.year} {vehicleData.vehicleInfo.make} {vehicleData.vehicleInfo.model}
+                      </p>
+                      <p className="text-xs text-muted-foreground">
+                        {[vehicleData.vehicleInfo.licensePlate, vehicleData.vehicleInfo.vin, vehicleData.vehicleInfo.customerName].filter(Boolean).join(" · ")}
+                      </p>
+                    </div>
+                  </div>
+                  <div className="flex flex-wrap gap-x-5 gap-y-1 text-sm tabular-nums">
+                    <span><span className="text-muted-foreground">{t("vehicles.totalServices")}:</span> <span className="font-medium">{vehicleData.summary.totalServices}</span></span>
+                    <span><span className="text-muted-foreground">{t("vehicles.totalCost")}:</span> <span className="font-medium">{fmtCurrency(vehicleData.summary.totalCost)}</span></span>
+                    <span><span className="text-muted-foreground">{t("vehicles.totalPartsUsed")}:</span> <span className="font-medium">{vehicleData.summary.totalPartsUsed}</span></span>
+                    <span><span className="text-muted-foreground">{t("vehicles.totalLaborHours")}:</span> <span className="font-medium">{vehicleData.summary.totalLaborHours.toFixed(1)}</span></span>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+
+            {/* Charts row */}
+            <div className="grid gap-4 lg:grid-cols-5">
+              <Card className="border-0 shadow-sm lg:col-span-3">
+                <CardHeader className="pb-2">
+                  <CardTitle className="text-sm font-medium">{t("vehicles.monthlyCosts")}</CardTitle>
+                </CardHeader>
+                <CardContent>
+                  <VehicleCostBarChart
+                    data={vehicleData.monthlyCosts}
+                    formatCurrency={fmtCurrency}
+                    labels={{ partsCost: t("charts.partsCost"), laborCost: t("charts.laborCost") }}
+                  />
+                </CardContent>
+              </Card>
+              <Card className="border-0 shadow-sm lg:col-span-2">
+                <CardHeader className="pb-2">
+                  <CardTitle className="text-sm font-medium">{t("vehicles.serviceTypeBreakdown")}</CardTitle>
+                </CardHeader>
+                <CardContent>
+                  <div className="space-y-3">
+                    {vehicleData.serviceTypeBreakdown.map((item) => {
+                      const pct = vehicleData.summary.totalCost > 0
+                        ? (item.totalCost / vehicleData.summary.totalCost) * 100
+                        : 0;
+                      return (
+                        <div key={item.type}>
+                          <div className="flex items-center justify-between mb-1">
+                            <span className="text-sm capitalize">{item.type}</span>
+                            <span className="text-sm font-medium tabular-nums">{fmtCurrency(item.totalCost)}</span>
+                          </div>
+                          <div className="flex items-center gap-2">
+                            <div className="h-2 flex-1 rounded-full bg-muted overflow-hidden">
+                              <div
+                                className="h-full rounded-full bg-blue-500"
+                                style={{ width: `${pct}%` }}
+                              />
+                            </div>
+                            <span className="text-xs text-muted-foreground tabular-nums w-16 text-right">
+                              {item.count} · {pct.toFixed(0)}%
+                            </span>
+                          </div>
+                        </div>
+                      );
+                    })}
+                    {vehicleData.serviceTypeBreakdown.length > 0 && (
+                      <div className="border-t pt-3 flex items-center justify-between">
+                        <span className="text-sm font-medium">{t("vehicles.totalCost")}</span>
+                        <span className="text-sm font-semibold tabular-nums">{fmtCurrency(vehicleData.summary.totalCost)}</span>
+                      </div>
+                    )}
+                  </div>
+                </CardContent>
+              </Card>
+            </div>
+
+            {/* Top Parts + Service History side by side on large screens */}
+            <div className="grid gap-4 lg:grid-cols-5">
+              {vehicleData.topParts.length > 0 && (
+                <Card className="border-0 shadow-sm lg:col-span-2">
+                  <CardHeader className="pb-2">
+                    <CardTitle className="text-sm font-medium">{t("vehicles.topParts")}</CardTitle>
+                  </CardHeader>
+                  <CardContent>
+                    <Table>
+                      <TableHeader>
+                        <TableRow>
+                          <TableHead>{t("vehicles.tableHeaders.partName")}</TableHead>
+                          <TableHead className="text-right">{t("vehicles.tableHeaders.quantity")}</TableHead>
+                          <TableHead className="text-right">{t("vehicles.tableHeaders.cost")}</TableHead>
+                        </TableRow>
+                      </TableHeader>
+                      <TableBody>
+                        {vehicleData.topParts.map((part, i) => (
+                          <TableRow key={i}>
+                            <TableCell className="text-sm">
+                              <span className="font-medium">{part.name}</span>
+                              {part.partNumber && <span className="text-muted-foreground ml-1.5 text-xs">#{part.partNumber}</span>}
+                            </TableCell>
+                            <TableCell className="text-right text-sm">{part.quantity}</TableCell>
+                            <TableCell className="text-right text-sm">{fmtCurrency(part.totalCost)}</TableCell>
+                          </TableRow>
+                        ))}
+                      </TableBody>
+                    </Table>
+                  </CardContent>
+                </Card>
+              )}
+
+              {vehicleData.serviceHistory.length > 0 && (() => {
+                const pageSize = 15;
+                const totalPages = Math.ceil(vehicleData.serviceHistory.length / pageSize);
+                const page = Math.min(historyPage, totalPages - 1);
+                const paged = vehicleData.serviceHistory.slice(page * pageSize, (page + 1) * pageSize);
+                return (
+                  <Card className={cn("border-0 shadow-sm", vehicleData.topParts.length > 0 ? "lg:col-span-3" : "lg:col-span-5")}>
+                    <CardHeader className="pb-2">
+                      <div className="flex items-center justify-between">
+                        <CardTitle className="text-sm font-medium">{t("vehicles.serviceHistory")}</CardTitle>
+                        {totalPages > 1 && (
+                          <div className="flex items-center gap-1 text-xs text-muted-foreground">
+                            <span>{page * pageSize + 1}–{Math.min((page + 1) * pageSize, vehicleData.serviceHistory.length)} / {vehicleData.serviceHistory.length}</span>
+                            <Button variant="ghost" size="icon" className="h-6 w-6" disabled={page === 0} onClick={() => setHistoryPage(page - 1)}>
+                              <ChevronLeft className="h-3.5 w-3.5" />
+                            </Button>
+                            <Button variant="ghost" size="icon" className="h-6 w-6" disabled={page >= totalPages - 1} onClick={() => setHistoryPage(page + 1)}>
+                              <ChevronRight className="h-3.5 w-3.5" />
+                            </Button>
+                          </div>
+                        )}
+                      </div>
+                    </CardHeader>
+                    <CardContent>
+                      <Table>
+                        <TableHeader>
+                          <TableRow>
+                            <TableHead>{t("vehicles.tableHeaders.date")}</TableHead>
+                            <TableHead>{t("vehicles.tableHeaders.title")}</TableHead>
+                            <TableHead>{t("vehicles.tableHeaders.type")}</TableHead>
+                            <TableHead className="text-right">{t("vehicles.tableHeaders.totalAmount")}</TableHead>
+                            <TableHead className="text-right hidden sm:table-cell">{t("vehicles.tableHeaders.laborHours")}</TableHead>
+                            <TableHead className="hidden md:table-cell">{t("vehicles.tableHeaders.techName")}</TableHead>
+                          </TableRow>
+                        </TableHeader>
+                        <TableBody>
+                          {paged.map((row) => (
+                            <TableRow
+                              key={row.id}
+                              className="cursor-pointer hover:bg-muted/50"
+                              onClick={() => router.push(`/vehicles/${vehicleData.vehicleInfo.id}/service/${row.id}`)}
+                            >
+                              <TableCell className="text-sm text-muted-foreground">{row.date}</TableCell>
+                              <TableCell className="text-sm font-medium">{row.title}</TableCell>
+                              <TableCell className="text-sm">
+                                <Badge variant="secondary">{row.type}</Badge>
+                              </TableCell>
+                              <TableCell className="text-right text-sm">{fmtCurrency(row.totalAmount)}</TableCell>
+                              <TableCell className="text-right text-sm hidden sm:table-cell">{row.laborHours.toFixed(1)}</TableCell>
+                              <TableCell className="text-sm hidden md:table-cell">{row.techName ?? "-"}</TableCell>
+                            </TableRow>
+                          ))}
+                        </TableBody>
+                      </Table>
+                    </CardContent>
+                  </Card>
+                );
+              })()}
+            </div>
+          </div>
+        )}
+        {!loading && !vehicleData && !selectedVehicleId && (
+          <EmptyState message={t("vehicles.selectVehiclePrompt")} />
+        )}
+        {!loading && !vehicleData && selectedVehicleId && (
+          <EmptyState message={t("empty")} />
+        )}
+      </TabsContent>
     </Tabs>
   );
 }

+ 124 - 0
src/features/reports/Actions/reportActions.ts

@@ -608,6 +608,130 @@ export async function getPastDueInvoicesReport() {
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.REPORTS }] });
 }
 
+export async function getVehicleReport(params: {
+  vehicleId: string;
+  startDate?: string;
+  endDate?: string;
+}) {
+  return withAuth(async ({ organizationId }) => {
+    const start = params.startDate ? new Date(params.startDate) : new Date(new Date().getFullYear(), 0, 1);
+    const end = params.endDate ? new Date(params.endDate) : new Date();
+    end.setHours(23, 59, 59, 999);
+
+    const vehicle = await db.vehicle.findFirst({
+      where: { id: params.vehicleId, organizationId },
+      select: {
+        id: true, year: true, make: true, model: true,
+        vin: true, licensePlate: true, mileage: true,
+        customer: { select: { name: true } },
+      },
+    });
+    if (!vehicle) throw new Error("Vehicle not found");
+
+    const records = await db.serviceRecord.findMany({
+      where: {
+        vehicleId: params.vehicleId,
+        vehicle: { organizationId },
+        startDateTime: { gte: start, lte: end },
+      },
+      select: {
+        id: true, title: true, type: true, status: true,
+        serviceDate: true, startDateTime: true,
+        totalAmount: true, cost: true,
+        techName: true,
+        technician: { select: { name: true } },
+        partItems: { select: { name: true, partNumber: true, quantity: true, unitCost: true, total: true } },
+        laborItems: { select: { hours: true, total: true } },
+      },
+      orderBy: [{ startDateTime: { sort: "desc", nulls: "last" } }, { serviceDate: "desc" }],
+    });
+
+    let totalCost = 0;
+    let totalPartsUsed = 0;
+    let totalLaborHours = 0;
+    const typeCounts: Record<string, { count: number; totalCost: number }> = {};
+    const monthly: Record<string, { partsCost: number; laborCost: number; totalCost: number; count: number }> = {};
+    const partUsage: Record<string, { partNumber: string | null; quantity: number; totalCost: number }> = {};
+    const partNames: Record<string, string> = {};
+
+    for (const r of records) {
+      const total = r.totalAmount > 0 ? r.totalAmount : r.cost;
+      totalCost += total;
+
+      const partsCost = r.partItems.reduce((s, p) => s + (p.unitCost * p.quantity), 0);
+      const laborCost = r.laborItems.reduce((s, l) => s + l.total, 0);
+      const laborHrs = r.laborItems.reduce((s, l) => s + l.hours, 0);
+      totalPartsUsed += r.partItems.reduce((s, p) => s + p.quantity, 0);
+      totalLaborHours += laborHrs;
+
+      if (!typeCounts[r.type]) typeCounts[r.type] = { count: 0, totalCost: 0 };
+      typeCounts[r.type].count += 1;
+      typeCounts[r.type].totalCost += total;
+
+      const _d = r.startDateTime ?? r.serviceDate;
+      const month = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, "0")}`;
+      if (!monthly[month]) monthly[month] = { partsCost: 0, laborCost: 0, totalCost: 0, count: 0 };
+      monthly[month].partsCost += partsCost;
+      monthly[month].laborCost += laborCost;
+      monthly[month].totalCost += total;
+      monthly[month].count += 1;
+
+      for (const p of r.partItems) {
+        const key = p.name.toLowerCase();
+        if (!partUsage[key]) partUsage[key] = { partNumber: p.partNumber, quantity: 0, totalCost: 0 };
+        partUsage[key].quantity += p.quantity;
+        partUsage[key].totalCost += p.unitCost * p.quantity;
+        if (p.partNumber && !partUsage[key].partNumber) partUsage[key].partNumber = p.partNumber;
+        if (!partNames[key]) partNames[key] = p.name;
+      }
+    }
+
+    return {
+      vehicleInfo: {
+        id: vehicle.id,
+        year: vehicle.year,
+        make: vehicle.make,
+        model: vehicle.model,
+        vin: vehicle.vin,
+        licensePlate: vehicle.licensePlate,
+        mileage: vehicle.mileage,
+        customerName: vehicle.customer?.name ?? null,
+      },
+      summary: {
+        totalServices: records.length,
+        totalCost,
+        totalPartsUsed,
+        totalLaborHours,
+        repairCount: typeCounts["repair"]?.count ?? 0,
+        maintenanceCount: typeCounts["maintenance"]?.count ?? 0,
+        upgradeCount: typeCounts["upgrade"]?.count ?? 0,
+        inspectionCount: typeCounts["inspection"]?.count ?? 0,
+      },
+      monthlyCosts: Object.entries(monthly)
+        .map(([month, data]) => ({ month, ...data }))
+        .sort((a, b) => a.month.localeCompare(b.month)),
+      serviceTypeBreakdown: Object.entries(typeCounts)
+        .map(([type, data]) => ({ type, ...data }))
+        .sort((a, b) => b.count - a.count),
+      topParts: Object.entries(partUsage)
+        .map(([key, data]) => ({ name: partNames[key] || key, ...data }))
+        .sort((a, b) => b.quantity - a.quantity)
+        .slice(0, 20),
+      serviceHistory: records.map((r) => ({
+        id: r.id,
+        title: r.title,
+        type: r.type,
+        status: r.status,
+        date: (r.startDateTime ?? r.serviceDate).toISOString().split("T")[0],
+        totalAmount: r.totalAmount > 0 ? r.totalAmount : r.cost,
+        partsCount: r.partItems.reduce((s, p) => s + p.quantity, 0),
+        laborHours: r.laborItems.reduce((s, l) => s + l.hours, 0),
+        techName: r.technician?.name ?? r.techName ?? null,
+      })),
+    };
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.REPORTS }] });
+}
+
 export async function getTaxReport(params: {
   startDate?: string;
   endDate?: string;

+ 126 - 70
src/features/reports/Components/ReportPDF.tsx

@@ -17,6 +17,7 @@ import type {
   CustomerRetentionReport,
   TaxReport,
   PastDueInvoicesReport,
+  VehicleReportData,
 } from "../Schema/reportTypes";
 import { formatCurrency } from "@/lib/format";
 
@@ -140,6 +141,7 @@ interface ReportPDFProps {
   dateRange: string;
   currencyCode: string;
   primaryColor: string;
+  organizationName?: string;
   labels: Record<string, string>;
   revenueData?: RevenueReport | null;
   serviceData?: ServiceReport | null;
@@ -151,6 +153,7 @@ interface ReportPDFProps {
   retentionData?: CustomerRetentionReport | null;
   taxData?: TaxReport | null;
   pastDueData?: PastDueInvoicesReport | null;
+  vehicleData?: VehicleReportData | null;
 }
 
 type ReportStyles = ReturnType<typeof createReportStyles>;
@@ -280,22 +283,29 @@ export function ReportPDF({
   retentionData,
   taxData,
   pastDueData,
+  vehicleData,
+  organizationName,
 }: ReportPDFProps) {
   const fmt = (n: number) => formatCurrency(n, currencyCode);
   const s = createReportStyles(primaryColor);
 
+  const ReportHeader = ({ title, subtitle }: { title: string; subtitle: string }) => (
+    <View style={s.headerRow}>
+      <View>
+        <Text style={s.title}>{title}</Text>
+        <Text style={s.subtitle}>{subtitle}</Text>
+        {organizationName ? <Text style={{ fontSize: 8, color: gray, marginTop: 2 }}>{organizationName}</Text> : null}
+      </View>
+      <Text style={s.dateBadge}>{dateRange}</Text>
+    </View>
+  );
+
   return (
     <Document>
       {/* ==================== REVENUE ==================== */}
       {revenueData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.revenueSection || "Revenue Report"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.revenueSection || "Revenue Report"} />
 
           <SummaryCards
             s={s}
@@ -366,13 +376,7 @@ export function ReportPDF({
       {/* ==================== TAX ==================== */}
       {taxData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.taxSection || "Tax Report"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.taxSection || "Tax Report"} />
 
           <SummaryCards
             s={s}
@@ -425,13 +429,7 @@ export function ReportPDF({
       {/* ==================== PAST DUE INVOICES ==================== */}
       {pastDueData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.pastDueSection || "Past Due Invoices"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.pastDueSection || "Past Due Invoices"} />
 
           <SummaryCards
             s={s}
@@ -473,13 +471,7 @@ export function ReportPDF({
       {/* ==================== SERVICES ==================== */}
       {serviceData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.servicesSection || "Services Report"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.servicesSection || "Services Report"} />
 
           <SummaryCards
             s={s}
@@ -518,13 +510,7 @@ export function ReportPDF({
       {/* ==================== CUSTOMERS ==================== */}
       {customerData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.customersSection || "Customers Report"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.customersSection || "Customers Report"} />
 
           <SummaryCards
             s={s}
@@ -562,13 +548,7 @@ export function ReportPDF({
       {/* ==================== TECHNICIANS ==================== */}
       {technicianData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.techniciansSection || "Technicians Report"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.techniciansSection || "Technicians Report"} />
 
           <SummaryCards
             s={s}
@@ -607,13 +587,7 @@ export function ReportPDF({
       {/* ==================== PARTS USAGE ==================== */}
       {partsData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.partsSection || "Parts Usage Report"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.partsSection || "Parts Usage Report"} />
 
           <SummaryCards
             s={s}
@@ -656,13 +630,7 @@ export function ReportPDF({
       {/* ==================== JOB ANALYTICS ==================== */}
       {jobAnalyticsData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.jobAnalyticsSection || "Job Analytics"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.jobAnalyticsSection || "Job Analytics"} />
 
           <SummaryCards
             s={s}
@@ -728,13 +696,7 @@ export function ReportPDF({
       {/* ==================== RETENTION ==================== */}
       {retentionData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.retentionSection || "Customer Retention"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.retentionSection || "Customer Retention"} />
 
           <SummaryCards
             s={s}
@@ -781,13 +743,7 @@ export function ReportPDF({
       {/* ==================== INVENTORY ==================== */}
       {inventoryData && (
         <Page size="A4" style={s.page}>
-          <View style={s.headerRow}>
-            <View>
-              <Text style={s.title}>{l.reportTitle || "Business Report"}</Text>
-              <Text style={s.subtitle}>{l.inventorySection || "Inventory Report"}</Text>
-            </View>
-            <Text style={s.dateBadge}>{dateRange}</Text>
-          </View>
+          <ReportHeader title={l.reportTitle || "Business Report"} subtitle={l.inventorySection || "Inventory Report"} />
 
           <SummaryCards
             s={s}
@@ -825,6 +781,106 @@ export function ReportPDF({
           <PageFooter s={s} />
         </Page>
       )}
+
+      {/* ==================== VEHICLE ==================== */}
+      {vehicleData && (
+        <Page size="A4" style={s.page}>
+          <ReportHeader
+            title={l.vehiclesSection || "Vehicle Report"}
+            subtitle={`${vehicleData.vehicleInfo.year} ${vehicleData.vehicleInfo.make} ${vehicleData.vehicleInfo.model}${[vehicleData.vehicleInfo.licensePlate, vehicleData.vehicleInfo.vin, vehicleData.vehicleInfo.customerName].filter(Boolean).length > 0 ? ` — ${[vehicleData.vehicleInfo.licensePlate, vehicleData.vehicleInfo.vin, vehicleData.vehicleInfo.customerName].filter(Boolean).join(" · ")}` : ""}`}
+          />
+
+          <View style={{ flexDirection: "row", backgroundColor: grayLight, borderRadius: 4, padding: 10, marginBottom: 14, justifyContent: "space-between" }}>
+            {[
+              { label: l.totalServices || "Total Services", value: String(vehicleData.summary.totalServices) },
+              { label: l.totalCost || "Total Cost", value: fmt(vehicleData.summary.totalCost) },
+              { label: l.totalPartsUsed || "Parts Used", value: String(vehicleData.summary.totalPartsUsed) },
+              { label: l.totalLaborHours || "Labor Hours", value: vehicleData.summary.totalLaborHours.toFixed(1) },
+            ].map((item, i) => (
+              <View key={i} style={{ alignItems: i === 0 ? "flex-start" : i === 3 ? "flex-end" : "center" }}>
+                <Text style={{ fontSize: 7, color: gray, textTransform: "uppercase", marginBottom: 2 }}>{item.label}</Text>
+                <Text style={{ fontSize: 12, fontFamily: "Roboto-Bold" }}>{item.value}</Text>
+              </View>
+            ))}
+          </View>
+
+          {vehicleData.serviceTypeBreakdown.length > 0 && (
+            <View style={s.section}>
+              <Text style={s.sectionTitle}>{l.serviceTypeBreakdown || "Expenses by Service Type"}</Text>
+              <TableBlock
+                s={s}
+                headers={[
+                  l.type || "Type",
+                  l.count || "Count",
+                  l.totalCost || "Cost",
+                ]}
+                widths={[40, 20, 40]}
+                rows={[
+                  ...vehicleData.serviceTypeBreakdown.map((item) => [
+                    item.type.charAt(0).toUpperCase() + item.type.slice(1),
+                    String(item.count),
+                    fmt(item.totalCost),
+                  ]),
+                  [
+                    l.totalCost || "Total",
+                    String(vehicleData.summary.totalServices),
+                    fmt(vehicleData.summary.totalCost),
+                  ],
+                ]}
+              />
+            </View>
+          )}
+
+          {vehicleData.monthlyCosts.length > 0 && (
+            <View style={s.section}>
+              <Text style={s.sectionTitle}>{l.monthlyBreakdown || "Monthly Breakdown"}</Text>
+              <TableBlock
+                s={s}
+                headers={[
+                  l.month || "Month",
+                  l.partsCostLabel || "Parts Cost",
+                  l.laborCostLabel || "Labor Cost",
+                  l.totalCost || "Total Cost",
+                  l.count || "Jobs",
+                ]}
+                widths={[24, 22, 22, 22, 10]}
+                rows={vehicleData.monthlyCosts.map((m) => [
+                  m.month,
+                  fmt(m.partsCost),
+                  fmt(m.laborCost),
+                  fmt(m.totalCost),
+                  String(m.count),
+                ])}
+              />
+            </View>
+          )}
+
+          {vehicleData.serviceHistory.length > 0 && (
+            <View style={s.section}>
+              <Text style={s.sectionTitle}>{l.serviceHistory || "Service History"}</Text>
+              <TableBlock
+                s={s}
+                headers={[
+                  l.date || "Date",
+                  l.title || "Title",
+                  l.type || "Type",
+                  l.status || "Status",
+                  l.totalAmount || "Total",
+                ]}
+                widths={[18, 32, 16, 16, 18]}
+                rows={vehicleData.serviceHistory.map((r) => [
+                  r.date,
+                  r.title,
+                  r.type,
+                  r.status,
+                  fmt(r.totalAmount),
+                ])}
+              />
+            </View>
+          )}
+          <PageFooter s={s} />
+        </Page>
+      )}
     </Document>
   );
 }

+ 130 - 0
src/features/reports/Components/VehicleCharts.tsx

@@ -0,0 +1,130 @@
+"use client";
+
+import {
+  BarChart,
+  Bar,
+  XAxis,
+  YAxis,
+  CartesianGrid,
+  Tooltip,
+  ResponsiveContainer,
+  PieChart,
+  Pie,
+  Cell,
+  Legend,
+} from "recharts";
+import type { VehicleMonthlyCost, VehicleServiceTypeBreakdown } from "../Schema/reportTypes";
+
+const CHART_COLORS = [
+  "#3b82f6",
+  "#10b981",
+  "#f59e0b",
+  "#8b5cf6",
+  "#f43f5e",
+  "#06b6d4",
+  "#ec4899",
+  "#84cc16",
+];
+
+interface VehicleCostBarChartProps {
+  data: VehicleMonthlyCost[];
+  formatCurrency: (value: number) => string;
+  labels?: { partsCost: string; laborCost: string };
+}
+
+export function VehicleCostBarChart({ data, formatCurrency, labels }: VehicleCostBarChartProps) {
+  if (data.length === 0) return null;
+
+  const compactCurrency = (value: number) => {
+    if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
+    if (value >= 1000) return `${(value / 1000).toFixed(0)}k`;
+    return value.toString();
+  };
+
+  return (
+    <ResponsiveContainer width="100%" height={350}>
+      <BarChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }} barGap={1} barCategoryGap="20%">
+        <CartesianGrid strokeDasharray="3 3" vertical={false} className="stroke-border" />
+        <XAxis
+          dataKey="month"
+          className="text-xs fill-muted-foreground"
+          tick={{ fontSize: 11 }}
+          axisLine={false}
+          tickLine={false}
+        />
+        <YAxis
+          className="text-xs fill-muted-foreground"
+          tick={{ fontSize: 11 }}
+          tickFormatter={compactCurrency}
+          axisLine={false}
+          tickLine={false}
+          width={45}
+        />
+        <Tooltip
+          formatter={(value) => formatCurrency(Number(value))}
+          wrapperStyle={{ outline: "none" }}
+          contentStyle={{
+            backgroundColor: "var(--popover)",
+            border: "1px solid var(--border)",
+            borderRadius: "8px",
+            color: "var(--popover-foreground)",
+            fontSize: "13px",
+          }}
+          cursor={{ fill: "var(--muted)", opacity: 0.5 }}
+        />
+        <Legend
+          formatter={(value) => (
+            <span className="text-xs text-muted-foreground">{value}</span>
+          )}
+        />
+        <Bar dataKey="partsCost" name={labels?.partsCost ?? "Parts Cost"} fill="#3b82f6" stackId="cost" radius={[0, 0, 0, 0]} />
+        <Bar dataKey="laborCost" name={labels?.laborCost ?? "Labor Cost"} fill="#10b981" stackId="cost" radius={[4, 4, 0, 0]} />
+      </BarChart>
+    </ResponsiveContainer>
+  );
+}
+
+interface VehicleServiceTypeDonutProps {
+  data: VehicleServiceTypeBreakdown[];
+  formatCurrency: (value: number) => string;
+}
+
+export function VehicleServiceTypeDonut({ data, formatCurrency }: VehicleServiceTypeDonutProps) {
+  if (data.length === 0) return null;
+
+  return (
+    <ResponsiveContainer width="100%" height={300}>
+      <PieChart>
+        <Pie
+          data={data}
+          dataKey="totalCost"
+          nameKey="type"
+          cx="50%"
+          cy="50%"
+          innerRadius={60}
+          outerRadius={80}
+          paddingAngle={2}
+        >
+          {data.map((_, i) => (
+            <Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
+          ))}
+        </Pie>
+        <Tooltip
+          formatter={(value) => formatCurrency(Number(value))}
+          wrapperStyle={{ outline: "none" }}
+          contentStyle={{
+            backgroundColor: "var(--popover)",
+            border: "1px solid var(--border)",
+            borderRadius: "8px",
+            color: "var(--popover-foreground)",
+          }}
+        />
+        <Legend
+          formatter={(value) => (
+            <span className="text-xs text-muted-foreground">{value}</span>
+          )}
+        />
+      </PieChart>
+    </ResponsiveContainer>
+  );
+}

+ 20 - 0
src/features/reports/Components/csv-export.ts

@@ -9,6 +9,7 @@ import type {
   CustomerRetentionReport,
   TaxReport,
   PastDueInvoicesReport,
+  VehicleReportData,
 } from "../Schema/reportTypes";
 import { formatCurrency } from "@/lib/format";
 
@@ -185,6 +186,25 @@ export function exportPastDueInvoicesCsv(
   downloadCsv("past-due-invoices-report.csv", headers, rows, ["customer", "company", "invoiceNumber", "totalAmount", "amountPaid", "amountDue", "dueDate", "daysPastDue"]);
 }
 
+export function exportVehicleReportCsv(
+  data: VehicleReportData,
+  currencyCode: string,
+  headers: [string, string, string, string, string, string, string] = ["Date", "Title", "Type", "Status", "Total", "Parts", "Labor Hours"],
+) {
+  const fmt = (n: number) => formatCurrency(n, currencyCode);
+  const rows = data.serviceHistory.map((s) => ({
+    date: s.date,
+    title: s.title,
+    type: s.type,
+    status: s.status,
+    totalAmount: fmt(s.totalAmount),
+    partsCount: s.partsCount,
+    laborHours: s.laborHours.toFixed(1),
+  }));
+  const vehicleLabel = `${data.vehicleInfo.year} ${data.vehicleInfo.make} ${data.vehicleInfo.model}`;
+  downloadCsv(`vehicle-report-${vehicleLabel.replace(/\s+/g, "-")}.csv`, headers, rows, ["date", "title", "type", "status", "totalAmount", "partsCount", "laborHours"]);
+}
+
 export function exportTaxCsv(
   data: TaxReport,
   currencyCode: string,

+ 64 - 0
src/features/reports/Schema/reportTypes.ts

@@ -207,3 +207,67 @@ export interface TaxReport {
     totalInvoices: number;
   };
 }
+
+// --- Vehicle Reports ---
+
+export interface VehicleReportSummary {
+  totalServices: number;
+  totalCost: number;
+  totalPartsUsed: number;
+  totalLaborHours: number;
+  repairCount: number;
+  maintenanceCount: number;
+  upgradeCount: number;
+  inspectionCount: number;
+}
+
+export interface VehicleMonthlyCost {
+  month: string;
+  partsCost: number;
+  laborCost: number;
+  totalCost: number;
+  count: number;
+}
+
+export interface VehicleServiceTypeBreakdown {
+  type: string;
+  count: number;
+  totalCost: number;
+}
+
+export interface VehiclePartUsage {
+  name: string;
+  partNumber: string | null;
+  quantity: number;
+  totalCost: number;
+}
+
+export interface VehicleServiceHistoryItem {
+  id: string;
+  title: string;
+  type: string;
+  status: string;
+  date: string;
+  totalAmount: number;
+  partsCount: number;
+  laborHours: number;
+  techName: string | null;
+}
+
+export interface VehicleReportData {
+  vehicleInfo: {
+    id: string;
+    year: number;
+    make: string;
+    model: string;
+    vin: string | null;
+    licensePlate: string | null;
+    mileage: number;
+    customerName: string | null;
+  };
+  summary: VehicleReportSummary;
+  monthlyCosts: VehicleMonthlyCost[];
+  serviceTypeBreakdown: VehicleServiceTypeBreakdown[];
+  topParts: VehiclePartUsage[];
+  serviceHistory: VehicleServiceHistoryItem[];
+}