Bladeren bron

Add status report management with feedback notifications (#101)

* workorder dates

* service details

* added set paid button in sidebar

* cleanup

* video upload

* lint

* public status report

* drawer

* translation
Bernt Christian Egeland 6 maanden geleden
bovenliggende
commit
4b403c46dc
62 gewijzigde bestanden met toevoegingen van 2714 en 162 verwijderingen
  1. 4 2
      messages/de/dashboard.json
  2. 2 1
      messages/de/service.json
  3. 75 0
      messages/de/statusReport.json
  4. 4 2
      messages/en/dashboard.json
  5. 2 1
      messages/en/service.json
  6. 75 0
      messages/en/statusReport.json
  7. 4 2
      messages/es/dashboard.json
  8. 2 1
      messages/es/service.json
  9. 75 0
      messages/es/statusReport.json
  10. 4 2
      messages/fr/dashboard.json
  11. 2 1
      messages/fr/service.json
  12. 75 0
      messages/fr/statusReport.json
  13. 4 2
      messages/it/dashboard.json
  14. 2 1
      messages/it/service.json
  15. 75 0
      messages/it/statusReport.json
  16. 4 2
      messages/nb/dashboard.json
  17. 2 1
      messages/nb/service.json
  18. 75 0
      messages/nb/statusReport.json
  19. 4 2
      messages/nl/dashboard.json
  20. 2 1
      messages/nl/service.json
  21. 75 0
      messages/nl/statusReport.json
  22. 4 2
      messages/pl/dashboard.json
  23. 2 1
      messages/pl/service.json
  24. 75 0
      messages/pl/statusReport.json
  25. 4 2
      messages/pt-BR/dashboard.json
  26. 2 1
      messages/pt-BR/service.json
  27. 75 0
      messages/pt-BR/statusReport.json
  28. 4 2
      messages/ru/dashboard.json
  29. 2 1
      messages/ru/service.json
  30. 75 0
      messages/ru/statusReport.json
  31. 4 2
      messages/tr/dashboard.json
  32. 2 1
      messages/tr/service.json
  33. 75 0
      messages/tr/statusReport.json
  34. 14 0
      next.config.ts
  35. 47 0
      prisma/migrations/20260327083943_customer_status_report/migration.sql
  36. 42 6
      prisma/schema.prisma
  37. 107 84
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  38. 133 0
      src/app/(public)/share/status-report/[orgId]/[token]/page.tsx
  39. 185 0
      src/app/(public)/share/status-report/[orgId]/[token]/status-report-view.tsx
  40. 14 0
      src/app/api/protected/cleanup/status-reports/route.ts
  41. 10 0
      src/app/api/public/files/[token]/[...path]/route.ts
  42. 15 0
      src/app/api/v1/cron/cleanup-status-reports/route.ts
  43. 1 1
      src/app/robots.ts
  44. 59 0
      src/features/status-reports/Actions/cleanupExpiredReports.ts
  45. 56 0
      src/features/status-reports/Actions/createStatusReport.ts
  46. 75 0
      src/features/status-reports/Actions/getPublicStatusReport.ts
  47. 49 0
      src/features/status-reports/Actions/getStatusReport.ts
  48. 39 0
      src/features/status-reports/Actions/getStatusReportsForService.ts
  49. 113 0
      src/features/status-reports/Actions/sendStatusReport.ts
  50. 25 0
      src/features/status-reports/Actions/submitFeedback.ts
  51. 243 0
      src/features/status-reports/Components/CreateStatusReportDialog.tsx
  52. 202 0
      src/features/status-reports/Components/SendStatusReportDialog.tsx
  53. 167 0
      src/features/status-reports/Components/StatusReportList.tsx
  54. 29 0
      src/features/status-reports/Schema/statusReportSchema.ts
  55. 44 29
      src/features/vehicles/Actions/getMyActiveJobs.ts
  56. 1 0
      src/features/vehicles/Actions/serviceActions.ts
  57. 93 6
      src/features/vehicles/Components/MyActiveJobs.tsx
  58. 1 0
      src/features/vehicles/Components/service-detail/types.ts
  59. 29 0
      src/features/vehicles/Components/service-page/ServicePageClient.tsx
  60. 3 1
      src/features/vehicles/Components/service-page/UnifiedServiceHeader.tsx
  61. 25 2
      src/features/vehicles/Components/service-page/service-page-types.ts
  62. 2 0
      src/i18n/request.ts

+ 4 - 2
messages/de/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Teil konnte nicht hinzugefügt werden",
     "inStock": "Auf Lager: {quantity}",
     "quantity": "Menge",
-    "addToJob": "{qty} zum Auftrag hinzufügen"
+    "addToJob": "{qty} zum Auftrag hinzufügen",
+    "statusReport": "Statusbericht",
+    "report": "Bericht"
   },
   "recentActivity": "Letzte Aktivitäten",
   "noRecentActivity": "Keine aktuellen Aktivitäten",
@@ -122,4 +124,4 @@
     "days": "{count}T"
   },
   "error": "Dashboard konnte nicht geladen werden"
-}
+}

+ 2 - 1
messages/de/service.json

@@ -20,7 +20,8 @@
       "details": "Details",
       "images": "Bilder",
       "video": "Video",
-      "documents": "Dokumente"
+      "documents": "Dokumente",
+      "statusReports": "Statusberichte"
     },
     "save": "Speichern",
     "pdf": "PDF",

+ 75 - 0
messages/de/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Statusbericht",
+    "description": "Statusbericht für {vehicle} erstellen",
+    "videoUpload": "Video hochladen",
+    "dragDrop": "Video hierher ziehen und ablegen, oder",
+    "clickToUpload": "klicken zum Hochladen",
+    "titleLabel": "Titel",
+    "titlePlaceholder": "z. B. Motorinspektion – Update",
+    "messageLabel": "Nachricht",
+    "messagePlaceholder": "Beschreiben Sie den aktuellen Zustand des Fahrzeugs...",
+    "uploading": "Video wird hochgeladen...",
+    "create": "Erstellen",
+    "createAndSend": "Erstellen & Senden",
+    "cancel": "Abbrechen",
+    "uploadFailed": "Video konnte nicht hochgeladen werden",
+    "created": "Statusbericht erstellt",
+    "noVideo": "Bitte laden Sie zuerst ein Video hoch",
+    "removeVideo": "Video entfernen",
+    "videoReady": "Video bereit",
+    "record": "Aufnehmen",
+    "uploadFile": "Hochladen",
+    "expiresLabel": "Link läuft ab"
+  },
+  "send": {
+    "title": "Statusbericht senden",
+    "description": "Statusbericht an {name} senden",
+    "messagePlaceholder": "Individuelle Nachricht hinzufügen (optional)...",
+    "sms": "SMS",
+    "email": "E-Mail",
+    "telegram": "Telegram",
+    "noPhone": "(keine Telefonnummer)",
+    "noEmail": "(keine E-Mail)",
+    "noTelegram": "(kein Chat verknüpft)",
+    "notAvailable": "(nicht verfügbar)",
+    "skip": "Überspringen",
+    "send": "Senden",
+    "sent": "Statusbericht gesendet",
+    "failed": "Statusbericht konnte nicht gesendet werden"
+  },
+  "view": {
+    "title": "Fahrzeug-Statusbericht",
+    "serviceTitle": "Service",
+    "vehicleInfo": "Fahrzeuginformationen",
+    "technician": "Techniker",
+    "createdAt": "Berichtsdatum",
+    "feedback": "Ihr Feedback",
+    "feedbackPlaceholder": "Teilen Sie Ihre Gedanken oder stellen Sie Fragen...",
+    "submitFeedback": "Feedback absenden",
+    "feedbackSubmitted": "Vielen Dank für Ihr Feedback!",
+    "feedbackError": "Feedback konnte nicht gesendet werden",
+    "submitting": "Wird gesendet...",
+    "poweredBy": "Bereitgestellt von"
+  },
+  "list": {
+    "title": "Statusberichte",
+    "newReport": "Neuer Statusbericht",
+    "empty": "Noch keine Statusberichte",
+    "emptyDescription": "Erstellen Sie einen Statusbericht, um Ihren Kunden über den Fahrzeugstatus zu informieren.",
+    "copyLink": "Link kopieren",
+    "linkCopied": "Link in Zwischenablage kopiert",
+    "send": "Senden",
+    "view": "Ansehen",
+    "draft": "Entwurf",
+    "published": "Veröffentlicht",
+    "sent": "Gesendet",
+    "viewed": "Angesehen",
+    "expires": "Läuft ab",
+    "expired": "Abgelaufen"
+  },
+  "notify": {
+    "defaultMessage": "Hallo {name}, wir haben ein Status-Update zu Ihrem {vehicle}. Hier ansehen: {url}",
+    "emailSubject": "Fahrzeug-Statusbericht - {vehicle}"
+  }
+}

+ 4 - 2
messages/en/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Failed to add part",
     "inStock": "In stock: {quantity}",
     "quantity": "Quantity",
-    "addToJob": "Add {qty} to job"
+    "addToJob": "Add {qty} to job",
+    "statusReport": "Status Report",
+    "report": "Report"
   },
   "recentActivity": "Recent Activity",
   "noRecentActivity": "No recent activity",
@@ -122,4 +124,4 @@
     "days": "{count}d"
   },
   "error": "Failed to load dashboard"
-}
+}

+ 2 - 1
messages/en/service.json

@@ -20,7 +20,8 @@
       "details": "Details",
       "images": "Images",
       "video": "Video",
-      "documents": "Documents"
+      "documents": "Documents",
+      "statusReports": "Status Reports"
     },
     "save": "Save",
     "pdf": "PDF",

+ 75 - 0
messages/en/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Status Report",
+    "description": "Create a status report for {vehicle}",
+    "videoUpload": "Upload Video",
+    "dragDrop": "Drag and drop a video file here, or",
+    "clickToUpload": "click to upload",
+    "titleLabel": "Title",
+    "titlePlaceholder": "e.g. Engine inspection update",
+    "messageLabel": "Message",
+    "messagePlaceholder": "Describe the current status of the vehicle...",
+    "uploading": "Uploading video...",
+    "create": "Create",
+    "createAndSend": "Create & Send",
+    "cancel": "Cancel",
+    "uploadFailed": "Failed to upload video",
+    "created": "Status report created",
+    "noVideo": "Please upload a video first",
+    "removeVideo": "Remove video",
+    "videoReady": "Video ready",
+    "record": "Record",
+    "uploadFile": "Upload",
+    "expiresLabel": "Link expires"
+  },
+  "send": {
+    "title": "Send Status Report",
+    "description": "Send the status report to {name}",
+    "messagePlaceholder": "Add a custom message (optional)...",
+    "sms": "SMS",
+    "email": "Email",
+    "telegram": "Telegram",
+    "noPhone": "(no phone)",
+    "noEmail": "(no email)",
+    "noTelegram": "(no chat linked)",
+    "notAvailable": "(not available)",
+    "skip": "Skip",
+    "send": "Send",
+    "sent": "Status report sent",
+    "failed": "Failed to send status report"
+  },
+  "view": {
+    "title": "Vehicle Status Report",
+    "serviceTitle": "Service",
+    "vehicleInfo": "Vehicle Information",
+    "technician": "Technician",
+    "createdAt": "Report Date",
+    "feedback": "Your Feedback",
+    "feedbackPlaceholder": "Share your thoughts or ask questions...",
+    "submitFeedback": "Submit Feedback",
+    "feedbackSubmitted": "Thank you for your feedback!",
+    "feedbackError": "Failed to submit feedback",
+    "submitting": "Submitting...",
+    "poweredBy": "Powered by"
+  },
+  "list": {
+    "title": "Status Reports",
+    "newReport": "New Status Report",
+    "empty": "No status reports yet",
+    "emptyDescription": "Create a status report to update your customer on the vehicle status.",
+    "copyLink": "Copy Link",
+    "linkCopied": "Link copied to clipboard",
+    "send": "Send",
+    "view": "View",
+    "draft": "Draft",
+    "published": "Published",
+    "sent": "Sent",
+    "viewed": "Viewed",
+    "expires": "Expires",
+    "expired": "Expired"
+  },
+  "notify": {
+    "defaultMessage": "Hi {name}, we have a status update on your {vehicle}. View it here: {url}",
+    "emailSubject": "Vehicle Status Update - {vehicle}"
+  }
+}

+ 4 - 2
messages/es/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Error al añadir la pieza",
     "inStock": "En stock: {quantity}",
     "quantity": "Cantidad",
-    "addToJob": "Añadir {qty} al trabajo"
+    "addToJob": "Añadir {qty} al trabajo",
+    "statusReport": "Informe de estado",
+    "report": "Informe"
   },
   "recentActivity": "Actividad reciente",
   "noRecentActivity": "Sin actividad reciente",
@@ -122,4 +124,4 @@
     "days": "{count}d"
   },
   "error": "Error al cargar el panel de control"
-}
+}

+ 2 - 1
messages/es/service.json

@@ -20,7 +20,8 @@
       "details": "Detalles",
       "images": "Imágenes",
       "video": "Video",
-      "documents": "Documentos"
+      "documents": "Documentos",
+      "statusReports": "Informes de estado"
     },
     "save": "Guardar",
     "pdf": "PDF",

