Преглед изворни кода

Add a past due invoice report to the reports page. (#102)

* workorder dates

* service details

* added set paid button in sidebar

* cleanup

* video upload

* lint

* public status report

* drawer

* translation

* translations

* translations

* cleanup

* translations

* report page

* hydration
Bernt Christian Egeland пре 6 месеци
родитељ
комит
d2dff797f5
93 измењених фајлова са 2241 додато и 1384 уклоњено
  1. 5 1
      messages/de/audit.json
  2. 4 1
      messages/de/laborPresets.json
  3. 40 3
      messages/de/reports.json
  4. 1 1
      messages/de/settings.json
  5. 2 0
      messages/de/share.json
  6. 18 2
      messages/de/statusReport.json
  7. 5 1
      messages/en/audit.json
  8. 40 3
      messages/en/reports.json
  9. 18 2
      messages/en/statusReport.json
  10. 5 1
      messages/es/audit.json
  11. 4 1
      messages/es/laborPresets.json
  12. 40 3
      messages/es/reports.json
  13. 1 1
      messages/es/settings.json
  14. 2 0
      messages/es/share.json
  15. 18 2
      messages/es/statusReport.json
  16. 5 1
      messages/fr/audit.json
  17. 4 1
      messages/fr/laborPresets.json
  18. 40 3
      messages/fr/reports.json
  19. 1 1
      messages/fr/settings.json
  20. 2 0
      messages/fr/share.json
  21. 18 2
      messages/fr/statusReport.json
  22. 5 1
      messages/it/audit.json
  23. 4 1
      messages/it/laborPresets.json
  24. 40 3
      messages/it/reports.json
  25. 1 1
      messages/it/settings.json
  26. 2 0
      messages/it/share.json
  27. 18 2
      messages/it/statusReport.json
  28. 5 1
      messages/nb/audit.json
  29. 4 1
      messages/nb/laborPresets.json
  30. 40 3
      messages/nb/reports.json
  31. 1 1
      messages/nb/settings.json
  32. 2 0
      messages/nb/share.json
  33. 18 2
      messages/nb/statusReport.json
  34. 5 1
      messages/nl/audit.json
  35. 40 3
      messages/nl/reports.json
  36. 1 1
      messages/nl/settings.json
  37. 2 0
      messages/nl/share.json
  38. 18 2
      messages/nl/statusReport.json
  39. 5 1
      messages/pl/audit.json
  40. 4 1
      messages/pl/laborPresets.json
  41. 40 3
      messages/pl/reports.json
  42. 1 1
      messages/pl/settings.json
  43. 2 0
      messages/pl/share.json
  44. 18 2
      messages/pl/statusReport.json
  45. 5 1
      messages/pt-BR/audit.json
  46. 4 1
      messages/pt-BR/laborPresets.json
  47. 40 3
      messages/pt-BR/reports.json
  48. 1 1
      messages/pt-BR/settings.json
  49. 2 0
      messages/pt-BR/share.json
  50. 18 2
      messages/pt-BR/statusReport.json
  51. 5 1
      messages/ru/audit.json
  52. 4 1
      messages/ru/laborPresets.json
  53. 40 3
      messages/ru/reports.json
  54. 1 1
      messages/ru/settings.json
  55. 2 0
      messages/ru/share.json
  56. 18 2
      messages/ru/statusReport.json
  57. 5 1
      messages/tr/audit.json
  58. 4 1
      messages/tr/laborPresets.json
  59. 40 3
      messages/tr/reports.json
  60. 1 1
      messages/tr/settings.json
  61. 2 0
      messages/tr/share.json
  62. 18 2
      messages/tr/statusReport.json
  63. 1 1
      src/app/(authenticated)/page.tsx
  64. 627 352
      src/app/(authenticated)/reports/reports-client.tsx
  65. 92 106
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  66. 2 9
      src/app/(public)/share/status-report/[orgId]/[token]/status-report-view.tsx
  67. 42 37
      src/app/api/protected/upload/inventory/route.ts
  68. 38 33
      src/app/api/protected/upload/quote-files/route.ts
  69. 42 37
      src/app/api/protected/upload/route.ts
  70. 0 73
      src/features/custom-fields/Components/CustomFieldsDisplay.tsx
  71. 0 17
      src/features/invoices/Schema/templateSchema.ts
  72. 94 0
      src/features/reports/Actions/reportActions.ts
  73. 20 0
      src/features/reports/Components/csv-export.ts
  74. 25 0
      src/features/reports/Schema/reportTypes.ts
  75. 2 1
      src/features/status-reports/Actions/cleanupExpiredReports.ts
  76. 56 0
      src/features/status-reports/Actions/deleteStatusReport.ts
  77. 5 0
      src/features/status-reports/Actions/getStatusReportsForService.ts
  78. 3 3
      src/features/status-reports/Actions/sendStatusReport.ts
  79. 35 1
      src/features/status-reports/Actions/submitFeedback.ts
  80. 225 42
      src/features/status-reports/Components/StatusReportList.tsx
  81. 1 1
      src/features/status-reports/Schema/statusReportSchema.ts
  82. 0 30
      src/features/team/Actions/getUserOrganizations.ts
  83. 0 122
      src/features/telegram/Components/SendTelegramDialog.tsx
  84. 0 23
      src/features/vehicles/Actions/deleteUploadedFile.ts
  85. 35 33
      src/features/vehicles/Actions/getMyActiveJobs.ts
  86. 48 64
      src/features/vehicles/Components/MyActiveJobs.tsx
  87. 0 63
      src/features/vehicles/Components/service-detail/ServiceNotesDisplay.tsx
  88. 0 97
      src/features/vehicles/Components/service-detail/ServiceSidebar.tsx
  89. 2 2
      src/features/vehicles/Components/service-edit/InvoiceDetailsSection.tsx
  90. 5 5
      src/features/vehicles/Components/service-edit/LaborEditor.tsx
  91. 76 121
      src/features/vehicles/Components/service-page/ServicePageClient.tsx
  92. 2 2
      src/features/vehicles/Components/service-page/UnifiedServiceHeader.tsx
  93. 4 24
      src/features/vehicles/Components/service-page/service-page-types.ts

+ 5 - 1
messages/de/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Zugriff verweigert",
     "labor_preset_create": "Arbeitspreset erstellt",
     "labor_preset_update": "Arbeitspreset aktualisiert",
-    "labor_preset_delete": "Arbeitspreset gelöscht"
+    "labor_preset_delete": "Arbeitspreset gelöscht",
+    "statusReport_create": "Statusbericht erstellt",
+    "statusReport_send": "Statusbericht gesendet",
+    "statusReport_delete": "Statusbericht gelöscht",
+    "telegram_send": "Telegram-Nachricht gesendet"
   }
 }

+ 4 - 1
messages/de/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Name",
     "description": "Beschreibung",
-    "itemCount": "Positionen"
+    "itemCount": "Positionen",
+    "type": "Typ",
+    "hrs": "Std.",
+    "units": "Einheit(en)"
   },
   "empty": {
     "noMatch": "Keine Vorlagen entsprechen Ihrer Suche.",

+ 40 - 3
messages/de/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Berichte & Analysen",
   "tabs": {
+    "financial": "Finanzberichte",
     "revenue": "Umsatz",
+    "pastDueInvoices": "Überfällige Rechnungen",
+    "tax": "Steuer",
     "services": "Dienstleistungen",
     "customers": "Kunden",
     "inventory": "Lagerbestand",
     "technicians": "Techniker",
     "parts": "Teile",
     "jobAnalytics": "Auftragsanalyse",
-    "retention": "Kundenbindung",
-    "tax": "Steuer"
+    "retention": "Kundenbindung"
   },
   "dateRange": {
     "to": "bis"
@@ -43,6 +45,33 @@
       "count": "Anzahl"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Gesamt überfällig",
+    "totalAmountDue": "Gesamtbetrag fällig",
+    "over30Days": "Über 30 Tage",
+    "over60Days": "Über 60 Tage",
+    "over90Days": "Über 90 Tage",
+    "totalPastDueDesc": "Rechnungen mit unbezahltem Saldo nach Fälligkeitsdatum",
+    "totalAmountDueDesc": "Gesamtbetrag aller überfälligen Rechnungen",
+    "filterByAge": "Nach Alter filtern",
+    "allPastDue": "Alle überfälligen",
+    "30plus": "30+ Tage",
+    "60plus": "60+ Tage",
+    "90plus": "90+ Tage",
+    "pastDueInvoicesList": "Überfällige Rechnungen",
+    "noInvoices": "Keine überfälligen Rechnungen gefunden.",
+    "tableHeaders": {
+      "customer": "Kunde",
+      "company": "Unternehmen",
+      "invoiceNumber": "Rechnungsnr.",
+      "vehicle": "Fahrzeug",
+      "totalAmount": "Gesamtbetrag",
+      "amountPaid": "Bezahlter Betrag",
+      "amountDue": "Fälliger Betrag",
+      "dueDate": "Fälligkeitsdatum",
+      "daysPastDue": "Tage überfällig"
+    }
+  },
   "services": {
     "totalServices": "Gesamtleistungen",
     "byStatus": "Nach Status",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Ø Tage zwischen Besuchen",
     "taxCollected": "Eingezogene Steuer",
     "taxableAmount": "Steuerpflichtiger Betrag",
-    "invoiceCount": "Rechnungsanzahl"
+    "invoiceCount": "Rechnungsanzahl",
+    "invoiceNumber": "Rechnungsnr.",
+    "totalAmount": "Gesamtbetrag",
+    "amountPaid": "Bezahlter Betrag",
+    "amountDue": "Fälliger Betrag",
+    "dueDate": "Fälligkeitsdatum",
+    "daysPastDue": "Tage überfällig",
+    "partsCost": "Teilekosten",
+    "netProfit": "Nettogewinn"
   },
   "empty": "Noch keine Daten geladen. Klicken Sie auf Aktualisieren, um den Bericht zu laden."
 }

+ 1 - 1
messages/de/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Logo wird hochgeladen...",
     "logoUpdated": "Logo aktualisiert",
     "failedUploadLogo": "Logo-Upload fehlgeschlagen",
-    "readMore": "Read More",
     "detailsTitle": "Firmendetails",
     "shopName": "Firmen-/Werkstattname",
     "shopNamePlaceholder": "Meiers Autowerkstatt",
@@ -530,6 +529,7 @@
     "layoutDocQuote": "Angebot"
   },
   "payment": {
+    "readMore": "Mehr erfahren",
     "detailsTitle": "Zahlungsdetails",
     "detailsDescription": "Bankkonto und Zahlungsbedingungen auf Rechnungen.",
     "bankAccount": "Bankkonto (IBAN / Til Konto)",

+ 2 - 0
messages/de/share.json

@@ -35,6 +35,7 @@
     "hours": "Stunden",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Einheit / Stunden",
     "rate": "Stundensatz",
     "ratePerHour": "{rate}/Std.",
     "subtotal": "Zwischensumme",
@@ -76,6 +77,7 @@
     "hours": "Stunden",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Einheit / Stunden",
     "rate": "Stundensatz",
     "ratePerHour": "{rate}/Std.",
     "subtotal": "Zwischensumme",

+ 18 - 2
messages/de/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Gesendet",
     "viewed": "Angesehen",
     "expires": "Läuft ab",
-    "expired": "Abgelaufen"
+    "expired": "Abgelaufen",
+    "statusLabel": "Status",
+    "created": "Erstellt",
+    "delete": "Löschen",
+    "deleteConfirm": "Möchten Sie diesen Statusbericht wirklich löschen?",
+    "deleted": "Statusbericht gelöscht",
+    "deleteTitle": "Statusbericht löschen",
+    "deleteFailed": "Statusbericht konnte nicht gelöscht werden",
+    "cancel": "Abbrechen",
+  "feedback": "Feedback",
+  "hasComment": "Erhalten",
+  "noFeedback": "Noch kein Feedback",
+  "sentAt": "Gesendet",
+  "sentVia": "Gesendet über",
+  "messageLabel": "Nachricht",
+  "videoLabel": "Video"
   },
   "notify": {
     "defaultMessage": "Hallo {name}, wir haben ein Status-Update zu Ihrem {vehicle}. Hier ansehen: {url}",
-    "emailSubject": "Fahrzeug-Statusbericht - {vehicle}"
+    "emailSubject": "Fahrzeug-Statusbericht - {vehicle}",
+  "viewReport": "Bericht hier ansehen: {url}"
   }
 }

+ 5 - 1
messages/en/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Permission Denied",
     "labor_preset_create": "Created Labor Preset",
     "labor_preset_update": "Updated Labor Preset",
-    "labor_preset_delete": "Deleted Labor Preset"
+    "labor_preset_delete": "Deleted Labor Preset",
+    "statusReport_create": "Created Status Report",
+    "statusReport_send": "Sent Status Report",
+    "statusReport_delete": "Deleted Status Report",
+    "telegram_send": "Sent Telegram Message"
   }
 }

+ 40 - 3
messages/en/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Reports & Analytics",
   "tabs": {
+    "financial": "Financial Reports",
     "revenue": "Revenue",
+    "pastDueInvoices": "Past Due Invoices",
+    "tax": "Tax",
     "services": "Services",
     "customers": "Customers",
     "inventory": "Inventory",
     "technicians": "Technicians",
     "parts": "Parts",
     "jobAnalytics": "Job Analytics",
-    "retention": "Retention",
-    "tax": "Tax"
+    "retention": "Retention"
   },
   "dateRange": {
     "to": "to"
@@ -43,6 +45,33 @@
       "count": "Count"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Total Past Due",
+    "totalAmountDue": "Total Amount Due",
+    "over30Days": "Over 30 Days",
+    "over60Days": "Over 60 Days",
+    "over90Days": "Over 90 Days",
+    "totalPastDueDesc": "Invoices with unpaid balance past due date",
+    "totalAmountDueDesc": "Total outstanding amount across all past due invoices",
+    "filterByAge": "Filter by age",
+    "allPastDue": "All Past Due",
+    "30plus": "30+ Days",
+    "60plus": "60+ Days",
+    "90plus": "90+ Days",
+    "pastDueInvoicesList": "Past Due Invoices",
+    "noInvoices": "No past due invoices found.",
+    "tableHeaders": {
+      "customer": "Customer",
+      "company": "Company",
+      "invoiceNumber": "Invoice #",
+      "vehicle": "Vehicle",
+      "totalAmount": "Total Amount",
+      "amountPaid": "Amount Paid",
+      "amountDue": "Amount Due",
+      "dueDate": "Due Date",
+      "daysPastDue": "Days Past Due"
+    }
+  },
   "services": {
     "totalServices": "Total Services",
     "byStatus": "By Status",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Avg Days Between Visits",
     "taxCollected": "Tax Collected",
     "taxableAmount": "Taxable Amount",
-    "invoiceCount": "Invoice Count"
+    "invoiceCount": "Invoice Count",
+    "invoiceNumber": "Invoice #",
+    "totalAmount": "Total Amount",
+    "amountPaid": "Amount Paid",
+    "amountDue": "Amount Due",
+    "dueDate": "Due Date",
+    "daysPastDue": "Days Past Due",
+    "partsCost": "Parts Cost",
+    "netProfit": "Net Profit"
   },
   "empty": "No data loaded yet. Click Refresh to load the report."
 }

+ 18 - 2
messages/en/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Sent",
     "viewed": "Viewed",
     "expires": "Expires",
-    "expired": "Expired"
+    "expired": "Expired",
+    "statusLabel": "Status",
+    "created": "Created",
+    "delete": "Delete",
+    "deleteConfirm": "Are you sure you want to delete this status report?",
+    "deleted": "Status report deleted",
+    "deleteTitle": "Delete Status Report",
+    "deleteFailed": "Failed to delete status report",
+    "cancel": "Cancel",
+  "feedback": "Feedback",
+  "hasComment": "Received",
+  "noFeedback": "No feedback yet",
+  "sentAt": "Sent",
+  "sentVia": "Sent via",
+  "messageLabel": "Message",
+  "videoLabel": "Video"
   },
   "notify": {
     "defaultMessage": "Hi {name}, we have a status update on your {vehicle}. View it here: {url}",
-    "emailSubject": "Vehicle Status Update - {vehicle}"
+    "emailSubject": "Vehicle Status Update - {vehicle}",
+    "viewReport": "View the report here: {url}"
   }
 }

+ 5 - 1
messages/es/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Permiso denegado",
     "labor_preset_create": "Preset de mano de obra creado",
     "labor_preset_update": "Preset de mano de obra actualizado",
-    "labor_preset_delete": "Preset de mano de obra eliminado"
+    "labor_preset_delete": "Preset de mano de obra eliminado",
+    "statusReport_create": "Informe de estado creado",
+    "statusReport_send": "Informe de estado enviado",
+    "statusReport_delete": "Informe de estado eliminado",
+    "telegram_send": "Mensaje de Telegram enviado"
   }
 }

+ 4 - 1
messages/es/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Nombre",
     "description": "Descripción",
-    "itemCount": "Elementos"
+    "itemCount": "Elementos",
+    "type": "Tipo",
+    "hrs": "hrs",
+    "units": "unidad(es)"
   },
   "empty": {
     "noMatch": "Ninguna plantilla coincide con su búsqueda.",

+ 40 - 3
messages/es/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Informes y Análisis",
   "tabs": {
+    "financial": "Informes Financieros",
     "revenue": "Ingresos",
+    "pastDueInvoices": "Facturas Vencidas",
+    "tax": "Impuestos",
     "services": "Servicios",
     "customers": "Clientes",
     "inventory": "Inventario",
     "technicians": "Técnicos",
     "parts": "Piezas",
     "jobAnalytics": "Análisis de Trabajos",
-    "retention": "Retención",
-    "tax": "Impuestos"
+    "retention": "Retención"
   },
   "dateRange": {
     "to": "a"
@@ -43,6 +45,33 @@
       "count": "Cantidad"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Total vencidas",
+    "totalAmountDue": "Monto total adeudado",
+    "over30Days": "Más de 30 días",
+    "over60Days": "Más de 60 días",
+    "over90Days": "Más de 90 días",
+    "totalPastDueDesc": "Facturas con saldo pendiente después de la fecha de vencimiento",
+    "totalAmountDueDesc": "Monto total pendiente de todas las facturas vencidas",
+    "filterByAge": "Filtrar por antigüedad",
+    "allPastDue": "Todas las vencidas",
+    "30plus": "30+ días",
+    "60plus": "60+ días",
+    "90plus": "90+ días",
+    "pastDueInvoicesList": "Facturas Vencidas",
+    "noInvoices": "No se encontraron facturas vencidas.",
+    "tableHeaders": {
+      "customer": "Cliente",
+      "company": "Empresa",
+      "invoiceNumber": "N.º Factura",
+      "vehicle": "Vehículo",
+      "totalAmount": "Monto Total",
+      "amountPaid": "Monto Pagado",
+      "amountDue": "Monto Adeudado",
+      "dueDate": "Fecha de Vencimiento",
+      "daysPastDue": "Días Vencidos"
+    }
+  },
   "services": {
     "totalServices": "Total de Servicios",
     "byStatus": "Por Estado",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Prom. Días Entre Visitas",
     "taxCollected": "Impuesto Recaudado",
     "taxableAmount": "Monto Gravable",
-    "invoiceCount": "Cant. de Facturas"
+    "invoiceCount": "Cant. de Facturas",
+    "invoiceNumber": "N.º Factura",
+    "totalAmount": "Monto Total",
+    "amountPaid": "Monto Pagado",
+    "amountDue": "Monto Adeudado",
+    "dueDate": "Fecha de Vencimiento",
+    "daysPastDue": "Días Vencidos",
+    "partsCost": "Coste de piezas",
+    "netProfit": "Beneficio neto"
   },
   "empty": "Aún no se han cargado datos. Haga clic en Actualizar para cargar el informe."
 }

+ 1 - 1
messages/es/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Subiendo logo...",
     "logoUpdated": "Logo actualizado",
     "failedUploadLogo": "Error al subir el logo",
-    "readMore": "Read More",
     "detailsTitle": "Datos de la empresa",
     "shopName": "Nombre de la empresa / taller",
     "shopNamePlaceholder": "Taller Mecanico Lopez",
@@ -530,6 +529,7 @@
     "layoutDocQuote": "Presupuesto"
   },
   "payment": {
+    "readMore": "Leer más",
     "detailsTitle": "Detalles de pago",
     "detailsDescription": "Cuenta bancaria y condiciones de pago en las facturas.",
     "bankAccount": "Cuenta bancaria (IBAN / Til Konto)",

+ 2 - 0
messages/es/share.json

@@ -35,6 +35,7 @@
     "hours": "Horas",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Unidad / Horas",
     "rate": "Tarifa",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
@@ -76,6 +77,7 @@
     "hours": "Horas",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Unidad / Horas",
     "rate": "Tarifa",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",

+ 18 - 2
messages/es/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Enviado",
     "viewed": "Visto",
     "expires": "Expira",
-    "expired": "Expirado"
+    "expired": "Expirado",
+    "statusLabel": "Estado",
+    "created": "Creado",
+    "delete": "Eliminar",
+    "deleteConfirm": "¿Está seguro de que desea eliminar este informe de estado?",
+    "deleted": "Informe de estado eliminado",
+    "deleteTitle": "Eliminar informe de estado",
+    "deleteFailed": "No se pudo eliminar el informe de estado",
+    "cancel": "Cancelar",
+  "feedback": "Comentario",
+  "hasComment": "Recibido",
+  "noFeedback": "Sin comentarios aún",
+  "sentAt": "Enviado",
+  "sentVia": "Enviado por",
+  "messageLabel": "Mensaje",
+  "videoLabel": "Video"
   },
   "notify": {
     "defaultMessage": "Hola {name}, tenemos una actualización de estado de su {vehicle}. Véala aquí: {url}",
-    "emailSubject": "Actualización de estado del vehículo - {vehicle}"
+    "emailSubject": "Actualización de estado del vehículo - {vehicle}",
+  "viewReport": "Ver el informe aquí: {url}"
   }
 }

+ 5 - 1
messages/fr/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Permission refusée",
     "labor_preset_create": "Préréglage de main-d'œuvre créé",
     "labor_preset_update": "Préréglage de main-d'œuvre mis à jour",
-    "labor_preset_delete": "Préréglage de main-d'œuvre supprimé"
+    "labor_preset_delete": "Préréglage de main-d'œuvre supprimé",
+    "statusReport_create": "Rapport d'état créé",
+    "statusReport_send": "Rapport d'état envoyé",
+    "statusReport_delete": "Rapport d'état supprimé",
+    "telegram_send": "Message Telegram envoyé"
   }
 }

+ 4 - 1
messages/fr/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Nom",
     "description": "Description",
-    "itemCount": "Éléments"
+    "itemCount": "Éléments",
+    "type": "Type",
+    "hrs": "h",
+    "units": "unité(s)"
   },
   "empty": {
     "noMatch": "Aucun modèle ne correspond à votre recherche.",

+ 40 - 3
messages/fr/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Rapports et Analyses",
   "tabs": {
+    "financial": "Rapports Financiers",
     "revenue": "Revenus",
+    "pastDueInvoices": "Factures en Retard",
+    "tax": "Taxes",
     "services": "Services",
     "customers": "Clients",
     "inventory": "Inventaire",
     "technicians": "Techniciens",
     "parts": "Pièces",
     "jobAnalytics": "Analyse des Travaux",
-    "retention": "Fidélisation",
-    "tax": "Taxes"
+    "retention": "Fidélisation"
   },
   "dateRange": {
     "to": "au"
@@ -43,6 +45,33 @@
       "count": "Nombre"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Total en retard",
+    "totalAmountDue": "Montant total dû",
+    "over30Days": "Plus de 30 jours",
+    "over60Days": "Plus de 60 jours",
+    "over90Days": "Plus de 90 jours",
+    "totalPastDueDesc": "Factures avec solde impayé après la date d'échéance",
+    "totalAmountDueDesc": "Montant total impayé de toutes les factures en retard",
+    "filterByAge": "Filtrer par ancienneté",
+    "allPastDue": "Toutes en retard",
+    "30plus": "30+ jours",
+    "60plus": "60+ jours",
+    "90plus": "90+ jours",
+    "pastDueInvoicesList": "Factures en Retard",
+    "noInvoices": "Aucune facture en retard trouvée.",
+    "tableHeaders": {
+      "customer": "Client",
+      "company": "Entreprise",
+      "invoiceNumber": "N° Facture",
+      "vehicle": "Véhicule",
+      "totalAmount": "Montant Total",
+      "amountPaid": "Montant Payé",
+      "amountDue": "Montant Dû",
+      "dueDate": "Date d'Échéance",
+      "daysPastDue": "Jours de Retard"
+    }
+  },
   "services": {
     "totalServices": "Total des Services",
     "byStatus": "Par Statut",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Moy. Jours Entre Visites",
     "taxCollected": "Taxe Perçue",
     "taxableAmount": "Montant Taxable",
-    "invoiceCount": "Nombre de Factures"
+    "invoiceCount": "Nombre de Factures",
+    "invoiceNumber": "N° Facture",
+    "totalAmount": "Montant Total",
+    "amountPaid": "Montant Payé",
+    "amountDue": "Montant Dû",
+    "dueDate": "Date d'Échéance",
+    "daysPastDue": "Jours de Retard",
+    "partsCost": "Coût des pièces",
+    "netProfit": "Bénéfice net"
   },
   "empty": "Aucune donnée chargée. Cliquez sur Actualiser pour charger le rapport."
 }