+ 75 - 0
messages/es/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Informe de estado",
+    "description": "Crear un informe de estado para {vehicle}",
+    "videoUpload": "Subir vídeo",
+    "dragDrop": "Arrastre y suelte un archivo de vídeo aquí, o",
+    "clickToUpload": "haga clic para subir",
+    "titleLabel": "Título",
+    "titlePlaceholder": "p. ej. Actualización de inspección del motor",
+    "messageLabel": "Mensaje",
+    "messagePlaceholder": "Describa el estado actual del vehículo...",
+    "uploading": "Subiendo vídeo...",
+    "create": "Crear",
+    "createAndSend": "Crear y enviar",
+    "cancel": "Cancelar",
+    "uploadFailed": "Error al subir el vídeo",
+    "created": "Informe de estado creado",
+    "noVideo": "Por favor, suba un vídeo primero",
+    "removeVideo": "Eliminar vídeo",
+    "videoReady": "Vídeo listo",
+    "record": "Grabar",
+    "uploadFile": "Subir",
+    "expiresLabel": "El enlace expira"
+  },
+  "send": {
+    "title": "Enviar informe de estado",
+    "description": "Enviar el informe de estado a {name}",
+    "messagePlaceholder": "Añadir un mensaje personalizado (opcional)...",
+    "sms": "SMS",
+    "email": "Correo electrónico",
+    "telegram": "Telegram",
+    "noPhone": "(sin teléfono)",
+    "noEmail": "(sin correo electrónico)",
+    "noTelegram": "(sin chat vinculado)",
+    "notAvailable": "(no disponible)",
+    "skip": "Omitir",
+    "send": "Enviar",
+    "sent": "Informe de estado enviado",
+    "failed": "Error al enviar el informe de estado"
+  },
+  "view": {
+    "title": "Informe de estado del vehículo",
+    "serviceTitle": "Servicio",
+    "vehicleInfo": "Información del vehículo",
+    "technician": "Técnico",
+    "createdAt": "Fecha del informe",
+    "feedback": "Su opinión",
+    "feedbackPlaceholder": "Comparta sus comentarios o haga preguntas...",
+    "submitFeedback": "Enviar opinión",
+    "feedbackSubmitted": "¡Gracias por su opinión!",
+    "feedbackError": "Error al enviar la opinión",
+    "submitting": "Enviando...",
+    "poweredBy": "Desarrollado por"
+  },
+  "list": {
+    "title": "Informes de estado",
+    "newReport": "Nuevo informe",
+    "empty": "Sin informes de estado",
+    "emptyDescription": "Crea un informe de estado para actualizar a tu cliente sobre el estado del vehículo.",
+    "copyLink": "Copiar enlace",
+    "linkCopied": "Enlace copiado",
+    "send": "Enviar",
+    "view": "Ver",
+    "draft": "Borrador",
+    "published": "Publicado",
+    "sent": "Enviado",
+    "viewed": "Visto",
+    "expires": "Expira",
+    "expired": "Expirado"
+  },
+  "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}"
+  }
+}

+ 4 - 2
messages/fr/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Échec de l'ajout de la pièce",
     "inStock": "En stock : {quantity}",
     "quantity": "Quantité",
-    "addToJob": "Ajouter {qty} au travail"
+    "addToJob": "Ajouter {qty} au travail",
+    "statusReport": "Rapport d'état",
+    "report": "Rapport"
   },
   "recentActivity": "Activité récente",
   "noRecentActivity": "Aucune activité récente",
@@ -122,4 +124,4 @@
     "days": "{count} j"
   },
   "error": "Échec du chargement du tableau de bord"
-}
+}

+ 2 - 1
messages/fr/service.json

@@ -20,7 +20,8 @@
       "details": "Détails",
       "images": "Images",
       "video": "Vidéo",
-      "documents": "Documents"
+      "documents": "Documents",
+      "statusReports": "Rapports d'état"
     },
     "save": "Enregistrer",
     "pdf": "PDF",

+ 75 - 0
messages/fr/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Rapport d'état",
+    "description": "Créer un rapport d'état pour {vehicle}",
+    "videoUpload": "Téléverser une vidéo",
+    "dragDrop": "Glissez-déposez un fichier vidéo ici, ou",
+    "clickToUpload": "cliquez pour téléverser",
+    "titleLabel": "Titre",
+    "titlePlaceholder": "ex. Mise à jour de l'inspection moteur",
+    "messageLabel": "Message",
+    "messagePlaceholder": "Décrivez l'état actuel du véhicule...",
+    "uploading": "Téléversement de la vidéo...",
+    "create": "Créer",
+    "createAndSend": "Créer et envoyer",
+    "cancel": "Annuler",
+    "uploadFailed": "Échec du téléversement de la vidéo",
+    "created": "Rapport d'état créé",
+    "noVideo": "Veuillez d'abord téléverser une vidéo",
+    "removeVideo": "Supprimer la vidéo",
+    "videoReady": "Vidéo prête",
+    "record": "Enregistrer",
+    "uploadFile": "Téléverser",
+    "expiresLabel": "Le lien expire"
+  },
+  "send": {
+    "title": "Envoyer le rapport d'état",
+    "description": "Envoyer le rapport d'état à {name}",
+    "messagePlaceholder": "Ajouter un message personnalisé (facultatif)...",
+    "sms": "SMS",
+    "email": "E-mail",
+    "telegram": "Telegram",
+    "noPhone": "(pas de téléphone)",
+    "noEmail": "(pas d'e-mail)",
+    "noTelegram": "(aucune discussion liée)",
+    "notAvailable": "(non disponible)",
+    "skip": "Ignorer",
+    "send": "Envoyer",
+    "sent": "Rapport d'état envoyé",
+    "failed": "Échec de l'envoi du rapport d'état"
+  },
+  "view": {
+    "title": "Rapport d'état du véhicule",
+    "serviceTitle": "Service",
+    "vehicleInfo": "Informations sur le véhicule",
+    "technician": "Technicien",
+    "createdAt": "Date du rapport",
+    "feedback": "Votre avis",
+    "feedbackPlaceholder": "Partagez vos impressions ou posez vos questions...",
+    "submitFeedback": "Envoyer l'avis",
+    "feedbackSubmitted": "Merci pour votre avis !",
+    "feedbackError": "Échec de l'envoi de l'avis",
+    "submitting": "Envoi en cours...",
+    "poweredBy": "Propulsé par"
+  },
+  "list": {
+    "title": "Rapports d'état",
+    "newReport": "Nouveau rapport",
+    "empty": "Aucun rapport d'état",
+    "emptyDescription": "Créez un rapport d'état pour informer votre client de l'état du véhicule.",
+    "copyLink": "Copier le lien",
+    "linkCopied": "Lien copié",
+    "send": "Envoyer",
+    "view": "Voir",
+    "draft": "Brouillon",
+    "published": "Publié",
+    "sent": "Envoyé",
+    "viewed": "Consulté",
+    "expires": "Expire",
+    "expired": "Expiré"
+  },
+  "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}"
+  }
+}

+ 4 - 2
messages/it/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Impossibile aggiungere il ricambio",
     "inStock": "Disponibile: {quantity}",
     "quantity": "Quantità",
-    "addToJob": "Aggiungi {qty} al lavoro"
+    "addToJob": "Aggiungi {qty} al lavoro",
+    "statusReport": "Rapporto di stato",
+    "report": "Rapporto"
   },
   "recentActivity": "Attività recente",
   "noRecentActivity": "Nessuna attività recente",
@@ -122,4 +124,4 @@
     "days": "{count}g"
   },
   "error": "Impossibile caricare la dashboard"
-}
+}

+ 2 - 1
messages/it/service.json

@@ -20,7 +20,8 @@
       "details": "Dettagli",
       "images": "Immagini",
       "video": "Video",
-      "documents": "Documenti"
+      "documents": "Documenti",
+      "statusReports": "Rapporti di stato"
     },
     "save": "Salva",
     "pdf": "PDF",

+ 75 - 0
messages/it/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Rapporto di stato",
+    "description": "Crea un rapporto di stato per {vehicle}",
+    "videoUpload": "Carica video",
+    "dragDrop": "Trascina e rilascia un file video qui, oppure",
+    "clickToUpload": "clicca per caricare",
+    "titleLabel": "Titolo",
+    "titlePlaceholder": "es. Aggiornamento ispezione motore",
+    "messageLabel": "Messaggio",
+    "messagePlaceholder": "Descrivi lo stato attuale del veicolo...",
+    "uploading": "Caricamento video in corso...",
+    "create": "Crea",
+    "createAndSend": "Crea e invia",
+    "cancel": "Annulla",
+    "uploadFailed": "Caricamento video non riuscito",
+    "created": "Rapporto di stato creato",
+    "noVideo": "Carica prima un video",
+    "removeVideo": "Rimuovi video",
+    "videoReady": "Video pronto",
+    "record": "Registra",
+    "uploadFile": "Carica",
+    "expiresLabel": "Il link scade"
+  },
+  "send": {
+    "title": "Invia rapporto di stato",
+    "description": "Invia il rapporto di stato a {name}",
+    "messagePlaceholder": "Aggiungi un messaggio personalizzato (facoltativo)...",
+    "sms": "SMS",
+    "email": "E-mail",
+    "telegram": "Telegram",
+    "noPhone": "(nessun telefono)",
+    "noEmail": "(nessuna e-mail)",
+    "noTelegram": "(nessuna chat collegata)",
+    "notAvailable": "(non disponibile)",
+    "skip": "Salta",
+    "send": "Invia",
+    "sent": "Rapporto di stato inviato",
+    "failed": "Invio del rapporto di stato non riuscito"
+  },
+  "view": {
+    "title": "Rapporto di stato del veicolo",
+    "serviceTitle": "Servizio",
+    "vehicleInfo": "Informazioni sul veicolo",
+    "technician": "Tecnico",
+    "createdAt": "Data del rapporto",
+    "feedback": "Il tuo feedback",
+    "feedbackPlaceholder": "Condividi le tue opinioni o fai domande...",
+    "submitFeedback": "Invia feedback",
+    "feedbackSubmitted": "Grazie per il tuo feedback!",
+    "feedbackError": "Invio del feedback non riuscito",
+    "submitting": "Invio in corso...",
+    "poweredBy": "Offerto da"
+  },
+  "list": {
+    "title": "Rapporti di stato",
+    "newReport": "Nuovo rapporto",
+    "empty": "Nessun rapporto di stato",
+    "emptyDescription": "Crea un rapporto di stato per aggiornare il cliente sullo stato del veicolo.",
+    "copyLink": "Copia link",
+    "linkCopied": "Link copiato",
+    "send": "Invia",
+    "view": "Visualizza",
+    "draft": "Bozza",
+    "published": "Pubblicato",
+    "sent": "Inviato",
+    "viewed": "Visualizzato",
+    "expires": "Scade",
+    "expired": "Scaduto"
+  },
+  "notify": {
+    "defaultMessage": "Ciao {name}, abbiamo un aggiornamento sullo stato del suo {vehicle}. Visualizzalo qui: {url}",
+    "emailSubject": "Aggiornamento stato veicolo - {vehicle}"
+  }
+}

+ 4 - 2
messages/nb/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Kunne ikke legge til del",
     "inStock": "På lager: {quantity}",
     "quantity": "Antall",
-    "addToJob": "Legg til {qty} i jobb"
+    "addToJob": "Legg til {qty} i jobb",
+    "statusReport": "Statusrapport",
+    "report": "Rapport"
   },
   "recentActivity": "Nylig aktivitet",
   "noRecentActivity": "Ingen nylig aktivitet",
@@ -122,4 +124,4 @@
     "days": "{count}d"
   },
   "error": "Kunne ikke laste dashbord"
-}
+}

+ 2 - 1
messages/nb/service.json

@@ -20,7 +20,8 @@
       "details": "Detaljer",
       "images": "Bilder",
       "video": "Video",
-      "documents": "Dokumenter"
+      "documents": "Dokumenter",
+      "statusReports": "Statusrapporter"
     },
     "save": "Lagre",
     "pdf": "PDF",

+ 75 - 0
messages/nb/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Statusrapport",
+    "description": "Opprett en statusrapport for {vehicle}",
+    "videoUpload": "Last opp video",
+    "dragDrop": "Dra og slipp en videofil her, eller",
+    "clickToUpload": "klikk for å laste opp",
+    "titleLabel": "Tittel",
+    "titlePlaceholder": "f.eks. Oppdatering fra motorinspeksjon",
+    "messageLabel": "Melding",
+    "messagePlaceholder": "Beskriv den nåværende statusen til kjøretøyet...",
+    "uploading": "Laster opp video...",
+    "create": "Opprett",
+    "createAndSend": "Opprett og send",
+    "cancel": "Avbryt",
+    "uploadFailed": "Kunne ikke laste opp video",
+    "created": "Statusrapport opprettet",
+    "noVideo": "Last opp en video først",
+    "removeVideo": "Fjern video",
+    "videoReady": "Video klar",
+    "record": "Ta opp",
+    "uploadFile": "Last opp",
+    "expiresLabel": "Lenken utløper"
+  },
+  "send": {
+    "title": "Send statusrapport",
+    "description": "Send statusrapporten til {name}",
+    "messagePlaceholder": "Legg til en egen melding (valgfritt)...",
+    "sms": "SMS",
+    "email": "E-post",
+    "telegram": "Telegram",
+    "noPhone": "(ingen telefon)",
+    "noEmail": "(ingen e-post)",
+    "noTelegram": "(ingen chat tilkoblet)",
+    "notAvailable": "(ikke tilgjengelig)",
+    "skip": "Hopp over",
+    "send": "Send",
+    "sent": "Statusrapport sendt",
+    "failed": "Kunne ikke sende statusrapport"
+  },
+  "view": {
+    "title": "Kjøretøyets statusrapport",
+    "serviceTitle": "Service",
+    "vehicleInfo": "Kjøretøyinformasjon",
+    "technician": "Tekniker",
+    "createdAt": "Rapportdato",
+    "feedback": "Din tilbakemelding",
+    "feedbackPlaceholder": "Del dine tanker eller still spørsmål...",
+    "submitFeedback": "Send tilbakemelding",
+    "feedbackSubmitted": "Takk for tilbakemeldingen!",
+    "feedbackError": "Kunne ikke sende tilbakemelding",
+    "submitting": "Sender...",
+    "poweredBy": "Drevet av"
+  },
+  "list": {
+    "title": "Statusrapporter",
+    "newReport": "Ny statusrapport",
+    "empty": "Ingen statusrapporter ennå",
+    "emptyDescription": "Opprett en statusrapport for å oppdatere kunden om kjøretøyets tilstand.",
+    "copyLink": "Kopier lenke",
+    "linkCopied": "Lenke kopiert",
+    "send": "Send",
+    "view": "Vis",
+    "draft": "Utkast",
+    "published": "Publisert",
+    "sent": "Sendt",
+    "viewed": "Sett",
+    "expires": "Utløper",
+    "expired": "Utløpt"
+  },
+  "notify": {
+    "defaultMessage": "Hei {name}, vi har en statusoppdatering på din {vehicle}. Se den her: {url}",
+    "emailSubject": "Statusoppdatering for kjøretøy - {vehicle}"
+  }
+}

+ 4 - 2
messages/nl/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Onderdeel toevoegen mislukt",
     "inStock": "Op voorraad: {quantity}",
     "quantity": "Aantal",
-    "addToJob": "{qty} toevoegen aan klus"
+    "addToJob": "{qty} toevoegen aan klus",
+    "statusReport": "Statusrapport",
+    "report": "Rapport"
   },
   "recentActivity": "Recente activiteit",
   "noRecentActivity": "Geen recente activiteit",
@@ -122,4 +124,4 @@
     "days": "{count}d"
   },
   "error": "Laden van dashboard mislukt"
-}
+}

+ 2 - 1
messages/nl/service.json

@@ -20,7 +20,8 @@
       "details": "Details",
       "images": "Afbeeldingen",
       "video": "Video",
-      "documents": "Documenten"
+      "documents": "Documenten",
+      "statusReports": "Statusrapporten"
     },
     "save": "Opslaan",
     "pdf": "PDF",

+ 75 - 0
messages/nl/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Statusrapport",
+    "description": "Maak een statusrapport aan voor {vehicle}",
+    "videoUpload": "Video uploaden",
+    "dragDrop": "Sleep een videobestand hierheen, of",
+    "clickToUpload": "klik om te uploaden",
+    "titleLabel": "Titel",
+    "titlePlaceholder": "bijv. Update motorinspectie",
+    "messageLabel": "Bericht",
+    "messagePlaceholder": "Beschrijf de huidige status van het voertuig...",
+    "uploading": "Video wordt geüpload...",
+    "create": "Aanmaken",
+    "createAndSend": "Aanmaken en verzenden",
+    "cancel": "Annuleren",
+    "uploadFailed": "Video uploaden mislukt",
+    "created": "Statusrapport aangemaakt",
+    "noVideo": "Upload eerst een video",
+    "removeVideo": "Video verwijderen",
+    "videoReady": "Video gereed",
+    "record": "Opnemen",
+    "uploadFile": "Uploaden",
+    "expiresLabel": "Link verloopt"
+  },
+  "send": {
+    "title": "Statusrapport verzenden",
+    "description": "Stuur het statusrapport naar {name}",
+    "messagePlaceholder": "Voeg een eigen bericht toe (optioneel)...",
+    "sms": "SMS",
+    "email": "E-mail",
+    "telegram": "Telegram",
+    "noPhone": "(geen telefoon)",
+    "noEmail": "(geen e-mail)",
+    "noTelegram": "(geen chat gekoppeld)",
+    "notAvailable": "(niet beschikbaar)",
+    "skip": "Overslaan",
+    "send": "Verzenden",
+    "sent": "Statusrapport verzonden",
+    "failed": "Statusrapport verzenden mislukt"
+  },
+  "view": {
+    "title": "Statusrapport voertuig",
+    "serviceTitle": "Service",
+    "vehicleInfo": "Voertuiginformatie",
+    "technician": "Monteur",
+    "createdAt": "Rapportdatum",
+    "feedback": "Uw feedback",
+    "feedbackPlaceholder": "Deel uw mening of stel vragen...",
+    "submitFeedback": "Feedback verzenden",
+    "feedbackSubmitted": "Bedankt voor uw feedback!",
+    "feedbackError": "Feedback verzenden mislukt",
+    "submitting": "Bezig met verzenden...",
+    "poweredBy": "Mogelijk gemaakt door"
+  },
+  "list": {
+    "title": "Statusrapporten",
+    "newReport": "Nieuw rapport",
+    "empty": "Nog geen statusrapporten",
+    "emptyDescription": "Maak een statusrapport om uw klant te informeren over de voertuigstatus.",
+    "copyLink": "Link kopiëren",
+    "linkCopied": "Link gekopieerd",
+    "send": "Verzenden",
+    "view": "Bekijken",
+    "draft": "Concept",
+    "published": "Gepubliceerd",
+    "sent": "Verzonden",
+    "viewed": "Bekeken",
+    "expires": "Verloopt",
+    "expired": "Verlopen"
+  },
+  "notify": {
+    "defaultMessage": "Hallo {name}, we hebben een statusupdate over uw {vehicle}. Bekijk het hier: {url}",
+    "emailSubject": "Voertuig statusupdate - {vehicle}"
+  }
+}

+ 4 - 2
messages/pl/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Nie udało się dodać części",
     "inStock": "Na stanie: {quantity}",
     "quantity": "Ilość",
-    "addToJob": "Dodaj {qty} do zlecenia"
+    "addToJob": "Dodaj {qty} do zlecenia",
+    "statusReport": "Raport statusu",
+    "report": "Raport"
   },
   "recentActivity": "Ostatnia aktywność",
   "noRecentActivity": "Brak ostatniej aktywności",
@@ -122,4 +124,4 @@
     "days": "{count}d"
   },
   "error": "Nie udało się załadować pulpitu"
-}
+}

+ 2 - 1
messages/pl/service.json

@@ -20,7 +20,8 @@
       "details": "Szczegóły",
       "images": "Obrazy",
       "video": "Wideo",
-      "documents": "Dokumenty"
+      "documents": "Dokumenty",
+      "statusReports": "Raporty statusu"
     },
     "save": "Zapisz",
     "pdf": "PDF",

+ 75 - 0
messages/pl/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Raport statusu",
+    "description": "Utwórz raport statusu dla {vehicle}",
+    "videoUpload": "Prześlij wideo",
+    "dragDrop": "Przeciągnij i upuść plik wideo tutaj, lub",
+    "clickToUpload": "kliknij, aby przesłać",
+    "titleLabel": "Tytuł",
+    "titlePlaceholder": "np. Aktualizacja przeglądu silnika",
+    "messageLabel": "Wiadomość",
+    "messagePlaceholder": "Opisz aktualny stan pojazdu...",
+    "uploading": "Przesyłanie wideo...",
+    "create": "Utwórz",
+    "createAndSend": "Utwórz i wyślij",
+    "cancel": "Anuluj",
+    "uploadFailed": "Nie udało się przesłać wideo",
+    "created": "Raport statusu utworzony",
+    "noVideo": "Najpierw prześlij wideo",
+    "removeVideo": "Usuń wideo",
+    "videoReady": "Wideo gotowe",
+    "record": "Nagraj",
+    "uploadFile": "Prześlij",
+    "expiresLabel": "Link wygasa"
+  },
+  "send": {
+    "title": "Wyślij raport statusu",
+    "description": "Wyślij raport statusu do {name}",
+    "messagePlaceholder": "Dodaj własną wiadomość (opcjonalnie)...",
+    "sms": "SMS",
+    "email": "E-mail",
+    "telegram": "Telegram",
+    "noPhone": "(brak telefonu)",
+    "noEmail": "(brak e-maila)",
+    "noTelegram": "(brak połączonego czatu)",
+    "notAvailable": "(niedostępne)",
+    "skip": "Pomiń",
+    "send": "Wyślij",
+    "sent": "Raport statusu wysłany",
+    "failed": "Nie udało się wysłać raportu statusu"
+  },
+  "view": {
+    "title": "Raport statusu pojazdu",
+    "serviceTitle": "Usługa",
+    "vehicleInfo": "Informacje o pojeździe",
+    "technician": "Technik",
+    "createdAt": "Data raportu",
+    "feedback": "Twoja opinia",
+    "feedbackPlaceholder": "Podziel się swoimi uwagami lub zadaj pytania...",
+    "submitFeedback": "Wyślij opinię",
+    "feedbackSubmitted": "Dziękujemy za opinię!",
+    "feedbackError": "Nie udało się wysłać opinii",
+    "submitting": "Wysyłanie...",
+    "poweredBy": "Obsługiwane przez"
+  },
+  "list": {
+    "title": "Raporty statusu",
+    "newReport": "Nowy raport",
+    "empty": "Brak raportów statusu",
+    "emptyDescription": "Utwórz raport statusu, aby poinformować klienta o stanie pojazdu.",
+    "copyLink": "Kopiuj link",
+    "linkCopied": "Link skopiowany",
+    "send": "Wyślij",
+    "view": "Zobacz",
+    "draft": "Szkic",
+    "published": "Opublikowany",
+    "sent": "Wysłany",
+    "viewed": "Wyświetlony",
+    "expires": "Wygasa",
+    "expired": "Wygasł"
+  },
+  "notify": {
+    "defaultMessage": "Witaj {name}, mamy aktualizację statusu Twojego {vehicle}. Zobacz tutaj: {url}",
+    "emailSubject": "Aktualizacja statusu pojazdu - {vehicle}"
+  }
+}

+ 4 - 2
messages/pt-BR/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Falha ao adicionar peça",
     "inStock": "Em estoque: {quantity}",
     "quantity": "Quantidade",
-    "addToJob": "Adicionar {qty} ao serviço"
+    "addToJob": "Adicionar {qty} ao serviço",
+    "statusReport": "Relatório de Status",
+    "report": "Relatório"
   },
   "recentActivity": "Atividade recente",
   "noRecentActivity": "Nenhuma atividade recente",
@@ -122,4 +124,4 @@
     "days": "{count}d"
   },
   "error": "Falha ao carregar o painel"
-}
+}

+ 2 - 1
messages/pt-BR/service.json

@@ -20,7 +20,8 @@
       "details": "Detalhes",
       "images": "Imagens",
       "video": "Vídeo",
-      "documents": "Documentos"
+      "documents": "Documentos",
+      "statusReports": "Relatórios de status"
     },
     "save": "Salvar",
     "pdf": "PDF",

+ 75 - 0
messages/pt-BR/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Relatório de Status",
+    "description": "Criar um relatório de status para {vehicle}",
+    "videoUpload": "Enviar Vídeo",
+    "dragDrop": "Arraste e solte um arquivo de vídeo aqui, ou",
+    "clickToUpload": "clique para enviar",
+    "titleLabel": "Título",
+    "titlePlaceholder": "ex. Atualização da inspeção do motor",
+    "messageLabel": "Mensagem",
+    "messagePlaceholder": "Descreva o estado atual do veículo...",
+    "uploading": "Enviando vídeo...",
+    "create": "Criar",
+    "createAndSend": "Criar e Enviar",
+    "cancel": "Cancelar",
+    "uploadFailed": "Falha ao enviar o vídeo",
+    "created": "Relatório de status criado",
+    "noVideo": "Por favor, envie um vídeo primeiro",
+    "removeVideo": "Remover vídeo",
+    "videoReady": "Vídeo pronto",
+    "record": "Gravar",
+    "uploadFile": "Enviar",
+    "expiresLabel": "O link expira"
+  },
+  "send": {
+    "title": "Enviar Relatório de Status",
+    "description": "Enviar o relatório de status para {name}",
+    "messagePlaceholder": "Adicionar uma mensagem personalizada (opcional)...",
+    "sms": "SMS",
+    "email": "E-mail",
+    "telegram": "Telegram",
+    "noPhone": "(sem telefone)",
+    "noEmail": "(sem e-mail)",
+    "noTelegram": "(nenhum chat vinculado)",
+    "notAvailable": "(não disponível)",
+    "skip": "Pular",
+    "send": "Enviar",
+    "sent": "Relatório de status enviado",
+    "failed": "Falha ao enviar o relatório de status"
+  },
+  "view": {
+    "title": "Relatório de Status do Veículo",
+    "serviceTitle": "Serviço",
+    "vehicleInfo": "Informações do Veículo",
+    "technician": "Técnico",
+    "createdAt": "Data do Relatório",
+    "feedback": "Seu Feedback",
+    "feedbackPlaceholder": "Compartilhe suas opiniões ou faça perguntas...",
+    "submitFeedback": "Enviar Feedback",
+    "feedbackSubmitted": "Obrigado pelo seu feedback!",
+    "feedbackError": "Falha ao enviar o feedback",
+    "submitting": "Enviando...",
+    "poweredBy": "Desenvolvido por"
+  },
+  "list": {
+    "title": "Relatórios de status",
+    "newReport": "Novo relatório",
+    "empty": "Nenhum relatório de status",
+    "emptyDescription": "Crie um relatório de status para atualizar seu cliente sobre o estado do veículo.",
+    "copyLink": "Copiar link",
+    "linkCopied": "Link copiado",
+    "send": "Enviar",
+    "view": "Ver",
+    "draft": "Rascunho",
+    "published": "Publicado",
+    "sent": "Enviado",
+    "viewed": "Visualizado",
+    "expires": "Expira",
+    "expired": "Expirado"
+  },
+  "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}"
+  }
+}