+ 1 - 1
messages/fr/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Telechargement du logo...",
     "logoUpdated": "Logo mis a jour",
     "failedUploadLogo": "Echec du telechargement du logo",
-    "readMore": "Read More",
     "detailsTitle": "Details de l'entreprise",
     "shopName": "Nom de l'entreprise / atelier",
     "shopNamePlaceholder": "Garage Auto Martin",
@@ -530,6 +529,7 @@
     "layoutDocQuote": "Devis"
   },
   "payment": {
+    "readMore": "En savoir plus",
     "detailsTitle": "Details de paiement",
     "detailsDescription": "Compte bancaire et conditions de paiement sur les factures.",
     "bankAccount": "Compte bancaire (IBAN / Til Konto)",

+ 2 - 0
messages/fr/share.json

@@ -35,6 +35,7 @@
     "hours": "Heures",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Unité / Heures",
     "rate": "Taux",
     "ratePerHour": "{rate}/h",
     "subtotal": "Sous-total",
@@ -76,6 +77,7 @@
     "hours": "Heures",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Unité / Heures",
     "rate": "Taux",
     "ratePerHour": "{rate}/h",
     "subtotal": "Sous-total",

+ 18 - 2
messages/fr/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Envoyé",
     "viewed": "Consulté",
     "expires": "Expire",
-    "expired": "Expiré"
+    "expired": "Expiré",
+    "statusLabel": "Statut",
+    "created": "Créé",
+    "delete": "Supprimer",
+    "deleteConfirm": "Êtes-vous sûr de vouloir supprimer ce rapport d'état ?",
+    "deleted": "Rapport d'état supprimé",
+    "deleteTitle": "Supprimer le rapport d'état",
+    "deleteFailed": "Échec de la suppression du rapport d'état",
+    "cancel": "Annuler",
+  "feedback": "Commentaire",
+  "hasComment": "Reçu",
+  "noFeedback": "Aucun commentaire",
+  "sentAt": "Envoyé",
+  "sentVia": "Envoyé via",
+  "messageLabel": "Message",
+  "videoLabel": "Vidéo"
   },
   "notify": {
     "defaultMessage": "Bonjour {name}, nous avons une mise à jour de l'état de votre {vehicle}. Consultez-la ici : {url}",
-    "emailSubject": "Mise à jour de l'état du véhicule - {vehicle}"
+    "emailSubject": "Mise à jour de l'état du véhicule - {vehicle}",
+  "viewReport": "Voir le rapport ici : {url}"
   }
 }

+ 5 - 1
messages/it/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Permesso negato",
     "labor_preset_create": "Preset manodopera creato",
     "labor_preset_update": "Preset manodopera aggiornato",
-    "labor_preset_delete": "Preset manodopera eliminato"
+    "labor_preset_delete": "Preset manodopera eliminato",
+    "statusReport_create": "Rapporto di stato creato",
+    "statusReport_send": "Rapporto di stato inviato",
+    "statusReport_delete": "Rapporto di stato eliminato",
+    "telegram_send": "Messaggio Telegram inviato"
   }
 }

+ 4 - 1
messages/it/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Nome",
     "description": "Descrizione",
-    "itemCount": "Voci"
+    "itemCount": "Voci",
+    "type": "Tipo",
+    "hrs": "ore",
+    "units": "unità"
   },
   "empty": {
     "noMatch": "Nessun modello corrisponde alla ricerca.",

+ 40 - 3
messages/it/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Report e Analisi",
   "tabs": {
+    "financial": "Report Finanziari",
     "revenue": "Ricavi",
+    "pastDueInvoices": "Fatture Scadute",
+    "tax": "Tasse",
     "services": "Servizi",
     "customers": "Clienti",
     "inventory": "Inventario",
     "technicians": "Tecnici",
     "parts": "Ricambi",
     "jobAnalytics": "Analisi Lavori",
-    "retention": "Fidelizzazione",
-    "tax": "Tasse"
+    "retention": "Fidelizzazione"
   },
   "dateRange": {
     "to": "a"
@@ -43,6 +45,33 @@
       "count": "Conteggio"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Totale scadute",
+    "totalAmountDue": "Importo totale dovuto",
+    "over30Days": "Oltre 30 giorni",
+    "over60Days": "Oltre 60 giorni",
+    "over90Days": "Oltre 90 giorni",
+    "totalPastDueDesc": "Fatture con saldo non pagato oltre la data di scadenza",
+    "totalAmountDueDesc": "Importo totale in sospeso di tutte le fatture scadute",
+    "filterByAge": "Filtra per anzianità",
+    "allPastDue": "Tutte le scadute",
+    "30plus": "30+ giorni",
+    "60plus": "60+ giorni",
+    "90plus": "90+ giorni",
+    "pastDueInvoicesList": "Fatture Scadute",
+    "noInvoices": "Nessuna fattura scaduta trovata.",
+    "tableHeaders": {
+      "customer": "Cliente",
+      "company": "Azienda",
+      "invoiceNumber": "N. Fattura",
+      "vehicle": "Veicolo",
+      "totalAmount": "Importo Totale",
+      "amountPaid": "Importo Pagato",
+      "amountDue": "Importo Dovuto",
+      "dueDate": "Data di Scadenza",
+      "daysPastDue": "Giorni di Ritardo"
+    }
+  },
   "services": {
     "totalServices": "Totale Servizi",
     "byStatus": "Per Stato",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Media Giorni Tra Visite",
     "taxCollected": "Tassa Riscossa",
     "taxableAmount": "Importo Tassabile",
-    "invoiceCount": "Numero Fatture"
+    "invoiceCount": "Numero Fatture",
+    "invoiceNumber": "N. Fattura",
+    "totalAmount": "Importo Totale",
+    "amountPaid": "Importo Pagato",
+    "amountDue": "Importo Dovuto",
+    "dueDate": "Data di Scadenza",
+    "daysPastDue": "Giorni di Ritardo",
+    "partsCost": "Costo ricambi",
+    "netProfit": "Utile netto"
   },
   "empty": "Nessun dato caricato. Clicca su Aggiorna per caricare il report."
 }

+ 1 - 1
messages/it/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Caricamento logo...",
     "logoUpdated": "Logo aggiornato",
     "failedUploadLogo": "Caricamento logo non riuscito",
-    "readMore": "Read More",
     "detailsTitle": "Dettagli aziendali",
     "shopName": "Nome azienda / officina",
     "shopNamePlaceholder": "Autofficina di Mario",
@@ -530,6 +529,7 @@
     "layoutDocQuote": "Preventivo"
   },
   "payment": {
+    "readMore": "Scopri di più",
     "detailsTitle": "Dettagli pagamento",
     "detailsDescription": "Conto bancario e termini di pagamento mostrati sulle fatture.",
     "bankAccount": "Conto bancario (IBAN / Til Konto)",

+ 2 - 0
messages/it/share.json

@@ -35,6 +35,7 @@
     "hours": "Ore",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Unità / Ore",
     "rate": "Tariffa",
     "ratePerHour": "{rate}/ora",
     "subtotal": "Subtotale",
@@ -76,6 +77,7 @@
     "hours": "Ore",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Unità / Ore",
     "rate": "Tariffa",
     "ratePerHour": "{rate}/ora",
     "subtotal": "Subtotale",

+ 18 - 2
messages/it/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Inviato",
     "viewed": "Visualizzato",
     "expires": "Scade",
-    "expired": "Scaduto"
+    "expired": "Scaduto",
+    "statusLabel": "Stato",
+    "created": "Creato",
+    "delete": "Elimina",
+    "deleteConfirm": "Sei sicuro di voler eliminare questo rapporto di stato?",
+    "deleted": "Rapporto di stato eliminato",
+    "deleteTitle": "Elimina rapporto di stato",
+    "deleteFailed": "Impossibile eliminare il rapporto di stato",
+    "cancel": "Annulla",
+  "feedback": "Commento",
+  "hasComment": "Ricevuto",
+  "noFeedback": "Nessun commento",
+  "sentAt": "Inviato",
+  "sentVia": "Inviato tramite",
+  "messageLabel": "Messaggio",
+  "videoLabel": "Video"
   },
   "notify": {
     "defaultMessage": "Ciao {name}, abbiamo un aggiornamento sullo stato del suo {vehicle}. Visualizzalo qui: {url}",
-    "emailSubject": "Aggiornamento stato veicolo - {vehicle}"
+    "emailSubject": "Aggiornamento stato veicolo - {vehicle}",
+  "viewReport": "Visualizza il rapporto qui: {url}"
   }
 }

+ 5 - 1
messages/nb/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Tilgang nektet",
     "labor_preset_create": "Opprettet arbeidspreset",
     "labor_preset_update": "Oppdaterte arbeidspreset",
-    "labor_preset_delete": "Slettet arbeidspreset"
+    "labor_preset_delete": "Slettet arbeidspreset",
+    "statusReport_create": "Opprettet statusrapport",
+    "statusReport_send": "Sendt statusrapport",
+    "statusReport_delete": "Slettet statusrapport",
+    "telegram_send": "Sendt Telegram-melding"
   }
 }

+ 4 - 1
messages/nb/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Navn",
     "description": "Beskrivelse",
-    "itemCount": "Elementer"
+    "itemCount": "Elementer",
+    "type": "Type",
+    "hrs": "timer",
+    "units": "enhet(er)"
   },
   "empty": {
     "noMatch": "Ingen maler samsvarer med søket ditt.",

+ 40 - 3
messages/nb/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Rapporter og Analyser",
   "tabs": {
+    "financial": "Finansrapporter",
     "revenue": "Inntekter",
+    "pastDueInvoices": "Forfalte Fakturaer",
+    "tax": "Skatt",
     "services": "Tjenester",
     "customers": "Kunder",
     "inventory": "Lagerbeholdning",
     "technicians": "Teknikere",
     "parts": "Deler",
     "jobAnalytics": "Jobbanalyse",
-    "retention": "Kundelojalitet",
-    "tax": "Skatt"
+    "retention": "Kundelojalitet"
   },
   "dateRange": {
     "to": "til"
@@ -43,6 +45,33 @@
       "count": "Antall"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Totalt forfalte",
+    "totalAmountDue": "Totalt utestående beløp",
+    "over30Days": "Over 30 dager",
+    "over60Days": "Over 60 dager",
+    "over90Days": "Over 90 dager",
+    "totalPastDueDesc": "Fakturaer med ubetalt saldo etter forfallsdato",
+    "totalAmountDueDesc": "Totalt utestående beløp for alle forfalte fakturaer",
+    "filterByAge": "Filtrer etter alder",
+    "allPastDue": "Alle forfalte",
+    "30plus": "30+ dager",
+    "60plus": "60+ dager",
+    "90plus": "90+ dager",
+    "pastDueInvoicesList": "Forfalte Fakturaer",
+    "noInvoices": "Ingen forfalte fakturaer funnet.",
+    "tableHeaders": {
+      "customer": "Kunde",
+      "company": "Firma",
+      "invoiceNumber": "Fakturanr.",
+      "vehicle": "Kjøretøy",
+      "totalAmount": "Totalbeløp",
+      "amountPaid": "Betalt Beløp",
+      "amountDue": "Utestående Beløp",
+      "dueDate": "Forfallsdato",
+      "daysPastDue": "Dager Forfalt"
+    }
+  },
   "services": {
     "totalServices": "Totalt Tjenester",
     "byStatus": "Etter Status",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Gj.sn. Dager Mellom Besøk",
     "taxCollected": "Skatt Innkrevd",
     "taxableAmount": "Skattepliktig Beløp",
-    "invoiceCount": "Antall Fakturaer"
+    "invoiceCount": "Antall Fakturaer",
+    "invoiceNumber": "Fakturanr.",
+    "totalAmount": "Totalbeløp",
+    "amountPaid": "Betalt Beløp",
+    "amountDue": "Utestående Beløp",
+    "dueDate": "Forfallsdato",
+    "daysPastDue": "Dager Forfalt",
+    "partsCost": "Delekostnad",
+    "netProfit": "Nettoresultat"
   },
   "empty": "Ingen data lastet ennå. Klikk Oppdater for å laste rapporten."
 }

+ 1 - 1
messages/nb/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Laster opp logo...",
     "logoUpdated": "Logo oppdatert",
     "failedUploadLogo": "Kunne ikke laste opp logo",
-    "readMore": "Read More",
     "detailsTitle": "Bedriftsdetaljer",
     "shopName": "Bedrift / Verkstednavn",
     "shopNamePlaceholder": "Hansens Bilverksted",
@@ -530,6 +529,7 @@
     }
   },
   "payment": {
+    "readMore": "Les mer",
     "detailsTitle": "Betalingsdetaljer",
     "detailsDescription": "Bankkonto og betalingsvilkar vist pa fakturaer.",
     "bankAccount": "Bankkonto (IBAN / Til Konto)",

+ 2 - 0
messages/nb/share.json

@@ -35,6 +35,7 @@
     "hours": "Timer",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Enhet / Timer",
     "rate": "Sats",
     "ratePerHour": "{rate}/t",
     "subtotal": "Delsum",
@@ -76,6 +77,7 @@
     "hours": "Timer",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Enhet / Timer",
     "rate": "Sats",
     "ratePerHour": "{rate}/t",
     "subtotal": "Delsum",

+ 18 - 2
messages/nb/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Sendt",
     "viewed": "Sett",
     "expires": "Utløper",
-    "expired": "Utløpt"
+    "expired": "Utløpt",
+    "statusLabel": "Status",
+    "created": "Opprettet",
+    "delete": "Slett",
+    "deleteConfirm": "Er du sikker på at du vil slette denne statusrapporten?",
+    "deleted": "Statusrapport slettet",
+    "deleteTitle": "Slett statusrapport",
+    "deleteFailed": "Kunne ikke slette statusrapport",
+    "cancel": "Avbryt",
+  "feedback": "Tilbakemelding",
+  "hasComment": "Mottatt",
+  "noFeedback": "Ingen tilbakemelding ennå",
+  "sentAt": "Sendt",
+  "sentVia": "Sendt via",
+  "messageLabel": "Melding",
+  "videoLabel": "Video"
   },
   "notify": {
     "defaultMessage": "Hei {name}, vi har en statusoppdatering på din {vehicle}. Se den her: {url}",
-    "emailSubject": "Statusoppdatering for kjøretøy - {vehicle}"
+    "emailSubject": "Statusoppdatering for kjøretøy - {vehicle}",
+  "viewReport": "Se rapporten her: {url}"
   }
 }

+ 5 - 1
messages/nl/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Toegang geweigerd",
     "labor_preset_create": "Arbeidspreset aangemaakt",
     "labor_preset_update": "Arbeidspreset bijgewerkt",
-    "labor_preset_delete": "Arbeidspreset verwijderd"
+    "labor_preset_delete": "Arbeidspreset verwijderd",
+    "statusReport_create": "Statusrapport aangemaakt",
+    "statusReport_send": "Statusrapport verzonden",
+    "statusReport_delete": "Statusrapport verwijderd",
+    "telegram_send": "Telegram-bericht verzonden"
   }
 }

+ 40 - 3
messages/nl/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Rapporten & Analyses",
   "tabs": {
+    "financial": "Financiële Rapporten",
     "revenue": "Omzet",
+    "pastDueInvoices": "Achterstallige Facturen",
+    "tax": "Belasting",
     "services": "Diensten",
     "customers": "Klanten",
     "inventory": "Voorraad",
     "technicians": "Technici",
     "parts": "Onderdelen",
     "jobAnalytics": "Opdrachtanalyse",
-    "retention": "Klantbehoud",
-    "tax": "Belasting"
+    "retention": "Klantbehoud"
   },
   "dateRange": {
     "to": "tot"
@@ -43,6 +45,33 @@
       "count": "Aantal"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Totaal achterstallig",
+    "totalAmountDue": "Totaal verschuldigd bedrag",
+    "over30Days": "Meer dan 30 dagen",
+    "over60Days": "Meer dan 60 dagen",
+    "over90Days": "Meer dan 90 dagen",
+    "totalPastDueDesc": "Facturen met onbetaald saldo na vervaldatum",
+    "totalAmountDueDesc": "Totaal openstaand bedrag van alle achterstallige facturen",
+    "filterByAge": "Filteren op ouderdom",
+    "allPastDue": "Alle achterstallige",
+    "30plus": "30+ dagen",
+    "60plus": "60+ dagen",
+    "90plus": "90+ dagen",
+    "pastDueInvoicesList": "Achterstallige Facturen",
+    "noInvoices": "Geen achterstallige facturen gevonden.",
+    "tableHeaders": {
+      "customer": "Klant",
+      "company": "Bedrijf",
+      "invoiceNumber": "Factuurnr.",
+      "vehicle": "Voertuig",
+      "totalAmount": "Totaalbedrag",
+      "amountPaid": "Betaald Bedrag",
+      "amountDue": "Verschuldigd Bedrag",
+      "dueDate": "Vervaldatum",
+      "daysPastDue": "Dagen Achterstallig"
+    }
+  },
   "services": {
     "totalServices": "Totaal Diensten",
     "byStatus": "Per Status",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Gem. Dagen Tussen Bezoeken",
     "taxCollected": "Geïnde Belasting",
     "taxableAmount": "Belastbaar Bedrag",
-    "invoiceCount": "Aantal Facturen"
+    "invoiceCount": "Aantal Facturen",
+    "invoiceNumber": "Factuurnr.",
+    "totalAmount": "Totaalbedrag",
+    "amountPaid": "Betaald Bedrag",
+    "amountDue": "Verschuldigd Bedrag",
+    "dueDate": "Vervaldatum",
+    "daysPastDue": "Dagen Achterstallig",
+    "partsCost": "Onderdeelkosten",
+    "netProfit": "Nettowinst"
   },
   "empty": "Nog geen gegevens geladen. Klik op Vernieuwen om het rapport te laden."
 }

+ 1 - 1
messages/nl/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Logo uploaden...",
     "logoUpdated": "Logo bijgewerkt",
     "failedUploadLogo": "Logo uploaden mislukt",
-    "readMore": "Read More",
     "detailsTitle": "Bedrijfsgegevens",
     "shopName": "Bedrijfs-/werkplaatsnaam",
     "shopNamePlaceholder": "Jansens Autogarage",
@@ -530,6 +529,7 @@
     "layoutDocQuote": "Offerte"
   },
   "payment": {
+    "readMore": "Lees meer",
     "detailsTitle": "Betalingsgegevens",
     "detailsDescription": "Bankrekening en betalingsvoorwaarden op facturen.",
     "bankAccount": "Bankrekening (IBAN / Til Konto)",

+ 2 - 0
messages/nl/share.json

@@ -35,6 +35,7 @@
     "hours": "Uren",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Eenheid / Uren",
     "rate": "Tarief",
     "ratePerHour": "{rate}/uur",
     "subtotal": "Subtotaal",
@@ -76,6 +77,7 @@
     "hours": "Uren",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Eenheid / Uren",
     "rate": "Tarief",
     "ratePerHour": "{rate}/uur",
     "subtotal": "Subtotaal",

+ 18 - 2
messages/nl/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Verzonden",
     "viewed": "Bekeken",
     "expires": "Verloopt",
-    "expired": "Verlopen"
+    "expired": "Verlopen",
+    "statusLabel": "Status",
+    "created": "Aangemaakt",
+    "delete": "Verwijderen",
+    "deleteConfirm": "Weet u zeker dat u dit statusrapport wilt verwijderen?",
+    "deleted": "Statusrapport verwijderd",
+    "deleteTitle": "Statusrapport verwijderen",
+    "deleteFailed": "Kan statusrapport niet verwijderen",
+    "cancel": "Annuleren",
+  "feedback": "Feedback",
+  "hasComment": "Ontvangen",
+  "noFeedback": "Nog geen feedback",
+  "sentAt": "Verzonden",
+  "sentVia": "Verzonden via",
+  "messageLabel": "Bericht",
+  "videoLabel": "Video"
   },
   "notify": {
     "defaultMessage": "Hallo {name}, we hebben een statusupdate over uw {vehicle}. Bekijk het hier: {url}",
-    "emailSubject": "Voertuig statusupdate - {vehicle}"
+    "emailSubject": "Voertuig statusupdate - {vehicle}",
+  "viewReport": "Bekijk het rapport hier: {url}"
   }
 }

+ 5 - 1
messages/pl/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Odmowa dostępu",
     "labor_preset_create": "Utworzono preset robocizny",
     "labor_preset_update": "Zaktualizowano preset robocizny",
-    "labor_preset_delete": "Usunięto preset robocizny"
+    "labor_preset_delete": "Usunięto preset robocizny",
+    "statusReport_create": "Utworzono raport statusu",
+    "statusReport_send": "Wysłano raport statusu",
+    "statusReport_delete": "Usunięto raport statusu",
+    "telegram_send": "Wysłano wiadomość Telegram"
   }
 }

+ 4 - 1
messages/pl/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Nazwa",
     "description": "Opis",
-    "itemCount": "Pozycje"
+    "itemCount": "Pozycje",
+    "type": "Typ",
+    "hrs": "godz.",
+    "units": "szt."
   },
   "empty": {
     "noMatch": "Żadne szablony nie pasują do wyszukiwania.",

+ 40 - 3
messages/pl/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Raporty i Analizy",
   "tabs": {
+    "financial": "Raporty Finansowe",
     "revenue": "Przychody",
+    "pastDueInvoices": "Przeterminowane Faktury",
+    "tax": "Podatki",
     "services": "Usługi",
     "customers": "Klienci",
     "inventory": "Magazyn",
     "technicians": "Technicy",
     "parts": "Części",
     "jobAnalytics": "Analiza Zleceń",
-    "retention": "Utrzymanie Klientów",
-    "tax": "Podatki"
+    "retention": "Utrzymanie Klientów"
   },
   "dateRange": {
     "to": "do"
@@ -43,6 +45,33 @@
       "count": "Ilość"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Łącznie przeterminowanych",
+    "totalAmountDue": "Łączna kwota zaległa",
+    "over30Days": "Ponad 30 dni",
+    "over60Days": "Ponad 60 dni",
+    "over90Days": "Ponad 90 dni",
+    "totalPastDueDesc": "Faktury z nieopłaconym saldem po terminie płatności",
+    "totalAmountDueDesc": "Łączna zaległa kwota ze wszystkich przeterminowanych faktur",
+    "filterByAge": "Filtruj wg okresu",
+    "allPastDue": "Wszystkie przeterminowane",
+    "30plus": "30+ dni",
+    "60plus": "60+ dni",
+    "90plus": "90+ dni",
+    "pastDueInvoicesList": "Przeterminowane Faktury",
+    "noInvoices": "Nie znaleziono przeterminowanych faktur.",
+    "tableHeaders": {
+      "customer": "Klient",
+      "company": "Firma",
+      "invoiceNumber": "Nr Faktury",
+      "vehicle": "Pojazd",
+      "totalAmount": "Kwota Całkowita",
+      "amountPaid": "Kwota Zapłacona",
+      "amountDue": "Kwota Zaległa",
+      "dueDate": "Termin Płatności",
+      "daysPastDue": "Dni Przeterminowania"
+    }
+  },
   "services": {
     "totalServices": "Łączna Liczba Usług",
     "byStatus": "Wg Statusu",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Śr. Dni Między Wizytami",
     "taxCollected": "Zebrany Podatek",
     "taxableAmount": "Kwota Opodatkowana",
-    "invoiceCount": "Liczba Faktur"
+    "invoiceCount": "Liczba Faktur",
+    "invoiceNumber": "Nr Faktury",
+    "totalAmount": "Kwota Całkowita",
+    "amountPaid": "Kwota Zapłacona",
+    "amountDue": "Kwota Zaległa",
+    "dueDate": "Termin Płatności",
+    "daysPastDue": "Dni Przeterminowania",
+    "partsCost": "Koszt części",
+    "netProfit": "Zysk netto"
   },
   "empty": "Nie załadowano jeszcze danych. Kliknij Odśwież, aby załadować raport."
 }

+ 1 - 1
messages/pl/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Przesylanie logo...",
     "logoUpdated": "Logo zaktualizowane",
     "failedUploadLogo": "Nie udalo sie przeslac logo",
-    "readMore": "Read More",
     "detailsTitle": "Dane firmy",
     "shopName": "Nazwa firmy / warsztatu",
     "shopNamePlaceholder": "Warsztat Samochodowy Kowalski",
@@ -530,6 +529,7 @@
     "layoutDocQuote": "Wycena"
   },
   "payment": {
+    "readMore": "Czytaj więcej",
     "detailsTitle": "Szczegoly platnosci",
     "detailsDescription": "Konto bankowe i warunki platnosci wyswietlane na fakturach.",
     "bankAccount": "Konto bankowe (IBAN / Til Konto)",

+ 2 - 0
messages/pl/share.json

@@ -35,6 +35,7 @@
     "hours": "Godziny",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Jednostka / Godziny",
     "rate": "Stawka",
     "ratePerHour": "{rate}/godz.",
     "subtotal": "Suma częściowa",
@@ -76,6 +77,7 @@
     "hours": "Godziny",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Jednostka / Godziny",
     "rate": "Stawka",
     "ratePerHour": "{rate}/godz.",
     "subtotal": "Suma częściowa",

+ 18 - 2
messages/pl/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Wysłany",
     "viewed": "Wyświetlony",
     "expires": "Wygasa",
-    "expired": "Wygasł"
+    "expired": "Wygasł",
+    "statusLabel": "Status",
+    "created": "Utworzono",
+    "delete": "Usuń",
+    "deleteConfirm": "Czy na pewno chcesz usunąć ten raport statusu?",
+    "deleted": "Raport statusu usunięty",
+    "deleteTitle": "Usuń raport statusu",
+    "deleteFailed": "Nie udało się usunąć raportu statusu",
+    "cancel": "Anuluj",
+  "feedback": "Komentarz",
+  "hasComment": "Otrzymano",
+  "noFeedback": "Brak komentarzy",
+  "sentAt": "Wysłano",
+  "sentVia": "Wysłano przez",
+  "messageLabel": "Wiadomość",
+  "videoLabel": "Wideo"
   },
   "notify": {
     "defaultMessage": "Witaj {name}, mamy aktualizację statusu Twojego {vehicle}. Zobacz tutaj: {url}",
-    "emailSubject": "Aktualizacja statusu pojazdu - {vehicle}"
+    "emailSubject": "Aktualizacja statusu pojazdu - {vehicle}",
+  "viewReport": "Zobacz raport tutaj: {url}"
   }
 }

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

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Permissão negada",
     "labor_preset_create": "Preset de mão de obra criado",
     "labor_preset_update": "Preset de mão de obra atualizado",
-    "labor_preset_delete": "Preset de mão de obra excluído"
+    "labor_preset_delete": "Preset de mão de obra excluído",
+    "statusReport_create": "Relatório de status criado",
+    "statusReport_send": "Relatório de status enviado",
+    "statusReport_delete": "Relatório de status excluído",
+    "telegram_send": "Mensagem do Telegram enviada"
   }
 }

+ 4 - 1
messages/pt-BR/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Nome",
     "description": "Descrição",
-    "itemCount": "Itens"
+    "itemCount": "Itens",
+    "type": "Tipo",
+    "hrs": "hrs",
+    "units": "unidade(s)"
   },
   "empty": {
     "noMatch": "Nenhum modelo corresponde à sua pesquisa.",

+ 40 - 3
messages/pt-BR/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Relatórios e Análises",
   "tabs": {
+    "financial": "Relatórios Financeiros",
     "revenue": "Receita",
+    "pastDueInvoices": "Faturas Vencidas",
+    "tax": "Impostos",
     "services": "Serviços",
     "customers": "Clientes",
     "inventory": "Estoque",
     "technicians": "Técnicos",
     "parts": "Peças",
     "jobAnalytics": "Análise de Trabalhos",
-    "retention": "Retenção",
-    "tax": "Impostos"
+    "retention": "Retenção"
   },
   "dateRange": {
     "to": "até"
@@ -43,6 +45,33 @@
       "count": "Quantidade"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Total vencidas",
+    "totalAmountDue": "Valor total em atraso",
+    "over30Days": "Mais de 30 dias",
+    "over60Days": "Mais de 60 dias",
+    "over90Days": "Mais de 90 dias",
+    "totalPastDueDesc": "Faturas com saldo pendente após a data de vencimento",
+    "totalAmountDueDesc": "Valor total pendente de todas as faturas vencidas",
+    "filterByAge": "Filtrar por período",
+    "allPastDue": "Todas as vencidas",
+    "30plus": "30+ dias",
+    "60plus": "60+ dias",
+    "90plus": "90+ dias",
+    "pastDueInvoicesList": "Faturas Vencidas",
+    "noInvoices": "Nenhuma fatura vencida encontrada.",
+    "tableHeaders": {
+      "customer": "Cliente",
+      "company": "Empresa",
+      "invoiceNumber": "N.º Fatura",
+      "vehicle": "Veículo",
+      "totalAmount": "Valor Total",
+      "amountPaid": "Valor Pago",
+      "amountDue": "Valor em Atraso",
+      "dueDate": "Data de Vencimento",
+      "daysPastDue": "Dias em Atraso"
+    }
+  },
   "services": {
     "totalServices": "Total de Serviços",
     "byStatus": "Por Status",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Méd. Dias Entre Visitas",
     "taxCollected": "Imposto Arrecadado",
     "taxableAmount": "Valor Tributável",
-    "invoiceCount": "Quantidade de Faturas"
+    "invoiceCount": "Quantidade de Faturas",
+    "invoiceNumber": "N.º Fatura",
+    "totalAmount": "Valor Total",
+    "amountPaid": "Valor Pago",
+    "amountDue": "Valor em Atraso",
+    "dueDate": "Data de Vencimento",
+    "daysPastDue": "Dias em Atraso",
+    "partsCost": "Custo de peças",
+    "netProfit": "Lucro líquido"
   },
   "empty": "Nenhum dado carregado ainda. Clique em Atualizar para carregar o relatório."
 }

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

@@ -118,7 +118,6 @@
     "uploadingLogo": "Enviando logo...",
     "logoUpdated": "Logo atualizado",
     "failedUploadLogo": "Falha ao enviar o logo",
-    "readMore": "Read More",
     "detailsTitle": "Detalhes da empresa",
     "shopName": "Nome da empresa / oficina",
     "shopNamePlaceholder": "Auto Mecanica Silva",
@@ -530,6 +529,7 @@
     "layoutDocQuote": "Orcamento"
   },
   "payment": {
+    "readMore": "Saiba mais",
     "detailsTitle": "Detalhes de pagamento",
     "detailsDescription": "Conta bancaria e condicoes de pagamento nas faturas.",
     "bankAccount": "Conta bancaria (IBAN / Til Konto)",

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

@@ -35,6 +35,7 @@
     "hours": "Horas",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Unidade / Horas",
     "rate": "Taxa",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
@@ -76,6 +77,7 @@
     "hours": "Horas",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Unidade / Horas",
     "rate": "Taxa",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",

+ 18 - 2
messages/pt-BR/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Enviado",
     "viewed": "Visualizado",
     "expires": "Expira",
-    "expired": "Expirado"
+    "expired": "Expirado",
+    "statusLabel": "Status",
+    "created": "Criado",
+    "delete": "Excluir",
+    "deleteConfirm": "Tem certeza de que deseja excluir este relatório de status?",
+    "deleted": "Relatório de status excluído",
+    "deleteTitle": "Excluir relatório de status",
+    "deleteFailed": "Falha ao excluir relatório de status",
+    "cancel": "Cancelar",
+  "feedback": "Comentário",
+  "hasComment": "Recebido",
+  "noFeedback": "Sem comentários ainda",
+  "sentAt": "Enviado",
+  "sentVia": "Enviado por",
+  "messageLabel": "Mensagem",
+  "videoLabel": "Vídeo"
   },
   "notify": {
     "defaultMessage": "Olá {name}, temos uma atualização de status do seu {vehicle}. Veja aqui: {url}",
-    "emailSubject": "Atualização de status do veículo - {vehicle}"
+    "emailSubject": "Atualização de status do veículo - {vehicle}",
+  "viewReport": "Veja o relatório aqui: {url}"
   }
 }

+ 5 - 1
messages/ru/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "Доступ запрещён",
     "labor_preset_create": "Создан шаблон работ",
     "labor_preset_update": "Обновлён шаблон работ",
-    "labor_preset_delete": "Удалён шаблон работ"
+    "labor_preset_delete": "Удалён шаблон работ",
+    "statusReport_create": "Создан отчёт о состоянии",
+    "statusReport_send": "Отправлен отчёт о состоянии",
+    "statusReport_delete": "Удалён отчёт о состоянии",
+    "telegram_send": "Отправлено сообщение в Telegram"
   }
 }

+ 4 - 1
messages/ru/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Название",
     "description": "Описание",
-    "itemCount": "Позиции"
+    "itemCount": "Позиции",
+    "type": "Тип",
+    "hrs": "ч.",
+    "units": "шт."
   },
   "empty": {
     "noMatch": "Шаблоны не найдены.",

+ 40 - 3
messages/ru/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Отчёты и аналитика",
   "tabs": {
+    "financial": "Финансовые отчёты",
     "revenue": "Доход",
+    "pastDueInvoices": "Просроченные счета",
+    "tax": "Налоги",
     "services": "Обслуживания",
     "customers": "Клиенты",
     "inventory": "Склад",
     "technicians": "Механики",
     "parts": "Запчасти",
     "jobAnalytics": "Аналитика работ",
-    "retention": "Удержание",
-    "tax": "Налоги"
+    "retention": "Удержание"
   },
   "dateRange": {
     "to": "до"
@@ -43,6 +45,33 @@
       "count": "Количество"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Всего просроченных",
+    "totalAmountDue": "Общая сумма задолженности",
+    "over30Days": "Более 30 дней",
+    "over60Days": "Более 60 дней",
+    "over90Days": "Более 90 дней",
+    "totalPastDueDesc": "Счета с неоплаченным остатком после даты оплаты",
+    "totalAmountDueDesc": "Общая сумма задолженности по всем просроченным счетам",
+    "filterByAge": "Фильтр по сроку",
+    "allPastDue": "Все просроченные",
+    "30plus": "30+ дней",
+    "60plus": "60+ дней",
+    "90plus": "90+ дней",
+    "pastDueInvoicesList": "Просроченные Счета",
+    "noInvoices": "Просроченные счета не найдены.",
+    "tableHeaders": {
+      "customer": "Клиент",
+      "company": "Компания",
+      "invoiceNumber": "№ Счёта",
+      "vehicle": "Транспорт",
+      "totalAmount": "Общая сумма",
+      "amountPaid": "Оплачено",
+      "amountDue": "Задолженность",
+      "dueDate": "Срок оплаты",
+      "daysPastDue": "Дней просрочки"
+    }
+  },
   "services": {
     "totalServices": "Всего обслуживаний",
     "byStatus": "По статусу",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Средний интервал между визитами (дни)",
     "taxCollected": "Собрано налога",
     "taxableAmount": "Налогооблагаемая сумма",
-    "invoiceCount": "Количество счетов"
+    "invoiceCount": "Количество счетов",
+    "invoiceNumber": "№ Счёта",
+    "totalAmount": "Общая сумма",
+    "amountPaid": "Оплачено",
+    "amountDue": "Задолженность",
+    "dueDate": "Срок оплаты",
+    "daysPastDue": "Дней просрочки",
+    "partsCost": "Стоимость запчастей",
+    "netProfit": "Чистая прибыль"
   },
   "empty": "Данные ещё не загружены. Нажмите «Обновить» для загрузки отчёта."
 }

+ 1 - 1
messages/ru/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Загрузка логотипа...",
     "logoUpdated": "Логотип обновлён",
     "failedUploadLogo": "Не удалось загрузить логотип",
-    "readMore": "Read More",
     "detailsTitle": "Данные компании",
     "shopName": "Название компании / мастерской",
     "shopNamePlaceholder": "Автосервис Ивана",
@@ -530,6 +529,7 @@
     }
   },
   "payment": {
+    "readMore": "Подробнее",
     "detailsTitle": "Платёжные реквизиты",
     "detailsDescription": "Банковский счёт и условия оплаты, отображаемые на счетах.",
     "bankAccount": "Банковский счёт (IBAN)",

+ 2 - 0
messages/ru/share.json

@@ -35,6 +35,7 @@
     "hours": "Часы",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Единица / Часы",
     "rate": "Ставка",
     "ratePerHour": "{rate}/ч",
     "subtotal": "Подитог",
@@ -76,6 +77,7 @@
     "hours": "Часы",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Единица / Часы",
     "rate": "Ставка",
     "ratePerHour": "{rate}/ч",
     "subtotal": "Подитог",

+ 18 - 2
messages/ru/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Отправлен",
     "viewed": "Просмотрен",
     "expires": "Истекает",
-    "expired": "Истёк"
+    "expired": "Истёк",
+    "statusLabel": "Статус",
+    "created": "Создано",
+    "delete": "Удалить",
+    "deleteConfirm": "Вы уверены, что хотите удалить этот отчёт о состоянии?",
+    "deleted": "Отчёт о состоянии удалён",
+    "deleteTitle": "Удалить отчёт о состоянии",
+    "deleteFailed": "Не удалось удалить отчёт о состоянии",
+    "cancel": "Отмена",
+  "feedback": "Отзыв",
+  "hasComment": "Получен",
+  "noFeedback": "Отзывов пока нет",
+  "sentAt": "Отправлено",
+  "sentVia": "Отправлено через",
+  "messageLabel": "Сообщение",
+  "videoLabel": "Видео"
   },
   "notify": {
     "defaultMessage": "Здравствуйте, {name}, у нас есть обновление по статусу вашего {vehicle}. Посмотрите здесь: {url}",
-    "emailSubject": "Обновление статуса транспортного средства - {vehicle}"
+    "emailSubject": "Обновление статуса транспортного средства - {vehicle}",
+  "viewReport": "Посмотреть отчёт здесь: {url}"
   }
 }

+ 5 - 1
messages/tr/audit.json

@@ -84,6 +84,10 @@
     "auth_permissionDenied": "İzin reddedildi",
     "labor_preset_create": "İşçilik ön ayarı oluşturuldu",
     "labor_preset_update": "İşçilik ön ayarı güncellendi",
-    "labor_preset_delete": "İşçilik ön ayarı silindi"
+    "labor_preset_delete": "İşçilik ön ayarı silindi",
+    "statusReport_create": "Durum raporu oluşturuldu",
+    "statusReport_send": "Durum raporu gönderildi",
+    "statusReport_delete": "Durum raporu silindi",
+    "telegram_send": "Telegram mesajı gönderildi"
   }
 }

+ 4 - 1
messages/tr/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
     "name": "Ad",
     "description": "Açıklama",
-    "itemCount": "Kalemler"
+    "itemCount": "Kalemler",
+    "type": "Tür",
+    "hrs": "saat",
+    "units": "adet"
   },
   "empty": {
     "noMatch": "Aramanızla eşleşen şablon bulunamadı.",

+ 40 - 3
messages/tr/reports.json

@@ -1,15 +1,17 @@
 {
   "title": "Raporlar ve Analizler",
   "tabs": {
+    "financial": "Finansal Raporlar",
     "revenue": "Gelir",
+    "pastDueInvoices": "Vadesi Geçmiş Faturalar",
+    "tax": "Vergi",
     "services": "Hizmetler",
     "customers": "Müşteriler",
     "inventory": "Envanter",
     "technicians": "Teknisyenler",
     "parts": "Parçalar",
     "jobAnalytics": "İş Analizi",
-    "retention": "Müşteri Sadakati",
-    "tax": "Vergi"
+    "retention": "Müşteri Sadakati"
   },
   "dateRange": {
     "to": "-"
@@ -43,6 +45,33 @@
       "count": "Adet"
     }
   },
+  "pastDueInvoices": {
+    "totalPastDue": "Toplam vadesi geçmiş",
+    "totalAmountDue": "Toplam borç tutarı",
+    "over30Days": "30 günden fazla",
+    "over60Days": "60 günden fazla",
+    "over90Days": "90 günden fazla",
+    "totalPastDueDesc": "Vade tarihinden sonra ödenmemiş bakiyesi olan faturalar",
+    "totalAmountDueDesc": "Tüm vadesi geçmiş faturaların toplam ödenmemiş tutarı",
+    "filterByAge": "Süreye göre filtrele",
+    "allPastDue": "Tüm vadesi geçmişler",
+    "30plus": "30+ gün",
+    "60plus": "60+ gün",
+    "90plus": "90+ gün",
+    "pastDueInvoicesList": "Vadesi Geçmiş Faturalar",
+    "noInvoices": "Vadesi geçmiş fatura bulunamadı.",
+    "tableHeaders": {
+      "customer": "Müşteri",
+      "company": "Şirket",
+      "invoiceNumber": "Fatura No",
+      "vehicle": "Araç",
+      "totalAmount": "Toplam Tutar",
+      "amountPaid": "Ödenen Tutar",
+      "amountDue": "Borç Tutarı",
+      "dueDate": "Vade Tarihi",
+      "daysPastDue": "Gecikme Günü"
+    }
+  },
   "services": {
     "totalServices": "Toplam Hizmetler",
     "byStatus": "Duruma Göre",
@@ -203,7 +232,15 @@
     "avgDaysBetweenVisits": "Ort. Ziyaret Arası Gün",
     "taxCollected": "Tahsil Edilen Vergi",
     "taxableAmount": "Vergilendirilebilir Tutar",
-    "invoiceCount": "Fatura Sayısı"
+    "invoiceCount": "Fatura Sayısı",
+    "invoiceNumber": "Fatura No",
+    "totalAmount": "Toplam Tutar",
+    "amountPaid": "Ödenen Tutar",
+    "amountDue": "Borç Tutarı",
+    "dueDate": "Vade Tarihi",
+    "daysPastDue": "Gecikme Günü",
+    "partsCost": "Parça maliyeti",
+    "netProfit": "Net kâr"
   },
   "empty": "Henüz veri yüklenmedi. Raporu yüklemek için Yenile'ye tıklayın."
 }

+ 1 - 1
messages/tr/settings.json

@@ -118,7 +118,6 @@
     "uploadingLogo": "Logo yükleniyor...",
     "logoUpdated": "Logo güncellendi",
     "failedUploadLogo": "Logo yüklenemedi",
-    "readMore": "Read More",
     "detailsTitle": "Şirket Bilgileri",
     "shopName": "Şirket / Atölye Adı",
     "shopNamePlaceholder": "Ali'nin Oto Tamir",
@@ -530,6 +529,7 @@
     "layoutDocQuote": "Teklif"
   },
   "payment": {
+    "readMore": "Devamını oku",
     "detailsTitle": "Ödeme Bilgileri",
     "detailsDescription": "Faturalarda gösterilen banka hesabı ve ödeme koşulları.",
     "bankAccount": "Banka Hesabı (IBAN / Til Konto)",

+ 2 - 0
messages/tr/share.json

@@ -35,6 +35,7 @@
     "hours": "Saat",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Birim / Saat",
     "rate": "Ücret",
     "ratePerHour": "{rate}/sa",
     "subtotal": "Ara Toplam",
@@ -76,6 +77,7 @@
     "hours": "Saat",
     "hrs": "hrs",
     "unit": "unit",
+    "qtyOrHours": "Birim / Saat",
     "rate": "Ücret",
     "ratePerHour": "{rate}/sa",
     "subtotal": "Ara Toplam",

+ 18 - 2
messages/tr/statusReport.json

@@ -66,10 +66,26 @@
     "sent": "Gönderildi",
     "viewed": "Görüntülendi",
     "expires": "Son kullanma",
-    "expired": "Süresi dolmuş"
+    "expired": "Süresi dolmuş",
+    "statusLabel": "Durum",
+    "created": "Oluşturuldu",
+    "delete": "Sil",
+    "deleteConfirm": "Bu durum raporunu silmek istediğinizden emin misiniz?",
+    "deleted": "Durum raporu silindi",
+    "deleteTitle": "Durum raporunu sil",
+    "deleteFailed": "Durum raporu silinemedi",
+    "cancel": "İptal",
+  "feedback": "Geri Bildirim",
+  "hasComment": "Alındı",
+  "noFeedback": "Henüz geri bildirim yok",
+  "sentAt": "Gönderildi",
+  "sentVia": "Gönderim yolu",
+  "messageLabel": "Mesaj",
+  "videoLabel": "Video"
   },
   "notify": {
     "defaultMessage": "Merhaba {name}, {vehicle} aracınız hakkında bir durum güncellemesi var. Buradan görüntüleyin: {url}",
-    "emailSubject": "Araç Durum Güncellemesi - {vehicle}"
+    "emailSubject": "Araç Durum Güncellemesi - {vehicle}",
+  "viewReport": "Raporu buradan görüntüleyin: {url}"
   }
 }

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