+ 4 - 2
messages/ru/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Не удалось добавить запчасть",
     "inStock": "В наличии: {quantity}",
     "quantity": "Количество",
-    "addToJob": "Добавить {qty} к заказу"
+    "addToJob": "Добавить {qty} к заказу",
+    "statusReport": "Отчёт о состоянии",
+    "report": "Отчёт"
   },
   "recentActivity": "Последняя активность",
   "noRecentActivity": "Нет недавней активности",
@@ -122,4 +124,4 @@
     "days": "{count}д"
   },
   "error": "Не удалось загрузить панель управления"
-}
+}

+ 2 - 1
messages/ru/service.json

@@ -20,7 +20,8 @@
       "details": "Детали",
       "images": "Изображения",
       "video": "Видео",
-      "documents": "Документы"
+      "documents": "Документы",
+      "statusReports": "Отчёты о состоянии"
     },
     "save": "Сохранить",
     "pdf": "PDF",

+ 75 - 0
messages/ru/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Отчёт о состоянии",
+    "description": "Создать отчёт о состоянии для {vehicle}",
+    "videoUpload": "Загрузить видео",
+    "dragDrop": "Перетащите видеофайл сюда или",
+    "clickToUpload": "нажмите для загрузки",
+    "titleLabel": "Заголовок",
+    "titlePlaceholder": "напр. Обновление по осмотру двигателя",
+    "messageLabel": "Сообщение",
+    "messagePlaceholder": "Опишите текущее состояние автомобиля...",
+    "uploading": "Загрузка видео...",
+    "create": "Создать",
+    "createAndSend": "Создать и отправить",
+    "cancel": "Отмена",
+    "uploadFailed": "Не удалось загрузить видео",
+    "created": "Отчёт о состоянии создан",
+    "noVideo": "Сначала загрузите видео",
+    "removeVideo": "Удалить видео",
+    "videoReady": "Видео готово",
+    "record": "Записать",
+    "uploadFile": "Загрузить",
+    "expiresLabel": "Ссылка истекает"
+  },
+  "send": {
+    "title": "Отправить отчёт о состоянии",
+    "description": "Отправить отчёт о состоянии для {name}",
+    "messagePlaceholder": "Добавить сообщение (необязательно)...",
+    "sms": "SMS",
+    "email": "Эл. почта",
+    "telegram": "Telegram",
+    "noPhone": "(нет телефона)",
+    "noEmail": "(нет эл. почты)",
+    "noTelegram": "(чат не подключён)",
+    "notAvailable": "(недоступно)",
+    "skip": "Пропустить",
+    "send": "Отправить",
+    "sent": "Отчёт о состоянии отправлен",
+    "failed": "Не удалось отправить отчёт о состоянии"
+  },
+  "view": {
+    "title": "Отчёт о состоянии автомобиля",
+    "serviceTitle": "Услуга",
+    "vehicleInfo": "Информация об автомобиле",
+    "technician": "Механик",
+    "createdAt": "Дата отчёта",
+    "feedback": "Ваш отзыв",
+    "feedbackPlaceholder": "Поделитесь мнением или задайте вопросы...",
+    "submitFeedback": "Отправить отзыв",
+    "feedbackSubmitted": "Спасибо за ваш отзыв!",
+    "feedbackError": "Не удалось отправить отзыв",
+    "submitting": "Отправка...",
+    "poweredBy": "Работает на"
+  },
+  "list": {
+    "title": "Отчёты о состоянии",
+    "newReport": "Новый отчёт",
+    "empty": "Отчётов пока нет",
+    "emptyDescription": "Создайте отчёт о состоянии, чтобы информировать клиента о статусе транспортного средства.",
+    "copyLink": "Копировать ссылку",
+    "linkCopied": "Ссылка скопирована",
+    "send": "Отправить",
+    "view": "Просмотр",
+    "draft": "Черновик",
+    "published": "Опубликован",
+    "sent": "Отправлен",
+    "viewed": "Просмотрен",
+    "expires": "Истекает",
+    "expired": "Истёк"
+  },
+  "notify": {
+    "defaultMessage": "Здравствуйте, {name}, у нас есть обновление по статусу вашего {vehicle}. Посмотрите здесь: {url}",
+    "emailSubject": "Обновление статуса транспортного средства - {vehicle}"
+  }
+}

+ 4 - 2
messages/tr/dashboard.json

@@ -16,7 +16,9 @@
     "partAddFailed": "Parça eklenemedi",
     "inStock": "Stokta: {quantity}",
     "quantity": "Miktar",
-    "addToJob": "{qty} işe ekle"
+    "addToJob": "{qty} işe ekle",
+    "statusReport": "Durum Raporu",
+    "report": "Rapor"
   },
   "recentActivity": "Son aktivite",
   "noRecentActivity": "Son aktivite yok",
@@ -122,4 +124,4 @@
     "days": "{count}g"
   },
   "error": "Kontrol paneli yüklenemedi"
-}
+}

+ 2 - 1
messages/tr/service.json

@@ -20,7 +20,8 @@
       "details": "Ayrıntılar",
       "images": "Resimler",
       "video": "Video",
-      "documents": "Belgeler"
+      "documents": "Belgeler",
+      "statusReports": "Durum Raporları"
     },
     "save": "Kaydet",
     "pdf": "PDF",

+ 75 - 0
messages/tr/statusReport.json

@@ -0,0 +1,75 @@
+{
+  "create": {
+    "title": "Durum Raporu",
+    "description": "{vehicle} için durum raporu oluştur",
+    "videoUpload": "Video Yükle",
+    "dragDrop": "Bir video dosyasını buraya sürükleyip bırakın veya",
+    "clickToUpload": "yüklemek için tıklayın",
+    "titleLabel": "Başlık",
+    "titlePlaceholder": "ör. Motor muayene güncellemesi",
+    "messageLabel": "Mesaj",
+    "messagePlaceholder": "Aracın mevcut durumunu açıklayın...",
+    "uploading": "Video yükleniyor...",
+    "create": "Oluştur",
+    "createAndSend": "Oluştur ve Gönder",
+    "cancel": "İptal",
+    "uploadFailed": "Video yüklenemedi",
+    "created": "Durum raporu oluşturuldu",
+    "noVideo": "Lütfen önce bir video yükleyin",
+    "removeVideo": "Videoyu kaldır",
+    "videoReady": "Video hazır",
+    "record": "Kaydet",
+    "uploadFile": "Yükle",
+    "expiresLabel": "Bağlantı sona erer"
+  },
+  "send": {
+    "title": "Durum Raporu Gönder",
+    "description": "Durum raporunu {name} adresine gönder",
+    "messagePlaceholder": "Özel bir mesaj ekleyin (isteğe bağlı)...",
+    "sms": "SMS",
+    "email": "E-posta",
+    "telegram": "Telegram",
+    "noPhone": "(telefon yok)",
+    "noEmail": "(e-posta yok)",
+    "noTelegram": "(bağlı sohbet yok)",
+    "notAvailable": "(mevcut değil)",
+    "skip": "Atla",
+    "send": "Gönder",
+    "sent": "Durum raporu gönderildi",
+    "failed": "Durum raporu gönderilemedi"
+  },
+  "view": {
+    "title": "Araç Durum Raporu",
+    "serviceTitle": "Servis",
+    "vehicleInfo": "Araç Bilgileri",
+    "technician": "Teknisyen",
+    "createdAt": "Rapor Tarihi",
+    "feedback": "Geri Bildiriminiz",
+    "feedbackPlaceholder": "Düşüncelerinizi paylaşın veya sorularınızı sorun...",
+    "submitFeedback": "Geri Bildirim Gönder",
+    "feedbackSubmitted": "Geri bildiriminiz için teşekkürler!",
+    "feedbackError": "Geri bildirim gönderilemedi",
+    "submitting": "Gönderiliyor...",
+    "poweredBy": "Altyapı"
+  },
+  "list": {
+    "title": "Durum Raporları",
+    "newReport": "Yeni Rapor",
+    "empty": "Henüz durum raporu yok",
+    "emptyDescription": "Müşterinizi araç durumu hakkında bilgilendirmek için bir durum raporu oluşturun.",
+    "copyLink": "Bağlantıyı kopyala",
+    "linkCopied": "Bağlantı kopyalandı",
+    "send": "Gönder",
+    "view": "Görüntüle",
+    "draft": "Taslak",
+    "published": "Yayınlandı",
+    "sent": "Gönderildi",
+    "viewed": "Görüntülendi",
+    "expires": "Son kullanma",
+    "expired": "Süresi dolmuş"
+  },
+  "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}"
+  }
+}

+ 14 - 0
next.config.ts

@@ -12,6 +12,20 @@ const nextConfig: NextConfig = {
   experimental: {
     proxyClientMaxBodySize: '2gb',
   },
+  async headers() {
+    return [
+      {
+        source: '/share/status-report/:path*',
+        headers: [
+          { key: 'X-Robots-Tag', value: 'noindex, nofollow, noarchive, nosnippet' },
+          { key: 'X-Frame-Options', value: 'DENY' },
+          { key: 'Referrer-Policy', value: 'no-referrer' },
+          { key: 'Cache-Control', value: 'no-store, no-cache, must-revalidate, private' },
+          { key: 'X-Content-Type-Options', value: 'nosniff' },
+        ],
+      },
+    ]
+  },
   async rewrites() {
     return [
       // Old /api/files/ URLs stored in the DB before the protected/ restructure

+ 47 - 0
prisma/migrations/20260327083943_customer_status_report/migration.sql

@@ -0,0 +1,47 @@
+-- CreateTable
+CREATE TABLE "status_reports" (
+    "id" TEXT NOT NULL,
+    "title" TEXT,
+    "message" TEXT,
+    "videoUrl" TEXT,
+    "videoFileName" TEXT,
+    "publicToken" TEXT NOT NULL,
+    "status" TEXT NOT NULL DEFAULT 'draft',
+    "sentVia" TEXT,
+    "sentAt" TIMESTAMP(3),
+    "viewedAt" TIMESTAMP(3),
+    "customerFeedback" TEXT,
+    "feedbackAt" TIMESTAMP(3),
+    "expiresAt" TIMESTAMP(3),
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "serviceRecordId" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    "technicianId" TEXT,
+
+    CONSTRAINT "status_reports_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "status_reports_publicToken_key" ON "status_reports"("publicToken");
+
+-- CreateIndex
+CREATE INDEX "status_reports_serviceRecordId_idx" ON "status_reports"("serviceRecordId");
+
+-- CreateIndex
+CREATE INDEX "status_reports_organizationId_idx" ON "status_reports"("organizationId");
+
+-- CreateIndex
+CREATE INDEX "status_reports_publicToken_idx" ON "status_reports"("publicToken");
+
+-- CreateIndex
+CREATE INDEX "status_reports_expiresAt_idx" ON "status_reports"("expiresAt");
+
+-- AddForeignKey
+ALTER TABLE "status_reports" ADD CONSTRAINT "status_reports_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "status_reports" ADD CONSTRAINT "status_reports_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "status_reports" ADD CONSTRAINT "status_reports_technicianId_fkey" FOREIGN KEY ("technicianId") REFERENCES "technicians"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 42 - 6
prisma/schema.prisma

@@ -217,10 +217,11 @@ model ServiceRecord {
   technician   Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
   sortOrder    Int         @default(0)
 
-  partItems   ServicePart[]
-  laborItems  ServiceLabor[]
-  attachments ServiceAttachment[]
-  payments    Payment[]
+  partItems     ServicePart[]
+  laborItems    ServiceLabor[]
+  attachments   ServiceAttachment[]
+  payments      Payment[]
+  statusReports StatusReport[]
 
   @@index([vehicleId])
   @@index([technicianId])
@@ -614,6 +615,7 @@ model Organization {
   telegramMessages       TelegramMessage[]
   technicians            Technician[]
   auditLogs              AuditLog[]
+  statusReports          StatusReport[]
 
   @@map("organizations")
 }
@@ -975,14 +977,15 @@ model Technician {
   sortOrder     Int     @default(0)
   dailyCapacity Int     @default(480)
 
-  userId   String?
-  user     User?   @relation(fields: [userId], references: [id], onDelete: SetNull)
+  userId String?
+  user   User?   @relation(fields: [userId], references: [id], onDelete: SetNull)
 
   organizationId String
   organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
 
   serviceRecords ServiceRecord[]
   inspections    Inspection[]
+  statusReports  StatusReport[]
 
   createdAt DateTime @default(now())
   updatedAt DateTime @updatedAt
@@ -1103,3 +1106,36 @@ model AiChatMessage {
   @@index([chatId, createdAt])
   @@map("ai_chat_messages")
 }
+
+model StatusReport {
+  id               String    @id @default(cuid())
+  title            String?
+  message          String?   @db.Text
+  videoUrl         String?
+  videoFileName    String?
+  publicToken      String    @unique
+  status           String    @default("draft") // draft | published | sent | viewed
+  sentVia          String? // email, sms, telegram (comma-separated)
+  sentAt           DateTime?
+  viewedAt         DateTime?
+  customerFeedback String?   @db.Text
+  feedbackAt       DateTime?
+  expiresAt        DateTime?
+  createdAt        DateTime  @default(now())
+  updatedAt        DateTime  @updatedAt
+
+  serviceRecordId String
+  serviceRecord   ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
+
+  organizationId String
+  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+
+  technicianId String?
+  technician   Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
+
+  @@index([serviceRecordId])
+  @@index([organizationId])
+  @@index([publicToken])
+  @@index([expiresAt])
+  @@map("status_reports")
+}

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

@@ -1,43 +1,54 @@
-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 { 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,
 }: {
-  params: Promise<{ id: string; serviceId: string }>;
+  params: Promise<{ id: string; serviceId: string }>
 }) {
-  const { id, serviceId } = await params;
+  const { id, serviceId } = await params
 
-  const [result, settingsResult, inventoryResult, techniciansResult, presetsResult, authContext, session, orgMembersResult] =
-    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(),
-    ]);
+  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 (
@@ -45,48 +56,38 @@ 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
@@ -105,34 +106,44 @@ 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,
@@ -154,20 +165,23 @@ 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">
@@ -196,9 +210,18 @@ export default async function ServiceDetailPage({
         maxDocumentsPerService={features?.maxDocumentsPerService ?? 999999}
         smsEnabled={features?.sms ?? false}
         emailEnabled={features?.smtp ?? false}
+        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,
+        }))}
       />
     </div>
-  );
+  )
 }