@@ -64,7 +64,7 @@ export default async function DashboardPage() {
     <>
       <PageHeader />
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
-        <MyActiveJobs jobs={myJobs} />
+        <MyActiveJobs jobs={myJobs} smsEnabled={features?.sms ?? false} emailEnabled={features?.smtp ?? false} telegramEnabled={features?.telegram ?? false} />
         <DashboardClient
           stats={result.data}
           currencyCode={currencyCode}

+ 627 - 352
src/app/(authenticated)/reports/reports-client.tsx

@@ -20,6 +20,13 @@ import {
 } from "@/components/ui/table";
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { Badge } from "@/components/ui/badge";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
 import {
   Loader2,
   BarChart3,
@@ -34,6 +41,9 @@ import {
   CalendarDays,
   UserCheck,
   Receipt,
+  Clock,
+  ArrowUpDown,
+  Landmark,
 } from "lucide-react";
 import {
   getRevenueReport,
@@ -45,6 +55,7 @@ import {
   getJobAnalyticsReport,
   getCustomerRetentionReport,
   getTaxReport,
+  getPastDueInvoicesReport,
 } from "@/features/reports/Actions/reportActions";
 import { formatCurrency } from "@/lib/format";
 import type {
@@ -57,6 +68,7 @@ import type {
   JobAnalyticsReport,
   CustomerRetentionReport,
   TaxReport,
+  PastDueInvoicesReport,
 } from "@/features/reports/Schema/reportTypes";
 import { RevenueBarChart, RevenueTypeDonut } from "@/features/reports/Components/RevenueCharts";
 import { ServiceStatusChart, ServiceTypeDonut } from "@/features/reports/Components/ServiceCharts";
@@ -76,9 +88,13 @@ import {
   exportJobAnalyticsCsv,
   exportRetentionCsv,
   exportTaxCsv,
+  exportPastDueInvoicesCsv,
 } from "@/features/reports/Components/csv-export";
 
-type ReportTab = "revenue" | "services" | "customers" | "inventory" | "technicians" | "parts" | "job-analytics" | "retention" | "tax";
+type ReportTab = "financial" | "services" | "customers" | "inventory" | "technicians" | "parts" | "job-analytics" | "retention";
+type FinancialSubTab = "revenue" | "past-due-invoices" | "tax";
+type PastDueSortKey = "customerName" | "amountDue" | "daysPastDue";
+type PastDueSortDir = "asc" | "desc";
 
 interface ReportsClientProps {
   currencyCode: string;
@@ -90,11 +106,17 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
   const defaultStart = `${currentYear}-01-01`;
   const defaultEnd = new Date().toISOString().split("T")[0];
 
-  const [activeTab, setActiveTab] = useState<ReportTab>("revenue");
+  const [activeTab, setActiveTab] = useState<ReportTab>("financial");
+  const [financialSubTab, setFinancialSubTab] = useState<FinancialSubTab>("revenue");
   const [startDate, setStartDate] = useState(defaultStart);
   const [endDate, setEndDate] = useState(defaultEnd);
   const [loading, setLoading] = useState(false);
 
+  // Past due invoices state
+  const [pastDueFilter, setPastDueFilter] = useState<string>("all");
+  const [pastDueSortKey, setPastDueSortKey] = useState<PastDueSortKey>("daysPastDue");
+  const [pastDueSortDir, setPastDueSortDir] = useState<PastDueSortDir>("desc");
+
   const [revenueData, setRevenueData] = useState<RevenueReport | null>(null);
   const [serviceData, setServiceData] = useState<ServiceReport | null>(null);
   const [customerData, setCustomerData] = useState<CustomerReport | null>(null);
@@ -104,14 +126,17 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
   const [jobAnalyticsData, setJobAnalyticsData] = useState<JobAnalyticsReport | null>(null);
   const [retentionData, setRetentionData] = useState<CustomerRetentionReport | null>(null);
   const [taxData, setTaxData] = useState<TaxReport | null>(null);
+  const [pastDueData, setPastDueData] = useState<PastDueInvoicesReport | null>(null);
 
   const fmtCurrency = useCallback(
     (value: number) => formatCurrency(value, currencyCode),
     [currencyCode],
   );
 
+  type FetchableReport = ReportTab | FinancialSubTab;
+
   const fetchReport = useCallback(
-    async (type: ReportTab) => {
+    async (type: FetchableReport) => {
       setLoading(true);
       try {
         const dateParams = { startDate, endDate };
@@ -121,6 +146,16 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
             if (result.success && result.data) setRevenueData(result.data);
             break;
           }
+          case "past-due-invoices": {
+            const result = await getPastDueInvoicesReport();
+            if (result.success && result.data) setPastDueData(result.data);
+            break;
+          }
+          case "tax": {
+            const result = await getTaxReport(dateParams);
+            if (result.success && result.data) setTaxData(result.data);
+            break;
+          }
           case "services": {
             const result = await getServiceReport(dateParams);
             if (result.success && result.data) setServiceData(result.data);
@@ -156,11 +191,6 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
             if (result.success && result.data) setRetentionData(result.data);
             break;
           }
-          case "tax": {
-            const result = await getTaxReport(dateParams);
-            if (result.success && result.data) setTaxData(result.data);
-            break;
-          }
         }
       } catch (error) {
         console.error("Failed to fetch report:", error);
@@ -179,82 +209,151 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
   const handleTabChange = (value: string) => {
     const tab = value as ReportTab;
     setActiveTab(tab);
-    const dataMap: Record<ReportTab, unknown> = {
+    if (tab === "financial") {
+      // Fetch the current financial sub-tab if not loaded
+      const subDataMap: Record<FinancialSubTab, unknown> = {
+        revenue: revenueData,
+        "past-due-invoices": pastDueData,
+        tax: taxData,
+      };
+      if (!subDataMap[financialSubTab]) {
+        fetchReport(financialSubTab);
+      }
+    } else {
+      const dataMap: Record<string, unknown> = {
+        services: serviceData,
+        customers: customerData,
+        inventory: inventoryData,
+        technicians: technicianData,
+        parts: partsData,
+        "job-analytics": jobAnalyticsData,
+        retention: retentionData,
+      };
+      if (!dataMap[tab]) {
+        fetchReport(tab);
+      }
+    }
+  };
+
+  const handleFinancialSubTabChange = (value: string) => {
+    const subTab = value as FinancialSubTab;
+    setFinancialSubTab(subTab);
+    const subDataMap: Record<FinancialSubTab, unknown> = {
       revenue: revenueData,
-      services: serviceData,
-      customers: customerData,
-      inventory: inventoryData,
-      technicians: technicianData,
-      parts: partsData,
-      "job-analytics": jobAnalyticsData,
-      retention: retentionData,
+      "past-due-invoices": pastDueData,
       tax: taxData,
     };
-    if (!dataMap[tab]) {
-      fetchReport(tab);
+    if (!subDataMap[subTab]) {
+      fetchReport(subTab);
     }
   };
 
   const handleRefresh = () => {
-    fetchReport(activeTab);
+    if (activeTab === "financial") {
+      fetchReport(financialSubTab);
+    } else {
+      fetchReport(activeTab);
+    }
   };
 
   const handleExport = () => {
     const h = (key: string) => t(`csvHeaders.${key}`);
-    switch (activeTab) {
-      case "revenue":
-        if (revenueData) exportRevenueCsv(revenueData, currencyCode, [h("month"), h("revenue"), h("collected"), h("count"), h("partsCost"), h("netProfit")]);
-        break;
-      case "services":
-        if (serviceData) exportServicesCsv(serviceData, [h("category"), h("label"), h("count")]);
-        break;
-      case "customers":
-        if (customerData) exportCustomersCsv(customerData, currencyCode, [h("name"), h("company"), h("services"), h("totalSpent")]);
-        break;
-      case "inventory":
-        if (inventoryData) exportInventoryCsv(inventoryData, currencyCode, [h("name"), h("partNumber"), h("quantity"), h("minQuantity"), h("unitCost")]);
-        break;
-      case "technicians":
-        if (technicianData) exportTechniciansCsv(technicianData, currencyCode, [h("technician"), h("jobs"), h("totalRevenue"), h("avgRevenue"), h("totalHours"), h("avgHours")]);
-        break;
-      case "parts":
-        if (partsData) exportPartsCsv(partsData, currencyCode, [h("partName"), h("partNumber"), h("usageCount"), h("totalQty"), h("totalRevenue")]);
-        break;
-      case "job-analytics":
-        if (jobAnalyticsData) exportJobAnalyticsCsv(jobAnalyticsData, currencyCode, [h("serviceType"), h("count"), h("avgValue"), h("avgHours")]);
-        break;
-      case "retention":
-        if (retentionData) exportRetentionCsv(retentionData, currencyCode, [h("customer"), h("company"), h("visits"), h("totalSpent"), h("avgDaysBetweenVisits")]);
-        break;
-      case "tax":
-        if (taxData) exportTaxCsv(taxData, currencyCode, [h("month"), h("taxCollected"), h("taxableAmount"), h("invoiceCount")]);
-        break;
+    if (activeTab === "financial") {
+      switch (financialSubTab) {
+        case "revenue":
+          if (revenueData) exportRevenueCsv(revenueData, currencyCode, [h("month"), h("revenue"), h("collected"), h("count"), h("partsCost"), h("netProfit")]);
+          break;
+        case "past-due-invoices":
+          if (pastDueData) exportPastDueInvoicesCsv(pastDueData, currencyCode, [h("customer"), h("company"), h("invoiceNumber"), h("totalAmount"), h("amountPaid"), h("amountDue"), h("dueDate"), h("daysPastDue")]);
+          break;
+        case "tax":
+          if (taxData) exportTaxCsv(taxData, currencyCode, [h("month"), h("taxCollected"), h("taxableAmount"), h("invoiceCount")]);
+          break;
+      }
+    } else {
+      switch (activeTab) {
+        case "services":
+          if (serviceData) exportServicesCsv(serviceData, [h("category"), h("label"), h("count")]);
+          break;
+        case "customers":
+          if (customerData) exportCustomersCsv(customerData, currencyCode, [h("name"), h("company"), h("services"), h("totalSpent")]);
+          break;
+        case "inventory":
+          if (inventoryData) exportInventoryCsv(inventoryData, currencyCode, [h("name"), h("partNumber"), h("quantity"), h("minQuantity"), h("unitCost")]);
+          break;
+        case "technicians":
+          if (technicianData) exportTechniciansCsv(technicianData, currencyCode, [h("technician"), h("jobs"), h("totalRevenue"), h("avgRevenue"), h("totalHours"), h("avgHours")]);
+          break;
+        case "parts":
+          if (partsData) exportPartsCsv(partsData, currencyCode, [h("partName"), h("partNumber"), h("usageCount"), h("totalQty"), h("totalRevenue")]);
+          break;
+        case "job-analytics":
+          if (jobAnalyticsData) exportJobAnalyticsCsv(jobAnalyticsData, currencyCode, [h("serviceType"), h("count"), h("avgValue"), h("avgHours")]);
+          break;
+        case "retention":
+          if (retentionData) exportRetentionCsv(retentionData, currencyCode, [h("customer"), h("company"), h("visits"), h("totalSpent"), h("avgDaysBetweenVisits")]);
+          break;
+      }
     }
   };
 
-  const dataMap: Record<ReportTab, unknown> = {
-    revenue: revenueData,
-    services: serviceData,
-    customers: customerData,
-    inventory: inventoryData,
-    technicians: technicianData,
-    parts: partsData,
-    "job-analytics": jobAnalyticsData,
-    retention: retentionData,
-    tax: taxData,
-  };
-  const hasData = !!dataMap[activeTab];
+  // Determine if we have data for the current view
+  const hasData = (() => {
+    if (activeTab === "financial") {
+      const subDataMap: Record<FinancialSubTab, unknown> = {
+        revenue: revenueData,
+        "past-due-invoices": pastDueData,
+        tax: taxData,
+      };
+      return !!subDataMap[financialSubTab];
+    }
+    const dataMap: Record<string, unknown> = {
+      services: serviceData,
+      customers: customerData,
+      inventory: inventoryData,
+      technicians: technicianData,
+      parts: partsData,
+      "job-analytics": jobAnalyticsData,
+      retention: retentionData,
+    };
+    return !!dataMap[activeTab];
+  })();
+
+  const showDateRange = activeTab !== "inventory" && !(activeTab === "financial" && financialSubTab === "past-due-invoices");
+
+  // Past due invoice sorting and filtering
+  const sortedFilteredPastDue = pastDueData ? (() => {
+    let filtered = pastDueData.invoices;
+    if (pastDueFilter === "30") filtered = filtered.filter((inv) => inv.daysPastDue > 30);
+    else if (pastDueFilter === "60") filtered = filtered.filter((inv) => inv.daysPastDue > 60);
+    else if (pastDueFilter === "90") filtered = filtered.filter((inv) => inv.daysPastDue > 90);
+
+    return [...filtered].sort((a, b) => {
+      let cmp = 0;
+      if (pastDueSortKey === "customerName") cmp = a.customerName.localeCompare(b.customerName);
+      else if (pastDueSortKey === "amountDue") cmp = a.amountDue - b.amountDue;
+      else if (pastDueSortKey === "daysPastDue") cmp = a.daysPastDue - b.daysPastDue;
+      return pastDueSortDir === "asc" ? cmp : -cmp;
+    });
+  })() : [];
 
-  const showDateRange = activeTab !== "inventory";
+  const toggleSort = (key: PastDueSortKey) => {
+    if (pastDueSortKey === key) {
+      setPastDueSortDir((d) => (d === "asc" ? "desc" : "asc"));
+    } else {
+      setPastDueSortKey(key);
+      setPastDueSortDir("desc");
+    }
+  };
 
   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">
         <TabsList className="flex-wrap h-auto">
-          <TabsTrigger value="revenue" className="gap-1.5">
-            <DollarSign className="h-4 w-4" />
-            {t("tabs.revenue")}
+          <TabsTrigger value="financial" className="gap-1.5">
+            <Landmark className="h-4 w-4" />
+            {t("tabs.financial")}
           </TabsTrigger>
           <TabsTrigger value="services" className="gap-1.5">
             <BarChart3 className="h-4 w-4" />
@@ -284,10 +383,6 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
             <UserCheck className="h-4 w-4" />
             {t("tabs.retention")}
           </TabsTrigger>
-          <TabsTrigger value="tax" className="gap-1.5">
-            <Receipt className="h-4 w-4" />
-            {t("tabs.tax")}
-          </TabsTrigger>
         </TabsList>
 
         <div className="flex items-center gap-2">
@@ -327,167 +422,474 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
         </div>
       )}
 
-      {/* Revenue Tab */}
-      <TabsContent value="revenue">
-        {!loading && revenueData && (
-          <div className="space-y-4">
-            {/* Summary cards */}
-            <div className="grid gap-3 grid-cols-2 lg:grid-cols-3">
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
-                    <DollarSign className="h-4 w-4 text-blue-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("revenue.revenue")}</p>
-                    <p className="text-lg font-semibold truncate">
-                      {fmtCurrency(revenueData.summary.totalRevenue)}
-                    </p>
-                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.revenueDesc")}</p>
-                  </div>
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
-                    <TrendingUp className="h-4 w-4 text-emerald-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("revenue.collected")}</p>
-                    <p className="text-lg font-semibold truncate">
-                      {fmtCurrency(revenueData.summary.totalCollected)}
-                    </p>
-                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.collectedDesc")}</p>
-                  </div>
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
-                    <AlertTriangle className="h-4 w-4 text-amber-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("revenue.outstanding")}</p>
-                    <p className="text-lg font-semibold truncate">
-                      {fmtCurrency(revenueData.summary.outstanding)}
-                    </p>
-                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.outstandingDesc")}</p>
-                  </div>
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
-                    <BarChart3 className="h-4 w-4 text-violet-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("revenue.services")}</p>
-                    <p className="text-lg font-semibold">
-                      {revenueData.summary.totalCount}
-                    </p>
-                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.servicesDesc")}</p>
-                  </div>
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-orange-500/10">
-                    <Package className="h-4 w-4 text-orange-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("revenue.partsCost")}</p>
-                    <p className="text-lg font-semibold truncate">
-                      {fmtCurrency(revenueData.summary.totalPartsCost)}
-                    </p>
-                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.partsCostDesc")}</p>
-                  </div>
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-green-500/10">
-                    <TrendingUp className="h-4 w-4 text-green-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("revenue.netProfit")}</p>
-                    <p className="text-lg font-semibold truncate">
-                      {fmtCurrency(revenueData.summary.netProfit)}
-                    </p>
-                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.netProfitDesc")}</p>
-                  </div>
-                </CardContent>
-              </Card>
-            </div>
+      {/* Financial Reports Tab */}
+      <TabsContent value="financial">
+        <Tabs value={financialSubTab} onValueChange={handleFinancialSubTabChange} className="space-y-4">
+          <TabsList variant="line">
+            <TabsTrigger value="revenue" className="gap-1.5">
+              <DollarSign className="h-4 w-4" />
+              {t("tabs.revenue")}
+            </TabsTrigger>
+            <TabsTrigger value="past-due-invoices" className="gap-1.5">
+              <Clock className="h-4 w-4" />
+              {t("tabs.pastDueInvoices")}
+            </TabsTrigger>
+            <TabsTrigger value="tax" className="gap-1.5">
+              <Receipt className="h-4 w-4" />
+              {t("tabs.tax")}
+            </TabsTrigger>
+          </TabsList>
 
-            {/* Charts */}
-            <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("revenue.monthlyRevenue")}</CardTitle>
-                </CardHeader>
-                <CardContent>
-                  <RevenueBarChart
-                    data={revenueData.monthly}
-                    formatCurrency={fmtCurrency}
-                    labels={{ revenue: t("charts.revenue"), collected: t("charts.collected"), netProfit: t("revenue.netProfit") }}
-                  />
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm lg:col-span-2">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("revenue.revenueByType")}</CardTitle>
-                </CardHeader>
-                <CardContent>
-                  <RevenueTypeDonut data={revenueData.byType} formatCurrency={fmtCurrency} />
-                </CardContent>
-              </Card>
-            </div>
+          {/* Revenue Sub-Tab */}
+          <TabsContent value="revenue">
+            {!loading && revenueData && (
+              <div className="space-y-4">
+                <div className="grid gap-3 grid-cols-2 lg:grid-cols-3">
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
+                        <DollarSign className="h-4 w-4 text-blue-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("revenue.revenue")}</p>
+                        <p className="text-lg font-semibold truncate">
+                          {fmtCurrency(revenueData.summary.totalRevenue)}
+                        </p>
+                        <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.revenueDesc")}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
+                        <TrendingUp className="h-4 w-4 text-emerald-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("revenue.collected")}</p>
+                        <p className="text-lg font-semibold truncate">
+                          {fmtCurrency(revenueData.summary.totalCollected)}
+                        </p>
+                        <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.collectedDesc")}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
+                        <AlertTriangle className="h-4 w-4 text-amber-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("revenue.outstanding")}</p>
+                        <p className="text-lg font-semibold truncate">
+                          {fmtCurrency(revenueData.summary.outstanding)}
+                        </p>
+                        <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.outstandingDesc")}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
+                        <BarChart3 className="h-4 w-4 text-violet-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("revenue.services")}</p>
+                        <p className="text-lg font-semibold">
+                          {revenueData.summary.totalCount}
+                        </p>
+                        <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.servicesDesc")}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-orange-500/10">
+                        <Package className="h-4 w-4 text-orange-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("revenue.partsCost")}</p>
+                        <p className="text-lg font-semibold truncate">
+                          {fmtCurrency(revenueData.summary.totalPartsCost)}
+                        </p>
+                        <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.partsCostDesc")}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-green-500/10">
+                        <TrendingUp className="h-4 w-4 text-green-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("revenue.netProfit")}</p>
+                        <p className="text-lg font-semibold truncate">
+                          {fmtCurrency(revenueData.summary.netProfit)}
+                        </p>
+                        <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.netProfitDesc")}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                </div>
 
-            {/* Monthly table */}
-            {revenueData.monthly.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("revenue.monthlyBreakdown")}</CardTitle>
-                </CardHeader>
-                <CardContent>
-                  <Table>
-                    <TableHeader>
-                      <TableRow>
-                        <TableHead>{t("revenue.tableHeaders.month")}</TableHead>
-                        <TableHead className="text-right">{t("revenue.tableHeaders.revenue")}</TableHead>
-                        <TableHead className="text-right">{t("revenue.tableHeaders.collected")}</TableHead>
-                        <TableHead className="text-right">{t("revenue.tableHeaders.partsCost")}</TableHead>
-                        <TableHead className="text-right">{t("revenue.tableHeaders.netProfit")}</TableHead>
-                        <TableHead className="text-right">{t("revenue.tableHeaders.count")}</TableHead>
-                      </TableRow>
-                    </TableHeader>
-                    <TableBody>
-                      {revenueData.monthly.map((row) => (
-                        <TableRow key={row.month}>
-                          <TableCell className="text-sm">{row.month}</TableCell>
-                          <TableCell className="text-right text-sm">
-                            {fmtCurrency(row.revenue)}
-                          </TableCell>
-                          <TableCell className="text-right text-sm">
-                            {fmtCurrency(row.collected)}
-                          </TableCell>
-                          <TableCell className="text-right text-sm">
-                            {fmtCurrency(row.partsCost)}
-                          </TableCell>
-                          <TableCell className="text-right text-sm">
-                            {fmtCurrency(row.netProfit)}
-                          </TableCell>
-                          <TableCell className="text-right text-sm">{row.count}</TableCell>
-                        </TableRow>
-                      ))}
-                    </TableBody>
-                  </Table>
-                </CardContent>
-              </Card>
+                <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("revenue.monthlyRevenue")}</CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <RevenueBarChart
+                        data={revenueData.monthly}
+                        formatCurrency={fmtCurrency}
+                        labels={{ revenue: t("charts.revenue"), collected: t("charts.collected"), netProfit: t("revenue.netProfit") }}
+                      />
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm lg:col-span-2">
+                    <CardHeader className="pb-2">
+                      <CardTitle className="text-sm font-medium">{t("revenue.revenueByType")}</CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <RevenueTypeDonut data={revenueData.byType} formatCurrency={fmtCurrency} />
+                    </CardContent>
+                  </Card>
+                </div>
+
+                {revenueData.monthly.length > 0 && (
+                  <Card className="border-0 shadow-sm">
+                    <CardHeader className="pb-2">
+                      <CardTitle className="text-sm font-medium">{t("revenue.monthlyBreakdown")}</CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <Table>
+                        <TableHeader>
+                          <TableRow>
+                            <TableHead>{t("revenue.tableHeaders.month")}</TableHead>
+                            <TableHead className="text-right">{t("revenue.tableHeaders.revenue")}</TableHead>
+                            <TableHead className="text-right">{t("revenue.tableHeaders.collected")}</TableHead>
+                            <TableHead className="text-right">{t("revenue.tableHeaders.partsCost")}</TableHead>
+                            <TableHead className="text-right">{t("revenue.tableHeaders.netProfit")}</TableHead>
+                            <TableHead className="text-right">{t("revenue.tableHeaders.count")}</TableHead>
+                          </TableRow>
+                        </TableHeader>
+                        <TableBody>
+                          {revenueData.monthly.map((row) => (
+                            <TableRow key={row.month}>
+                              <TableCell className="text-sm">{row.month}</TableCell>
+                              <TableCell className="text-right text-sm">
+                                {fmtCurrency(row.revenue)}
+                              </TableCell>
+                              <TableCell className="text-right text-sm">
+                                {fmtCurrency(row.collected)}
+                              </TableCell>
+                              <TableCell className="text-right text-sm">
+                                {fmtCurrency(row.partsCost)}
+                              </TableCell>
+                              <TableCell className="text-right text-sm">
+                                {fmtCurrency(row.netProfit)}
+                              </TableCell>
+                              <TableCell className="text-right text-sm">{row.count}</TableCell>
+                            </TableRow>
+                          ))}
+                        </TableBody>
+                      </Table>
+                    </CardContent>
+                  </Card>
+                )}
+              </div>
             )}
-          </div>
-        )}
-        {!loading && !revenueData && <EmptyState message={t("empty")} />}
+            {!loading && !revenueData && <EmptyState message={t("empty")} />}
+          </TabsContent>
+
+          {/* Past Due Invoices Sub-Tab */}
+          <TabsContent value="past-due-invoices">
+            {!loading && pastDueData && (
+              <div className="space-y-4">
+                {/* Summary cards */}
+                <div className="grid gap-3 grid-cols-2 lg:grid-cols-3">
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-red-500/10">
+                        <AlertTriangle className="h-4 w-4 text-red-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("pastDueInvoices.totalPastDue")}</p>
+                        <p className="text-lg font-semibold">{pastDueData.summary.totalPastDue}</p>
+                        <p className="text-[10px] leading-tight text-muted-foreground/70">{t("pastDueInvoices.totalPastDueDesc")}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
+                        <DollarSign className="h-4 w-4 text-amber-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("pastDueInvoices.totalAmountDue")}</p>
+                        <p className="text-lg font-semibold truncate">{fmtCurrency(pastDueData.summary.totalAmountDue)}</p>
+                        <p className="text-[10px] leading-tight text-muted-foreground/70">{t("pastDueInvoices.totalAmountDueDesc")}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-orange-500/10">
+                        <Clock className="h-4 w-4 text-orange-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("pastDueInvoices.over30Days")}</p>
+                        <p className="text-lg font-semibold">{pastDueData.summary.over30}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-red-500/10">
+                        <Clock className="h-4 w-4 text-red-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("pastDueInvoices.over60Days")}</p>
+                        <p className="text-lg font-semibold">{pastDueData.summary.over60}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-red-600/10">
+                        <Clock className="h-4 w-4 text-red-600" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("pastDueInvoices.over90Days")}</p>
+                        <p className="text-lg font-semibold">{pastDueData.summary.over90}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                </div>
+
+                {/* Filter and table */}
+                <Card className="border-0 shadow-sm">
+                  <CardHeader className="pb-2">
+                    <div className="flex items-center justify-between">
+                      <CardTitle className="text-sm font-medium">{t("pastDueInvoices.pastDueInvoicesList")}</CardTitle>
+                      <Select value={pastDueFilter} onValueChange={setPastDueFilter}>
+                        <SelectTrigger className="w-[160px] h-8 text-sm">
+                          <SelectValue placeholder={t("pastDueInvoices.filterByAge")} />
+                        </SelectTrigger>
+                        <SelectContent>
+                          <SelectItem value="all">{t("pastDueInvoices.allPastDue")}</SelectItem>
+                          <SelectItem value="30">{t("pastDueInvoices.30plus")}</SelectItem>
+                          <SelectItem value="60">{t("pastDueInvoices.60plus")}</SelectItem>
+                          <SelectItem value="90">{t("pastDueInvoices.90plus")}</SelectItem>
+                        </SelectContent>
+                      </Select>
+                    </div>
+                  </CardHeader>
+                  <CardContent>
+                    {sortedFilteredPastDue.length > 0 ? (
+                      <Table>
+                        <TableHeader>
+                          <TableRow>
+                            <TableHead>
+                              <button
+                                type="button"
+                                className="flex items-center gap-1 hover:text-foreground"
+                                onClick={() => toggleSort("customerName")}
+                              >
+                                {t("pastDueInvoices.tableHeaders.customer")}
+                                <ArrowUpDown className="h-3 w-3" />
+                              </button>
+                            </TableHead>
+                            <TableHead className="hidden md:table-cell">{t("pastDueInvoices.tableHeaders.company")}</TableHead>
+                            <TableHead className="hidden lg:table-cell">{t("pastDueInvoices.tableHeaders.invoiceNumber")}</TableHead>
+                            <TableHead className="hidden lg:table-cell">{t("pastDueInvoices.tableHeaders.vehicle")}</TableHead>
+                            <TableHead className="text-right">
+                              <button
+                                type="button"
+                                className="flex items-center gap-1 ml-auto hover:text-foreground"
+                                onClick={() => toggleSort("amountDue")}
+                              >
+                                {t("pastDueInvoices.tableHeaders.amountDue")}
+                                <ArrowUpDown className="h-3 w-3" />
+                              </button>
+                            </TableHead>
+                            <TableHead className="text-right hidden sm:table-cell">{t("pastDueInvoices.tableHeaders.totalAmount")}</TableHead>
+                            <TableHead className="text-right hidden sm:table-cell">{t("pastDueInvoices.tableHeaders.amountPaid")}</TableHead>
+                            <TableHead className="text-right hidden md:table-cell">{t("pastDueInvoices.tableHeaders.dueDate")}</TableHead>
+                            <TableHead className="text-right">
+                              <button
+                                type="button"
+                                className="flex items-center gap-1 ml-auto hover:text-foreground"
+                                onClick={() => toggleSort("daysPastDue")}
+                              >
+                                {t("pastDueInvoices.tableHeaders.daysPastDue")}
+                                <ArrowUpDown className="h-3 w-3" />
+                              </button>
+                            </TableHead>
+                          </TableRow>
+                        </TableHeader>
+                        <TableBody>
+                          {sortedFilteredPastDue.map((inv) => (
+                            <TableRow key={inv.id}>
+                              <TableCell className="text-sm font-medium">{inv.customerName}</TableCell>
+                              <TableCell className="text-sm hidden md:table-cell">{inv.customerCompany ?? "-"}</TableCell>
+                              <TableCell className="text-sm hidden lg:table-cell">{inv.invoiceNumber ?? "-"}</TableCell>
+                              <TableCell className="text-sm hidden lg:table-cell">{inv.vehicleInfo}</TableCell>
+                              <TableCell className="text-right text-sm font-medium text-red-600 dark:text-red-400">
+                                {fmtCurrency(inv.amountDue)}
+                              </TableCell>
+                              <TableCell className="text-right text-sm hidden sm:table-cell">{fmtCurrency(inv.totalAmount)}</TableCell>
+                              <TableCell className="text-right text-sm hidden sm:table-cell">{fmtCurrency(inv.amountPaid)}</TableCell>
+                              <TableCell className="text-right text-sm hidden md:table-cell">{inv.dueDate}</TableCell>
+                              <TableCell className="text-right">
+                                <Badge
+                                  variant={inv.daysPastDue > 90 ? "destructive" : inv.daysPastDue > 60 ? "destructive" : inv.daysPastDue > 30 ? "outline" : "secondary"}
+                                  className={inv.daysPastDue > 60 ? "" : inv.daysPastDue > 30 ? "border-amber-500 text-amber-600 dark:text-amber-400" : ""}
+                                >
+                                  {inv.daysPastDue}d
+                                </Badge>
+                              </TableCell>
+                            </TableRow>
+                          ))}
+                        </TableBody>
+                      </Table>
+                    ) : (
+                      <p className="text-sm text-muted-foreground text-center py-8">
+                        {t("pastDueInvoices.noInvoices")}
+                      </p>
+                    )}
+                  </CardContent>
+                </Card>
+              </div>
+            )}
+            {!loading && !pastDueData && <EmptyState message={t("empty")} />}
+          </TabsContent>
+
+          {/* Tax Sub-Tab */}
+          <TabsContent value="tax">
+            {!loading && taxData && (
+              <div className="space-y-4">
+                <div className="grid gap-3 grid-cols-1 sm:grid-cols-3">
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
+                        <Receipt className="h-4 w-4 text-amber-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("tax.totalTaxCollected")}</p>
+                        <p className="text-lg font-semibold truncate">
+                          {fmtCurrency(taxData.summary.totalTaxCollected)}
+                        </p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
+                        <DollarSign className="h-4 w-4 text-blue-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("tax.taxableRevenue")}</p>
+                        <p className="text-lg font-semibold truncate">
+                          {fmtCurrency(taxData.summary.totalTaxableAmount)}
+                        </p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card className="border-0 shadow-sm">
+                    <CardContent className="flex items-center gap-3 p-4">
+                      <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
+                        <BarChart3 className="h-4 w-4 text-violet-500" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="text-xs text-muted-foreground">{t("tax.invoicesWithTax")}</p>
+                        <p className="text-lg font-semibold">{taxData.summary.totalInvoices}</p>
+                      </div>
+                    </CardContent>
+                  </Card>
+                </div>
+
+                {taxData.monthly.length > 0 && (
+                  <Card className="border-0 shadow-sm">
+                    <CardHeader className="pb-2">
+                      <CardTitle className="text-sm font-medium">{t("tax.monthlyTaxCollected")}</CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <TaxBarChart
+                        data={taxData.monthly}
+                        formatCurrency={fmtCurrency}
+                        labels={{ taxCollected: t("charts.taxCollected") }}
+                      />
+                    </CardContent>
+                  </Card>
+                )}
+
+                {taxData.byRate.length > 0 && (
+                  <Card className="border-0 shadow-sm">
+                    <CardHeader className="pb-2">
+                      <CardTitle className="text-sm font-medium">{t("tax.taxByRate")}</CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <Table>
+                        <TableHeader>
+                          <TableRow>
+                            <TableHead>{t("tax.tableHeaders.taxRate")}</TableHead>
+                            <TableHead className="text-right">{t("tax.tableHeaders.taxCollected")}</TableHead>
+                            <TableHead className="text-right">{t("tax.tableHeaders.invoices")}</TableHead>
+                          </TableRow>
+                        </TableHeader>
+                        <TableBody>
+                          {taxData.byRate.map((row) => (
+                            <TableRow key={row.taxRate}>
+                              <TableCell className="text-sm font-medium">{row.taxRate}%</TableCell>
+                              <TableCell className="text-right text-sm">
+                                {fmtCurrency(row.taxCollected)}
+                              </TableCell>
+                              <TableCell className="text-right text-sm">{row.invoiceCount}</TableCell>
+                            </TableRow>
+                          ))}
+                        </TableBody>
+                      </Table>
+                    </CardContent>
+                  </Card>
+                )}
+
+                {taxData.monthly.length > 0 && (
+                  <Card className="border-0 shadow-sm">
+                    <CardHeader className="pb-2">
+                      <CardTitle className="text-sm font-medium">{t("tax.monthlyBreakdown")}</CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <Table>
+                        <TableHeader>
+                          <TableRow>
+                            <TableHead>{t("tax.tableHeaders.month")}</TableHead>
+                            <TableHead className="text-right">{t("tax.tableHeaders.taxCollected")}</TableHead>
+                            <TableHead className="text-right">{t("tax.tableHeaders.taxableAmount")}</TableHead>
+                            <TableHead className="text-right">{t("tax.tableHeaders.invoices")}</TableHead>
+                          </TableRow>
+                        </TableHeader>
+                        <TableBody>
+                          {taxData.monthly.map((row) => (
+                            <TableRow key={row.month}>
+                              <TableCell className="text-sm">{row.month}</TableCell>
+                              <TableCell className="text-right text-sm">
+                                {fmtCurrency(row.taxCollected)}
+                              </TableCell>
+                              <TableCell className="text-right text-sm">
+                                {fmtCurrency(row.taxableAmount)}
+                              </TableCell>
+                              <TableCell className="text-right text-sm">{row.invoiceCount}</TableCell>
+                            </TableRow>
+                          ))}
+                        </TableBody>
+                      </Table>
+                    </CardContent>
+                  </Card>
+                )}
+              </div>
+            )}
+            {!loading && !taxData && <EmptyState message={t("empty")} />}
+          </TabsContent>
+        </Tabs>
       </TabsContent>
 
       {/* Services Tab */}
@@ -1082,133 +1484,6 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
         )}
         {!loading && !retentionData && <EmptyState message={t("empty")} />}
       </TabsContent>
-
-      {/* Tax Tab */}
-      <TabsContent value="tax">
-        {!loading && taxData && (
-          <div className="space-y-4">
-            <div className="grid gap-3 grid-cols-1 sm:grid-cols-3">
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
-                    <Receipt className="h-4 w-4 text-amber-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("tax.totalTaxCollected")}</p>
-                    <p className="text-lg font-semibold truncate">
-                      {fmtCurrency(taxData.summary.totalTaxCollected)}
-                    </p>
-                  </div>
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
-                    <DollarSign className="h-4 w-4 text-blue-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("tax.taxableRevenue")}</p>
-                    <p className="text-lg font-semibold truncate">
-                      {fmtCurrency(taxData.summary.totalTaxableAmount)}
-                    </p>
-                  </div>
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm">
-                <CardContent className="flex items-center gap-3 p-4">
-                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
-                    <BarChart3 className="h-4 w-4 text-violet-500" />
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">{t("tax.invoicesWithTax")}</p>
-                    <p className="text-lg font-semibold">{taxData.summary.totalInvoices}</p>
-                  </div>
-                </CardContent>
-              </Card>
-            </div>
-
-            {taxData.monthly.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("tax.monthlyTaxCollected")}</CardTitle>
-                </CardHeader>
-                <CardContent>
-                  <TaxBarChart
-                    data={taxData.monthly}
-                    formatCurrency={fmtCurrency}
-                    labels={{ taxCollected: t("charts.taxCollected") }}
-                  />
-                </CardContent>
-              </Card>
-            )}
-
-            {taxData.byRate.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("tax.taxByRate")}</CardTitle>
-                </CardHeader>
-                <CardContent>
-                  <Table>
-                    <TableHeader>
-                      <TableRow>
-                        <TableHead>{t("tax.tableHeaders.taxRate")}</TableHead>
-                        <TableHead className="text-right">{t("tax.tableHeaders.taxCollected")}</TableHead>
-                        <TableHead className="text-right">{t("tax.tableHeaders.invoices")}</TableHead>
-                      </TableRow>
-                    </TableHeader>
-                    <TableBody>
-                      {taxData.byRate.map((row) => (
-                        <TableRow key={row.taxRate}>
-                          <TableCell className="text-sm font-medium">{row.taxRate}%</TableCell>
-                          <TableCell className="text-right text-sm">
-                            {fmtCurrency(row.taxCollected)}
-                          </TableCell>
-                          <TableCell className="text-right text-sm">{row.invoiceCount}</TableCell>
-                        </TableRow>
-                      ))}
-                    </TableBody>
-                  </Table>
-                </CardContent>
-              </Card>
-            )}
-
-            {taxData.monthly.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("tax.monthlyBreakdown")}</CardTitle>
-                </CardHeader>
-                <CardContent>
-                  <Table>
-                    <TableHeader>
-                      <TableRow>
-                        <TableHead>{t("tax.tableHeaders.month")}</TableHead>
-                        <TableHead className="text-right">{t("tax.tableHeaders.taxCollected")}</TableHead>
-                        <TableHead className="text-right">{t("tax.tableHeaders.taxableAmount")}</TableHead>
-                        <TableHead className="text-right">{t("tax.tableHeaders.invoices")}</TableHead>
-                      </TableRow>
-                    </TableHeader>
-                    <TableBody>
-                      {taxData.monthly.map((row) => (
-                        <TableRow key={row.month}>
-                          <TableCell className="text-sm">{row.month}</TableCell>
-                          <TableCell className="text-right text-sm">
-                            {fmtCurrency(row.taxCollected)}
-                          </TableCell>
-                          <TableCell className="text-right text-sm">
-                            {fmtCurrency(row.taxableAmount)}
-                          </TableCell>
-                          <TableCell className="text-right text-sm">{row.invoiceCount}</TableCell>
-                        </TableRow>
-                      ))}
-                    </TableBody>
-                  </Table>
-                </CardContent>
-              </Card>
-            )}
-          </div>
-        )}
-        {!loading && !taxData && <EmptyState message={t("empty")} />}
-      </TabsContent>
     </Tabs>
   );
 }

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

@@ -1,54 +1,49 @@
-import { getServiceRecord } from '@/features/vehicles/Actions/serviceActions'
-import { getSettings } from '@/features/settings/Actions/settingsActions'
-import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
-import { getInventoryPartsList } from '@/features/inventory/Actions/inventoryActions'
-import { getLaborPresetsList } from '@/features/labor-presets/Actions/laborPresetActions'
+import { getServiceRecord } from "@/features/vehicles/Actions/serviceActions";
+import { getSettings } from "@/features/settings/Actions/settingsActions";
+import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
+import { getInventoryPartsList } from "@/features/inventory/Actions/inventoryActions";
+import { getLaborPresetsList } from "@/features/labor-presets/Actions/laborPresetActions";
 
-import { getTechnicians, getOrgMembers } from '@/features/workboard/Actions/technicianActions'
-import { getAuthContext } from '@/lib/get-auth-context'
-import { getFeatures } from '@/lib/features'
-import { getStatusReportsForService } from '@/features/status-reports/Actions/getStatusReportsForService'
-import { db } from '@/lib/db'
-import { getCachedSession, getCachedMembership } from '@/lib/cached-session'
-import { ServicePageClient } from '@/features/vehicles/Components/service-page/ServicePageClient'
-import { PageHeader } from '@/components/page-header'
-import { getTranslations } from 'next-intl/server'
+import { getTechnicians, getOrgMembers } from "@/features/workboard/Actions/technicianActions";
+import { getAuthContext } from "@/lib/get-auth-context";
+import { getFeatures } from "@/lib/features";
+import { getStatusReportsForService } from "@/features/status-reports/Actions/getStatusReportsForService";
+import { db } from "@/lib/db";
+import { getCachedSession, getCachedMembership } from "@/lib/cached-session";
+import { ServicePageClient } from "@/features/vehicles/Components/service-page/ServicePageClient";
+import { PageHeader } from "@/components/page-header";
+import { getTranslations } from "next-intl/server";
 
 export default async function ServiceDetailPage({
   params,
+  searchParams,
 }: {
-  params: Promise<{ id: string; serviceId: string }>
+  params: Promise<{ id: string; serviceId: string }>;
+  searchParams: Promise<Record<string, string | string[] | undefined>>;
 }) {
-  const { id, serviceId } = await params
+  const { id, serviceId } = await params;
+  const sp = await searchParams;
+  const initialTab = typeof sp.tab === "string" ? sp.tab : undefined;
 
-  const [
-    result,
-    settingsResult,
-    inventoryResult,
-    techniciansResult,
-    presetsResult,
-    authContext,
-    session,
-    orgMembersResult,
-    statusReportsResult,
-  ] = await Promise.all([
-    getServiceRecord(serviceId),
-    getSettings([
-      SETTING_KEYS.CURRENCY_CODE,
-      SETTING_KEYS.UNIT_SYSTEM,
-      SETTING_KEYS.DEFAULT_TAX_RATE,
-      SETTING_KEYS.TAX_ENABLED,
-      SETTING_KEYS.DEFAULT_LABOR_RATE,
-      SETTING_KEYS.INVOICE_DUE_DAYS,
-    ]),
-    getInventoryPartsList(),
-    getTechnicians(),
-    getLaborPresetsList(),
-    getAuthContext(),
-    getCachedSession(),
-    getOrgMembers(),
-    getStatusReportsForService(serviceId),
-  ])
+  const [result, settingsResult, inventoryResult, techniciansResult, presetsResult, authContext, session, orgMembersResult, statusReportsResult] =
+    await Promise.all([
+      getServiceRecord(serviceId),
+      getSettings([
+        SETTING_KEYS.CURRENCY_CODE,
+        SETTING_KEYS.UNIT_SYSTEM,
+        SETTING_KEYS.DEFAULT_TAX_RATE,
+        SETTING_KEYS.TAX_ENABLED,
+        SETTING_KEYS.DEFAULT_LABOR_RATE,
+        SETTING_KEYS.INVOICE_DUE_DAYS,
+      ]),
+      getInventoryPartsList(),
+      getTechnicians(),
+      getLaborPresetsList(),
+      getAuthContext(),
+      getCachedSession(),
+      getOrgMembers(),
+      getStatusReportsForService(serviceId),
+    ]);
 
   if (!result.success || !result.data) {
     return (
@@ -56,38 +51,48 @@ export default async function ServiceDetailPage({
         <PageHeader />
         <div className="flex h-[50vh] items-center justify-center">
           <p className="text-muted-foreground">
-            {result.error || (await getTranslations('service.page'))('notFound')}
+            {result.error || (await getTranslations("service.page"))("notFound")}
           </p>
         </div>
       </>
-    )
+    );
   }
 
-  const record = result.data
-  const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {}
-  const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || 'USD'
-  const unitSystem = (settings[SETTING_KEYS.UNIT_SYSTEM] || 'imperial') as 'metric' | 'imperial'
-  const taxEnabled = settings[SETTING_KEYS.TAX_ENABLED] !== 'false'
-  const defaultTaxRate = taxEnabled ? Number(settings[SETTING_KEYS.DEFAULT_TAX_RATE]) || 0 : 0
-  const defaultLaborRate = Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0
-  const defaultDueDays = Number(settings[SETTING_KEYS.INVOICE_DUE_DAYS]) || 0
-  const inventoryParts = inventoryResult.success && inventoryResult.data ? inventoryResult.data : []
-  const laborPresets = presetsResult.success && presetsResult.data ? presetsResult.data : []
+  const record = result.data;
+  const settings =
+    settingsResult.success && settingsResult.data ? settingsResult.data : {};
+  const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
+  const unitSystem = (settings[SETTING_KEYS.UNIT_SYSTEM] || "imperial") as
+    | "metric"
+    | "imperial";
+  const taxEnabled = settings[SETTING_KEYS.TAX_ENABLED] !== "false";
+  const defaultTaxRate = taxEnabled
+    ? Number(settings[SETTING_KEYS.DEFAULT_TAX_RATE]) || 0
+    : 0;
+  const defaultLaborRate =
+    Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0;
+  const defaultDueDays = Number(settings[SETTING_KEYS.INVOICE_DUE_DAYS]) || 0;
+  const inventoryParts =
+    inventoryResult.success && inventoryResult.data ? inventoryResult.data : [];
+  const laborPresets =
+    presetsResult.success && presetsResult.data ? presetsResult.data : [];
   const initialVehicle = {
     id: record.vehicle.id,
     make: record.vehicle.make,
     model: record.vehicle.model,
     year: record.vehicle.year,
     licensePlate: record.vehicle.licensePlate,
-  }
+  };
   const boardTechnicians = (
     techniciansResult.success && techniciansResult.data ? techniciansResult.data : []
-  ).map((t) => ({ id: t.id, name: t.name, userId: t.userId }))
-  const organizationId = authContext?.organizationId || ''
+  ).map((t) => ({ id: t.id, name: t.name, userId: t.userId }));
+  const organizationId = authContext?.organizationId || "";
 
   // Fetch team members and features
-  const membership = session?.user?.id ? await getCachedMembership(session.user.id) : null
-  const orgId = membership?.organizationId
+  const membership = session?.user?.id
+    ? await getCachedMembership(session.user.id)
+    : null;
+  const orgId = membership?.organizationId;
 
   const [currentUser, features, aiSettings] = await Promise.all([
     session?.user?.id
@@ -106,44 +111,34 @@ export default async function ServiceDetailPage({
           select: { key: true, value: true },
         })
       : Promise.resolve([]),
-  ])
+  ]);
 
-  const currentUserName = currentUser?.name || ''
-  const aiSettingsMap = Object.fromEntries(aiSettings.map((s) => [s.key, s.value]))
-  const aiEnabled =
-    features?.ai === true &&
-    aiSettingsMap[SETTING_KEYS.AI_ENABLED] === 'true' &&
-    !!aiSettingsMap[SETTING_KEYS.AI_API_KEY]
+  const currentUserName = currentUser?.name || "";
+  const aiSettingsMap = Object.fromEntries(aiSettings.map((s) => [s.key, s.value]));
+  const aiEnabled = features?.ai === true && aiSettingsMap[SETTING_KEYS.AI_ENABLED] === "true" && !!aiSettingsMap[SETTING_KEYS.AI_API_KEY];
 
   const initialData = {
     id: record.id,
     title: record.title,
-    description: record.description || '',
+    description: record.description || "",
     type: record.type,
     status: record.status,
     mileage: record.mileage,
-    serviceDate: new Date(record.serviceDate).toISOString().split('T')[0],
+    serviceDate: new Date(record.serviceDate).toISOString().split("T")[0],
     startDateTime: record.startDateTime?.toISOString() ?? null,
     endDateTime: record.endDateTime?.toISOString() ?? null,
-    techName: record.techName || '',
-    diagnosticNotes: record.diagnosticNotes || '',
-    invoiceNotes: record.invoiceNotes || '',
-    invoiceNumber: record.invoiceNumber || '',
-    invoiceDate: (record.invoiceDate ?? record.startDateTime ?? record.serviceDate)
-      .toISOString()
-      .split('T')[0],
+    techName: record.techName || "",
+    diagnosticNotes: record.diagnosticNotes || "",
+    invoiceNotes: record.invoiceNotes || "",
+    invoiceNumber: record.invoiceNumber || "",
+    invoiceDate: (record.invoiceDate ?? record.startDateTime ?? record.serviceDate).toISOString().split("T")[0],
     invoiceDueDate: record.invoiceDueDate
-      ? record.invoiceDueDate.toISOString().split('T')[0]
+      ? record.invoiceDueDate.toISOString().split("T")[0]
       : defaultDueDays > 0
-        ? new Date(
-            (record.invoiceDate ?? record.startDateTime ?? record.serviceDate).getTime() +
-              defaultDueDays * 86400000
-          )
-            .toISOString()
-            .split('T')[0]
-        : '',
+        ? new Date((record.invoiceDate ?? record.startDateTime ?? record.serviceDate).getTime() + defaultDueDays * 86400000).toISOString().split("T")[0]
+        : "",
     partItems: record.partItems.map((p) => ({
-      partNumber: p.partNumber || '',
+      partNumber: p.partNumber || "",
       name: p.name,
       quantity: p.quantity,
       unitPrice: p.unitPrice,
@@ -165,23 +160,20 @@ export default async function ServiceDetailPage({
     discountType: record.discountType || undefined,
     discountValue: record.discountValue,
     discountAmount: record.discountAmount,
-  }
+  };
 
   // Prepare media attachments for managers
   // The Prisma query returns includeInInvoice on each attachment; cast to include it
-  const allAttachments =
-    (record.attachments as ((typeof record.attachments)[number] & {
-      includeInInvoice: boolean
-    })[]) || []
+  const allAttachments = record.attachments as (typeof record.attachments[number] & { includeInInvoice: boolean })[] || [];
   const imageAttachmentsForManager = allAttachments
-    .filter((a) => a.category === 'image')
-    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
+    .filter((a) => a.category === "image")
+    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }));
   const videoAttachments = allAttachments
-    .filter((a) => a.category === 'video')
-    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
+    .filter((a) => a.category === "video")
+    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }));
   const documentAttachments = allAttachments