+ 133 - 0
src/app/(public)/share/status-report/[orgId]/[token]/page.tsx

@@ -0,0 +1,133 @@
+import { db } from "@/lib/db";
+import { notFound } from "next/navigation";
+import { StatusReportView } from "./status-report-view";
+import { resolvePortalOrg } from "@/lib/portal-slug";
+import { getFeatures } from "@/lib/features";
+import type { Metadata } from "next";
+
+function toPublicFileUrl(fileUrl: string, token: string): string {
+  const match = fileUrl.match(/^\/api\/protected\/files\/[^/]+\/(.+)$/);
+  if (match) return `/api/public/files/${token}/${match[1]}`;
+  return fileUrl;
+}
+
+export const revalidate = 0;
+export const dynamic = "force-dynamic";
+export const metadata: Metadata = {
+  robots: { index: false, follow: false, nocache: true, noarchive: true, nosnippet: true },
+};
+
+export default async function PublicStatusReportPage({
+  params,
+}: {
+  params: Promise<{ orgId: string; token: string }>;
+}) {
+  const { orgId: orgParam, token } = await params;
+  const resolvedOrg = await resolvePortalOrg(orgParam);
+  const orgId = resolvedOrg?.id ?? orgParam;
+
+  const report = await db.statusReport.findUnique({
+    where: { publicToken: token },
+    include: {
+      serviceRecord: {
+        select: {
+          title: true,
+          description: true,
+          status: true,
+        },
+      },
+      technician: {
+        select: { name: true },
+      },
+      organization: {
+        select: { name: true },
+      },
+    },
+  });
+
+  if (!report || report.organizationId !== orgId) {
+    notFound();
+  }
+
+  // Check if expired
+  if (report.expiresAt && report.expiresAt < new Date()) {
+    notFound();
+  }
+
+  // Get vehicle info through service record
+  const serviceWithVehicle = await db.serviceRecord.findUnique({
+    where: { id: report.serviceRecordId },
+    select: {
+      vehicle: {
+        select: {
+          make: true,
+          model: true,
+          year: true,
+          licensePlate: true,
+        },
+      },
+    },
+  });
+
+  if (!serviceWithVehicle) notFound();
+
+  // Mark as viewed on first visit
+  if (!report.viewedAt) {
+    await db.statusReport.update({
+      where: { id: report.id },
+      data: { viewedAt: new Date(), status: "viewed" },
+    });
+  }
+
+  // Fetch workshop settings
+  const [settings, features] = await Promise.all([
+    db.appSetting.findMany({
+      where: {
+        organizationId: orgId,
+        key: {
+          in: [
+            "workshop.address",
+            "workshop.phone",
+            "workshop.email",
+            "workshop.logo",
+            "invoice.primaryColor",
+            "workshop.serviceType",
+          ],
+        },
+      },
+    }),
+    getFeatures(orgId),
+  ]);
+
+  const settingsMap: Record<string, string> = {};
+  for (const s of settings) settingsMap[s.key] = s.value;
+
+  // Rewrite video URL if needed
+  const videoUrl = report.videoUrl
+    ? toPublicFileUrl(report.videoUrl, token)
+    : null;
+
+  return (
+    <StatusReportView
+      report={{
+        id: report.id,
+        title: report.title,
+        message: report.message,
+        videoUrl,
+        videoFileName: report.videoFileName,
+        status: report.status,
+        createdAt: report.createdAt.toISOString(),
+        customerFeedback: report.customerFeedback,
+        feedbackAt: report.feedbackAt?.toISOString() || null,
+      }}
+      vehicle={serviceWithVehicle.vehicle}
+      serviceTitle={report.serviceRecord.title}
+      technicianName={report.technician?.name || null}
+      workshopName={report.organization.name}
+      workshopPhone={settingsMap["workshop.phone"] || ""}
+      primaryColor={settingsMap["invoice.primaryColor"] || "#3b82f6"}
+      token={token}
+      showBranding={!features.brandingRemoved}
+    />
+  );
+}

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

@@ -0,0 +1,185 @@
+"use client";
+
+import { useState } from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Loader2, CheckCircle, User, Calendar, Wrench, CarFront } from "lucide-react";
+import { submitStatusReportFeedback } from "@/features/status-reports/Actions/submitFeedback";
+
+interface StatusReportViewProps {
+  report: {
+    id: string;
+    title: string | null;
+    message: string | null;
+    videoUrl: string | null;
+    videoFileName: string | null;
+    status: string;
+    createdAt: string;
+    customerFeedback: string | null;
+    feedbackAt: string | null;
+  };
+  vehicle: { make: string; model: string; year: number; licensePlate: string | null };
+  serviceTitle: string;
+  technicianName: string | null;
+  workshopName: string;
+  workshopPhone: string;
+  primaryColor: string;
+  token: string;
+  showBranding: boolean;
+}
+
+export function StatusReportView({
+  report, vehicle, serviceTitle, technicianName,
+  workshopName, workshopPhone, primaryColor, token, showBranding,
+}: StatusReportViewProps) {
+  const [feedback, setFeedback] = useState("");
+  const [submitting, setSubmitting] = useState(false);
+  const [submitted, setSubmitted] = useState(!!report.customerFeedback);
+
+  const createdDate = new Date(report.createdAt).toLocaleDateString(undefined, {
+    year: "numeric", month: "long", day: "numeric",
+  });
+  const vehicleLabel = `${vehicle.year} ${vehicle.make} ${vehicle.model}`;
+
+  async function handleSubmitFeedback() {
+    if (!feedback.trim()) return;
+    setSubmitting(true);
+    try {
+      await submitStatusReportFeedback({ token, feedback: feedback.trim() });
+      setSubmitted(true);
+    } catch {
+      // Allow retry on failure
+    } finally {
+      setSubmitting(false);
+    }
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      {/* Header */}
+      <div style={{ backgroundColor: primaryColor }} className="text-white">
+        <div className="mx-auto max-w-2xl px-4 py-6">
+          <h1 className="text-xl font-bold">{workshopName}</h1>
+          {workshopPhone && <p className="mt-1 text-sm text-white/80">{workshopPhone}</p>}
+        </div>
+      </div>
+
+      <div className="mx-auto max-w-2xl px-4 py-6 space-y-6">
+        {/* Vehicle & Service Info */}
+        <Card>
+          <CardContent className="space-y-3 pt-6">
+            <div className="flex items-start gap-3">
+              <Wrench className="mt-0.5 h-5 w-5 text-gray-400 shrink-0" />
+              <div>
+                <p className="text-sm text-gray-500">Service</p>
+                <p className="font-medium">{serviceTitle}</p>
+              </div>
+            </div>
+            <div className="flex items-start gap-3">
+              <CarFront className="mt-0.5 h-5 w-5 text-gray-400 shrink-0" />
+              <div>
+                <p className="text-sm text-gray-500">Vehicle</p>
+                <p className="font-medium">{vehicleLabel}</p>
+                {vehicle.licensePlate && (
+                  <p className="text-sm text-gray-500">Plate: {vehicle.licensePlate}</p>
+                )}
+              </div>
+            </div>
+            {report.title && (
+              <div className="pt-2 border-t">
+                <p className="font-semibold text-lg">{report.title}</p>
+              </div>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Video */}
+        {report.videoUrl && (
+          <Card>
+            <CardContent className="pt-6">
+              <div className="aspect-video w-full overflow-hidden rounded-lg bg-black">
+                <video className="h-full w-full" controls preload="metadata" playsInline>
+                  <source src={report.videoUrl} />
+                  Your browser does not support the video element.
+                </video>
+              </div>
+              {report.videoFileName && (
+                <p className="mt-2 text-xs text-gray-400">{report.videoFileName}</p>
+              )}
+            </CardContent>
+          </Card>
+        )}
+
+        {/* Message */}
+        {report.message && (
+          <Card>
+            <CardContent className="pt-6">
+              <h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
+                Message from technician
+              </h2>
+              <p className="text-gray-700 whitespace-pre-wrap leading-relaxed">
+                {report.message}
+              </p>
+            </CardContent>
+          </Card>
+        )}
+
+        {/* Technician & date info */}
+        <div className="flex items-center gap-4 text-sm text-gray-500">
+          {technicianName && (
+            <span className="flex items-center gap-1.5">
+              <User className="h-4 w-4" /> {technicianName}
+            </span>
+          )}
+          <span className="flex items-center gap-1.5">
+            <Calendar className="h-4 w-4" /> {createdDate}
+          </span>
+        </div>
+
+        {/* Feedback */}
+        <Card>
+          <CardContent className="pt-6">
+            <h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
+              Your Feedback
+            </h2>
+            {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>
+              </div>
+            ) : (
+              <div className="space-y-3">
+                <Textarea
+                  placeholder="Let us know your thoughts or concerns..."
+                  value={feedback}
+                  onChange={(e) => setFeedback(e.target.value)}
+                  rows={4}
+                />
+                <Button
+                  onClick={handleSubmitFeedback}
+                  disabled={submitting || !feedback.trim()}
+                  style={{ backgroundColor: primaryColor }}
+                  className="text-white hover:opacity-90"
+                >
+                  {submitting && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
+                  Submit Feedback
+                </Button>
+              </div>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Footer */}
+        {showBranding && (
+          <p className="text-center text-xs text-gray-400 py-4">Powered by Torqvoice</p>
+        )}
+      </div>
+    </div>
+  );
+}

+ 14 - 0
src/app/api/protected/cleanup/status-reports/route.ts

@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+import { cleanupExpiredReports } from "@/features/status-reports/Actions/cleanupExpiredReports";
+import { getAuthContext } from "@/lib/get-auth-context";
+
+export async function POST() {
+  // Require authentication - only admins/super admins should call this
+  const auth = await getAuthContext();
+  if (!auth) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }
+
+  const result = await cleanupExpiredReports();
+  return NextResponse.json(result);
+}

+ 10 - 0
src/app/api/public/files/[token]/[...path]/route.ts

@@ -69,6 +69,16 @@ export async function GET(
     }
   }
 
+  if (!orgId) {
+    const statusReport = await db.statusReport.findFirst({
+      where: { publicToken: token },
+      select: { organizationId: true },
+    });
+    if (statusReport?.organizationId) {
+      orgId = statusReport.organizationId;
+    }
+  }
+
   if (!orgId) {
     return NextResponse.json({ error: "Not found" }, { status: 404 });
   }

+ 15 - 0
src/app/api/v1/cron/cleanup-status-reports/route.ts

@@ -0,0 +1,15 @@
+import { NextRequest, NextResponse } from "next/server";
+import { cleanupExpiredReports } from "@/features/status-reports/Actions/cleanupExpiredReports";
+
+export async function POST(request: NextRequest) {
+  const authHeader = request.headers.get("authorization");
+  const cronSecret = process.env.CRON_SECRET;
+
+  // Require CRON_SECRET env var for security
+  if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }
+
+  const result = await cleanupExpiredReports();
+  return NextResponse.json(result);
+}