-    .filter((a) => a.category === 'document' || a.category === 'diagnostic')
-    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }))
+    .filter((a) => a.category === "document" || a.category === "diagnostic")
+    .map((a) => ({ ...a, includeInInvoice: a.includeInInvoice ?? true }));
 
   return (
     <div className="flex h-svh flex-col overflow-hidden">
@@ -190,6 +182,7 @@ export default async function ServiceDetailPage({
         record={result.data}
         vehicleId={id}
         organizationId={organizationId}
+        initialTab={initialTab}
         currencyCode={currencyCode}
         unitSystem={unitSystem}
         defaultTaxRate={defaultTaxRate}
@@ -213,15 +206,8 @@ export default async function ServiceDetailPage({
         telegramEnabled={features?.telegram ?? false}
         aiEnabled={aiEnabled}
         defaultDueDays={defaultDueDays}
-        statusReports={(statusReportsResult.success && statusReportsResult.data
-          ? statusReportsResult.data
-          : []
-        ).map((r) => ({
-          ...r,
-          createdAt: r.createdAt.toISOString(),
-          expiresAt: r.expiresAt?.toISOString() || null,
-        }))}
+        statusReports={(statusReportsResult.success && statusReportsResult.data ? statusReportsResult.data : []).map(r => ({ ...r, createdAt: r.createdAt.toISOString(), expiresAt: r.expiresAt?.toISOString() || null, feedbackAt: r.feedbackAt?.toISOString() || null, sentAt: r.sentAt?.toISOString() || null }))}
       />
     </div>
-  )
+  );
 }

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

@@ -43,14 +43,12 @@ export function StatusReportView({
   const vehicleLabel = `${vehicle.year} ${vehicle.make} ${vehicle.model}`;
 
   async function handleSubmitFeedback() {
-    if (!feedback.trim()) return;
+    if (!feedback.trim() || submitting) return;
     setSubmitting(true);
     try {
       await submitStatusReportFeedback({ token, feedback: feedback.trim() });
       setSubmitted(true);
     } catch {
-      // Allow retry on failure
-    } finally {
       setSubmitting(false);
     }
   }
@@ -146,12 +144,7 @@ export function StatusReportView({
             {submitted ? (
               <div className="flex items-start gap-3 rounded-lg bg-green-50 p-4">
                 <CheckCircle className="h-5 w-5 text-green-600 mt-0.5 shrink-0" />
-                <div>
-                  <p className="font-medium text-green-800">Thank you for your feedback!</p>
-                  <p className="mt-1 text-sm text-green-700 whitespace-pre-wrap">
-                    {report.customerFeedback || feedback}
-                  </p>
-                </div>
+                <p className="font-medium text-green-800">Thank you for your feedback!</p>
               </div>
             ) : (
               <div className="space-y-3">

+ 42 - 37
src/app/api/protected/upload/inventory/route.ts

@@ -5,42 +5,47 @@ import path from "path";
 import crypto from "crypto";
 
 export async function POST(request: NextRequest) {
-  const ctx = await getAuthContext();
-
-  if (!ctx) {
-    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
-  }
-
-  const formData = await request.formData();
-  const file = formData.get("file") as File | null;
-
-  if (!file) {
-    return NextResponse.json({ error: "No file provided" }, { status: 400 });
-  }
-
-  const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/avif"];
-  if (!allowedTypes.includes(file.type)) {
-    return NextResponse.json(
-      { error: "Only JPEG, PNG, WebP, and AVIF images are allowed" },
-      { status: 400 }
-    );
+  try {
+    const ctx = await getAuthContext();
+
+    if (!ctx) {
+      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }
+
+    const formData = await request.formData();
+    const file = formData.get("file") as File | null;
+
+    if (!file) {
+      return NextResponse.json({ error: "No file provided" }, { status: 400 });
+    }
+
+    const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/avif"];
+    if (!allowedTypes.includes(file.type)) {
+      return NextResponse.json(
+        { error: "Only JPEG, PNG, WebP, and AVIF images are allowed" },
+        { status: 400 }
+      );
+    }
+
+    if (file.size > 5 * 1024 * 1024) {
+      return NextResponse.json(
+        { error: "File size must be under 5MB" },
+        { status: 400 }
+      );
+    }
+
+    const ext = file.name.split(".").pop() || "jpg";
+    const filename = `${crypto.randomUUID()}.${ext}`;
+    const uploadDir = path.join(process.cwd(), "data", "uploads", ctx.organizationId, "inventory");
+
+    await mkdir(uploadDir, { recursive: true });
+
+    const bytes = new Uint8Array(await file.arrayBuffer());
+    await writeFile(path.join(uploadDir, filename), bytes);
+
+    return NextResponse.json({ url: `/api/protected/files/${ctx.organizationId}/inventory/${filename}` });
+  } catch (error) {
+    console.error("[Upload/Inventory] Error:", error);
+    return NextResponse.json({ error: "Upload failed" }, { status: 500 });
   }
-
-  if (file.size > 5 * 1024 * 1024) {
-    return NextResponse.json(
-      { error: "File size must be under 5MB" },
-      { status: 400 }
-    );
-  }
-
-  const ext = file.name.split(".").pop() || "jpg";
-  const filename = `${crypto.randomUUID()}.${ext}`;
-  const uploadDir = path.join(process.cwd(), "data", "uploads", ctx.organizationId, "inventory");
-
-  await mkdir(uploadDir, { recursive: true });
-
-  const bytes = new Uint8Array(await file.arrayBuffer());
-  await writeFile(path.join(uploadDir, filename), bytes);
-
-  return NextResponse.json({ url: `/api/protected/files/${ctx.organizationId}/inventory/${filename}` });
 }

+ 38 - 33
src/app/api/protected/upload/quote-files/route.ts

@@ -19,46 +19,51 @@ const ALLOWED_TYPES = [
 const MAX_SIZE = 10 * 1024 * 1024; // 10MB
 
 export async function POST(request: NextRequest) {
-  const ctx = await getAuthContext();
+  try {
+    const ctx = await getAuthContext();
 
-  if (!ctx) {
-    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
-  }
+    if (!ctx) {
+      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }
 
-  const formData = await request.formData();
-  const file = formData.get("file") as File | null;
+    const formData = await request.formData();
+    const file = formData.get("file") as File | null;
 
-  if (!file) {
-    return NextResponse.json({ error: "No file provided" }, { status: 400 });
-  }
+    if (!file) {
+      return NextResponse.json({ error: "No file provided" }, { status: 400 });
+    }
 
-  if (!ALLOWED_TYPES.includes(file.type)) {
-    return NextResponse.json(
-      { error: "File type not allowed. Supported: JPEG, PNG, WebP, PDF, CSV, TXT, MP4, WebM, MOV" },
-      { status: 400 }
-    );
-  }
+    if (!ALLOWED_TYPES.includes(file.type)) {
+      return NextResponse.json(
+        { error: "File type not allowed. Supported: JPEG, PNG, WebP, PDF, CSV, TXT, MP4, WebM, MOV" },
+        { status: 400 }
+      );
+    }
 
-  if (file.size > MAX_SIZE) {
-    return NextResponse.json(
-      { error: "File size must be under 10MB" },
-      { status: 400 }
-    );
-  }
+    if (file.size > MAX_SIZE) {
+      return NextResponse.json(
+        { error: "File size must be under 10MB" },
+        { status: 400 }
+      );
+    }
 
-  const ext = file.name.split(".").pop() || "bin";
-  const filename = `${crypto.randomUUID()}.${ext}`;
-  const uploadDir = path.join(process.cwd(), "data", "uploads", ctx.organizationId, "quotes");
+    const ext = file.name.split(".").pop() || "bin";
+    const filename = `${crypto.randomUUID()}.${ext}`;
+    const uploadDir = path.join(process.cwd(), "data", "uploads", ctx.organizationId, "quotes");
 
-  await mkdir(uploadDir, { recursive: true });
+    await mkdir(uploadDir, { recursive: true });
 
-  const bytes = new Uint8Array(await file.arrayBuffer());
-  await writeFile(path.join(uploadDir, filename), bytes);
+    const bytes = new Uint8Array(await file.arrayBuffer());
+    await writeFile(path.join(uploadDir, filename), bytes);
 
-  return NextResponse.json({
-    url: `/api/protected/files/${ctx.organizationId}/quotes/${filename}`,
-    fileName: file.name,
-    fileType: file.type,
-    fileSize: file.size,
-  });
+    return NextResponse.json({
+      url: `/api/protected/files/${ctx.organizationId}/quotes/${filename}`,
+      fileName: file.name,
+      fileType: file.type,
+      fileSize: file.size,
+    });
+  } catch (error) {
+    console.error("[Upload/QuoteFiles] Error:", error);
+    return NextResponse.json({ error: "Upload failed" }, { status: 500 });
+  }
 }

+ 42 - 37
src/app/api/protected/upload/route.ts

@@ -5,42 +5,47 @@ import path from "path";
 import crypto from "crypto";
 
 export async function POST(request: NextRequest) {
-  const ctx = await getAuthContext();
-
-  if (!ctx) {
-    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
-  }
-
-  const formData = await request.formData();
-  const file = formData.get("file") as File | null;
-
-  if (!file) {
-    return NextResponse.json({ error: "No file provided" }, { status: 400 });
-  }
-
-  const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/avif"];
-  if (!allowedTypes.includes(file.type)) {
-    return NextResponse.json(
-      { error: "Only JPEG, PNG, WebP, and AVIF images are allowed" },
-      { status: 400 }
-    );
+  try {
+    const ctx = await getAuthContext();
+
+    if (!ctx) {
+      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }
+
+    const formData = await request.formData();
+    const file = formData.get("file") as File | null;
+
+    if (!file) {
+      return NextResponse.json({ error: "No file provided" }, { status: 400 });
+    }
+
+    const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/avif"];
+    if (!allowedTypes.includes(file.type)) {
+      return NextResponse.json(
+        { error: "Only JPEG, PNG, WebP, and AVIF images are allowed" },
+        { status: 400 }
+      );
+    }
+
+    if (file.size > 5 * 1024 * 1024) {
+      return NextResponse.json(
+        { error: "File size must be under 5MB" },
+        { status: 400 }
+      );
+    }
+
+    const ext = file.name.split(".").pop() || "jpg";
+    const filename = `${crypto.randomUUID()}.${ext}`;
+    const uploadDir = path.join(process.cwd(), "data", "uploads", ctx.organizationId, "vehicles");
+
+    await mkdir(uploadDir, { recursive: true });
+
+    const bytes = new Uint8Array(await file.arrayBuffer());
+    await writeFile(path.join(uploadDir, filename), bytes);
+
+    return NextResponse.json({ url: `/api/protected/files/${ctx.organizationId}/vehicles/${filename}` });
+  } catch (error) {
+    console.error("[Upload] Error:", error);
+    return NextResponse.json({ error: "Upload failed" }, { status: 500 });
   }
-
-  if (file.size > 5 * 1024 * 1024) {
-    return NextResponse.json(
-      { error: "File size must be under 5MB" },
-      { status: 400 }
-    );
-  }
-
-  const ext = file.name.split(".").pop() || "jpg";
-  const filename = `${crypto.randomUUID()}.${ext}`;
-  const uploadDir = path.join(process.cwd(), "data", "uploads", ctx.organizationId, "vehicles");
-
-  await mkdir(uploadDir, { recursive: true });
-
-  const bytes = new Uint8Array(await file.arrayBuffer());
-  await writeFile(path.join(uploadDir, filename), bytes);
-
-  return NextResponse.json({ url: `/api/protected/files/${ctx.organizationId}/vehicles/${filename}` });
 }

+ 0 - 73
src/features/custom-fields/Components/CustomFieldsDisplay.tsx

@@ -1,73 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import { getCustomFieldValues } from "@/features/custom-fields/Actions/customFieldActions";
-import { Loader2 } from "lucide-react";
-
-interface FieldWithValue {
-  id: string;
-  name: string;
-  label: string;
-  fieldType: string;
-  value: string;
-}
-
-export function CustomFieldsDisplay({
-  entityId,
-  entityType,
-}: {
-  entityId: string;
-  entityType: "service_record" | "quote";
-}) {
-  const [fields, setFields] = useState<FieldWithValue[]>([]);
-  const [loading, setLoading] = useState(true);
-
-  useEffect(() => {
-    async function load() {
-      const result = await getCustomFieldValues(entityId, entityType);
-      if (result.success && result.data) {
-        setFields(result.data.filter((f) => f.value));
-      }
-      setLoading(false);
-    }
-    load();
-  }, [entityId, entityType]);
-
-  if (loading) {
-    return (
-      <Card className="border-0 shadow-sm">
-        <CardContent className="flex items-center justify-center py-6">
-          <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
-        </CardContent>
-      </Card>
-    );
-  }
-
-  if (fields.length === 0) return null;
-
-  return (
-    <Card className="border-0 shadow-sm">
-      <CardHeader>
-        <CardTitle className="text-base">Custom Fields</CardTitle>
-      </CardHeader>
-      <CardContent>
-        <div className="grid gap-3 sm:grid-cols-2">
-          {fields.map((field) => (
-            <div key={field.id} className="space-y-0.5">
-              <p className="text-xs font-medium text-muted-foreground">{field.label}</p>
-              {field.fieldType === "checkbox" ? (
-                <Badge variant="outline" className={field.value === "true" ? "bg-emerald-500/10 text-emerald-600 border-emerald-500/20" : ""}>
-                  {field.value === "true" ? "Yes" : "No"}
-                </Badge>
-              ) : (
-                <p className="text-sm">{field.value}</p>
-              )}
-            </div>
-          ))}
-        </div>
-      </CardContent>
-    </Card>
-  );
-}

+ 0 - 17
src/features/invoices/Schema/templateSchema.ts

@@ -1,17 +0,0 @@
-import { z } from "zod";
-
-export const templateConfigSchema = z.object({
-  primaryColor: z.string().default("#d97706"),
-  fontFamily: z.enum(["Helvetica", "Times-Roman", "Courier"]).default("Helvetica"),
-  showLogo: z.boolean().default(true),
-  headerStyle: z.enum(["standard", "compact", "modern"]).default("standard"),
-});
-
-export type TemplateConfig = z.infer<typeof templateConfigSchema>;
-
-export const defaultTemplate: TemplateConfig = {
-  primaryColor: "#d97706",
-  fontFamily: "Helvetica",
-  showLogo: true,
-  headerStyle: "standard",
-};

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

@@ -492,6 +492,100 @@ export async function getInventoryReport() {
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.REPORTS }] });
 }
 
+export async function getPastDueInvoicesReport() {
+  return withAuth(async ({ organizationId }) => {
+    const now = new Date();
+
+    const records = await db.serviceRecord.findMany({
+      where: {
+        vehicle: { organizationId },
+        invoiceDueDate: { lt: now },
+        status: { not: "cancelled" },
+      },
+      select: {
+        id: true,
+        invoiceNumber: true,
+        invoiceDueDate: true,
+        totalAmount: true,
+        cost: true,
+        manuallyPaid: true,
+        payments: { select: { amount: true } },
+        vehicle: {
+          select: {
+            year: true,
+            make: true,
+            model: true,
+            customer: { select: { name: true, company: true } },
+          },
+        },
+      },
+      orderBy: { invoiceDueDate: "asc" },
+    });
+
+    const invoices: {
+      id: string;
+      invoiceNumber: string | null;
+      customerName: string;
+      customerCompany: string | null;
+      vehicleInfo: string;
+      totalAmount: number;
+      amountPaid: number;
+      amountDue: number;
+      dueDate: string;
+      daysPastDue: number;
+    }[] = [];
+
+    let totalAmountDue = 0;
+    let over30 = 0;
+    let over60 = 0;
+    let over90 = 0;
+
+    for (const r of records) {
+      const total = r.totalAmount > 0 ? r.totalAmount : r.cost;
+      const paid = r.manuallyPaid ? total : r.payments.reduce((s, p) => s + p.amount, 0);
+      const amountDue = total - paid;
+
+      if (amountDue <= 0) continue;
+
+      const dueDate = r.invoiceDueDate!;
+      const daysPastDue = Math.floor((now.getTime() - dueDate.getTime()) / (1000 * 60 * 60 * 24));
+
+      const vehicleParts = [r.vehicle.year, r.vehicle.make, r.vehicle.model].filter(Boolean);
+      const vehicleInfo = vehicleParts.length > 0 ? vehicleParts.join(" ") : "N/A";
+
+      invoices.push({
+        id: r.id,
+        invoiceNumber: r.invoiceNumber,
+        customerName: r.vehicle.customer?.name ?? "Unknown",
+        customerCompany: r.vehicle.customer?.company ?? null,
+        vehicleInfo,
+        totalAmount: total,
+        amountPaid: paid,
+        amountDue,
+        dueDate: dueDate.toISOString().split("T")[0],
+        daysPastDue,
+      });
+
+      totalAmountDue += amountDue;
+      if (daysPastDue > 30) over30++;
+      if (daysPastDue > 60) over60++;
+      if (daysPastDue > 90) over90++;
+    }
+
+    return {
+      invoices,
+      summary: {
+        totalPastDue: invoices.length,
+        totalAmountDue,
+        totalInvoices: invoices.length,
+        over30,
+        over60,
+        over90,
+      },
+    };
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.REPORTS }] });
+}
+
 export async function getTaxReport(params: {
   startDate?: string;
   endDate?: string;

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

@@ -8,6 +8,7 @@ import type {
   JobAnalyticsReport,
   CustomerRetentionReport,
   TaxReport,
+  PastDueInvoicesReport,
 } from "../Schema/reportTypes";
 import { formatCurrency } from "@/lib/format";
 
@@ -161,6 +162,25 @@ export function exportRetentionCsv(
   downloadCsv("retention-report.csv", headers, rows, ["name", "company", "visitCount", "totalSpent", "avgDaysBetweenVisits"]);
 }
 
+export function exportPastDueInvoicesCsv(
+  data: PastDueInvoicesReport,
+  currencyCode: string,
+  headers: [string, string, string, string, string, string, string, string] = ["Customer", "Company", "Invoice #", "Total Amount", "Amount Paid", "Amount Due", "Due Date", "Days Past Due"],
+) {
+  const fmt = (n: number) => formatCurrency(n, currencyCode);
+  const rows = data.invoices.map((inv) => ({
+    customer: inv.customerName,
+    company: inv.customerCompany ?? "",
+    invoiceNumber: inv.invoiceNumber ?? "",
+    totalAmount: fmt(inv.totalAmount),
+    amountPaid: fmt(inv.amountPaid),
+    amountDue: fmt(inv.amountDue),
+    dueDate: inv.dueDate,
+    daysPastDue: inv.daysPastDue,
+  }));
+  downloadCsv("past-due-invoices-report.csv", headers, rows, ["customer", "company", "invoiceNumber", "totalAmount", "amountPaid", "amountDue", "dueDate", "daysPastDue"]);
+}
+
 export function exportTaxCsv(
   data: TaxReport,
   currencyCode: string,

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

@@ -150,6 +150,31 @@ export interface CustomerRetentionReport {
   topReturning: RetentionCustomer[];
 }
 
+export interface PastDueInvoice {
+  id: string;
+  invoiceNumber: string | null;
+  customerName: string;
+  customerCompany: string | null;
+  vehicleInfo: string;
+  totalAmount: number;
+  amountPaid: number;
+  amountDue: number;
+  dueDate: string;
+  daysPastDue: number;
+}
+
+export interface PastDueInvoicesReport {
+  invoices: PastDueInvoice[];
+  summary: {
+    totalPastDue: number;
+    totalAmountDue: number;
+    totalInvoices: number;
+    over30: number;
+    over60: number;
+    over90: number;
+  };
+}
+
 export interface MonthlyTax {
   month: string;
   taxCollected: number;

+ 2 - 1
src/features/status-reports/Actions/cleanupExpiredReports.ts

@@ -32,13 +32,14 @@ export async function cleanupExpiredReports() {
         // videoUrl format: /api/protected/files/{orgId}/services/{filename}
         const match = report.videoUrl.match(/\/services\/(.+)$/);
         if (match) {
+          const filename = path.basename(match[1]);
           const filePath = path.join(
             process.cwd(),
             "data",
             "uploads",
             report.organizationId,
             "services",
-            match[1]
+            filename
           );
           await unlink(filePath);
         }

+ 56 - 0
src/features/status-reports/Actions/deleteStatusReport.ts

@@ -0,0 +1,56 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { unlink } from "fs/promises";
+import path from "path";
+
+export async function deleteStatusReport(statusReportId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const report = await db.statusReport.findFirst({
+        where: { id: statusReportId, organizationId },
+        select: { id: true, videoUrl: true, organizationId: true },
+      });
+
+      if (!report) throw new Error("Status report not found");
+
+      // Delete video file from disk if present
+      if (report.videoUrl) {
+        try {
+          const match = report.videoUrl.match(/\/services\/(.+)$/);
+          if (match) {
+            const filename = path.basename(match[1]);
+            const filePath = path.join(
+              process.cwd(),
+              "data",
+              "uploads",
+              report.organizationId,
+              "services",
+              filename
+            );
+            await unlink(filePath);
+          }
+        } catch {
+          // File may already be deleted
+        }
+      }
+
+      await db.statusReport.delete({ where: { id: report.id } });
+
+      return { deleted: true, statusReportId };
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.DELETE, subject: PermissionSubject.SERVICES },
+      ],
+      audit: ({ result }) => ({
+        action: "statusReport.delete",
+        entity: "StatusReport",
+        entityId: result.statusReportId,
+        message: "Deleted status report",
+      }),
+    }
+  );
+}

+ 5 - 0
src/features/status-reports/Actions/getStatusReportsForService.ts

@@ -19,11 +19,16 @@ export async function getStatusReportsForService(serviceRecordId: string) {
         select: {
           id: true,
           title: true,
+          message: true,
           status: true,
           videoUrl: true,
           createdAt: true,
           publicToken: true,
           expiresAt: true,
+          customerFeedback: true,
+          feedbackAt: true,
+          sentVia: true,
+          sentAt: true,
         },
         orderBy: { createdAt: "desc" },
       });

+ 3 - 3
src/features/status-reports/Actions/sendStatusReport.ts

@@ -51,9 +51,9 @@ export async function sendStatusReport(input: unknown) {
       const vehicleName = `${vehicle.year} ${vehicle.make} ${vehicle.model}`;
 
       const t = await getTranslations("statusReport.notify");
-      const messageBody =
-        data.customMessage ||
-        t("defaultMessage", { name: customer.name, vehicle: vehicleName, url: publicUrl });
+      const messageBody = data.customMessage
+        ? `${data.customMessage}\n\n${t("viewReport", { url: publicUrl })}`
+        : t("defaultMessage", { name: customer.name, vehicle: vehicleName, url: publicUrl });
 
       const sentChannels: string[] = [];
 

+ 35 - 1
src/features/status-reports/Actions/submitFeedback.ts

@@ -1,6 +1,7 @@
 "use server";
 
 import { db } from "@/lib/db";
+import { notify } from "@/lib/notify";
 import { submitFeedbackSchema } from "../Schema/statusReportSchema";
 
 export async function submitStatusReportFeedback(input: unknown) {
@@ -8,18 +9,51 @@ export async function submitStatusReportFeedback(input: unknown) {
 
   const report = await db.statusReport.findUnique({
     where: { publicToken: data.token },
+    include: {
+      serviceRecord: {
+        select: {
+          id: true,
+          title: true,
+          vehicleId: true,
+          vehicle: {
+            select: {
+              year: true,
+              make: true,
+              model: true,
+              customer: { select: { name: true } },
+            },
+          },
+        },
+      },
+    },
   });
 
   if (!report) throw new Error("Status report not found");
   if (report.expiresAt && report.expiresAt < new Date()) throw new Error("This status report has expired");
+  if (report.customerFeedback) throw new Error("Feedback has already been submitted");
 
   await db.statusReport.update({
     where: { id: report.id },
     data: {
-      customerFeedback: data.feedback,
+      customerFeedback: data.feedback.slice(0, 2000),
       feedbackAt: new Date(),
     },
   });
 
+  // Notify the organization about the feedback
+  const vehicle = report.serviceRecord.vehicle;
+  const vehicleName = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean).join(" ");
+  const customerName = vehicle.customer?.name ?? "Customer";
+
+  await notify({
+    organizationId: report.organizationId,
+    type: "status_report_feedback",
+    title: "New Status Report Feedback",
+    message: `${customerName} responded to the status report for ${vehicleName}: "${data.feedback.length > 100 ? data.feedback.slice(0, 100) + "..." : data.feedback}"`,
+    entityType: "ServiceRecord",
+    entityId: report.serviceRecord.id,
+    entityUrl: `/vehicles/${report.serviceRecord.vehicleId}/service/${report.serviceRecord.id}?tab=statusReports`,
+  });
+
   return { success: true };
 }

+ 225 - 42
src/features/status-reports/Components/StatusReportList.tsx

@@ -3,22 +3,52 @@
 import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { Plus, Copy, Send, ExternalLink, FileVideo } from "lucide-react";
+import { Plus, Copy, Send, ExternalLink, FileVideo, Video, Trash2, MessageCircle } from "lucide-react";
 import { Card, CardContent } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
 import { toast } from "sonner";
 import { CreateStatusReportDialog } from "./CreateStatusReportDialog";
 import { SendStatusReportDialog } from "./SendStatusReportDialog";
+import { deleteStatusReport } from "../Actions/deleteStatusReport";
 
 interface StatusReportSummary {
   id: string;
   title: string | null;
+  message: string | null;
   status: string;
   videoUrl: string | null;
   createdAt: string;
   expiresAt: string | null;
   publicToken: string;
+  customerFeedback: string | null;
+  feedbackAt: string | null;
+  sentVia: string | null;
+  sentAt: string | null;
 }
 
 interface StatusReportListProps {
@@ -59,6 +89,9 @@ export function StatusReportList({
   const router = useRouter();
   const [showCreate, setShowCreate] = useState(false);
   const [sendReportId, setSendReportId] = useState<string | null>(null);
+  const [deleteReportId, setDeleteReportId] = useState<string | null>(null);
+  const [deleting, setDeleting] = useState(false);
+  const [detailReport, setDetailReport] = useState<StatusReportSummary | null>(null);
 
   function copyLink(publicToken: string) {
     const url = `${window.location.origin}/share/status-report/${organizationId}/${publicToken}`;
@@ -70,9 +103,34 @@ export function StatusReportList({
     window.open(`/share/status-report/${organizationId}/${publicToken}`, "_blank");
   }
 
+  async function confirmDelete() {
+    if (!deleteReportId) return;
+    setDeleting(true);
+    try {
+      const result = await deleteStatusReport(deleteReportId);
+      if (result.success) {
+        toast.success(t("deleted"));
+        router.refresh();
+      } else {
+        toast.error(result.error || t("deleteFailed"));
+      }
+    } catch {
+      toast.error(t("deleteFailed"));
+    } finally {
+      setDeleting(false);
+      setDeleteReportId(null);
+    }
+  }
+
+  function fmtDate(iso: string) {
+    return new Date(iso).toLocaleDateString(undefined, {
+      month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit",
+    });
+  }
+
   return (
     <>
-      <div className="space-y-4">
+      <div className="space-y-3">
         <div className="flex items-center justify-between">
           <h3 className="text-sm font-medium">{t("title")}</h3>
           <Button size="sm" onClick={() => setShowCreate(true)}>
@@ -90,52 +148,162 @@ export function StatusReportList({
             </CardContent>
           </Card>
         ) : (
-          <div className="space-y-2">
-            {initialReports.map((report) => (
-              <Card key={report.id}>
-                <CardContent className="flex items-center justify-between gap-3 py-3">
-                  <div className="min-w-0">
-                    <div className="flex items-center gap-2">
-                      <p className="truncate text-sm font-medium">
-                        {report.title || t("title")}
-                      </p>
-                      <Badge variant="outline" className={`shrink-0 text-[10px] ${STATUS_VARIANT[report.status] || ""}`}>
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead className="w-[35%]">{t("title")}</TableHead>
+                  <TableHead>{t("statusLabel")}</TableHead>
+                  <TableHead className="hidden sm:table-cell">{t("created")}</TableHead>
+                  <TableHead className="hidden md:table-cell">{t("feedback")}</TableHead>
+                  <TableHead className="text-right" />
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {initialReports.map((report) => (
+                  <TableRow
+                    key={report.id}
+                    className="cursor-pointer"
+                    onClick={() => setDetailReport(report)}
+                  >
+                    <TableCell className="py-2">
+                      <div className="flex items-center gap-1.5 min-w-0">
+                        {report.videoUrl && <Video className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
+                        <span className="truncate text-sm font-medium">{report.title || t("title")}</span>
+                      </div>
+                    </TableCell>
+                    <TableCell className="py-2">
+                      <Badge variant="outline" className={`text-[10px] ${STATUS_VARIANT[report.status] || ""}`}>
                         {t(report.status as "draft" | "published" | "sent" | "viewed")}
                       </Badge>
-                    </div>
-                    <p className="text-xs text-muted-foreground">
+                    </TableCell>
+                    <TableCell className="py-2 text-xs text-muted-foreground hidden sm:table-cell" suppressHydrationWarning>
                       {new Date(report.createdAt).toLocaleDateString(undefined, {
-                        year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
+                        month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
                       })}
-                    </p>
-                    {report.expiresAt && (
-                      <p className="text-xs text-muted-foreground">
-                        {t("expires")}: {new Date(report.expiresAt).toLocaleDateString(undefined, {
-                          year: "numeric", month: "short", day: "numeric",
-                        })}
-                      </p>
-                    )}
-                  </div>
-                  <div className="flex shrink-0 items-center gap-1">
-                    <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => copyLink(report.publicToken)}>
-                      <Copy className="h-3.5 w-3.5" />
-                    </Button>
-                    {customer && (
-                      <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setSendReportId(report.id)}>
-                        <Send className="h-3.5 w-3.5" />
-                      </Button>
-                    )}
-                    <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => openReport(report.publicToken)}>
-                      <ExternalLink className="h-3.5 w-3.5" />
-                    </Button>
-                  </div>
-                </CardContent>
-              </Card>
-            ))}
+                    </TableCell>
+                    <TableCell className="py-2 hidden md:table-cell">
+                      {report.customerFeedback ? (
+                        <Badge variant="outline" className="text-[10px] bg-green-500/10 text-green-600">
+                          <MessageCircle className="h-3 w-3 mr-1" />
+                          {t("hasComment")}
+                        </Badge>
+                      ) : (
+                        <span className="text-xs text-muted-foreground">-</span>
+                      )}
+                    </TableCell>
+                    <TableCell className="py-2 text-right" onClick={(e) => e.stopPropagation()}>
+                      <div className="flex items-center justify-end gap-1">
+                        <Button variant="ghost" size="sm" className="h-7 px-2" onClick={() => copyLink(report.publicToken)}>
+                          <Copy className="h-3.5 w-3.5" />
+                          <span className="hidden xl:inline ml-1.5">{t("copyLink")}</span>
+                        </Button>
+                        {customer && (
+                          <Button variant="ghost" size="sm" className="h-7 px-2" onClick={() => setSendReportId(report.id)}>
+                            <Send className="h-3.5 w-3.5" />
+                            <span className="hidden xl:inline ml-1.5">{t("send")}</span>
+                          </Button>
+                        )}
+                        <Button variant="ghost" size="sm" className="h-7 px-2" onClick={() => openReport(report.publicToken)}>
+                          <ExternalLink className="h-3.5 w-3.5" />
+                          <span className="hidden xl:inline ml-1.5">{t("view")}</span>
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          className="h-7 px-2 text-destructive hover:text-destructive"
+                          onClick={() => setDeleteReportId(report.id)}
+                        >
+                          <Trash2 className="h-3.5 w-3.5" />
+                          <span className="hidden xl:inline ml-1.5">{t("delete")}</span>
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
           </div>
         )}
       </div>
 
+      {/* Detail dialog */}
+      <Dialog open={!!detailReport} onOpenChange={(open) => { if (!open) setDetailReport(null); }}>
+        <DialogContent className="sm:max-w-lg">
+          {detailReport && (
+            <>
+              <DialogHeader>
+                <DialogTitle>{detailReport.title || t("title")}</DialogTitle>
+              </DialogHeader>
+              <div className="space-y-4">
+                {/* Status & dates */}
+                <div className="grid grid-cols-2 gap-3 text-sm">
+                  <div>
+                    <p className="text-xs text-muted-foreground">{t("statusLabel")}</p>
+                    <Badge variant="outline" className={`mt-0.5 text-[10px] ${STATUS_VARIANT[detailReport.status] || ""}`}>
+                      {t(detailReport.status as "draft" | "published" | "sent" | "viewed")}
+                    </Badge>
+                  </div>
+                  <div>
+                    <p className="text-xs text-muted-foreground">{t("created")}</p>
+                    <p className="mt-0.5">{fmtDate(detailReport.createdAt)}</p>
+                  </div>
+                  {detailReport.sentAt && (
+                    <div>
+                      <p className="text-xs text-muted-foreground">{t("sentAt")}</p>
+                      <p className="mt-0.5">{fmtDate(detailReport.sentAt)}</p>
+                    </div>
+                  )}
+                  {detailReport.sentVia && (
+                    <div>
+                      <p className="text-xs text-muted-foreground">{t("sentVia")}</p>
+                      <p className="mt-0.5 capitalize">{detailReport.sentVia}</p>
+                    </div>
+                  )}
+                  {detailReport.expiresAt && (
+                    <div>
+                      <p className="text-xs text-muted-foreground">{t("expires")}</p>
+                      <p className="mt-0.5">{new Date(detailReport.expiresAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}</p>
+                    </div>
+                  )}
+                </div>
+
+                {/* Message */}
+                {detailReport.message && (
+                  <div>
+                    <p className="text-xs text-muted-foreground mb-1">{t("messageLabel")}</p>
+                    <p className="text-sm whitespace-pre-wrap rounded-md bg-muted/50 p-3">{detailReport.message}</p>
+                  </div>
+                )}
+
+                {/* Video */}
+                {detailReport.videoUrl && (
+                  <div>
+                    <p className="text-xs text-muted-foreground mb-1">{t("videoLabel")}</p>
+                    <video src={detailReport.videoUrl} controls preload="metadata" playsInline className="w-full rounded-md max-h-64" />
+                  </div>
+                )}
+
+                {/* Customer feedback */}
+                <div>
+                  <p className="text-xs text-muted-foreground mb-1">{t("feedback")}</p>
+                  {detailReport.customerFeedback ? (
+                    <div className="rounded-md border border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-950/30 p-3">
+                      <p className="text-sm whitespace-pre-wrap">{detailReport.customerFeedback}</p>
+                      {detailReport.feedbackAt && (
+                        <p className="mt-2 text-xs text-muted-foreground">{fmtDate(detailReport.feedbackAt)}</p>
+                      )}
+                    </div>
+                  ) : (
+                    <p className="text-sm text-muted-foreground">{t("noFeedback")}</p>
+                  )}
+                </div>
+              </div>
+            </>
+          )}
+        </DialogContent>
+      </Dialog>
+
       <CreateStatusReportDialog
         open={showCreate}
         onOpenChange={setShowCreate}
@@ -146,15 +314,15 @@ export function StatusReportList({
         emailEnabled={emailEnabled}
         telegramEnabled={telegramEnabled}
         onCreated={(reportId) => {
-          router.refresh();
           if (customer) setSendReportId(reportId);
+          else router.refresh();
         }}
       />
 
       {sendReportId && customer && (
         <SendStatusReportDialog
           open={!!sendReportId}
-          onOpenChange={(open) => { if (!open) setSendReportId(null); }}
+          onOpenChange={(open) => { if (!open) { setSendReportId(null); router.refresh(); } }}
           reportId={sendReportId}
           customer={customer}
           smsEnabled={smsEnabled}
@@ -162,6 +330,21 @@ export function StatusReportList({
           telegramEnabled={telegramEnabled}
         />
       )}
+
+      <AlertDialog open={!!deleteReportId} onOpenChange={(open) => { if (!open) setDeleteReportId(null); }}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>{t("deleteTitle")}</AlertDialogTitle>
+            <AlertDialogDescription>{t("deleteConfirm")}</AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel disabled={deleting}>{t("cancel")}</AlertDialogCancel>
+            <AlertDialogAction variant="destructive" disabled={deleting} onClick={confirmDelete}>
+              {t("delete")}
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
     </>
   );
 }

+ 1 - 1
src/features/status-reports/Schema/statusReportSchema.ts

@@ -21,7 +21,7 @@ export const sendStatusReportSchema = z.object({
 
 export const submitFeedbackSchema = z.object({
   token: z.string(),
-  feedback: z.string().min(1),
+  feedback: z.string().min(1).max(2000),
 });
 
 export type CreateStatusReportInput = z.infer<typeof createStatusReportSchema>;

+ 0 - 30
src/features/team/Actions/getUserOrganizations.ts

@@ -1,30 +0,0 @@
-"use server";
-
-import { getCachedSession } from "@/lib/cached-session";
-import { db } from "@/lib/db";
-
-export async function getUserOrganizations() {
-  const session = await getCachedSession();
-
-  if (!session?.user?.id) {
-    return { success: false as const, error: "Unauthorized" };
-  }
-
-  const memberships = await db.organizationMember.findMany({
-    where: { userId: session.user.id },
-    select: {
-      role: true,
-      organization: {
-        select: { id: true, name: true },
-      },
-    },
-  });
-
-  const organizations = memberships.map((m) => ({
-    id: m.organization.id,
-    name: m.organization.name,
-    role: m.role,
-  }));
-
-  return { success: true as const, data: organizations };
-}

+ 0 - 122
src/features/telegram/Components/SendTelegramDialog.tsx

@@ -1,122 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useTranslations } from "next-intl";
-import { Button } from "@/components/ui/button";
-import { Textarea } from "@/components/ui/textarea";
-import { Label } from "@/components/ui/label";
-import {
-  Dialog,
-  DialogContent,
-  DialogHeader,
-  DialogTitle,
-} from "@/components/ui/dialog";
-import { Loader2, Send } from "lucide-react";
-import { useGlassModal } from "@/components/glass-modal";
-import { sendTelegramToCustomer } from "../Actions/telegramActions";
-
-interface SendTelegramDialogProps {
-  open: boolean;
-  onOpenChange: (open: boolean) => void;
-  customerId: string;
-  customerName: string;
-  entityLabel?: string;
-  defaultMessage?: string;
-  relatedEntityType?: string;
-  relatedEntityId?: string;
-}
-
-export function SendTelegramDialog({
-  open,
-  onOpenChange,
-  customerId,
-  customerName,
-  entityLabel = "Telegram",
-  defaultMessage = "",
-  relatedEntityType,
-  relatedEntityId,
-}: SendTelegramDialogProps) {
-  const modal = useGlassModal();
-  const t = useTranslations("telegramMessages.sendDialog");
-  const tc = useTranslations("common.buttons");
-  const te = useTranslations("common.errors");
-  const [sending, setSending] = useState(false);
-  const [message, setMessage] = useState(defaultMessage);
-
-  const handleSend = async (e: React.FormEvent) => {
-    e.preventDefault();
-    if (!message.trim()) return;
-    setSending(true);
-
-    const result = await sendTelegramToCustomer({
-      customerId,
-      body: message.trim(),
-      relatedEntityType,
-      relatedEntityId,
-    });
-
-    if (result.success) {
-      modal.open(
-        "success",
-        t("messageSent"),
-        t("sentTo", { name: customerName }),
-      );
-      onOpenChange(false);
-      setMessage("");
-    } else {
-      modal.open("error", te("error"), result.error || t("sendError"));
-    }
-    setSending(false);
-  };
-
-  return (
-    <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className="max-w-md">
-        <DialogHeader>
-          <DialogTitle className="flex items-center gap-2">
-            <Send className="h-5 w-5" />
-            {t("send", { label: entityLabel })}
-          </DialogTitle>
-        </DialogHeader>
-        <form onSubmit={handleSend} className="space-y-4">
-          <div className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2">
-            <span className="text-sm font-medium">{customerName}</span>
-            <span className="text-sm text-muted-foreground">Telegram</span>
-          </div>
-          <div className="space-y-2">
-            <Label htmlFor="telegram-message">{t("message")}</Label>
-            <Textarea
-              id="telegram-message"
-              placeholder={t("placeholder")}
-              value={message}
-              onChange={(e) => setMessage(e.target.value)}
-              rows={4}
-              maxLength={4096}
-              required
-            />
-            <p className="text-xs text-muted-foreground">
-              {t("characters", { count: message.length })}
-            </p>
-          </div>
-          <div className="flex justify-end gap-2">
-            <Button
-              type="button"
-              variant="ghost"
-              onClick={() => onOpenChange(false)}
-            >
-              {tc("cancel")}
-            </Button>
-            <Button type="submit" disabled={sending || !message.trim()}>
-              {sending ? (
-                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-              ) : (
-                <Send className="mr-2 h-4 w-4" />
-              )}
-              {t("sendTelegram")}
-            </Button>
-          </div>
-        </form>
-      </DialogContent>
-    </Dialog>
-  );
-}

+ 0 - 23
src/features/vehicles/Actions/deleteUploadedFile.ts

@@ -1,23 +0,0 @@
-"use server";
-
-import { withAuth } from "@/lib/with-auth";
-import { PermissionAction, PermissionSubject } from "@/lib/permissions";
-import { unlink } from "fs/promises";
-import { resolveUploadPath } from "@/lib/resolve-upload-path";
-
-/**
- * Delete an uploaded file from disk.
- * Verifies the file URL belongs to the caller's organization.
- */
-export async function deleteUploadedFile(fileUrl: string) {
-  return withAuth(async ({ organizationId }) => {
-    // Only allow deleting files that belong to this org
-    if (!fileUrl.startsWith(`/api/protected/files/${organizationId}/`)) {
-      throw new Error("Forbidden");
-    }
-
-    const filePath = resolveUploadPath(fileUrl);
-    await unlink(filePath);
-    return { deleted: true };
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.SERVICES }] });
-}

+ 35 - 33
src/features/vehicles/Actions/getMyActiveJobs.ts

@@ -1,30 +1,30 @@
-'use server'
+"use server";
 
-import { db } from '@/lib/db'
-import { withAuth } from '@/lib/with-auth'
-import { PermissionAction, PermissionSubject } from '@/lib/permissions'
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 export interface MyActiveJob {
-  id: string
-  title: string
-  status: string
-  vehicleId: string
+  id: string;
+  title: string;
+  status: string;
+  vehicleId: string;
   vehicle: {
-    make: string
-    model: string
-    year: number
-    licensePlate: string | null
-  }
+    make: string;
+    model: string;
+    year: number;
+    licensePlate: string | null;
+  };
   customer: {
-    id: string
-    name: string
-    email: string | null
-    phone: string | null
-    telegramChatId: string | null
-  } | null
-  imageCount: number
-  videoCount: number
-  partCount: number
+    id: string;
+    name: string;
+    email: string | null;
+    phone: string | null;
+    telegramChatId: string | null;
+  } | null;
+  imageCount: number;
+  videoCount: number;
+  partCount: number;
 }
 
 export async function getMyActiveJobs() {
@@ -34,16 +34,16 @@ export async function getMyActiveJobs() {
       const myTechnicians = await db.technician.findMany({
         where: { organizationId, userId, isActive: true },
         select: { id: true },
-      })
+      });
 
-      if (myTechnicians.length === 0) return []
+      if (myTechnicians.length === 0) return [];
 
-      const techIds = myTechnicians.map((t) => t.id)
+      const techIds = myTechnicians.map((t) => t.id);
 
       const records = await db.serviceRecord.findMany({
         where: {
           vehicle: { organizationId },
-          status: { in: ['in-progress', 'pending', 'waiting-parts'] },
+          status: { in: ["in-progress", "pending", "waiting-parts"] },
           technicianId: { in: techIds },
         },
         select: {
@@ -77,9 +77,9 @@ export async function getMyActiveJobs() {
             },
           },
         },
-        orderBy: { updatedAt: 'desc' },
+        orderBy: { updatedAt: "desc" },
         take: 20,
-      })
+      });
 
       return records.map((r) => ({
         id: r.id,
@@ -88,13 +88,15 @@ export async function getMyActiveJobs() {
         vehicleId: r.vehicleId,
         vehicle: r.vehicle,
         customer: r.vehicle.customer,
-        imageCount: r.attachments.filter((a) => a.category === 'image').length,
-        videoCount: r.attachments.filter((a) => a.category === 'video').length,
+        imageCount: r.attachments.filter((a) => a.category === "image").length,
+        videoCount: r.attachments.filter((a) => a.category === "video").length,
         partCount: r._count.partItems,
-      }))
+      }));
     },
     {
-      requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.SERVICES }],
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.SERVICES },
+      ],
     }
-  )
+  );
 }

+ 48 - 64
src/features/vehicles/Components/MyActiveJobs.tsx

@@ -40,6 +40,9 @@ import type { MyActiveJob } from '@/features/vehicles/Actions/getMyActiveJobs'
 
 interface MyActiveJobsProps {
   jobs: MyActiveJob[]
+  smsEnabled: boolean
+  emailEnabled: boolean
+  telegramEnabled: boolean
 }
 
 const STATUS_ICON: Record<string, typeof Wrench> = {
@@ -54,12 +57,10 @@ const STATUS_COLOR: Record<string, string> = {
   'waiting-parts': 'bg-orange-500/10 text-orange-600',
 }
 
-export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
+export function MyActiveJobs({ jobs, smsEnabled, emailEnabled, telegramEnabled }: MyActiveJobsProps) {
   const t = useTranslations('dashboard.myJobs')
   const router = useRouter()
-  const [uploading, setUploading] = useState<{ jobId: string; type: 'photo' | 'video' } | null>(
-    null
-  )
+  const [uploading, setUploading] = useState<{ jobId: string; type: 'photo' | 'video' } | null>(null)
   const [imageCounts, setImageCounts] = useState<Record<string, number>>(() =>
     Object.fromEntries(jobs.map((j) => [j.id, j.imageCount]))
   )
@@ -518,14 +519,8 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                 <div className="min-w-0 flex-1">
                   <p className="font-medium truncate">{scannedPart.name}</p>
                   <div className="flex items-center gap-2 text-sm text-muted-foreground">
-                    {scannedPart.partNumber && (
-                      <span className="font-mono text-xs">{scannedPart.partNumber}</span>
-                    )}
-                    {scannedPart.category && (
-                      <Badge variant="secondary" className="text-[10px]">
-                        {scannedPart.category}
-                      </Badge>
-                    )}
+                    {scannedPart.partNumber && <span className="font-mono text-xs">{scannedPart.partNumber}</span>}
+                    {scannedPart.category && <Badge variant="secondary" className="text-[10px]">{scannedPart.category}</Badge>}
                   </div>
                 </div>
                 <div className="text-right text-sm text-muted-foreground shrink-0">
@@ -543,9 +538,7 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                 >
                   -
                 </Button>
-                <span className="w-16 text-center text-3xl font-semibold tabular-nums">
-                  {addQty}
-                </span>
+                <span className="w-16 text-center text-3xl font-semibold tabular-nums">{addQty}</span>
                 <Button
                   variant="outline"
                   size="icon"
@@ -584,55 +577,46 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
         initialBarcode={pendingBarcode}
       />
 
-      {statusReportJobId &&
-        (() => {
-          const job = jobs.find((j) => j.id === statusReportJobId)
-          if (!job) return null
-          return (
-            <CreateStatusReportDialog
-              open={!!statusReportJobId}
-              onOpenChange={(open) => {
-                if (!open) setStatusReportJobId(null)
-              }}
-              serviceRecordId={statusReportJobId}
-              vehicleName={`${job.vehicle.year} ${job.vehicle.make} ${job.vehicle.model}`}
-              customer={job.customer}
-              smsEnabled={false}
-              emailEnabled={true}
-              telegramEnabled={false}
-              onCreated={(reportId) => {
-                const currentJobId = statusReportJobId
-                setStatusReportJobId(null)
-                if (job.customer) {
-                  setSendReportId(reportId)
-                  setSendReportJobId(currentJobId)
-                }
-              }}
-            />
-          )
-        })()}
-
-      {sendReportId &&
-        (() => {
-          const job = sendReportJobId ? jobs.find((j) => j.id === sendReportJobId) : null
-          if (!job?.customer) return null
-          return (
-            <SendStatusReportDialog
-              open={!!sendReportId}
-              onOpenChange={(open) => {
-                if (!open) {
-                  setSendReportId(null)
-                  setSendReportJobId(null)
-                }
-              }}
-              reportId={sendReportId}
-              customer={job.customer}
-              smsEnabled={false}
-              emailEnabled={true}
-              telegramEnabled={false}
-            />
-          )
-        })()}
+      {statusReportJobId && (() => {
+        const job = jobs.find(j => j.id === statusReportJobId)
+        if (!job) return null
+        return (
+          <CreateStatusReportDialog
+            open={!!statusReportJobId}
+            onOpenChange={(open) => { if (!open) setStatusReportJobId(null) }}
+            serviceRecordId={statusReportJobId}
+            vehicleName={`${job.vehicle.year} ${job.vehicle.make} ${job.vehicle.model}`}
+            customer={job.customer}
+            smsEnabled={smsEnabled}
+            emailEnabled={emailEnabled}
+            telegramEnabled={telegramEnabled}
+            onCreated={(reportId) => {
+              const currentJobId = statusReportJobId
+              setStatusReportJobId(null)
+              if (job.customer) {
+                setSendReportId(reportId)
+                setSendReportJobId(currentJobId)
+              }
+            }}
+          />
+        )
+      })()}
+
+      {sendReportId && (() => {
+        const job = sendReportJobId ? jobs.find(j => j.id === sendReportJobId) : null
+        if (!job?.customer) return null
+        return (
+          <SendStatusReportDialog
+            open={!!sendReportId}
+            onOpenChange={(open) => { if (!open) { setSendReportId(null); setSendReportJobId(null) } }}
+            reportId={sendReportId}
+            customer={job.customer}
+            smsEnabled={smsEnabled}
+            emailEnabled={emailEnabled}
+            telegramEnabled={telegramEnabled}
+          />
+        )
+      })()}
     </>
   )
 }