+ 1 - 1
src/app/robots.ts

@@ -5,7 +5,7 @@ export default function robots(): MetadataRoute.Robots {
     rules: [
       {
         userAgent: "*",
-        disallow: ["/share/invoice/", "/api/public/share/invoice/"],
+        disallow: ["/share/invoice/", "/share/status-report/", "/api/public/share/invoice/", "/api/public/files/"],
       },
     ],
   };

+ 59 - 0
src/features/status-reports/Actions/cleanupExpiredReports.ts

@@ -0,0 +1,59 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { unlink } from "fs/promises";
+import path from "path";
+
+/**
+ * Deletes expired status reports and their associated video files.
+ * Called by the cleanup cron API route.
+ */
+export async function cleanupExpiredReports() {
+  const now = new Date();
+
+  // Find all expired reports with video files
+  const expiredReports = await db.statusReport.findMany({
+    where: {
+      expiresAt: { lt: now },
+    },
+    select: {
+      id: true,
+      videoUrl: true,
+      organizationId: true,
+    },
+  });
+
+  if (expiredReports.length === 0) return { deleted: 0 };
+
+  // Delete video files from disk
+  for (const report of expiredReports) {
+    if (report.videoUrl) {
+      try {
+        // videoUrl format: /api/protected/files/{orgId}/services/{filename}
+        const match = report.videoUrl.match(/\/services\/(.+)$/);
+        if (match) {
+          const filePath = path.join(
+            process.cwd(),
+            "data",
+            "uploads",
+            report.organizationId,
+            "services",
+            match[1]
+          );
+          await unlink(filePath);
+        }
+      } catch {
+        // File may already be deleted, continue
+      }
+    }
+  }
+
+  // Delete all expired reports from database
+  const result = await db.statusReport.deleteMany({
+    where: {
+      expiresAt: { lt: now },
+    },
+  });
+
+  return { deleted: result.count };
+}

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

@@ -0,0 +1,56 @@
+"use server";
+
+import { randomBytes } from "crypto";
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { createStatusReportSchema } from "../Schema/statusReportSchema";
+
+export async function createStatusReport(input: unknown) {
+  return withAuth(
+    async ({ userId, organizationId }) => {
+      const data = createStatusReportSchema.parse(input);
+
+      // Verify the service record belongs to this org
+      const serviceRecord = await db.serviceRecord.findFirst({
+        where: { id: data.serviceRecordId, vehicle: { organizationId } },
+        select: { id: true, technicianId: true },
+      });
+      if (!serviceRecord) throw new Error("Service record not found");
+
+      // Find technician linked to current user
+      const technician = await db.technician.findFirst({
+        where: { organizationId, userId, isActive: true },
+        select: { id: true },
+      });
+
+      const report = await db.statusReport.create({
+        data: {
+          publicToken: randomBytes(32).toString("hex"),
+          title: data.title,
+          message: data.message,
+          videoUrl: data.videoUrl,
+          videoFileName: data.videoFileName,
+          serviceRecordId: data.serviceRecordId,
+          organizationId,
+          technicianId: technician?.id || serviceRecord.technicianId,
+          status: data.videoUrl ? "published" : "draft",
+          expiresAt: data.expiresAt ? new Date(data.expiresAt) : new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
+        },
+      });
+
+      return report;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES },
+      ],
+      audit: ({ result }) => ({
+        action: "statusReport.create",
+        entity: "StatusReport",
+        entityId: result.id,
+        message: `Created status report for service ${result.serviceRecordId}`,
+      }),
+    }
+  );
+}

+ 75 - 0
src/features/status-reports/Actions/getPublicStatusReport.ts

@@ -0,0 +1,75 @@
+"use server";
+
+import { db } from "@/lib/db";
+
+export async function getPublicStatusReport(token: string) {
+  const report = await db.statusReport.findUnique({
+    where: { publicToken: token },
+    include: {
+      serviceRecord: {
+        select: {
+          title: true,
+          description: true,
+          status: true,
+          vehicle: {
+            select: {
+              make: true,
+              model: true,
+              year: true,
+              licensePlate: true,
+              customer: {
+                select: { name: true },
+              },
+            },
+          },
+        },
+      },
+      technician: {
+        select: { name: true },
+      },
+      organization: {
+        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");
+  }
+
+  // Update viewedAt on first view
+  if (!report.viewedAt) {
+    await db.statusReport.update({
+      where: { id: report.id },
+      data: { viewedAt: new Date() },
+    });
+  }
+
+  return {
+    id: report.id,
+    title: report.title,
+    message: report.message,
+    videoUrl: report.videoUrl,
+    videoFileName: report.videoFileName,
+    status: report.status,
+    createdAt: report.createdAt,
+    expiresAt: report.expiresAt,
+    customerFeedback: report.customerFeedback,
+    feedbackAt: report.feedbackAt,
+    serviceRecord: {
+      title: report.serviceRecord.title,
+      description: report.serviceRecord.description,
+      status: report.serviceRecord.status,
+    },
+    vehicle: {
+      make: report.serviceRecord.vehicle.make,
+      model: report.serviceRecord.vehicle.model,
+      year: report.serviceRecord.vehicle.year,
+      licensePlate: report.serviceRecord.vehicle.licensePlate,
+    },
+    technician: report.technician ? { name: report.technician.name } : null,
+    organization: { name: report.organization.name },
+  };
+}

+ 49 - 0
src/features/status-reports/Actions/getStatusReport.ts

@@ -0,0 +1,49 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function getStatusReport(statusReportId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const report = await db.statusReport.findFirst({
+        where: { id: statusReportId, organizationId },
+        include: {
+          serviceRecord: {
+            include: {
+              vehicle: {
+                select: {
+                  make: true,
+                  model: true,
+                  year: true,
+                  licensePlate: true,
+                  customer: {
+                    select: {
+                      name: true,
+                      email: true,
+                      phone: true,
+                      telegramChatId: true,
+                    },
+                  },
+                },
+              },
+            },
+          },
+          technician: {
+            select: { name: true },
+          },
+        },
+      });
+
+      if (!report) throw new Error("Status report not found");
+
+      return report;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.SERVICES },
+      ],
+    }
+  );
+}

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

@@ -0,0 +1,39 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function getStatusReportsForService(serviceRecordId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      // Validate the service record belongs to this org
+      const serviceRecord = await db.serviceRecord.findFirst({
+        where: { id: serviceRecordId, vehicle: { organizationId } },
+        select: { id: true },
+      });
+      if (!serviceRecord) throw new Error("Service record not found");
+
+      const reports = await db.statusReport.findMany({
+        where: { serviceRecordId, organizationId },
+        select: {
+          id: true,
+          title: true,
+          status: true,
+          videoUrl: true,
+          createdAt: true,
+          publicToken: true,
+          expiresAt: true,
+        },
+        orderBy: { createdAt: "desc" },
+      });
+
+      return reports;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.SERVICES },
+      ],
+    }
+  );
+}

+ 113 - 0
src/features/status-reports/Actions/sendStatusReport.ts