+ 0 - 63
src/features/vehicles/Components/service-detail/ServiceNotesDisplay.tsx

@@ -1,63 +0,0 @@
-'use client'
-
-import { FileText, Globe, Lock } from 'lucide-react'
-import { Separator } from '@/components/ui/separator'
-import { sanitizeHtml } from '@/lib/sanitize-html'
-
-interface ServiceNotesDisplayProps {
-  invoiceNotes: string | null
-  diagnosticNotes: string | null
-  description: string | null
-}
-
-export function ServiceNotesDisplay({
-  invoiceNotes,
-  diagnosticNotes,
-  description,
-}: ServiceNotesDisplayProps) {
-  const hasPublic = !!invoiceNotes
-  const hasInternal = !!diagnosticNotes || !!description
-
-  if (!hasPublic && !hasInternal) return null
-
-  return (
-    <div className="rounded-lg border p-3">
-      <h3 className="mb-2 flex items-center gap-2 text-sm font-semibold">
-        <FileText className="h-3.5 w-3.5" />
-        Notes
-      </h3>
-      <div className="space-y-3">
-        {hasPublic && (
-          <div>
-            <span className="mb-1 flex items-center gap-1 text-xs font-medium text-muted-foreground">
-              <Globe className="h-3 w-3" />
-              Public
-            </span>
-            <div
-              className="notes-content text-sm"
-              dangerouslySetInnerHTML={{ __html: sanitizeHtml(invoiceNotes!) }}
-            />
-          </div>
-        )}
-        {hasPublic && hasInternal && <Separator />}
-        {hasInternal && (
-          <div>
-            <span className="mb-1 flex items-center gap-1 text-xs font-medium text-muted-foreground">
-              <Lock className="h-3 w-3" />
-              Internal
-            </span>
-            {diagnosticNotes && (
-              <div
-                className="notes-content text-sm"
-                dangerouslySetInnerHTML={{ __html: sanitizeHtml(diagnosticNotes) }}
-              />
-            )}
-            {description && !diagnosticNotes && (
-              <p className="whitespace-pre-wrap text-sm">{description}</p>
-            )}
-          </div>
-        )}
-      </div>
-    </div>
-  )
-}

+ 0 - 97
src/features/vehicles/Components/service-detail/ServiceSidebar.tsx

@@ -1,97 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { useTranslations } from "next-intl";
-import { useServiceType } from "@/components/service-type-context";
-import { Building2, Car, Mail, MapPin, Phone, Users } from "lucide-react";
-import { CustomFieldsForm } from "@/features/custom-fields/Components/CustomFieldsForm";
-import type { Vehicle } from "./types";
-
-interface ServiceSidebarProps {
-  recordId: string;
-  vehicle: Vehicle;
-  vehicleName: string;
-  distUnit: string;
-  mileage: number | null;
-}
-
-export function ServiceSidebar({
-  recordId,
-  vehicle,
-  vehicleName,
-  distUnit,
-  mileage,
-}: ServiceSidebarProps) {
-  const t = useTranslations("service.sidebar");
-  const serviceType = useServiceType();
-  return (
-    <>
-      {/* Vehicle Info */}
-      <div className="rounded-lg border p-3">
-        <h3 className="mb-1.5 flex items-center gap-2 text-xs font-medium text-muted-foreground">
-          <Car className="h-3.5 w-3.5" />
-          {t("vehicle")}
-        </h3>
-        <Link href={`/vehicles/${vehicle.id}`} className="text-sm font-semibold hover:underline">
-          {vehicleName}
-        </Link>
-        {vehicle.licensePlate && (
-          <p className="font-mono text-xs text-muted-foreground">{vehicle.licensePlate}</p>
-        )}
-        {vehicle.vin && (
-          <p className="font-mono text-xs text-muted-foreground">{serviceType === 'boat' ? `HIN: ${vehicle.vin}` : t("vin", { vin: vehicle.vin })}</p>
-        )}
-        {mileage && (
-          <p className="mt-0.5 text-xs text-muted-foreground">
-            {serviceType === 'boat' ? t("engineHoursAtService", { mileage: mileage.toLocaleString(), unit: distUnit }) : t("mileageAtService", { mileage: mileage.toLocaleString(), unit: distUnit })}
-          </p>
-        )}
-      </div>
-
-      {/* Customer Info */}
-      {vehicle.customer && (
-        <div className="rounded-lg border p-3">
-          <h3 className="mb-1.5 flex items-center gap-2 text-xs font-medium text-muted-foreground">
-            <Users className="h-3.5 w-3.5" />
-            {t("customer")}
-          </h3>
-          <Link
-            href={`/customers/${vehicle.customer.id}`}
-            className="text-sm font-semibold hover:underline"
-          >
-            {vehicle.customer.name}
-          </Link>
-          <div className="mt-1 space-y-0.5 text-xs text-muted-foreground">
-            {vehicle.customer.company && (
-              <div className="flex items-center gap-1.5">
-                <Building2 className="h-3 w-3 shrink-0" />
-                {vehicle.customer.company}
-              </div>
-            )}
-            {vehicle.customer.email && (
-              <div className="flex items-center gap-1.5">
-                <Mail className="h-3 w-3 shrink-0" />
-                {vehicle.customer.email}
-              </div>
-            )}
-            {vehicle.customer.phone && (
-              <div className="flex items-center gap-1.5">
-                <Phone className="h-3 w-3 shrink-0" />
-                {vehicle.customer.phone}
-              </div>
-            )}
-            {vehicle.customer.address && (
-              <div className="flex items-center gap-1.5">
-                <MapPin className="h-3 w-3 shrink-0" />
-                {vehicle.customer.address}
-              </div>
-            )}
-          </div>
-        </div>
-      )}
-
-      {/* Custom Fields */}
-      <CustomFieldsForm entityId={recordId} entityType="service_record" />
-    </>
-  );
-}

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

@@ -151,7 +151,7 @@ export function InvoiceDetailsSection({
                 className={cn('w-full justify-start text-left font-normal h-9 text-sm', !invoiceDate && 'text-muted-foreground')}
               >
                 <CalendarIcon className="mr-2 h-3.5 w-3.5" />
-                {invoiceDate ? formatDate(invoiceDate) : t('invoiceDate')}
+                <span suppressHydrationWarning>{invoiceDate ? formatDate(invoiceDate) : t('invoiceDate')}</span>
               </Button>
             </PopoverTrigger>
             <PopoverContent className="w-auto p-0" align="start">
@@ -169,7 +169,7 @@ export function InvoiceDetailsSection({
                 className={cn('w-full justify-start text-left font-normal h-9 text-sm', !invoiceDueDate && 'text-muted-foreground')}
               >
                 <CalendarIcon className="mr-2 h-3.5 w-3.5" />
-                {invoiceDueDate ? formatDate(invoiceDueDate) : t('invoiceDueDate')}
+                <span suppressHydrationWarning>{invoiceDueDate ? formatDate(invoiceDueDate) : t('invoiceDueDate')}</span>
               </Button>
             </PopoverTrigger>
             <PopoverContent className="w-auto p-0" align="start">

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

@@ -35,13 +35,13 @@ export function LaborEditor({
 
   return (
     <div className="rounded-lg border p-3 space-y-2">
-      <div className="flex items-center justify-between">
+      <div className="flex flex-wrap items-center justify-between gap-2">
         <h3 className="text-sm font-semibold">{t('title')}</h3>
-        <div className="flex gap-2">
+        <div className="flex flex-wrap gap-1.5">
           {hasPresets && onOpenPresets && (
             <Button type="button" variant="outline" size="sm" onClick={onOpenPresets}>
               <Layers className="mr-1 h-3.5 w-3.5" />
-              {t('fromPresets')}
+              <span className="hidden sm:inline">{t('fromPresets')}</span>
             </Button>
           )}
           <Button
@@ -51,7 +51,7 @@ export function LaborEditor({
             onClick={() => setLaborItems((prev) => [...prev, makeEmptyLabor(defaultLaborRate)])}
           >
             <Plus className="mr-1 h-3.5 w-3.5" />
-            {t('addLabor')}
+            <span className="hidden sm:inline">{t('addLabor')}</span>
           </Button>
           <Button
             type="button"
@@ -60,7 +60,7 @@ export function LaborEditor({
             onClick={() => setLaborItems((prev) => [...prev, makeEmptyService()])}
           >
             <Wrench className="mr-1 h-3.5 w-3.5" />
-            {t('addService')}
+            <span className="hidden sm:inline">{t('addService')}</span>
           </Button>
         </div>
       </div>

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

@@ -40,9 +40,9 @@ import { useServiceFormState } from './useServiceFormState'
 import { useServiceActions } from './useServiceActions'
 import { DetailsLeftColumn } from './DetailsLeftColumn'
 import { DetailsRightColumn } from './DetailsRightColumn'
-import type { ServicePageClientProps } from './service-page-types'
 
 export type { ServicePageClientProps, BoardTechnicianOption } from './service-page-types'
+import type { ServicePageClientProps } from './service-page-types'
 
 export function ServicePageClient({
   record,
@@ -72,25 +72,35 @@ export function ServicePageClient({
   aiEnabled = false,
   defaultDueDays = 0,
   statusReports = [],
+  initialTab,
 }: ServicePageClientProps) {
   const t = useTranslations('service')
   const router = useRouter()
-  const [activeTab, setActiveTab] = useState<ServiceTab>('details')
+
+  const validTabs: ServiceTab[] = ['details', 'images', 'video', 'documents', 'statusReports']
+  const resolvedInitialTab = initialTab && validTabs.includes(initialTab as ServiceTab) ? (initialTab as ServiceTab) : 'details'
+
+  const [activeTab, setActiveTab] = useState<ServiceTab>(resolvedInitialTab)
+
+  const handleTabChange = useCallback((tab: ServiceTab) => {
+    setActiveTab(tab)
+    const url = new URL(window.location.href)
+    if (tab === 'details') url.searchParams.delete('tab')
+    else url.searchParams.set('tab', tab)
+    window.history.replaceState(null, '', url.pathname + url.search)
+  }, [])
 
   // Date check dialog state
   const [showDateCheck, setShowDateCheck] = useState(false)
   const dateCheckResolveRef = useRef<((proceed: boolean) => void) | null>(null)
   const today = new Date(new Date().toISOString().split('T')[0])
-  const suggestedDueDate =
-    defaultDueDays > 0 ? new Date(today.getTime() + defaultDueDays * 86400000) : today
+  const suggestedDueDate = defaultDueDays > 0 ? new Date(today.getTime() + defaultDueDays * 86400000) : today
   const [pendingInvoiceDate, setPendingInvoiceDate] = useState<Date>(today)
   const [pendingDueDate, setPendingDueDate] = useState<Date>(suggestedDueDate)
   const [updatingDates, setUpdatingDates] = useState(false)
 
-  const formatDate = (date: Date) =>
-    date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
-  const toISODate = (date: Date) =>
-    `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+  const formatDate = (date: Date) => date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
+  const toISODate = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
 
   const areDatesExpired = useMemo(() => {
     const invoiceDateStr = initialData.invoiceDate
@@ -112,48 +122,33 @@ export function ServicePageClient({
   const checkDates = useCallback(async () => {
     if (!areDatesExpired || formState.paymentStatus === 'paid') return true
     // Show current (expired) dates so user sees what's wrong
-    setPendingInvoiceDate(
-      initialData.invoiceDate ? new Date(initialData.invoiceDate + 'T00:00:00') : today
-    )
-    setPendingDueDate(
-      initialData.invoiceDueDate ? new Date(initialData.invoiceDueDate + 'T00:00:00') : today
-    )
+    setPendingInvoiceDate(initialData.invoiceDate ? new Date(initialData.invoiceDate + 'T00:00:00') : today)
+    setPendingDueDate(initialData.invoiceDueDate ? new Date(initialData.invoiceDueDate + 'T00:00:00') : today)
     setShowDateCheck(true)
     return new Promise<boolean>((resolve) => {
       dateCheckResolveRef.current = resolve
     })
-  }, [
-    areDatesExpired,
-    formState.paymentStatus,
-    initialData.invoiceDate,
-    initialData.invoiceDueDate,
-  ])
-
-  const handleBarcodeScan = useCallback(
-    async (barcode: string) => {
-      const result = await lookupPartByBarcode(barcode)
-      if (result.success && result.data) {
-        const part = result.data
-        const price = part.sellPrice > 0 ? part.sellPrice : part.unitCost
-        formState.dirtySetPartItems((prev) => [
-          ...prev,
-          {
-            partNumber: part.partNumber || '',
-            name: part.name,
-            quantity: 1,
-            unitPrice: price,
-            total: price,
-            unitCost: part.unitCost,
-            inventoryPartId: part.id,
-          },
-        ])
-        toast.success(t('parts.partFound', { name: part.name }))
-      } else {
-        toast.error(t('parts.partNotFound', { barcode }))
-      }
-    },
-    [formState, t]
-  )
+  }, [areDatesExpired, formState.paymentStatus, initialData.invoiceDate, initialData.invoiceDueDate])
+
+  const handleBarcodeScan = useCallback(async (barcode: string) => {
+    const result = await lookupPartByBarcode(barcode)
+    if (result.success && result.data) {
+      const part = result.data
+      const price = part.sellPrice > 0 ? part.sellPrice : part.unitCost
+      formState.dirtySetPartItems((prev) => [...prev, {
+        partNumber: part.partNumber || '',
+        name: part.name,
+        quantity: 1,
+        unitPrice: price,
+        total: price,
+        unitCost: part.unitCost,
+        inventoryPartId: part.id,
+      }])
+      toast.success(t('parts.partFound', { name: part.name }))
+    } else {
+      toast.error(t('parts.partNotFound', { barcode }))
+    }
+  }, [formState, t])
 
   useHardwareScanner({ onScan: handleBarcodeScan, enabled: activeTab === 'details' })
 
@@ -161,10 +156,8 @@ export function ServicePageClient({
     const newItems = preset.items.map((item) => ({
       description: item.description,
       hours: item.hours,
-      rate: item.rate > 0 ? item.rate : item.pricingType === 'service' ? 0 : defaultLaborRate,
-      total:
-        item.hours *
-        (item.rate > 0 ? item.rate : item.pricingType === 'service' ? 0 : defaultLaborRate),
+      rate: item.rate > 0 ? item.rate : (item.pricingType === 'service' ? 0 : defaultLaborRate),
+      total: item.hours * (item.rate > 0 ? item.rate : (item.pricingType === 'service' ? 0 : defaultLaborRate)),
       pricingType: (item.pricingType as 'hourly' | 'service') || 'hourly',
     }))
     formState.dirtySetLaborItems((prev) => [...prev, ...newItems])
@@ -186,7 +179,7 @@ export function ServicePageClient({
         status={formState.status}
         paymentStatus={formState.paymentStatus}
         activeTab={activeTab}
-        onTabChange={setActiveTab}
+        onTabChange={handleTabChange}
         tabCounts={{
           images: imageAttachmentsForManager.length,
           video: videoAttachments.length,
@@ -197,32 +190,14 @@ export function ServicePageClient({
         saving={formState.loading}
         hasUnsavedChanges={formState.hasUnsavedChanges}
         showSaved={formState.showSaved}
-        onDownloadPDF={async () => {
-          if (!(await checkDates())) return
-          if (formState.hasUnsavedChanges) await actions.saveNow()
-          actions.handleDownloadPDF()
-        }}
+        onDownloadPDF={async () => { if (!await checkDates()) return; if (formState.hasUnsavedChanges) await actions.saveNow(); actions.handleDownloadPDF() }}
         onDelete={actions.handleDelete}
-        onShowEmail={async () => {
-          if (!(await checkDates())) return
-          if (formState.hasUnsavedChanges) await actions.saveNow()
-          actions.setShowEmailDialog(true)
-        }}
-        onShowShare={async () => {
-          if (!(await checkDates())) return
-          if (formState.hasUnsavedChanges) await actions.saveNow()
-          actions.setShowShareDialog(true)
-        }}
+        onShowEmail={async () => { if (!await checkDates()) return; if (formState.hasUnsavedChanges) await actions.saveNow(); actions.setShowEmailDialog(true) }}
+        onShowShare={async () => { if (!await checkDates()) return; if (formState.hasUnsavedChanges) await actions.saveNow(); actions.setShowShareDialog(true) }}
       />
 
       {activeTab === 'details' && (
-        <form
-          id="service-record-form"
-          ref={formState.formRef}
-          onSubmit={actions.handleSubmit}
-          onInput={formState.markDirty}
-          className="flex min-h-0 flex-1 flex-col"
-        >
+        <form id="service-record-form" ref={formState.formRef} onSubmit={actions.handleSubmit} onInput={formState.markDirty} className="flex min-h-0 flex-1 flex-col">
           <ServiceDetailContent
             leftColumn={
               <DetailsLeftColumn
@@ -268,7 +243,10 @@ export function ServicePageClient({
 
       {activeTab === 'video' && (
         <div className="flex-1 overflow-y-auto overscroll-contain p-4">
-          <ServiceVideoManager serviceRecordId={record.id} initialVideos={videoAttachments} />
+          <ServiceVideoManager
+            serviceRecordId={record.id}
+            initialVideos={videoAttachments}
+          />
         </div>
       )}
 
@@ -289,17 +267,13 @@ export function ServicePageClient({
             serviceRecordId={record.id}
             organizationId={organizationId}
             vehicleName={formState.vehicleName}
-            customer={
-              record.vehicle.customer
-                ? {
-                    id: record.vehicle.customer.id,
-                    name: record.vehicle.customer.name,
-                    email: record.vehicle.customer.email,
-                    phone: record.vehicle.customer.phone,
-                    telegramChatId: record.vehicle.customer.telegramChatId || null,
-                  }
-                : null
-            }
+            customer={record.vehicle.customer ? {
+              id: record.vehicle.customer.id,
+              name: record.vehicle.customer.name,
+              email: record.vehicle.customer.email,
+              phone: record.vehicle.customer.phone,
+              telegramChatId: record.vehicle.customer.telegramChatId || null,
+            } : null}
             smsEnabled={smsEnabled}
             emailEnabled={emailEnabled}
             telegramEnabled={telegramEnabled}
@@ -373,23 +347,22 @@ export function ServicePageClient({
       )}
 
       {/* Expired dates check dialog */}
-      <Dialog
-        open={showDateCheck}
-        onOpenChange={(open) => {
-          if (!open) {
-            dateCheckResolveRef.current?.(false)
-            dateCheckResolveRef.current = null
-          }
-          setShowDateCheck(open)
-        }}
-      >
+      <Dialog open={showDateCheck} onOpenChange={(open) => {
+        if (!open) {
+          dateCheckResolveRef.current?.(false)
+          dateCheckResolveRef.current = null
+        }
+        setShowDateCheck(open)
+      }}>
         <DialogContent className="sm:max-w-sm">
           <DialogHeader>
             <DialogTitle className="flex items-center gap-2">
               <AlertTriangle className="h-5 w-5 text-amber-500" />
               {t('page.datesExpiredTitle')}
             </DialogTitle>
-            <DialogDescription>{t('page.datesExpiredDescription')}</DialogDescription>
+            <DialogDescription>
+              {t('page.datesExpiredDescription')}
+            </DialogDescription>
           </DialogHeader>
 
           <div className="space-y-3">
@@ -400,11 +373,7 @@ export function ServicePageClient({
               onClick={() => {
                 const now = new Date(new Date().toISOString().split('T')[0])
                 setPendingInvoiceDate(now)
-                setPendingDueDate(
-                  defaultDueDays > 0
-                    ? new Date(now.getTime() + defaultDueDays * 86400000)
-                    : new Date(now.getTime() + 14 * 86400000)
-                )
+                setPendingDueDate(defaultDueDays > 0 ? new Date(now.getTime() + defaultDueDays * 86400000) : new Date(now.getTime() + 14 * 86400000))
               }}
             >
               {t('page.datesExpiredSetToday')}
@@ -414,20 +383,13 @@ export function ServicePageClient({
               <Label className="text-xs">{t('basicInfo.invoiceDate')}</Label>
               <Popover>
                 <PopoverTrigger asChild>
-                  <Button
-                    variant="outline"
-                    className="w-full justify-start text-left font-normal h-9 text-sm"
-                  >
+                  <Button variant="outline" className="w-full justify-start text-left font-normal h-9 text-sm">
                     <CalendarIcon className="mr-2 h-3.5 w-3.5" />
-                    {formatDate(pendingInvoiceDate)}
+                    <span suppressHydrationWarning>{formatDate(pendingInvoiceDate)}</span>
                   </Button>
                 </PopoverTrigger>
                 <PopoverContent className="w-auto p-0" align="start">
-                  <Calendar
-                    mode="single"
-                    selected={pendingInvoiceDate}
-                    onSelect={(d) => d && setPendingInvoiceDate(d)}
-                  />
+                  <Calendar mode="single" selected={pendingInvoiceDate} onSelect={(d) => d && setPendingInvoiceDate(d)} />
                 </PopoverContent>
               </Popover>
             </div>
@@ -436,20 +398,13 @@ export function ServicePageClient({
               <Label className="text-xs">{t('basicInfo.invoiceDueDate')}</Label>
               <Popover>
                 <PopoverTrigger asChild>
-                  <Button
-                    variant="outline"
-                    className="w-full justify-start text-left font-normal h-9 text-sm"
-                  >
+                  <Button variant="outline" className="w-full justify-start text-left font-normal h-9 text-sm">
                     <CalendarIcon className="mr-2 h-3.5 w-3.5" />
-                    {formatDate(pendingDueDate)}
+                    <span suppressHydrationWarning>{formatDate(pendingDueDate)}</span>
                   </Button>
                 </PopoverTrigger>
                 <PopoverContent className="w-auto p-0" align="start">
-                  <Calendar
-                    mode="single"
-                    selected={pendingDueDate}
-                    onSelect={(d) => d && setPendingDueDate(d)}
-                  />
+                  <Calendar mode="single" selected={pendingDueDate} onSelect={(d) => d && setPendingDueDate(d)} />
                 </PopoverContent>
               </Popover>
             </div>

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

@@ -147,14 +147,14 @@ export function UnifiedServiceHeader({
           </ButtonGroup>
         </div>
       </div>
-      <nav className="flex gap-1 border-b">
+      <nav className="flex gap-1 border-b overflow-x-auto scrollbar-none">
         {tabs.map((tab) => (
           <button
             key={tab.value}
             type="button"
             onClick={() => onTabChange(tab.value)}
             className={cn(
-              'cursor-pointer px-3 py-1.5 text-sm font-medium transition-colors -mb-px border-b-2',
+              'cursor-pointer shrink-0 whitespace-nowrap px-3 py-1.5 text-sm font-medium transition-colors -mb-px border-b-2',
               activeTab === tab.value
                 ? 'border-primary text-foreground'
                 : 'border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground/50'

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

@@ -38,13 +38,7 @@ export interface ServicePageClientProps {
   defaultLaborRate: number
   initialData: InitialData
   inventoryParts: InventoryPartOption[]
-  initialVehicle: {
-    id: string
-    make: string
-    model: string
-    year: number
-    licensePlate: string | null
-  }
+  initialVehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null }
   boardTechnicians?: BoardTechnicianOption[]
   orgMembers?: OrgMemberOption[]
   currentUserName: string
@@ -60,22 +54,8 @@ export interface ServicePageClientProps {
   telegramEnabled?: boolean
   aiEnabled?: boolean
   defaultDueDays?: number
-  statusReports?: {
-    id: string
-    title: string | null
-    status: string
-    videoUrl: string | null
-    createdAt: string
-    publicToken: string
-    expiresAt: string | null
-  }[]
+  statusReports?: { id: string; title: string | null; message: string | null; status: string; videoUrl: string | null; createdAt: string; publicToken: string; expiresAt: string | null; customerFeedback: string | null; feedbackAt: string | null; sentVia: string | null; sentAt: string | null }[]
+  initialTab?: string
 }
 
-export type {
-  ServicePartInput,
-  ServiceLaborInput,
-  ServiceDetail,
-  InitialData,
-  InventoryPartOption,
-  LaborPresetOption,
-}
+export type { ServicePartInput, ServiceLaborInput, ServiceDetail, InitialData, InventoryPartOption, LaborPresetOption }