@@ -0,0 +1,113 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { getTranslations } from "next-intl/server";
+import { sendStatusReportSchema } from "../Schema/statusReportSchema";
+import { sendSmsToCustomer } from "@/features/sms/Actions/smsActions";
+import { sendNotificationEmail } from "@/features/email/Actions/emailActions";
+import { sendTelegramToCustomer } from "@/features/telegram/Actions/telegramActions";
+
+export async function sendStatusReport(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const data = sendStatusReportSchema.parse(input);
+
+      // Get the status report with service record and customer info
+      const report = await db.statusReport.findFirst({
+        where: { id: data.statusReportId, organizationId },
+        include: {
+          serviceRecord: {
+            include: {
+              vehicle: {
+                include: {
+                  customer: {
+                    select: {
+                      id: true,
+                      name: true,
+                      email: true,
+                      phone: true,
+                      telegramChatId: true,
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      });
+
+      if (!report) throw new Error("Status report not found");
+
+      const customer = report.serviceRecord.vehicle.customer;
+      if (!customer) throw new Error("No customer linked to this vehicle");
+
+      // Build the public URL for the status report
+      const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
+      const publicUrl = `${appUrl}/share/status-report/${organizationId}/${report.publicToken}`;
+
+      const vehicle = report.serviceRecord.vehicle;
+      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 sentChannels: string[] = [];
+
+      if (data.channels.email && customer.email) {
+        await sendNotificationEmail({
+          recipientEmail: customer.email,
+          subject: t("emailSubject", { vehicle: vehicleName }),
+          body: messageBody,
+        });
+        sentChannels.push("email");
+      }
+
+      if (data.channels.sms && customer.phone) {
+        await sendSmsToCustomer({
+          customerId: customer.id,
+          body: messageBody,
+          relatedEntityType: "status_report",
+          relatedEntityId: report.id,
+        });
+        sentChannels.push("sms");
+      }
+
+      if (data.channels.telegram && customer.telegramChatId) {
+        await sendTelegramToCustomer({
+          customerId: customer.id,
+          body: messageBody,
+          relatedEntityType: "status_report",
+          relatedEntityId: report.id,
+        });
+        sentChannels.push("telegram");
+      }
+
+      // Update the report status
+      await db.statusReport.update({
+        where: { id: report.id },
+        data: {
+          status: "sent",
+          sentVia: sentChannels.join(","),
+          sentAt: new Date(),
+        },
+      });
+
+      return { sent: true, channels: sentChannels, statusReportId: data.statusReportId };
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES },
+      ],
+      audit: ({ result }) => ({
+        action: "statusReport.send",
+        entity: "StatusReport",
+        entityId: result.statusReportId,
+        message: `Sent status report via ${result.channels.join(", ")}`,
+      }),
+    }
+  );
+}

+ 25 - 0
src/features/status-reports/Actions/submitFeedback.ts

@@ -0,0 +1,25 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { submitFeedbackSchema } from "../Schema/statusReportSchema";
+
+export async function submitStatusReportFeedback(input: unknown) {
+  const data = submitFeedbackSchema.parse(input);
+
+  const report = await db.statusReport.findUnique({
+    where: { publicToken: data.token },
+  });
+
+  if (!report) throw new Error("Status report not found");
+  if (report.expiresAt && report.expiresAt < new Date()) throw new Error("This status report has expired");
+
+  await db.statusReport.update({
+    where: { id: report.id },
+    data: {
+      customerFeedback: data.feedback,
+      feedbackAt: new Date(),
+    },
+  });
+
+  return { success: true };
+}

+ 243 - 0
src/features/status-reports/Components/CreateStatusReportDialog.tsx

@@ -0,0 +1,243 @@
+"use client";
+
+import { useState, useRef, useEffect } from "react";
+import { useTranslations } from "next-intl";
+import {
+  Dialog, DialogContent, DialogHeader, DialogTitle,
+  DialogDescription, DialogFooter,
+} from "@/components/ui/dialog";
+import {
+  Drawer, DrawerContent, DrawerHeader, DrawerTitle,
+  DrawerDescription, DrawerFooter,
+} from "@/components/ui/drawer";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import { Camera, Loader2, Upload, Video, Send, X } from "lucide-react";
+import { toast } from "sonner";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { createStatusReport } from "../Actions/createStatusReport";
+
+interface CreateStatusReportDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  serviceRecordId: string;
+  vehicleName: string;
+  customer: {
+    id: string;
+    name: string;
+    email: string | null;
+    phone: string | null;
+    telegramChatId: string | null;
+  } | null;
+  smsEnabled: boolean;
+  emailEnabled: boolean;
+  telegramEnabled: boolean;
+  onCreated?: (reportId: string) => void;
+}
+
+export function CreateStatusReportDialog({
+  open, onOpenChange, serviceRecordId, vehicleName, onCreated,
+}: CreateStatusReportDialogProps) {
+  const t = useTranslations("statusReport.create");
+  const isMobile = useIsMobile();
+  const fileInputRef = useRef<HTMLInputElement>(null);
+  const captureInputRef = useRef<HTMLInputElement>(null);
+  const [title, setTitle] = useState("");
+  const [message, setMessage] = useState("");
+  const [videoUrl, setVideoUrl] = useState<string | null>(null);
+  const [videoFileName, setVideoFileName] = useState<string | null>(null);
+  const [uploading, setUploading] = useState(false);
+  const [uploadProgress, setUploadProgress] = useState(0);
+  const [submitting, setSubmitting] = useState(false);
+  const [expiresAt, setExpiresAt] = useState(
+    new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
+  );
+
+  useEffect(() => {
+    if (open) {
+      setTitle(""); setMessage(""); setVideoUrl(null); setVideoFileName(null);
+      setUploading(false); setUploadProgress(0); setSubmitting(false);
+      setExpiresAt(new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]);
+    }
+  }, [open]);
+
+  async function handleFileUpload(file: File) {
+    setUploading(true);
+    setUploadProgress(0);
+    const formData = new FormData();
+    formData.append("file", file);
+    try {
+      const xhr = new XMLHttpRequest();
+      const result = await new Promise<{ url: string; fileName: string }>((resolve, reject) => {
+        xhr.upload.addEventListener("progress", (e) => {
+          if (e.lengthComputable) setUploadProgress(Math.round((e.loaded / e.total) * 100));
+        });
+        xhr.addEventListener("load", () => {
+          if (xhr.status >= 200 && xhr.status < 300) resolve(JSON.parse(xhr.responseText));
+          else reject(new Error(t("uploadFailed")));
+        });
+        xhr.addEventListener("error", () => reject(new Error(t("uploadFailed"))));
+        xhr.open("POST", "/api/protected/upload/service-files");
+        xhr.send(formData);
+      });
+      setVideoUrl(result.url);
+      setVideoFileName(result.fileName);
+    } catch {
+      toast.error(t("uploadFailed"));
+    } finally {
+      setUploading(false);
+    }
+  }
+
+  function handleDrop(e: React.DragEvent) {
+    e.preventDefault();
+    const file = e.dataTransfer.files[0];
+    if (file?.type.startsWith("video/")) handleFileUpload(file);
+  }
+
+  async function handleSubmit(andSend: boolean) {
+    if (!videoUrl) { toast.error(t("noVideo")); return; }
+    setSubmitting(true);
+    try {
+      const result = await createStatusReport({
+        serviceRecordId,
+        title: title || undefined,
+        message: message || undefined,
+        videoUrl,
+        videoFileName: videoFileName || undefined,
+        expiresAt,
+      });
+      if (!result.success || !result.data) {
+        toast.error(result.error || "Failed to create status report");
+        return;
+      }
+      toast.success(t("created"));
+      onOpenChange(false);
+      if (andSend && onCreated) onCreated(result.data.id);
+    } catch {
+      toast.error("Failed to create status report");
+    } finally {
+      setSubmitting(false);
+    }
+  }
+
+  const formContent = (
+    <div className="space-y-4">
+      {videoUrl ? (
+        <div className="relative overflow-hidden rounded-lg border">
+          <video src={videoUrl} controls preload="metadata" playsInline className="w-full max-h-56" />
+          <div className="flex items-center justify-between p-2">
+            <div className="flex items-center gap-2 min-w-0">
+              <Video className="h-4 w-4 shrink-0 text-muted-foreground" />
+              <span className="truncate text-sm">{videoFileName}</span>
+            </div>
+            <Button type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0"
+              onClick={() => { setVideoUrl(null); setVideoFileName(null); }}>
+              <X className="h-4 w-4" />
+            </Button>
+          </div>
+        </div>
+      ) : uploading ? (
+        <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-8">
+          <Loader2 className="mb-2 h-8 w-8 animate-spin text-muted-foreground/50" />
+          <p className="text-sm font-medium">{t("uploading")} {uploadProgress}%</p>
+        </div>
+      ) : (
+        <div
+          onDrop={handleDrop}
+          onDragOver={(e) => e.preventDefault()}
+          className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
+        >
+          <Video className="mb-3 h-8 w-8 text-muted-foreground/50" />
+          <div className="flex gap-2">
+            <Button type="button" variant="outline" size="sm" onClick={() => captureInputRef.current?.click()}>
+              <Camera className="mr-1.5 h-4 w-4" />
+              {t("record")}
+            </Button>
+            <Button type="button" variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>
+              <Upload className="mr-1.5 h-4 w-4" />
+              {t("uploadFile")}
+            </Button>
+          </div>
+          <input ref={captureInputRef} type="file" accept="video/*" capture="environment" className="hidden"
+            onChange={(e) => { const f = e.target.files?.[0]; if (f) { handleFileUpload(f); e.target.value = ""; } }} />
+          <input ref={fileInputRef} type="file" accept="video/*" className="hidden"
+            onChange={(e) => { const f = e.target.files?.[0]; if (f) { handleFileUpload(f); e.target.value = ""; } }} />
+        </div>
+      )}
+
+      <div className="space-y-1.5">
+        <Label htmlFor="sr-title">{t("titleLabel")}</Label>
+        <Input id="sr-title" value={title} onChange={(e) => setTitle(e.target.value)}
+          placeholder={t("titlePlaceholder")} />
+      </div>
+
+      <div className="space-y-1.5">
+        <Label htmlFor="sr-message">{t("messageLabel")}</Label>
+        <Textarea id="sr-message" value={message} onChange={(e) => setMessage(e.target.value)}
+          placeholder={t("messagePlaceholder")} rows={3} />
+      </div>
+
+      <div className="space-y-1.5">
+        <Label htmlFor="sr-expires">{t("expiresLabel")}</Label>
+        <Input id="sr-expires" type="date" value={expiresAt}
+          onChange={(e) => setExpiresAt(e.target.value)}
+          min={new Date().toISOString().split("T")[0]} />
+      </div>
+    </div>
+  );
+
+  const footerButtons = (
+    <>
+      <Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
+        {t("cancel")}
+      </Button>
+      <Button onClick={() => handleSubmit(false)} disabled={submitting || uploading}>
+        {submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+        {t("create")}
+      </Button>
+      {onCreated && (
+        <Button onClick={() => handleSubmit(true)} disabled={submitting || uploading}>
+          {submitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Send className="mr-2 h-4 w-4" />}
+          {t("createAndSend")}
+        </Button>
+      )}
+    </>
+  );
+
+  if (isMobile) {
+    return (
+      <Drawer open={open} onOpenChange={onOpenChange}>
+        <DrawerContent>
+          <DrawerHeader>
+            <DrawerTitle>{t("title")}</DrawerTitle>
+            <DrawerDescription>{vehicleName}</DrawerDescription>
+          </DrawerHeader>
+          <div className="overflow-y-auto px-4 pb-2">
+            {formContent}
+          </div>
+          <DrawerFooter className="flex-row gap-2">
+            {footerButtons}
+          </DrawerFooter>
+        </DrawerContent>
+      </Drawer>
+    );
+  }
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-lg">
+        <DialogHeader>
+          <DialogTitle>{t("title")}</DialogTitle>
+          <DialogDescription>{vehicleName}</DialogDescription>
+        </DialogHeader>
+        {formContent}
+        <DialogFooter>
+          {footerButtons}
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 202 - 0
src/features/status-reports/Components/SendStatusReportDialog.tsx

@@ -0,0 +1,202 @@
+"use client";
+
+import { useState } from "react";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+  DialogDescription,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+import { Loader2, Mail, MessageSquare, Send } from "lucide-react";
+import { toast } from "sonner";
+import { useTranslations } from "next-intl";
+import { sendStatusReport } from "../Actions/sendStatusReport";
+
+interface SendStatusReportDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  reportId: string;
+  customer: {
+    name: string;
+    email: string | null;
+    phone: string | null;
+    telegramChatId: string | null;
+  };
+  smsEnabled: boolean;
+  emailEnabled: boolean;
+  telegramEnabled: boolean;
+}
+
+export function SendStatusReportDialog({
+  open,
+  onOpenChange,
+  reportId,
+  customer,
+  smsEnabled,
+  emailEnabled,
+  telegramEnabled,
+}: SendStatusReportDialogProps) {
+  const t = useTranslations("statusReport.send");
+  const [message, setMessage] = useState("");
+  const [sendSms, setSendSms] = useState(false);
+  const [sendEmail, setSendEmail] = useState(false);
+  const [sendTelegram, setSendTelegram] = useState(false);
+  const [sending, setSending] = useState(false);
+
+  const hasPhone = !!customer.phone;
+  const hasEmail = !!customer.email;
+  const hasTelegram = !!customer.telegramChatId;
+
+  const handleOpenChange = (next: boolean) => {
+    if (next) {
+      setMessage("");
+      setSendSms(false);
+      setSendEmail(false);
+      setSendTelegram(false);
+    }
+    onOpenChange(next);
+  };
+
+  const handleSend = async () => {
+    if (!sendSms && !sendEmail && !sendTelegram) return;
+    setSending(true);
+
+    try {
+      const res = await sendStatusReport({
+        statusReportId: reportId,
+        channels: {
+          sms: sendSms,
+          email: sendEmail,
+          telegram: sendTelegram,
+        },
+        customMessage: message.trim() || undefined,
+      });
+
+      if (res.success) {
+        const channels: string[] = [];
+        if (sendSms) channels.push(t("sms"));
+        if (sendEmail) channels.push(t("email"));
+        if (sendTelegram) channels.push(t("telegram"));
+        toast.success(`${t("sent")}: ${channels.join(", ")}`);
+        onOpenChange(false);
+      } else {
+        toast.error(res.error || t("failed"));
+      }
+    } catch {
+      toast.error(t("failed"));
+    } finally {
+      setSending(false);
+    }
+  };
+
+  const canSend = sendSms || sendEmail || sendTelegram;
+
+  return (
+    <Dialog open={open} onOpenChange={handleOpenChange}>
+      <DialogContent className="sm:max-w-md">
+        <DialogHeader>
+          <DialogTitle>{t("title")}</DialogTitle>
+          <DialogDescription>
+            {t("description", { name: customer.name })}
+          </DialogDescription>
+        </DialogHeader>
+
+        <div className="space-y-4">
+          <Textarea
+            value={message}
+            onChange={(e) => setMessage(e.target.value)}
+            placeholder={t("messagePlaceholder")}
+            rows={4}
+            className="resize-none"
+          />
+
+          <div className="space-y-2">
+            {/* SMS */}
+            <div className="flex items-center gap-2">
+              <Checkbox
+                id="report-sms"
+                checked={sendSms}
+                onCheckedChange={(v) => setSendSms(v === true)}
+                disabled={!smsEnabled || !hasPhone}
+              />
+              <Label
+                htmlFor="report-sms"
+                className={`flex items-center gap-1.5 text-sm ${!smsEnabled || !hasPhone ? "text-muted-foreground/50" : ""}`}
+              >
+                <MessageSquare className="h-3.5 w-3.5" />
+                {t("sms")}
+                {!hasPhone && <span className="text-xs">{t("noPhone")}</span>}
+                {hasPhone && !smsEnabled && (
+                  <span className="text-xs">{t("notAvailable")}</span>
+                )}
+              </Label>
+            </div>
+
+            {/* Email */}
+            <div className="flex items-center gap-2">
+              <Checkbox
+                id="report-email"
+                checked={sendEmail}
+                onCheckedChange={(v) => setSendEmail(v === true)}
+                disabled={!emailEnabled || !hasEmail}
+              />
+              <Label
+                htmlFor="report-email"
+                className={`flex items-center gap-1.5 text-sm ${!emailEnabled || !hasEmail ? "text-muted-foreground/50" : ""}`}
+              >
+                <Mail className="h-3.5 w-3.5" />
+                {t("email")}
+                {!hasEmail && <span className="text-xs">{t("noEmail")}</span>}
+                {hasEmail && !emailEnabled && (
+                  <span className="text-xs">{t("notAvailable")}</span>
+                )}
+              </Label>
+            </div>
+
+            {/* Telegram */}
+            <div className="flex items-center gap-2">
+              <Checkbox
+                id="report-telegram"
+                checked={sendTelegram}
+                onCheckedChange={(v) => setSendTelegram(v === true)}
+                disabled={!telegramEnabled || !hasTelegram}
+              />
+              <Label
+                htmlFor="report-telegram"
+                className={`flex items-center gap-1.5 text-sm ${!telegramEnabled || !hasTelegram ? "text-muted-foreground/50" : ""}`}
+              >
+                <Send className="h-3.5 w-3.5" />
+                {t("telegram")}
+                {!hasTelegram && (
+                  <span className="text-xs">{t("noTelegram")}</span>
+                )}
+                {hasTelegram && !telegramEnabled && (
+                  <span className="text-xs">{t("notAvailable")}</span>
+                )}
+              </Label>
+            </div>
+          </div>
+
+          <div className="flex justify-end gap-2">
+            <Button variant="ghost" onClick={() => onOpenChange(false)}>
+              {t("skip")}
+            </Button>
+            <Button onClick={handleSend} disabled={!canSend || sending}>
+              {sending ? (
+                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+              ) : (
+                <Send className="mr-2 h-4 w-4" />
+              )}
+              {t("send")}
+            </Button>
+          </div>
+        </div>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 167 - 0
src/features/status-reports/Components/StatusReportList.tsx

@@ -0,0 +1,167 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { Plus, Copy, Send, ExternalLink, FileVideo } from "lucide-react";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+import { CreateStatusReportDialog } from "./CreateStatusReportDialog";
+import { SendStatusReportDialog } from "./SendStatusReportDialog";
+
+interface StatusReportSummary {
+  id: string;
+  title: string | null;
+  status: string;
+  videoUrl: string | null;
+  createdAt: string;
+  expiresAt: string | null;
+  publicToken: string;
+}
+
+interface StatusReportListProps {
+  serviceRecordId: string;
+  organizationId: string;
+  vehicleName: string;
+  customer: {
+    id: string;
+    name: string;
+    email: string | null;
+    phone: string | null;
+    telegramChatId: string | null;
+  } | null;
+  smsEnabled: boolean;
+  emailEnabled: boolean;
+  telegramEnabled: boolean;
+  initialReports: StatusReportSummary[];
+}
+
+const STATUS_VARIANT: Record<string, string> = {
+  draft: "bg-muted text-muted-foreground",
+  published: "bg-blue-500/10 text-blue-600",
+  sent: "bg-green-500/10 text-green-600",
+  viewed: "bg-purple-500/10 text-purple-600",
+};
+
+export function StatusReportList({
+  serviceRecordId,
+  organizationId,
+  vehicleName,
+  customer,
+  smsEnabled,
+  emailEnabled,
+  telegramEnabled,
+  initialReports,
+}: StatusReportListProps) {
+  const t = useTranslations("statusReport.list");
+  const router = useRouter();
+  const [showCreate, setShowCreate] = useState(false);
+  const [sendReportId, setSendReportId] = useState<string | null>(null);
+
+  function copyLink(publicToken: string) {
+    const url = `${window.location.origin}/share/status-report/${organizationId}/${publicToken}`;
+    navigator.clipboard.writeText(url);
+    toast.success(t("linkCopied"));
+  }
+
+  function openReport(publicToken: string) {
+    window.open(`/share/status-report/${organizationId}/${publicToken}`, "_blank");
+  }
+
+  return (
+    <>
+      <div className="space-y-4">
+        <div className="flex items-center justify-between">
+          <h3 className="text-sm font-medium">{t("title")}</h3>
+          <Button size="sm" onClick={() => setShowCreate(true)}>
+            <Plus className="mr-1.5 h-4 w-4" />
+            {t("newReport")}
+          </Button>
+        </div>
+
+        {initialReports.length === 0 ? (
+          <Card>
+            <CardContent className="flex flex-col items-center justify-center py-10 text-center">
+              <FileVideo className="mb-3 h-10 w-10 text-muted-foreground/40" />
+              <p className="font-medium text-sm">{t("empty")}</p>
+              <p className="mt-1 text-xs text-muted-foreground">{t("emptyDescription")}</p>
+            </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] || ""}`}>
+                        {t(report.status as "draft" | "published" | "sent" | "viewed")}
+                      </Badge>
+                    </div>
+                    <p className="text-xs text-muted-foreground">
+                      {new Date(report.createdAt).toLocaleDateString(undefined, {
+                        year: "numeric", 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>
+            ))}
+          </div>
+        )}
+      </div>
+
+      <CreateStatusReportDialog
+        open={showCreate}
+        onOpenChange={setShowCreate}
+        serviceRecordId={serviceRecordId}
+        vehicleName={vehicleName}
+        customer={customer}
+        smsEnabled={smsEnabled}
+        emailEnabled={emailEnabled}
+        telegramEnabled={telegramEnabled}
+        onCreated={(reportId) => {
+          router.refresh();
+          if (customer) setSendReportId(reportId);
+        }}
+      />
+
+      {sendReportId && customer && (
+        <SendStatusReportDialog
+          open={!!sendReportId}
+          onOpenChange={(open) => { if (!open) setSendReportId(null); }}
+          reportId={sendReportId}
+          customer={customer}
+          smsEnabled={smsEnabled}
+          emailEnabled={emailEnabled}
+          telegramEnabled={telegramEnabled}
+        />
+      )}
+    </>
+  );
+}

+ 29 - 0
src/features/status-reports/Schema/statusReportSchema.ts

@@ -0,0 +1,29 @@
+import { z } from "zod";
+
+export const createStatusReportSchema = z.object({
+  serviceRecordId: z.string(),
+  title: z.string().optional(),
+  message: z.string().optional(),
+  videoUrl: z.string().optional(),
+  videoFileName: z.string().optional(),
+  expiresAt: z.string().optional(),
+});
+
+export const sendStatusReportSchema = z.object({
+  statusReportId: z.string(),
+  channels: z.object({
+    sms: z.boolean(),
+    email: z.boolean(),
+    telegram: z.boolean(),
+  }),
+  customMessage: z.string().optional(),
+});
+
+export const submitFeedbackSchema = z.object({
+  token: z.string(),
+  feedback: z.string().min(1),
+});
+
+export type CreateStatusReportInput = z.infer<typeof createStatusReportSchema>;
+export type SendStatusReportInput = z.infer<typeof sendStatusReportSchema>;
+export type SubmitFeedbackInput = z.infer<typeof submitFeedbackSchema>;

+ 44 - 29
src/features/vehicles/Actions/getMyActiveJobs.ts

@@ -1,23 +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;
-  };
-  imageCount: number;
-  videoCount: number;
-  partCount: number;
+    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
 }
 
 export async function getMyActiveJobs() {
@@ -27,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: {
@@ -50,6 +57,15 @@ export async function getMyActiveJobs() {
               model: true,
               year: true,
               licensePlate: true,
+              customer: {
+                select: {
+                  id: true,
+                  name: true,
+                  email: true,
+                  phone: true,
+                  telegramChatId: true,
+                },
+              },
             },
           },
           attachments: {
@@ -61,9 +77,9 @@ export async function getMyActiveJobs() {
             },
           },
         },
-        orderBy: { updatedAt: "desc" },
+        orderBy: { updatedAt: 'desc' },
         take: 20,
-      });
+      })
 
       return records.map((r) => ({
         id: r.id,
@@ -71,15 +87,14 @@ export async function getMyActiveJobs() {
         status: r.status,
         vehicleId: r.vehicleId,
         vehicle: r.vehicle,
-        imageCount: r.attachments.filter((a) => a.category === "image").length,
-        videoCount: r.attachments.filter((a) => a.category === "video").length,
+        customer: r.vehicle.customer,
+        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 }],
     }
-  );
+  )
 }

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

@@ -194,6 +194,7 @@ export async function getServiceRecord(recordId: string) {
                 phone: true,
                 address: true,
                 company: true,
+                telegramChatId: true,
               },
             },
           },

+ 93 - 6
src/features/vehicles/Components/MyActiveJobs.tsx

@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
 import {
   Camera,
+  FileVideo,
   ImageIcon,
   Loader2,
   Video,
@@ -33,6 +34,8 @@ import { addPartToServiceRecord } from '@/features/vehicles/Actions/addPartToSer
 import { lookupPartByBarcode } from '@/features/inventory/Actions/lookupPartByBarcode'
 import { BarcodeScannerDialog } from '@/components/barcode-scanner-dialog'
 import { InventoryPartForm } from '@/features/inventory/Components/InventoryPartForm'
+import { CreateStatusReportDialog } from '@/features/status-reports/Components/CreateStatusReportDialog'
+import { SendStatusReportDialog } from '@/features/status-reports/Components/SendStatusReportDialog'
 import type { MyActiveJob } from '@/features/vehicles/Actions/getMyActiveJobs'
 
 interface MyActiveJobsProps {
@@ -54,7 +57,9 @@ const STATUS_COLOR: Record<string, string> = {
 export function MyActiveJobs({ jobs }: 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]))
   )
@@ -86,6 +91,10 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
     Object.fromEntries(jobs.map((j) => [j.id, j.partCount]))
   )
 
+  const [statusReportJobId, setStatusReportJobId] = useState<string | null>(null)
+  const [sendReportId, setSendReportId] = useState<string | null>(null)
+  const [sendReportJobId, setSendReportJobId] = useState<string | null>(null)
+
   if (jobs.length === 0) return null
 
   const handleCameraClick = (jobId: string) => {
@@ -374,6 +383,15 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                         <ScanBarcode className="mr-1.5 h-4 w-4" />
                         {t('scanPart')}
                       </Button>
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        className="h-9"
+                        onClick={() => setStatusReportJobId(job.id)}
+                      >
+                        <FileVideo className="mr-1.5 h-4 w-4" />
+                        {t('statusReport')}
+                      </Button>
                     </div>
                     {/* Mobile: counters only */}
                     <div className="shrink-0 ml-3 flex sm:hidden items-center gap-1.5 text-xs text-muted-foreground">
@@ -397,8 +415,8 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                       )}
                     </div>
                   </div>
-                  {/* Mobile: action buttons on second line */}
-                  <div className="mt-2 sm:hidden">
+                  {/* Mobile: action buttons on two rows */}
+                  <div className="mt-2 flex flex-col gap-1.5 sm:hidden">
                     <ButtonGroup className="w-full">
                       <Button
                         variant="outline"
@@ -428,6 +446,8 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                         )}
                         {t('video')}
                       </Button>
+                    </ButtonGroup>
+                    <ButtonGroup className="w-full">
                       <Button
                         variant="outline"
                         size="sm"
@@ -437,6 +457,15 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                         <ScanBarcode className="mr-1 h-3.5 w-3.5" />
                         {t('parts')}
                       </Button>
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        className="flex-1 h-9"
+                        onClick={() => setStatusReportJobId(job.id)}
+                      >
+                        <FileVideo className="mr-1 h-3.5 w-3.5" />
+                        {t('report')}
+                      </Button>
                     </ButtonGroup>
                   </div>
                   <input
@@ -489,8 +518,14 @@ 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">
@@ -508,7 +543,9 @@ 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"
@@ -546,6 +583,56 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
         onOpenChange={handlePartFormClose}
         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}
+            />
+          )
+        })()}
     </>
   )
 }

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

@@ -105,6 +105,7 @@ export interface Vehicle {
     phone: string | null;
     address: string | null;
     company: string | null;
+    telegramChatId: string | null;
   } | null;
 }
 

+ 29 - 0
src/features/vehicles/Components/service-page/ServicePageClient.tsx

@@ -33,6 +33,7 @@ import type { LaborPresetOption } from './service-page-types'
 import { ServiceImagesManager } from '../service-images-manager'
 import { ServiceVideoManager } from '../service-video-manager'
 import { ServiceDocumentsManager } from '../service-documents-manager'
+import { StatusReportList } from '@/features/status-reports/Components/StatusReportList'
 import { UnifiedServiceHeader, type ServiceTab } from './UnifiedServiceHeader'
 
 import { useServiceFormState } from './useServiceFormState'
@@ -67,8 +68,10 @@ export function ServicePageClient({
   laborPresets = [],
   smsEnabled = false,
   emailEnabled = false,
+  telegramEnabled = false,
   aiEnabled = false,
   defaultDueDays = 0,
+  statusReports = [],
 }: ServicePageClientProps) {
   const t = useTranslations('service')
   const router = useRouter()
@@ -188,6 +191,7 @@ export function ServicePageClient({
           images: imageAttachmentsForManager.length,
           video: videoAttachments.length,
           documents: documentAttachments.length,
+          statusReports: statusReports.length,
         }}
         downloading={actions.downloading}
         saving={formState.loading}
@@ -279,6 +283,31 @@ export function ServicePageClient({
         </div>
       )}
 
+      {activeTab === 'statusReports' && (
+        <div className="flex-1 overflow-y-auto overscroll-contain p-4">
+          <StatusReportList
+            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
+            }
+            smsEnabled={smsEnabled}
+            emailEnabled={emailEnabled}
+            telegramEnabled={telegramEnabled}
+            initialReports={statusReports}
+          />
+        </div>
+      )}
+
       <InventoryPickerDialog
         open={formState.showInventoryPicker}
         onOpenChange={formState.setShowInventoryPicker}

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

@@ -17,12 +17,13 @@ import {
 } from 'lucide-react'
 import { statusColors, paymentStatusColors, paymentStatusLabels } from '../service-detail/types'
 
-export type ServiceTab = 'details' | 'images' | 'video' | 'documents'
+export type ServiceTab = 'details' | 'images' | 'video' | 'documents' | 'statusReports'
 
 export interface TabCounts {
   images: number
   video: number
   documents: number
+  statusReports: number
 }
 
 interface UnifiedServiceHeaderProps {
@@ -68,6 +69,7 @@ export function UnifiedServiceHeader({
     { label: t('tabs.images'), value: 'images' },
     { label: t('tabs.video'), value: 'video' },
     { label: t('tabs.documents'), value: 'documents' },
+    { label: t('tabs.statusReports'), value: 'statusReports' },
   ]
 
   return (

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

@@ -38,7 +38,13 @@ 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
@@ -51,8 +57,25 @@ export interface ServicePageClientProps {
   laborPresets?: LaborPresetOption[]
   smsEnabled?: boolean
   emailEnabled?: boolean
+  telegramEnabled?: boolean
   aiEnabled?: boolean
   defaultDueDays?: number
+  statusReports?: {
+    id: string
+    title: string | null
+    status: string
+    videoUrl: string | null
+    createdAt: string
+    publicToken: string
+    expiresAt: string | null
+  }[]
 }
 
-export type { ServicePartInput, ServiceLaborInput, ServiceDetail, InitialData, InventoryPartOption, LaborPresetOption }
+export type {
+  ServicePartInput,
+  ServiceLaborInput,
+  ServiceDetail,
+  InitialData,
+  InventoryPartOption,
+  LaborPresetOption,
+}

+ 2 - 0
src/i18n/request.ts

@@ -69,6 +69,7 @@ export default getRequestConfig(async () => {
   const laborPresets = (await import(`../../messages/${locale}/laborPresets.json`)).default
   const telegram = (await import(`../../messages/${locale}/telegram.json`)).default
   const telegramMessages = (await import(`../../messages/${locale}/telegramMessages.json`)).default
+  const statusReport = (await import(`../../messages/${locale}/statusReport.json`)).default
 
   return {
     locale,
@@ -100,6 +101,7 @@ export default getRequestConfig(async () => {
       laborPresets,
       telegram,
       telegramMessages,
+      statusReport,
     },
   }
 })