Selaa lähdekoodia

Calendar date picker, and workboard improvements (#95)

* service units

* units

* button

* set selected day for work order

* dashboard card mobile

* tech dropdown

* new tech link to user

* translations

* translations

* migration

* migration

* drag drop

* work

* docs links

* docs links
Bernt Christian Egeland 6 kuukautta sitten
vanhempi
commit
cc288f129f
73 muutettua tiedostoa jossa 1474 lisäystä ja 99 poistoa
  1. 6 0
      messages/de/calendar.json
  2. 25 0
      messages/de/dashboard.json
  3. 4 1
      messages/de/service.json
  4. 2 0
      messages/de/settings.json
  5. 6 0
      messages/en/calendar.json
  6. 25 0
      messages/en/dashboard.json
  7. 4 1
      messages/en/service.json
  8. 2 0
      messages/en/settings.json
  9. 6 0
      messages/es/calendar.json
  10. 25 0
      messages/es/dashboard.json
  11. 4 1
      messages/es/service.json
  12. 2 0
      messages/es/settings.json
  13. 6 0
      messages/fr/calendar.json
  14. 25 0
      messages/fr/dashboard.json
  15. 4 1
      messages/fr/service.json
  16. 2 0
      messages/fr/settings.json
  17. 6 0
      messages/it/calendar.json
  18. 25 0
      messages/it/dashboard.json
  19. 4 1
      messages/it/service.json
  20. 2 0
      messages/it/settings.json
  21. 6 0
      messages/nb/calendar.json
  22. 25 0
      messages/nb/dashboard.json
  23. 4 1
      messages/nb/service.json
  24. 2 0
      messages/nb/settings.json
  25. 6 0
      messages/nl/calendar.json
  26. 25 0
      messages/nl/dashboard.json
  27. 4 1
      messages/nl/service.json
  28. 2 0
      messages/nl/settings.json
  29. 6 0
      messages/pl/calendar.json
  30. 25 0
      messages/pl/dashboard.json
  31. 4 1
      messages/pl/service.json
  32. 2 0
      messages/pl/settings.json
  33. 6 0
      messages/pt-BR/calendar.json
  34. 25 0
      messages/pt-BR/dashboard.json
  35. 4 1
      messages/pt-BR/service.json
  36. 2 0
      messages/pt-BR/settings.json
  37. 6 0
      messages/ru/calendar.json
  38. 25 0
      messages/ru/dashboard.json
  39. 4 1
      messages/ru/service.json
  40. 2 0
      messages/ru/settings.json
  41. 6 0
      messages/tr/calendar.json
  42. 25 0
      messages/tr/dashboard.json
  43. 4 1
      messages/tr/service.json
  44. 2 0
      messages/tr/settings.json
  45. 40 0
      prisma/migrations/20260326082336_technician_assignment/migration.sql
  46. 3 1
      prisma/schema.prisma
  47. 6 4
      src/app/(authenticated)/dashboard-client.tsx
  48. 7 1
      src/app/(authenticated)/page.tsx
  49. 13 3
      src/app/(authenticated)/settings/payment/payment-settings.tsx
  50. 12 9
      src/app/(authenticated)/settings/workshop/workshop-settings.tsx
  51. 5 3
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  52. 1 1
      src/app/api/protected/backup/import/route.ts
  53. 14 4
      src/features/ai/Components/AiSettingsForm.tsx
  54. 50 1
      src/features/calendar/Components/CalendarClient.tsx
  55. 1 1
      src/features/inspections/Actions/inspectionActions.ts
  56. 14 10
      src/features/reports/Actions/reportActions.ts
  57. 1 0
      src/features/settings/Schema/settingsSchema.ts
  58. 1 1
      src/features/telegram/Components/TelegramSettingsForm.tsx
  59. 84 0
      src/features/vehicles/Actions/addPartToServiceRecord.ts
  60. 24 9
      src/features/vehicles/Actions/createDraftServiceRecord.ts
  61. 83 0
      src/features/vehicles/Actions/getMyActiveJobs.ts
  62. 152 0
      src/features/vehicles/Components/CreatePartDialog.tsx
  63. 331 0
      src/features/vehicles/Components/MyActiveJobs.tsx
  64. 103 20
      src/features/vehicles/Components/service-edit/ScheduleTimesSection.tsx
  65. 4 1
      src/features/vehicles/Components/service-page/DetailsRightColumn.tsx
  66. 2 0
      src/features/vehicles/Components/service-page/ServicePageClient.tsx
  67. 8 0
      src/features/vehicles/Components/service-page/service-page-types.ts
  68. 39 1
      src/features/workboard/Actions/technicianActions.ts
  69. 7 7
      src/features/workboard/Components/TechnicianDialog.tsx
  70. 14 1
      src/features/workboard/Components/UnassignedJobsPanel.tsx
  71. 45 7
      src/features/workboard/Components/WorkBoardClient.tsx
  72. 2 2
      src/features/workboard/Schema/workboardSchema.ts
  73. 1 1
      src/features/workboard/store/workboardStore.ts

+ 6 - 0
messages/de/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Angebote"
     "quotes": "Angebote"
   },
   },
   "selectVehicle": "Fahrzeug f\u00fcr Arbeitsauftrag ausw\u00e4hlen",
   "selectVehicle": "Fahrzeug f\u00fcr Arbeitsauftrag ausw\u00e4hlen",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} weitere",
   "more": "+ {count} weitere",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/de/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Letzte Aktivitäten",
   "recentActivity": "Letzte Aktivitäten",
   "noRecentActivity": "Keine aktuellen Aktivitäten",
   "noRecentActivity": "Keine aktuellen Aktivitäten",
   "unknownUser": "Benutzer",
   "unknownUser": "Benutzer",
@@ -73,7 +96,9 @@
     "accepted": "Angenommen",
     "accepted": "Angenommen",
     "changesRequested": "Änderungen gewünscht",
     "changesRequested": "Änderungen gewünscht",
     "convertToWorkOrder": "In Arbeitsauftrag umwandeln",
     "convertToWorkOrder": "In Arbeitsauftrag umwandeln",
+    "convert": "Convert",
     "viewQuote": "Angebot anzeigen",
     "viewQuote": "Angebot anzeigen",
+    "view": "View",
     "noData": "Keine Kundenantworten."
     "noData": "Keine Kundenantworten."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/de/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Zuweisung fehlgeschlagen",
     "failedAssign": "Zuweisung fehlgeschlagen",
     "endBeforeStart": "Endzeit muss nach der Startzeit liegen",
     "endBeforeStart": "Endzeit muss nach der Startzeit liegen",
     "failedUpdate": "Zeitplan konnte nicht aktualisiert werden",
     "failedUpdate": "Zeitplan konnte nicht aktualisiert werden",
-    "splitCreated": "Auftrag erstreckt sich über mehrere Tage — Fortsetzung für den nächsten Tag erstellt"
+    "splitCreated": "Auftrag erstreckt sich über mehrere Tage — Fortsetzung für den nächsten Tag erstellt",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/de/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Logo wird hochgeladen...",
     "uploadingLogo": "Logo wird hochgeladen...",
     "logoUpdated": "Logo aktualisiert",
     "logoUpdated": "Logo aktualisiert",
     "failedUploadLogo": "Logo-Upload fehlgeschlagen",
     "failedUploadLogo": "Logo-Upload fehlgeschlagen",
+    "readMore": "Read More",
     "detailsTitle": "Firmendetails",
     "detailsTitle": "Firmendetails",
     "shopName": "Firmen-/Werkstattname",
     "shopName": "Firmen-/Werkstattname",
     "shopNamePlaceholder": "Meiers Autowerkstatt",
     "shopNamePlaceholder": "Meiers Autowerkstatt",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "KI-Assistent",
     "title": "KI-Assistent",
+    "readMore": "Read More",
     "description": "Konfigurieren Sie KI, um automatisch Servicenotizen zu erstellen und Fahrzeughistorien zusammenzufassen.",
     "description": "Konfigurieren Sie KI, um automatisch Servicenotizen zu erstellen und Fahrzeughistorien zusammenzufassen.",
     "enableLabel": "KI aktivieren",
     "enableLabel": "KI aktivieren",
     "enableHint": "KI-gestutzte Funktionen in der gesamten Anwendung nutzen",
     "enableHint": "KI-gestutzte Funktionen in der gesamten Anwendung nutzen",

+ 6 - 0
messages/en/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Quotes"
     "quotes": "Quotes"
   },
   },
   "selectVehicle": "Select Vehicle for Work Order",
   "selectVehicle": "Select Vehicle for Work Order",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} more",
   "more": "+ {count} more",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/en/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Recent Activity",
   "recentActivity": "Recent Activity",
   "noRecentActivity": "No recent activity",
   "noRecentActivity": "No recent activity",
   "unknownUser": "User",
   "unknownUser": "User",
@@ -73,7 +96,9 @@
     "accepted": "Accepted",
     "accepted": "Accepted",
     "changesRequested": "Changes Requested",
     "changesRequested": "Changes Requested",
     "convertToWorkOrder": "Convert to Work Order",
     "convertToWorkOrder": "Convert to Work Order",
+    "convert": "Convert",
     "viewQuote": "View Quote",
     "viewQuote": "View Quote",
+    "view": "View",
     "noData": "No customer responses."
     "noData": "No customer responses."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/en/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Failed to assign",
     "failedAssign": "Failed to assign",
     "endBeforeStart": "End time must be after start time",
     "endBeforeStart": "End time must be after start time",
     "failedUpdate": "Failed to update schedule times",
     "failedUpdate": "Failed to update schedule times",
-    "splitCreated": "Job spans multiple days — continuation created for next day"
+    "splitCreated": "Job spans multiple days — continuation created for next day",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/en/settings.json

@@ -529,6 +529,7 @@
     }
     }
   },
   },
   "payment": {
   "payment": {
+    "readMore": "Read More",
     "detailsTitle": "Payment Details",
     "detailsTitle": "Payment Details",
     "detailsDescription": "Bank account and payment terms shown on invoices.",
     "detailsDescription": "Bank account and payment terms shown on invoices.",
     "bankAccount": "Bank Account (IBAN / Til Konto)",
     "bankAccount": "Bank Account (IBAN / Til Konto)",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "AI Assistant",
     "title": "AI Assistant",
+    "readMore": "Read More",
     "description": "Configure AI to auto-generate service notes and summarize vehicle history.",
     "description": "Configure AI to auto-generate service notes and summarize vehicle history.",
     "enableLabel": "Enable AI",
     "enableLabel": "Enable AI",
     "enableHint": "Use AI-powered features throughout the application",
     "enableHint": "Use AI-powered features throughout the application",

+ 6 - 0
messages/es/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Presupuestos"
     "quotes": "Presupuestos"
   },
   },
   "selectVehicle": "Seleccionar veh\u00edculo para orden de trabajo",
   "selectVehicle": "Seleccionar veh\u00edculo para orden de trabajo",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} m\u00e1s",
   "more": "+ {count} m\u00e1s",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/es/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Actividad reciente",
   "recentActivity": "Actividad reciente",
   "noRecentActivity": "Sin actividad reciente",
   "noRecentActivity": "Sin actividad reciente",
   "unknownUser": "Usuario",
   "unknownUser": "Usuario",
@@ -73,7 +96,9 @@
     "accepted": "Aceptado",
     "accepted": "Aceptado",
     "changesRequested": "Cambios solicitados",
     "changesRequested": "Cambios solicitados",
     "convertToWorkOrder": "Convertir en orden de trabajo",
     "convertToWorkOrder": "Convertir en orden de trabajo",
+    "convert": "Convert",
     "viewQuote": "Ver presupuesto",
     "viewQuote": "Ver presupuesto",
+    "view": "View",
     "noData": "No hay respuestas de clientes."
     "noData": "No hay respuestas de clientes."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/es/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Error al asignar",
     "failedAssign": "Error al asignar",
     "endBeforeStart": "La hora de fin debe ser posterior a la hora de inicio",
     "endBeforeStart": "La hora de fin debe ser posterior a la hora de inicio",
     "failedUpdate": "Error al actualizar el horario",
     "failedUpdate": "Error al actualizar el horario",
-    "splitCreated": "El trabajo abarca varios días — continuación creada para el día siguiente"
+    "splitCreated": "El trabajo abarca varios días — continuación creada para el día siguiente",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/es/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Subiendo logo...",
     "uploadingLogo": "Subiendo logo...",
     "logoUpdated": "Logo actualizado",
     "logoUpdated": "Logo actualizado",
     "failedUploadLogo": "Error al subir el logo",
     "failedUploadLogo": "Error al subir el logo",
+    "readMore": "Read More",
     "detailsTitle": "Datos de la empresa",
     "detailsTitle": "Datos de la empresa",
     "shopName": "Nombre de la empresa / taller",
     "shopName": "Nombre de la empresa / taller",
     "shopNamePlaceholder": "Taller Mecanico Lopez",
     "shopNamePlaceholder": "Taller Mecanico Lopez",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "Asistente de IA",
     "title": "Asistente de IA",
+    "readMore": "Read More",
     "description": "Configure la IA para generar automaticamente notas de servicio y resumir el historial del vehiculo.",
     "description": "Configure la IA para generar automaticamente notas de servicio y resumir el historial del vehiculo.",
     "enableLabel": "Habilitar IA",
     "enableLabel": "Habilitar IA",
     "enableHint": "Usar funciones impulsadas por IA en toda la aplicacion",
     "enableHint": "Usar funciones impulsadas por IA en toda la aplicacion",

+ 6 - 0
messages/fr/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Devis"
     "quotes": "Devis"
   },
   },
   "selectVehicle": "S\u00e9lectionner un v\u00e9hicule pour l\u2019ordre de travail",
   "selectVehicle": "S\u00e9lectionner un v\u00e9hicule pour l\u2019ordre de travail",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} de plus",
   "more": "+ {count} de plus",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/fr/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Activité récente",
   "recentActivity": "Activité récente",
   "noRecentActivity": "Aucune activité récente",
   "noRecentActivity": "Aucune activité récente",
   "unknownUser": "Utilisateur",
   "unknownUser": "Utilisateur",
@@ -73,7 +96,9 @@
     "accepted": "Accepté",
     "accepted": "Accepté",
     "changesRequested": "Modifications demandées",
     "changesRequested": "Modifications demandées",
     "convertToWorkOrder": "Convertir en ordre de travail",
     "convertToWorkOrder": "Convertir en ordre de travail",
+    "convert": "Convert",
     "viewQuote": "Voir le devis",
     "viewQuote": "Voir le devis",
+    "view": "View",
     "noData": "Aucune réponse client."
     "noData": "Aucune réponse client."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/fr/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Échec de l'assignation",
     "failedAssign": "Échec de l'assignation",
     "endBeforeStart": "L'heure de fin doit être après l'heure de début",
     "endBeforeStart": "L'heure de fin doit être après l'heure de début",
     "failedUpdate": "Impossible de mettre à jour le planning",
     "failedUpdate": "Impossible de mettre à jour le planning",
-    "splitCreated": "Le travail s'étend sur plusieurs jours — suite créée pour le jour suivant"
+    "splitCreated": "Le travail s'étend sur plusieurs jours — suite créée pour le jour suivant",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/fr/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Telechargement du logo...",
     "uploadingLogo": "Telechargement du logo...",
     "logoUpdated": "Logo mis a jour",
     "logoUpdated": "Logo mis a jour",
     "failedUploadLogo": "Echec du telechargement du logo",
     "failedUploadLogo": "Echec du telechargement du logo",
+    "readMore": "Read More",
     "detailsTitle": "Details de l'entreprise",
     "detailsTitle": "Details de l'entreprise",
     "shopName": "Nom de l'entreprise / atelier",
     "shopName": "Nom de l'entreprise / atelier",
     "shopNamePlaceholder": "Garage Auto Martin",
     "shopNamePlaceholder": "Garage Auto Martin",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "Assistant IA",
     "title": "Assistant IA",
+    "readMore": "Read More",
     "description": "Configurez l'IA pour generer automatiquement des notes de service et resumer l'historique du vehicule.",
     "description": "Configurez l'IA pour generer automatiquement des notes de service et resumer l'historique du vehicule.",
     "enableLabel": "Activer l'IA",
     "enableLabel": "Activer l'IA",
     "enableHint": "Utiliser les fonctionnalites alimentees par l'IA dans toute l'application",
     "enableHint": "Utiliser les fonctionnalites alimentees par l'IA dans toute l'application",

+ 6 - 0
messages/it/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Preventivi"
     "quotes": "Preventivi"
   },
   },
   "selectVehicle": "Seleziona veicolo per ordine di lavoro",
   "selectVehicle": "Seleziona veicolo per ordine di lavoro",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} altri",
   "more": "+ {count} altri",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/it/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Attività recente",
   "recentActivity": "Attività recente",
   "noRecentActivity": "Nessuna attività recente",
   "noRecentActivity": "Nessuna attività recente",
   "unknownUser": "Utente",
   "unknownUser": "Utente",
@@ -73,7 +96,9 @@
     "accepted": "Accettato",
     "accepted": "Accettato",
     "changesRequested": "Modifiche richieste",
     "changesRequested": "Modifiche richieste",
     "convertToWorkOrder": "Converti in ordine di lavoro",
     "convertToWorkOrder": "Converti in ordine di lavoro",
+    "convert": "Convert",
     "viewQuote": "Visualizza preventivo",
     "viewQuote": "Visualizza preventivo",
+    "view": "View",
     "noData": "Nessuna risposta dai clienti."
     "noData": "Nessuna risposta dai clienti."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/it/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Assegnazione fallita",
     "failedAssign": "Assegnazione fallita",
     "endBeforeStart": "L'ora di fine deve essere dopo l'ora di inizio",
     "endBeforeStart": "L'ora di fine deve essere dopo l'ora di inizio",
     "failedUpdate": "Impossibile aggiornare la pianificazione",
     "failedUpdate": "Impossibile aggiornare la pianificazione",
-    "splitCreated": "Il lavoro si estende su più giorni — continuazione creata per il giorno successivo"
+    "splitCreated": "Il lavoro si estende su più giorni — continuazione creata per il giorno successivo",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/it/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Caricamento logo...",
     "uploadingLogo": "Caricamento logo...",
     "logoUpdated": "Logo aggiornato",
     "logoUpdated": "Logo aggiornato",
     "failedUploadLogo": "Caricamento logo non riuscito",
     "failedUploadLogo": "Caricamento logo non riuscito",
+    "readMore": "Read More",
     "detailsTitle": "Dettagli aziendali",
     "detailsTitle": "Dettagli aziendali",
     "shopName": "Nome azienda / officina",
     "shopName": "Nome azienda / officina",
     "shopNamePlaceholder": "Autofficina di Mario",
     "shopNamePlaceholder": "Autofficina di Mario",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "Assistente IA",
     "title": "Assistente IA",
+    "readMore": "Read More",
     "description": "Configura l'IA per generare automaticamente note di servizio e riassumere la cronologia del veicolo.",
     "description": "Configura l'IA per generare automaticamente note di servizio e riassumere la cronologia del veicolo.",
     "enableLabel": "Abilita IA",
     "enableLabel": "Abilita IA",
     "enableHint": "Usa le funzionalita basate sull'IA in tutta l'applicazione",
     "enableHint": "Usa le funzionalita basate sull'IA in tutta l'applicazione",

+ 6 - 0
messages/nb/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Tilbud"
     "quotes": "Tilbud"
   },
   },
   "selectVehicle": "Velg kj\u00f8ret\u00f8y for arbeidsordre",
   "selectVehicle": "Velg kj\u00f8ret\u00f8y for arbeidsordre",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} flere",
   "more": "+ {count} flere",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/nb/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Nylig aktivitet",
   "recentActivity": "Nylig aktivitet",
   "noRecentActivity": "Ingen nylig aktivitet",
   "noRecentActivity": "Ingen nylig aktivitet",
   "unknownUser": "Bruker",
   "unknownUser": "Bruker",
@@ -73,7 +96,9 @@
     "accepted": "Akseptert",
     "accepted": "Akseptert",
     "changesRequested": "Endringer forespurt",
     "changesRequested": "Endringer forespurt",
     "convertToWorkOrder": "Konverter til arbeidsordre",
     "convertToWorkOrder": "Konverter til arbeidsordre",
+    "convert": "Convert",
     "viewQuote": "Vis tilbud",
     "viewQuote": "Vis tilbud",
+    "view": "View",
     "noData": "Ingen kundesvar."
     "noData": "Ingen kundesvar."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/nb/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Kunne ikke tilordne",
     "failedAssign": "Kunne ikke tilordne",
     "endBeforeStart": "Sluttid må være etter starttid",
     "endBeforeStart": "Sluttid må være etter starttid",
     "failedUpdate": "Kunne ikke oppdatere tidsplan",
     "failedUpdate": "Kunne ikke oppdatere tidsplan",
-    "splitCreated": "Jobben går over flere dager — fortsettelse opprettet for neste dag"
+    "splitCreated": "Jobben går over flere dager — fortsettelse opprettet for neste dag",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/nb/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Laster opp logo...",
     "uploadingLogo": "Laster opp logo...",
     "logoUpdated": "Logo oppdatert",
     "logoUpdated": "Logo oppdatert",
     "failedUploadLogo": "Kunne ikke laste opp logo",
     "failedUploadLogo": "Kunne ikke laste opp logo",
+    "readMore": "Read More",
     "detailsTitle": "Bedriftsdetaljer",
     "detailsTitle": "Bedriftsdetaljer",
     "shopName": "Bedrift / Verkstednavn",
     "shopName": "Bedrift / Verkstednavn",
     "shopNamePlaceholder": "Hansens Bilverksted",
     "shopNamePlaceholder": "Hansens Bilverksted",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "AI-assistent",
     "title": "AI-assistent",
+    "readMore": "Read More",
     "description": "Konfigurer AI for automatisk generering av servicenotater og oppsummering av kjøretøyhistorikk.",
     "description": "Konfigurer AI for automatisk generering av servicenotater og oppsummering av kjøretøyhistorikk.",
     "enableLabel": "Aktiver AI",
     "enableLabel": "Aktiver AI",
     "enableHint": "Bruk AI-drevne funksjoner i applikasjonen",
     "enableHint": "Bruk AI-drevne funksjoner i applikasjonen",

+ 6 - 0
messages/nl/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Offertes"
     "quotes": "Offertes"
   },
   },
   "selectVehicle": "Selecteer voertuig voor werkorder",
   "selectVehicle": "Selecteer voertuig voor werkorder",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} meer",
   "more": "+ {count} meer",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/nl/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Recente activiteit",
   "recentActivity": "Recente activiteit",
   "noRecentActivity": "Geen recente activiteit",
   "noRecentActivity": "Geen recente activiteit",
   "unknownUser": "Gebruiker",
   "unknownUser": "Gebruiker",
@@ -73,7 +96,9 @@
     "accepted": "Geaccepteerd",
     "accepted": "Geaccepteerd",
     "changesRequested": "Wijzigingen aangevraagd",
     "changesRequested": "Wijzigingen aangevraagd",
     "convertToWorkOrder": "Omzetten naar werkorder",
     "convertToWorkOrder": "Omzetten naar werkorder",
+    "convert": "Convert",
     "viewQuote": "Offerte bekijken",
     "viewQuote": "Offerte bekijken",
+    "view": "View",
     "noData": "Geen klantreacties."
     "noData": "Geen klantreacties."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/nl/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Toewijzing mislukt",
     "failedAssign": "Toewijzing mislukt",
     "endBeforeStart": "Eindtijd moet na de starttijd zijn",
     "endBeforeStart": "Eindtijd moet na de starttijd zijn",
     "failedUpdate": "Planning bijwerken mislukt",
     "failedUpdate": "Planning bijwerken mislukt",
-    "splitCreated": "Taak beslaat meerdere dagen — vervolg aangemaakt voor de volgende dag"
+    "splitCreated": "Taak beslaat meerdere dagen — vervolg aangemaakt voor de volgende dag",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/nl/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Logo uploaden...",
     "uploadingLogo": "Logo uploaden...",
     "logoUpdated": "Logo bijgewerkt",
     "logoUpdated": "Logo bijgewerkt",
     "failedUploadLogo": "Logo uploaden mislukt",
     "failedUploadLogo": "Logo uploaden mislukt",
+    "readMore": "Read More",
     "detailsTitle": "Bedrijfsgegevens",
     "detailsTitle": "Bedrijfsgegevens",
     "shopName": "Bedrijfs-/werkplaatsnaam",
     "shopName": "Bedrijfs-/werkplaatsnaam",
     "shopNamePlaceholder": "Jansens Autogarage",
     "shopNamePlaceholder": "Jansens Autogarage",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "AI-assistent",
     "title": "AI-assistent",
+    "readMore": "Read More",
     "description": "Configureer AI om automatisch servicenotities te genereren en voertuiggeschiedenis samen te vatten.",
     "description": "Configureer AI om automatisch servicenotities te genereren en voertuiggeschiedenis samen te vatten.",
     "enableLabel": "AI inschakelen",
     "enableLabel": "AI inschakelen",
     "enableHint": "Gebruik AI-functies in de hele applicatie",
     "enableHint": "Gebruik AI-functies in de hele applicatie",

+ 6 - 0
messages/pl/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Wyceny"
     "quotes": "Wyceny"
   },
   },
   "selectVehicle": "Wybierz pojazd do zlecenia",
   "selectVehicle": "Wybierz pojazd do zlecenia",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} wi\u0119cej",
   "more": "+ {count} wi\u0119cej",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/pl/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Ostatnia aktywność",
   "recentActivity": "Ostatnia aktywność",
   "noRecentActivity": "Brak ostatniej aktywności",
   "noRecentActivity": "Brak ostatniej aktywności",
   "unknownUser": "Użytkownik",
   "unknownUser": "Użytkownik",
@@ -73,7 +96,9 @@
     "accepted": "Zaakceptowane",
     "accepted": "Zaakceptowane",
     "changesRequested": "Zgłoszono zmiany",
     "changesRequested": "Zgłoszono zmiany",
     "convertToWorkOrder": "Przekształć w zlecenie",
     "convertToWorkOrder": "Przekształć w zlecenie",
+    "convert": "Convert",
     "viewQuote": "Pokaż wycenę",
     "viewQuote": "Pokaż wycenę",
+    "view": "View",
     "noData": "Brak odpowiedzi klientów."
     "noData": "Brak odpowiedzi klientów."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/pl/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Przypisanie nie powiodło się",
     "failedAssign": "Przypisanie nie powiodło się",
     "endBeforeStart": "Czas zakończenia musi być po czasie rozpoczęcia",
     "endBeforeStart": "Czas zakończenia musi być po czasie rozpoczęcia",
     "failedUpdate": "Nie udało się zaktualizować harmonogramu",
     "failedUpdate": "Nie udało się zaktualizować harmonogramu",
-    "splitCreated": "Zadanie obejmuje wiele dni — kontynuacja utworzona na następny dzień"
+    "splitCreated": "Zadanie obejmuje wiele dni — kontynuacja utworzona na następny dzień",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/pl/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Przesylanie logo...",
     "uploadingLogo": "Przesylanie logo...",
     "logoUpdated": "Logo zaktualizowane",
     "logoUpdated": "Logo zaktualizowane",
     "failedUploadLogo": "Nie udalo sie przeslac logo",
     "failedUploadLogo": "Nie udalo sie przeslac logo",
+    "readMore": "Read More",
     "detailsTitle": "Dane firmy",
     "detailsTitle": "Dane firmy",
     "shopName": "Nazwa firmy / warsztatu",
     "shopName": "Nazwa firmy / warsztatu",
     "shopNamePlaceholder": "Warsztat Samochodowy Kowalski",
     "shopNamePlaceholder": "Warsztat Samochodowy Kowalski",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "Asystent AI",
     "title": "Asystent AI",
+    "readMore": "Read More",
     "description": "Skonfiguruj AI do automatycznego generowania notatek serwisowych i podsumowywania historii pojazdu.",
     "description": "Skonfiguruj AI do automatycznego generowania notatek serwisowych i podsumowywania historii pojazdu.",
     "enableLabel": "Wlacz AI",
     "enableLabel": "Wlacz AI",
     "enableHint": "Korzystaj z funkcji opartych na AI w calej aplikacji",
     "enableHint": "Korzystaj z funkcji opartych na AI w calej aplikacji",

+ 6 - 0
messages/pt-BR/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Or\u00e7amentos"
     "quotes": "Or\u00e7amentos"
   },
   },
   "selectVehicle": "Selecionar ve\u00edculo para ordem de servi\u00e7o",
   "selectVehicle": "Selecionar ve\u00edculo para ordem de servi\u00e7o",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} mais",
   "more": "+ {count} mais",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/pt-BR/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Atividade recente",
   "recentActivity": "Atividade recente",
   "noRecentActivity": "Nenhuma atividade recente",
   "noRecentActivity": "Nenhuma atividade recente",
   "unknownUser": "Usuário",
   "unknownUser": "Usuário",
@@ -73,7 +96,9 @@
     "accepted": "Aceito",
     "accepted": "Aceito",
     "changesRequested": "Alterações Solicitadas",
     "changesRequested": "Alterações Solicitadas",
     "convertToWorkOrder": "Converter em Ordem de Serviço",
     "convertToWorkOrder": "Converter em Ordem de Serviço",
+    "convert": "Convert",
     "viewQuote": "Ver Orçamento",
     "viewQuote": "Ver Orçamento",
+    "view": "View",
     "noData": "Nenhuma resposta de cliente."
     "noData": "Nenhuma resposta de cliente."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

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

@@ -296,6 +296,9 @@
     "failedAssign": "Falha ao atribuir",
     "failedAssign": "Falha ao atribuir",
     "endBeforeStart": "A hora de término deve ser após a hora de início",
     "endBeforeStart": "A hora de término deve ser após a hora de início",
     "failedUpdate": "Falha ao atualizar o agendamento",
     "failedUpdate": "Falha ao atualizar o agendamento",
-    "splitCreated": "O trabalho abrange vários dias — continuação criada para o dia seguinte"
+    "splitCreated": "O trabalho abrange vários dias — continuação criada para o dia seguinte",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

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

@@ -118,6 +118,7 @@
     "uploadingLogo": "Enviando logo...",
     "uploadingLogo": "Enviando logo...",
     "logoUpdated": "Logo atualizado",
     "logoUpdated": "Logo atualizado",
     "failedUploadLogo": "Falha ao enviar o logo",
     "failedUploadLogo": "Falha ao enviar o logo",
+    "readMore": "Read More",
     "detailsTitle": "Detalhes da empresa",
     "detailsTitle": "Detalhes da empresa",
     "shopName": "Nome da empresa / oficina",
     "shopName": "Nome da empresa / oficina",
     "shopNamePlaceholder": "Auto Mecanica Silva",
     "shopNamePlaceholder": "Auto Mecanica Silva",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "Assistente de IA",
     "title": "Assistente de IA",
+    "readMore": "Read More",
     "description": "Configure a IA para gerar automaticamente notas de servico e resumir o historico do veiculo.",
     "description": "Configure a IA para gerar automaticamente notas de servico e resumir o historico do veiculo.",
     "enableLabel": "Ativar IA",
     "enableLabel": "Ativar IA",
     "enableHint": "Usar recursos de IA em toda a aplicacao",
     "enableHint": "Usar recursos de IA em toda a aplicacao",

+ 6 - 0
messages/ru/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Предложения"
     "quotes": "Предложения"
   },
   },
   "selectVehicle": "Выберите транспортное средство для заказ-наряда",
   "selectVehicle": "Выберите транспортное средство для заказ-наряда",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ ещё {count}",
   "more": "+ ещё {count}",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/ru/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Последняя активность",
   "recentActivity": "Последняя активность",
   "noRecentActivity": "Нет недавней активности",
   "noRecentActivity": "Нет недавней активности",
   "unknownUser": "Пользователь",
   "unknownUser": "Пользователь",
@@ -73,7 +96,9 @@
     "accepted": "Принято",
     "accepted": "Принято",
     "changesRequested": "Запрошены изменения",
     "changesRequested": "Запрошены изменения",
     "convertToWorkOrder": "Преобразовать в заказ-наряд",
     "convertToWorkOrder": "Преобразовать в заказ-наряд",
+    "convert": "Convert",
     "viewQuote": "Просмотреть предложение",
     "viewQuote": "Просмотреть предложение",
+    "view": "View",
     "noData": "Нет ответов от клиентов."
     "noData": "Нет ответов от клиентов."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/ru/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Не удалось назначить",
     "failedAssign": "Не удалось назначить",
     "endBeforeStart": "Время окончания должно быть позже времени начала",
     "endBeforeStart": "Время окончания должно быть позже времени начала",
     "failedUpdate": "Не удалось обновить время в расписании",
     "failedUpdate": "Не удалось обновить время в расписании",
-    "splitCreated": "Работа занимает несколько дней — создано продолжение на следующий день"
+    "splitCreated": "Работа занимает несколько дней — создано продолжение на следующий день",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/ru/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Загрузка логотипа...",
     "uploadingLogo": "Загрузка логотипа...",
     "logoUpdated": "Логотип обновлён",
     "logoUpdated": "Логотип обновлён",
     "failedUploadLogo": "Не удалось загрузить логотип",
     "failedUploadLogo": "Не удалось загрузить логотип",
+    "readMore": "Read More",
     "detailsTitle": "Данные компании",
     "detailsTitle": "Данные компании",
     "shopName": "Название компании / мастерской",
     "shopName": "Название компании / мастерской",
     "shopNamePlaceholder": "Автосервис Ивана",
     "shopNamePlaceholder": "Автосервис Ивана",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "ИИ-ассистент",
     "title": "ИИ-ассистент",
+    "readMore": "Read More",
     "description": "Настройте ИИ для автоматического создания описаний обслуживания и обобщения истории транспортных средств.",
     "description": "Настройте ИИ для автоматического создания описаний обслуживания и обобщения истории транспортных средств.",
     "enableLabel": "Включить ИИ",
     "enableLabel": "Включить ИИ",
     "enableHint": "Используйте функции ИИ по всему приложению",
     "enableHint": "Используйте функции ИИ по всему приложению",

+ 6 - 0
messages/tr/calendar.json

@@ -36,6 +36,12 @@
     "quotes": "Teklifler"
     "quotes": "Teklifler"
   },
   },
   "selectVehicle": "\u0130\u015f emri i\u00e7in ara\u00e7 se\u00e7in",
   "selectVehicle": "\u0130\u015f emri i\u00e7in ara\u00e7 se\u00e7in",
+  "dateChoice": {
+    "title": "Work Order Date",
+    "description": "You have {selectedDate} selected. Which date should this work order be for?",
+    "useSelected": "Use {date}",
+    "useToday": "Use today"
+  },
   "more": "+ {count} daha",
   "more": "+ {count} daha",
   "events": {
   "events": {
     "status": {
     "status": {

+ 25 - 0
messages/tr/dashboard.json

@@ -1,4 +1,27 @@
 {
 {
+  "myJobs": {
+    "title": "My Active Jobs",
+    "description": "Work orders assigned to you.",
+    "uploadFailed": "Failed to upload image",
+    "uploadSuccess": "{count} image(s) uploaded",
+    "takePhoto": "Take photo",
+    "scanPart": "Scan part",
+    "partAdded": "Added \"{name}\" to work order",
+    "partAddFailed": "Failed to add part",
+    "createPart": {
+      "title": "Part Not Found",
+      "description": "No part found for barcode \"{barcode}\". Create a new one?",
+      "name": "Part Name",
+      "namePlaceholder": "e.g. Oil Filter",
+      "partNumber": "Part Number",
+      "partNumberPlaceholder": "e.g. OF-1234",
+      "cost": "Cost",
+      "sellPrice": "Sell Price",
+      "cancel": "Cancel",
+      "create": "Create & Add",
+      "failed": "Failed to create part"
+    }
+  },
   "recentActivity": "Son aktivite",
   "recentActivity": "Son aktivite",
   "noRecentActivity": "Son aktivite yok",
   "noRecentActivity": "Son aktivite yok",
   "unknownUser": "Kullanıcı",
   "unknownUser": "Kullanıcı",
@@ -73,7 +96,9 @@
     "accepted": "Kabul Edildi",
     "accepted": "Kabul Edildi",
     "changesRequested": "Değişiklik Talep Edildi",
     "changesRequested": "Değişiklik Talep Edildi",
     "convertToWorkOrder": "İş Emrine Dönüştür",
     "convertToWorkOrder": "İş Emrine Dönüştür",
+    "convert": "Convert",
     "viewQuote": "Teklifi Görüntüle",
     "viewQuote": "Teklifi Görüntüle",
+    "view": "View",
     "noData": "Müşteri yanıtı yok."
     "noData": "Müşteri yanıtı yok."
   },
   },
   "recentCompleted": {
   "recentCompleted": {

+ 4 - 1
messages/tr/service.json

@@ -296,6 +296,9 @@
     "failedAssign": "Atama başarısız",
     "failedAssign": "Atama başarısız",
     "endBeforeStart": "Bitiş saati başlangıç saatinden sonra olmalıdır",
     "endBeforeStart": "Bitiş saati başlangıç saatinden sonra olmalıdır",
     "failedUpdate": "Zamanlama güncellenemedi",
     "failedUpdate": "Zamanlama güncellenemedi",
-    "splitCreated": "İş birden fazla güne yayılıyor — sonraki gün için devam oluşturuldu"
+    "splitCreated": "İş birden fazla güne yayılıyor — sonraki gün için devam oluşturuldu",
+    "platformUsers": "Platform Users",
+    "customTechnicians": "Custom",
+    "readMore": "Read More"
   }
   }
 }
 }

+ 2 - 0
messages/tr/settings.json

@@ -118,6 +118,7 @@
     "uploadingLogo": "Logo yükleniyor...",
     "uploadingLogo": "Logo yükleniyor...",
     "logoUpdated": "Logo güncellendi",
     "logoUpdated": "Logo güncellendi",
     "failedUploadLogo": "Logo yüklenemedi",
     "failedUploadLogo": "Logo yüklenemedi",
+    "readMore": "Read More",
     "detailsTitle": "Şirket Bilgileri",
     "detailsTitle": "Şirket Bilgileri",
     "shopName": "Şirket / Atölye Adı",
     "shopName": "Şirket / Atölye Adı",
     "shopNamePlaceholder": "Ali'nin Oto Tamir",
     "shopNamePlaceholder": "Ali'nin Oto Tamir",
@@ -996,6 +997,7 @@
   },
   },
   "ai": {
   "ai": {
     "title": "Yapay Zeka Asistani",
     "title": "Yapay Zeka Asistani",
+    "readMore": "Read More",
     "description": "Servis notlarini otomatik olusturmak ve arac gecmisini ozetlemek icin yapay zekayi yapilandirin.",
     "description": "Servis notlarini otomatik olusturmak ve arac gecmisini ozetlemek icin yapay zekayi yapilandirin.",
     "enableLabel": "Yapay zekayi etkinlestir",
     "enableLabel": "Yapay zekayi etkinlestir",
     "enableHint": "Uygulama genelinde yapay zeka destekli ozellikleri kullanin",
     "enableHint": "Uygulama genelinde yapay zeka destekli ozellikleri kullanin",

+ 40 - 0
prisma/migrations/20260326082336_technician_assignment/migration.sql

@@ -0,0 +1,40 @@
+-- Step 1: Add userId column if it doesn't already exist
+ALTER TABLE "technicians" ADD COLUMN IF NOT EXISTS "userId" TEXT;
+
+-- Step 2: Backfill userId from memberId where memberId points to a valid user
+DO $$
+BEGIN
+  IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'technicians' AND column_name = 'memberId') THEN
+    UPDATE "technicians"
+    SET "userId" = "memberId"
+    WHERE "memberId" IS NOT NULL
+      AND "userId" IS NULL
+      AND "memberId" IN (SELECT "id" FROM "users");
+  END IF;
+END $$;
+
+-- Step 3: Drop memberId if it exists
+ALTER TABLE "technicians" DROP COLUMN IF EXISTS "memberId";
+
+-- Step 4: Add foreign key for userId if not already present
+DO $$
+BEGIN
+  IF NOT EXISTS (
+    SELECT 1 FROM information_schema.table_constraints
+    WHERE constraint_name = 'technicians_userId_fkey' AND table_name = 'technicians'
+  ) THEN
+    ALTER TABLE "technicians" ADD CONSTRAINT "technicians_userId_fkey"
+      FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+  END IF;
+END $$;
+
+-- Step 5: Backfill technicianId on service records where techName matches but technicianId is null
+UPDATE "service_records" sr
+SET "technicianId" = t."id"
+FROM "technicians" t
+JOIN "vehicles" v ON sr."vehicleId" = v."id"
+WHERE sr."technicianId" IS NULL
+  AND sr."techName" IS NOT NULL
+  AND t."name" = sr."techName"
+  AND t."organizationId" = v."organizationId"
+  AND t."isActive" = true;

+ 3 - 1
prisma/schema.prisma

@@ -34,6 +34,7 @@ model User {
   passkeys               Passkey[]
   passkeys               Passkey[]
   organizationMembers    OrganizationMember[]
   organizationMembers    OrganizationMember[]
   auditLogs              AuditLog[]
   auditLogs              AuditLog[]
+  technicians            Technician[]
 
 
   @@map("users")
   @@map("users")
 }
 }
@@ -972,7 +973,8 @@ model Technician {
   sortOrder     Int     @default(0)
   sortOrder     Int     @default(0)
   dailyCapacity Int     @default(480)
   dailyCapacity Int     @default(480)
 
 
-  memberId String?
+  userId   String?
+  user     User?   @relation(fields: [userId], references: [id], onDelete: SetNull)
 
 
   organizationId String
   organizationId String
   organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
   organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)

+ 6 - 4
src/app/(authenticated)/dashboard-client.tsx

@@ -943,7 +943,7 @@ export function DashboardClient({
                 {quoteResponses.map((resp) => (
                 {quoteResponses.map((resp) => (
                   <div
                   <div
                     key={resp.id}
                     key={resp.id}
-                    className="flex items-center justify-between px-5 py-3"
+                    className="flex items-center justify-between px-5 py-3 overflow-hidden"
                   >
                   >
                     <div
                     <div
                       className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
                       className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
@@ -970,7 +970,7 @@ export function DashboardClient({
                         </p>
                         </p>
                       </div>
                       </div>
                     </div>
                     </div>
-                    <div className="shrink-0 ml-3 flex items-center gap-1.5">
+                    <div className="shrink-0 ml-3 flex flex-wrap items-center gap-1.5 max-w-[50%] justify-end sm:max-w-none sm:flex-nowrap">
                       {resp.status === "accepted" ? (
                       {resp.status === "accepted" ? (
                         <Badge className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20 text-[10px]">
                         <Badge className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20 text-[10px]">
                           {t("quoteResponses.accepted")}
                           {t("quoteResponses.accepted")}
@@ -995,7 +995,8 @@ export function DashboardClient({
                           }}
                           }}
                         >
                         >
                           <ArrowRight className="mr-1 h-3 w-3" />
                           <ArrowRight className="mr-1 h-3 w-3" />
-                          {t("quoteResponses.convertToWorkOrder")}
+                          <span className="hidden sm:inline">{t("quoteResponses.convertToWorkOrder")}</span>
+                          <span className="sm:hidden">{t("quoteResponses.convert")}</span>
                         </Button>
                         </Button>
                       )}
                       )}
                       <Button
                       <Button
@@ -1004,7 +1005,8 @@ export function DashboardClient({
                         className="h-7 text-xs"
                         className="h-7 text-xs"
                         onClick={() => router.push(`/quotes/${resp.id}`)}
                         onClick={() => router.push(`/quotes/${resp.id}`)}
                       >
                       >
-                        {t("quoteResponses.viewQuote")}
+                        <span className="hidden sm:inline">{t("quoteResponses.viewQuote")}</span>
+                        <span className="sm:hidden">{t("quoteResponses.view")}</span>
                       </Button>
                       </Button>
                       <Button
                       <Button
                         variant="ghost"
                         variant="ghost"

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

@@ -11,7 +11,9 @@ import { getFeatures } from "@/lib/features";
 import { getRecentSmsThreads } from "@/features/sms/Actions/smsActions";
 import { getRecentSmsThreads } from "@/features/sms/Actions/smsActions";
 import { getNotifications } from "@/features/notifications/Actions/notificationActions";
 import { getNotifications } from "@/features/notifications/Actions/notificationActions";
 import { getRecentAuditLogs } from "@/features/audit/Actions/auditActions";
 import { getRecentAuditLogs } from "@/features/audit/Actions/auditActions";
+import { getMyActiveJobs } from "@/features/vehicles/Actions/getMyActiveJobs";
 import { DashboardClient } from "./dashboard-client";
 import { DashboardClient } from "./dashboard-client";
+import { MyActiveJobs } from "@/features/vehicles/Components/MyActiveJobs";
 import { PageHeader } from "@/components/page-header";
 import { PageHeader } from "@/components/page-header";
 
 
 export default async function DashboardPage() {
 export default async function DashboardPage() {
@@ -19,7 +21,7 @@ export default async function DashboardPage() {
   const features = auth ? await getFeatures(auth.organizationId) : null;
   const features = auth ? await getFeatures(auth.organizationId) : null;
   const smsEnabled = features?.sms ?? false;
   const smsEnabled = features?.sms ?? false;
 
 
-  const [result, settingsResult, remindersResult, maintenanceResult, dismissedMaintenanceResult, inProgressResult, completedResult, quoteRequestsResult, quoteResponsesResult, smsResult, notificationsResult, auditLogsResult] = await Promise.all([
+  const [result, settingsResult, remindersResult, maintenanceResult, dismissedMaintenanceResult, inProgressResult, completedResult, quoteRequestsResult, quoteResponsesResult, smsResult, notificationsResult, auditLogsResult, myJobsResult] = await Promise.all([
     getDashboardStats(),
     getDashboardStats(),
     getSettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.UNIT_SYSTEM]),
     getSettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.UNIT_SYSTEM]),
     getUpcomingReminders(),
     getUpcomingReminders(),
@@ -32,6 +34,7 @@ export default async function DashboardPage() {
     smsEnabled ? getRecentSmsThreads(0, 5) : Promise.resolve(null),
     smsEnabled ? getRecentSmsThreads(0, 5) : Promise.resolve(null),
     getNotifications(),
     getNotifications(),
     getRecentAuditLogs(10),
     getRecentAuditLogs(10),
+    getMyActiveJobs(),
   ]);
   ]);
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
@@ -55,10 +58,13 @@ export default async function DashboardPage() {
   const notifications = notificationsResult.success && notificationsResult.data ? notificationsResult.data.notifications : [];
   const notifications = notificationsResult.success && notificationsResult.data ? notificationsResult.data.notifications : [];
   const recentAuditLogs = auditLogsResult.success && auditLogsResult.data ? auditLogsResult.data : [];
   const recentAuditLogs = auditLogsResult.success && auditLogsResult.data ? auditLogsResult.data : [];
 
 
+  const myJobs = myJobsResult.success && myJobsResult.data ? myJobsResult.data : [];
+
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+        <MyActiveJobs jobs={myJobs} />
         <DashboardClient
         <DashboardClient
           stats={result.data}
           stats={result.data}
           currencyCode={currencyCode}
           currencyCode={currencyCode}

+ 13 - 3
src/app/(authenticated)/settings/payment/payment-settings.tsx

@@ -102,9 +102,19 @@ export function PaymentSettings({ settings, orgId }: { settings: Record<string,
       <ReadOnlyBanner />
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
       <ReadOnlyWrapper>
       <Card className="border-0 shadow-sm">
       <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Banknote className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('payment.detailsTitle')}</CardTitle>
+        <CardHeader className="flex flex-row items-center justify-between pb-4">
+          <div className="flex items-center gap-3">
+            <Banknote className="h-5 w-5 text-muted-foreground" />
+            <CardTitle className="text-lg">{t('payment.detailsTitle')}</CardTitle>
+          </div>
+          <a
+            href="https://torqvoice.com/docs/configuration/payment-providers"
+            target="_blank"
+            rel="noopener noreferrer"
+            className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
+          >
+            {t('payment.readMore')} →
+          </a>
         </CardHeader>
         </CardHeader>
         <CardContent className="space-y-6">
         <CardContent className="space-y-6">
           <p className="text-sm text-muted-foreground">
           <p className="text-sm text-muted-foreground">

+ 12 - 9
src/app/(authenticated)/settings/workshop/workshop-settings.tsx

@@ -43,8 +43,8 @@ export function WorkshopSettings({ settings, technicians: initialTechnicians = [
   const t = useTranslations('settings');
   const t = useTranslations('settings');
   const [saving, setSaving] = useState(false);
   const [saving, setSaving] = useState(false);
 
 
-  const [defaultTechnician, setDefaultTechnician] = useState(
-    settings[SETTING_KEYS.DEFAULT_TECHNICIAN] || ""
+  const [defaultTechnicianId, setDefaultTechnicianId] = useState(
+    settings[SETTING_KEYS.DEFAULT_TECHNICIAN_ID] || ""
   );
   );
   const [technicians, setTechnicians] = useState<TechnicianOption[]>(initialTechnicians);
   const [technicians, setTechnicians] = useState<TechnicianOption[]>(initialTechnicians);
   const [techOpen, setTechOpen] = useState(false);
   const [techOpen, setTechOpen] = useState(false);
@@ -68,8 +68,10 @@ export function WorkshopSettings({ settings, technicians: initialTechnicians = [
     settings[SETTING_KEYS.WORKBOARD_WORK_DAY_END] || "15:00"
     settings[SETTING_KEYS.WORKBOARD_WORK_DAY_END] || "15:00"
   );
   );
 
 
-  const handleTechSelect = (techName: string) => {
-    setDefaultTechnician(techName);
+  const selectedTechName = technicians.find((t) => t.id === defaultTechnicianId)?.name || "";
+
+  const handleTechSelect = (techId: string) => {
+    setDefaultTechnicianId(techId);
     setTechOpen(false);
     setTechOpen(false);
   };
   };
 
 
@@ -84,7 +86,7 @@ export function WorkshopSettings({ settings, technicians: initialTechnicians = [
       setTechSearch('');
       setTechSearch('');
       setNewTechName('');
       setNewTechName('');
       setShowNewInput(false);
       setShowNewInput(false);
-      handleTechSelect(newTech.name);
+      handleTechSelect(newTech.id);
     } else {
     } else {
       toast.error(t('workshop.failedCreateTech'));
       toast.error(t('workshop.failedCreateTech'));
     }
     }
@@ -96,7 +98,8 @@ export function WorkshopSettings({ settings, technicians: initialTechnicians = [
   const handleSave = async () => {
   const handleSave = async () => {
     setSaving(true);
     setSaving(true);
     await setSettings({
     await setSettings({
-      [SETTING_KEYS.DEFAULT_TECHNICIAN]: defaultTechnician,
+      [SETTING_KEYS.DEFAULT_TECHNICIAN_ID]: defaultTechnicianId,
+      [SETTING_KEYS.DEFAULT_TECHNICIAN]: selectedTechName,
       [SETTING_KEYS.DEFAULT_LABOR_RATE]: defaultLaborRate,
       [SETTING_KEYS.DEFAULT_LABOR_RATE]: defaultLaborRate,
       [SETTING_KEYS.UNIT_SYSTEM]: unitSystem,
       [SETTING_KEYS.UNIT_SYSTEM]: unitSystem,
       [SETTING_KEYS.WORKBOARD_WEEK_START_DAY]: weekStartDay,
       [SETTING_KEYS.WORKBOARD_WEEK_START_DAY]: weekStartDay,
@@ -134,7 +137,7 @@ export function WorkshopSettings({ settings, technicians: initialTechnicians = [
                     className="w-full justify-between font-normal"
                     className="w-full justify-between font-normal"
                   >
                   >
                     <span className="truncate">
                     <span className="truncate">
-                      {defaultTechnician || t('workshop.technicianPlaceholder')}
+                      {selectedTechName || t('workshop.technicianPlaceholder')}
                     </span>
                     </span>
                     <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                     <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                   </Button>
                   </Button>
@@ -153,12 +156,12 @@ export function WorkshopSettings({ settings, technicians: initialTechnicians = [
                           <CommandItem
                           <CommandItem
                             key={tech.id}
                             key={tech.id}
                             value={tech.name}
                             value={tech.name}
-                            onSelect={() => handleTechSelect(tech.name)}
+                            onSelect={() => handleTechSelect(tech.id)}
                           >
                           >
                             <Check
                             <Check
                               className={cn(
                               className={cn(
                                 'mr-2 h-4 w-4',
                                 'mr-2 h-4 w-4',
-                                defaultTechnician === tech.name ? 'opacity-100' : 'opacity-0',
+                                defaultTechnicianId === tech.id ? 'opacity-100' : 'opacity-0',
                               )}
                               )}
                             />
                             />
                             {tech.name}
                             {tech.name}

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

@@ -4,7 +4,7 @@ import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { getInventoryPartsList } from "@/features/inventory/Actions/inventoryActions";
 import { getInventoryPartsList } from "@/features/inventory/Actions/inventoryActions";
 import { getLaborPresetsList } from "@/features/labor-presets/Actions/laborPresetActions";
 import { getLaborPresetsList } from "@/features/labor-presets/Actions/laborPresetActions";
 
 
-import { getTechnicians } from "@/features/workboard/Actions/technicianActions";
+import { getTechnicians, getOrgMembers } from "@/features/workboard/Actions/technicianActions";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { db } from "@/lib/db";
 import { db } from "@/lib/db";
@@ -20,7 +20,7 @@ export default async function ServiceDetailPage({
 }) {
 }) {
   const { id, serviceId } = await params;
   const { id, serviceId } = await params;
 
 
-  const [result, settingsResult, inventoryResult, techniciansResult, presetsResult, authContext, session] =
+  const [result, settingsResult, inventoryResult, techniciansResult, presetsResult, authContext, session, orgMembersResult] =
     await Promise.all([
     await Promise.all([
       getServiceRecord(serviceId),
       getServiceRecord(serviceId),
       getSettings([
       getSettings([
@@ -35,6 +35,7 @@ export default async function ServiceDetailPage({
       getLaborPresetsList(),
       getLaborPresetsList(),
       getAuthContext(),
       getAuthContext(),
       getCachedSession(),
       getCachedSession(),
+      getOrgMembers(),
     ]);
     ]);
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
@@ -76,7 +77,7 @@ export default async function ServiceDetailPage({
   };
   };
   const boardTechnicians = (
   const boardTechnicians = (
     techniciansResult.success && techniciansResult.data ? techniciansResult.data : []
     techniciansResult.success && techniciansResult.data ? techniciansResult.data : []
-  ).map((t) => ({ id: t.id, name: t.name }));
+  ).map((t) => ({ id: t.id, name: t.name, userId: t.userId }));
   const organizationId = authContext?.organizationId || "";
   const organizationId = authContext?.organizationId || "";
 
 
   // Fetch team members and features
   // Fetch team members and features
@@ -177,6 +178,7 @@ export default async function ServiceDetailPage({
         laborPresets={laborPresets}
         laborPresets={laborPresets}
         initialVehicle={initialVehicle}
         initialVehicle={initialVehicle}
         boardTechnicians={boardTechnicians}
         boardTechnicians={boardTechnicians}
+        orgMembers={orgMembersResult.success && orgMembersResult.data ? orgMembersResult.data : []}
         currentUserName={currentUserName}
         currentUserName={currentUserName}
         imageAttachmentsForManager={imageAttachmentsForManager}
         imageAttachmentsForManager={imageAttachmentsForManager}
         videoAttachments={videoAttachments}
         videoAttachments={videoAttachments}

+ 1 - 1
src/app/api/protected/backup/import/route.ts

@@ -253,7 +253,7 @@ export async function POST(request: NextRequest) {
               isActive: t.isActive !== false,
               isActive: t.isActive !== false,
               sortOrder: (t.sortOrder as number) || 0,
               sortOrder: (t.sortOrder as number) || 0,
               dailyCapacity: (t.dailyCapacity as number) || 480,
               dailyCapacity: (t.dailyCapacity as number) || 480,
-              memberId: (t.memberId as string) || null,
+              userId: (t.userId as string) || (t.memberId as string) || null,
               createdAt: t.createdAt
               createdAt: t.createdAt
                 ? new Date(t.createdAt as string)
                 ? new Date(t.createdAt as string)
                 : undefined,
                 : undefined,

+ 14 - 4
src/features/ai/Components/AiSettingsForm.tsx

@@ -158,10 +158,20 @@ export function AiSettingsForm({
       <ReadOnlyWrapper>
       <ReadOnlyWrapper>
         <Card>
         <Card>
           <CardHeader>
           <CardHeader>
-            <CardTitle className="flex items-center gap-2">
-              <Sparkles className="h-5 w-5" />
-              {t("ai.title")}
-            </CardTitle>
+            <div className="flex items-center justify-between">
+              <CardTitle className="flex items-center gap-2">
+                <Sparkles className="h-5 w-5" />
+                {t("ai.title")}
+              </CardTitle>
+              <a
+                href="https://torqvoice.com/docs/integrations/ai"
+                target="_blank"
+                rel="noopener noreferrer"
+                className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
+              >
+                {t("ai.readMore")} →
+              </a>
+            </div>
             <CardDescription>{t("ai.description")}</CardDescription>
             <CardDescription>{t("ai.description")}</CardDescription>
           </CardHeader>
           </CardHeader>
           <CardContent className="space-y-6">
           <CardContent className="space-y-6">

+ 50 - 1
src/features/calendar/Components/CalendarClient.tsx

@@ -20,6 +20,13 @@ import { toLocalDateStr } from "./calendar-utils";
 import { getCalendarEvents } from "../Actions/calendarActions";
 import { getCalendarEvents } from "../Actions/calendarActions";
 import type { CalendarEvent } from "../Actions/calendarActions";
 import type { CalendarEvent } from "../Actions/calendarActions";
 import { VehiclePickerDialog } from "@/components/vehicle-picker-dialog";
 import { VehiclePickerDialog } from "@/components/vehicle-picker-dialog";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { CalendarDays } from "lucide-react";
 
 
 interface Vehicle {
 interface Vehicle {
   id: string;
   id: string;
@@ -104,9 +111,26 @@ export default function CalendarClient({
 
 
   // Vehicle picker
   // Vehicle picker
   const [showPicker, setShowPicker] = useState(false);
   const [showPicker, setShowPicker] = useState(false);
+  const [showDateChoice, setShowDateChoice] = useState(false);
+  const [workOrderDate, setWorkOrderDate] = useState<string | undefined>(undefined);
 
 
   const selectedDateStr = toLocalDateStr(selectedDate);
   const selectedDateStr = toLocalDateStr(selectedDate);
 
 
+  const handleNewWorkOrder = useCallback(() => {
+    if (selectedDateStr !== todayStr) {
+      setShowDateChoice(true);
+    } else {
+      setWorkOrderDate(undefined);
+      setShowPicker(true);
+    }
+  }, [selectedDateStr, todayStr]);
+
+  const handleDateChoice = useCallback((useSelectedDate: boolean) => {
+    setShowDateChoice(false);
+    setWorkOrderDate(useSelectedDate ? selectedDateStr : undefined);
+    setShowPicker(true);
+  }, [selectedDateStr]);
+
   const filteredEvents = useMemo(() => {
   const filteredEvents = useMemo(() => {
     return events.filter((e) => {
     return events.filter((e) => {
       if (e.type === "service" && !showServices) return false;
       if (e.type === "service" && !showServices) return false;
@@ -258,7 +282,7 @@ export default function CalendarClient({
                   </Button>
                   </Button>
                   <Button
                   <Button
                     size="sm"
                     size="sm"
-                    onClick={() => setShowPicker(true)}
+                    onClick={handleNewWorkOrder}
                   >
                   >
                     <Plus className="h-4 w-4 mr-1" />
                     <Plus className="h-4 w-4 mr-1" />
                     {t('workOrder')}
                     {t('workOrder')}
@@ -339,12 +363,37 @@ export default function CalendarClient({
         </div>
         </div>
       </div>
       </div>
 
 
+      <Dialog open={showDateChoice} onOpenChange={setShowDateChoice}>
+        <DialogContent className="max-w-sm">
+          <DialogHeader>
+            <DialogTitle>{t('dateChoice.title')}</DialogTitle>
+          </DialogHeader>
+          <p className="text-sm text-muted-foreground">
+            {t('dateChoice.description', {
+              selectedDate: selectedDate.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
+            })}
+          </p>
+          <div className="flex flex-col gap-2 pt-2">
+            <Button onClick={() => handleDateChoice(true)}>
+              <CalendarDays className="h-4 w-4 mr-2" />
+              {t('dateChoice.useSelected', {
+                date: selectedDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
+              })}
+            </Button>
+            <Button variant="outline" onClick={() => handleDateChoice(false)}>
+              {t('dateChoice.useToday')}
+            </Button>
+          </div>
+        </DialogContent>
+      </Dialog>
+
       <VehiclePickerDialog
       <VehiclePickerDialog
         open={showPicker}
         open={showPicker}
         onOpenChange={setShowPicker}
         onOpenChange={setShowPicker}
         vehicles={vehicles}
         vehicles={vehicles}
         customers={customers}
         customers={customers}
         title={t('selectVehicle')}
         title={t('selectVehicle')}
+        redirectQuery={workOrderDate ? { boardDate: workOrderDate } : undefined}
       />
       />
     </div>
     </div>
   );
   );

+ 1 - 1
src/features/inspections/Actions/inspectionActions.ts

@@ -141,7 +141,7 @@ export async function createInspection(input: unknown) {
 
 
     // Look up technician linked to current user
     // Look up technician linked to current user
     const technician = await db.technician.findFirst({
     const technician = await db.technician.findFirst({
-      where: { memberId: userId, organizationId, isActive: true },
+      where: { userId, organizationId, isActive: true },
       select: { id: true },
       select: { id: true },
     });
     });
 
 

+ 14 - 10
src/features/reports/Actions/reportActions.ts

@@ -181,29 +181,33 @@ export async function getTechnicianReport(params: {
       where: {
       where: {
         vehicle: { organizationId },
         vehicle: { organizationId },
         serviceDate: { gte: start, lte: end },
         serviceDate: { gte: start, lte: end },
-        techName: { not: null },
+        OR: [{ technicianId: { not: null } }, { techName: { not: null } }],
       },
       },
       select: {
       select: {
         techName: true,
         techName: true,
+        technicianId: true,
+        technician: { select: { name: true } },
         totalAmount: true,
         totalAmount: true,
         cost: true,
         cost: true,
         laborItems: { select: { hours: true } },
         laborItems: { select: { hours: true } },
       },
       },
     });
     });
 
 
-    const byTech: Record<string, { jobCount: number; totalRevenue: number; totalLaborHours: number }> = {};
+    // Group by technicianId (canonical) with techName fallback for legacy records
+    const byTech: Record<string, { techName: string; jobCount: number; totalRevenue: number; totalLaborHours: number }> = {};
 
 
     for (const r of records) {
     for (const r of records) {
-      const tech = r.techName || "Unassigned";
-      if (!byTech[tech]) byTech[tech] = { jobCount: 0, totalRevenue: 0, totalLaborHours: 0 };
-      byTech[tech].jobCount += 1;
-      byTech[tech].totalRevenue += r.totalAmount > 0 ? r.totalAmount : r.cost;
-      byTech[tech].totalLaborHours += r.laborItems.reduce((s, l) => s + l.hours, 0);
+      const key = r.technicianId || `name:${r.techName}`;
+      const displayName = r.technician?.name || r.techName || "Unassigned";
+      if (!byTech[key]) byTech[key] = { techName: displayName, jobCount: 0, totalRevenue: 0, totalLaborHours: 0 };
+      byTech[key].jobCount += 1;
+      byTech[key].totalRevenue += r.totalAmount > 0 ? r.totalAmount : r.cost;
+      byTech[key].totalLaborHours += r.laborItems.reduce((s, l) => s + l.hours, 0);
     }
     }
 
 
-    const technicians = Object.entries(byTech)
-      .map(([techName, data]) => ({
-        techName,
+    const technicians = Object.values(byTech)
+      .map((data) => ({
+        techName: data.techName,
         jobCount: data.jobCount,
         jobCount: data.jobCount,
         totalRevenue: data.totalRevenue,
         totalRevenue: data.totalRevenue,
         avgRevenue: data.jobCount > 0 ? data.totalRevenue / data.jobCount : 0,
         avgRevenue: data.jobCount > 0 ? data.totalRevenue / data.jobCount : 0,

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

@@ -20,6 +20,7 @@ export const SETTING_KEYS = {
   INVOICE_DUE_DAYS: "invoice.dueDays",
   INVOICE_DUE_DAYS: "invoice.dueDays",
   UNIT_SYSTEM: "workshop.unitSystem",
   UNIT_SYSTEM: "workshop.unitSystem",
   DEFAULT_TECHNICIAN: "workshop.defaultTechnician",
   DEFAULT_TECHNICIAN: "workshop.defaultTechnician",
+  DEFAULT_TECHNICIAN_ID: "workshop.defaultTechnicianId",
   DEFAULT_LABOR_RATE: "workshop.defaultLaborRate",
   DEFAULT_LABOR_RATE: "workshop.defaultLaborRate",
   QUOTE_PREFIX: "workshop.quotePrefix",
   QUOTE_PREFIX: "workshop.quotePrefix",
   QUOTE_VALID_DAYS: "workshop.quoteValidDays",
   QUOTE_VALID_DAYS: "workshop.quoteValidDays",

+ 1 - 1
src/features/telegram/Components/TelegramSettingsForm.tsx

@@ -71,7 +71,7 @@ export function TelegramSettingsForm({
             <CardDescription>
             <CardDescription>
               {t("description")}{" "}
               {t("description")}{" "}
               <a
               <a
-                href="https://torqvoice.com/docs/configuration#telegram"
+                href="https://torqvoice.com/docs/integrations/telegram"
                 target="_blank"
                 target="_blank"
                 rel="noopener noreferrer"
                 rel="noopener noreferrer"
                 className="inline-flex items-center gap-1 font-medium text-primary hover:underline"
                 className="inline-flex items-center gap-1 font-medium text-primary hover:underline"

+ 84 - 0
src/features/vehicles/Actions/addPartToServiceRecord.ts

@@ -0,0 +1,84 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { revalidatePath } from "next/cache";
+
+export async function addPartToServiceRecord(input: {
+  serviceRecordId: string;
+  partNumber?: string;
+  name: string;
+  quantity: number;
+  unitPrice: number;
+  total: number;
+  unitCost: number;
+  inventoryPartId?: string;
+}) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const record = await db.serviceRecord.findFirst({
+        where: { id: input.serviceRecordId, vehicle: { organizationId } },
+        select: { id: true, vehicleId: true, subtotal: true, taxRate: true, discountType: true, discountValue: true },
+      });
+      if (!record) throw new Error("Service record not found");
+
+      const part = await db.servicePart.create({
+        data: {
+          partNumber: input.partNumber || null,
+          name: input.name,
+          quantity: input.quantity,
+          unitPrice: input.unitPrice,
+          total: input.total,
+          unitCost: input.unitCost,
+          inventoryPartId: input.inventoryPartId || null,
+          serviceRecordId: record.id,
+        },
+      });
+
+      // Recalculate totals
+      const [partsAgg, laborAgg] = await Promise.all([
+        db.servicePart.aggregate({
+          where: { serviceRecordId: record.id },
+          _sum: { total: true },
+        }),
+        db.serviceLabor.aggregate({
+          where: { serviceRecordId: record.id },
+          _sum: { total: true },
+        }),
+      ]);
+
+      const subtotal = (partsAgg._sum.total || 0) + (laborAgg._sum.total || 0);
+      const discountAmount =
+        record.discountType === "percentage"
+          ? subtotal * ((record.discountValue ?? 0) / 100)
+          : record.discountType === "fixed"
+            ? Math.min(record.discountValue ?? 0, subtotal)
+            : 0;
+      const taxAmount = (subtotal - discountAmount) * (record.taxRate / 100);
+      const totalAmount = subtotal - discountAmount + taxAmount;
+
+      await db.serviceRecord.update({
+        where: { id: record.id },
+        data: { subtotal, taxAmount, totalAmount },
+      });
+
+      // Deduct inventory stock if linked
+      if (input.inventoryPartId) {
+        await db.inventoryPart.update({
+          where: { id: input.inventoryPartId },
+          data: { quantity: { decrement: input.quantity } },
+        });
+      }
+
+      revalidatePath(`/vehicles/${record.vehicleId}/service/${record.id}`);
+
+      return part;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES },
+      ],
+    }
+  );
+}

+ 24 - 9
src/features/vehicles/Actions/createDraftServiceRecord.ts

@@ -26,6 +26,7 @@ export async function createDraftServiceRecord(
                 "workshop.invoicePrefix",
                 "workshop.invoicePrefix",
                 "workshop.invoiceStartNumber",
                 "workshop.invoiceStartNumber",
                 "workshop.defaultTechnician",
                 "workshop.defaultTechnician",
+                "workshop.defaultTechnicianId",
                 "workboard.workDayStart",
                 "workboard.workDayStart",
               ],
               ],
             },
             },
@@ -46,16 +47,30 @@ export async function createDraftServiceRecord(
       const shopName = org?.name || undefined;
       const shopName = org?.name || undefined;
       let techName = currentUser?.name || undefined;
       let techName = currentUser?.name || undefined;
 
 
-      // If no technician specified, look up the default technician from settings
+      // Resolve technician: explicit param > default setting by ID > legacy default by name
       let resolvedTechId = technicianId;
       let resolvedTechId = technicianId;
-      if (!resolvedTechId && settingsMap["workshop.defaultTechnician"]) {
-        const defaultTech = await db.technician.findFirst({
-          where: { organizationId, name: settingsMap["workshop.defaultTechnician"], isActive: true },
-          select: { id: true, name: true },
-        });
-        if (defaultTech) {
-          resolvedTechId = defaultTech.id;
-          techName = defaultTech.name;
+      if (!resolvedTechId) {
+        const defaultId = settingsMap["workshop.defaultTechnicianId"];
+        if (defaultId) {
+          const defaultTech = await db.technician.findFirst({
+            where: { id: defaultId, organizationId, isActive: true },
+            select: { id: true, name: true },
+          });
+          if (defaultTech) {
+            resolvedTechId = defaultTech.id;
+            techName = defaultTech.name;
+          }
+        }
+        // Legacy fallback: look up by name
+        if (!resolvedTechId && settingsMap["workshop.defaultTechnician"]) {
+          const defaultTech = await db.technician.findFirst({
+            where: { organizationId, name: settingsMap["workshop.defaultTechnician"], isActive: true },
+            select: { id: true, name: true },
+          });
+          if (defaultTech) {
+            resolvedTechId = defaultTech.id;
+            techName = defaultTech.name;
+          }
         }
         }
       }
       }
 
 

+ 83 - 0
src/features/vehicles/Actions/getMyActiveJobs.ts

@@ -0,0 +1,83 @@
+"use server";
+
+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;
+  vehicle: {
+    make: string;
+    model: string;
+    year: number;
+    licensePlate: string | null;
+  };
+  imageCount: number;
+  partCount: number;
+}
+
+export async function getMyActiveJobs() {
+  return withAuth(
+    async ({ userId, organizationId }) => {
+      // Find technician records linked to this user
+      const myTechnicians = await db.technician.findMany({
+        where: { organizationId, userId, isActive: true },
+        select: { id: true },
+      });
+
+      if (myTechnicians.length === 0) return [];
+
+      const techIds = myTechnicians.map((t) => t.id);
+
+      const records = await db.serviceRecord.findMany({
+        where: {
+          vehicle: { organizationId },
+          status: { in: ["in-progress", "pending", "waiting-parts"] },
+          technicianId: { in: techIds },
+        },
+        select: {
+          id: true,
+          title: true,
+          status: true,
+          vehicleId: true,
+          vehicle: {
+            select: {
+              make: true,
+              model: true,
+              year: true,
+              licensePlate: true,
+            },
+          },
+          _count: {
+            select: {
+              attachments: {
+                where: { category: "image" },
+              },
+              partItems: true,
+            },
+          },
+        },
+        orderBy: { updatedAt: "desc" },
+        take: 20,
+      });
+
+      return records.map((r) => ({
+        id: r.id,
+        title: r.title,
+        status: r.status,
+        vehicleId: r.vehicleId,
+        vehicle: r.vehicle,
+        imageCount: r._count.attachments,
+        partCount: r._count.partItems,
+      }));
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.SERVICES },
+      ],
+    }
+  );
+}

+ 152 - 0
src/features/vehicles/Components/CreatePartDialog.tsx

@@ -0,0 +1,152 @@
+"use client";
+
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { Loader2 } from "lucide-react";
+import { createInventoryPart } from "@/features/inventory/Actions/inventoryActions";
+import { toast } from "sonner";
+
+interface CreatePartDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  barcode: string;
+  onCreated: (part: {
+    id: string;
+    name: string;
+    partNumber: string | null;
+    sellPrice: number;
+    unitCost: number;
+  }) => void;
+}
+
+export function CreatePartDialog({
+  open,
+  onOpenChange,
+  barcode,
+  onCreated,
+}: CreatePartDialogProps) {
+  const t = useTranslations("dashboard.myJobs.createPart");
+  const [name, setName] = useState("");
+  const [partNumber, setPartNumber] = useState("");
+  const [sellPrice, setSellPrice] = useState("");
+  const [unitCost, setUnitCost] = useState("");
+  const [loading, setLoading] = useState(false);
+
+  const handleOpenChange = (isOpen: boolean) => {
+    if (isOpen) {
+      setName("");
+      setPartNumber("");
+      setSellPrice("");
+      setUnitCost("");
+    }
+    onOpenChange(isOpen);
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!name.trim()) return;
+    setLoading(true);
+
+    const result = await createInventoryPart({
+      name: name.trim(),
+      partNumber: partNumber.trim() || undefined,
+      barcode: barcode || undefined,
+      sellPrice: Number(sellPrice) || 0,
+      unitCost: Number(unitCost) || 0,
+      quantity: 1,
+    });
+
+    setLoading(false);
+
+    if (result.success && result.data) {
+      onCreated({
+        id: result.data.id,
+        name: result.data.name,
+        partNumber: result.data.partNumber ?? null,
+        sellPrice: result.data.sellPrice,
+        unitCost: result.data.unitCost,
+      });
+    } else {
+      toast.error(result.error || t("failed"));
+    }
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={handleOpenChange}>
+      <DialogContent className="max-w-sm">
+        <DialogHeader>
+          <DialogTitle>{t("title")}</DialogTitle>
+        </DialogHeader>
+        <p className="text-sm text-muted-foreground">
+          {t("description", { barcode })}
+        </p>
+        <form onSubmit={handleSubmit} className="space-y-3">
+          <div className="space-y-1.5">
+            <Label htmlFor="part-name">{t("name")}</Label>
+            <Input
+              id="part-name"
+              placeholder={t("namePlaceholder")}
+              value={name}
+              onChange={(e) => setName(e.target.value)}
+              autoFocus
+              required
+            />
+          </div>
+          <div className="space-y-1.5">
+            <Label htmlFor="part-number">{t("partNumber")}</Label>
+            <Input
+              id="part-number"
+              placeholder={t("partNumberPlaceholder")}
+              value={partNumber}
+              onChange={(e) => setPartNumber(e.target.value)}
+            />
+          </div>
+          <div className="grid grid-cols-2 gap-3">
+            <div className="space-y-1.5">
+              <Label htmlFor="part-cost">{t("cost")}</Label>
+              <Input
+                id="part-cost"
+                type="number"
+                min="0"
+                step="0.01"
+                placeholder="0.00"
+                value={unitCost}
+                onChange={(e) => setUnitCost(e.target.value)}
+              />
+            </div>
+            <div className="space-y-1.5">
+              <Label htmlFor="part-price">{t("sellPrice")}</Label>
+              <Input
+                id="part-price"
+                type="number"
+                min="0"
+                step="0.01"
+                placeholder="0.00"
+                value={sellPrice}
+                onChange={(e) => setSellPrice(e.target.value)}
+              />
+            </div>
+          </div>
+          <div className="flex justify-end gap-2 pt-2">
+            <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
+              {t("cancel")}
+            </Button>
+            <Button type="submit" disabled={loading || !name.trim()}>
+              {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              {t("create")}
+            </Button>
+          </div>
+        </form>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 331 - 0
src/features/vehicles/Components/MyActiveJobs.tsx

@@ -0,0 +1,331 @@
+"use client";
+
+import { useRef, useState, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { Camera, ImageIcon, Loader2, Wrench, Clock, Pause, ScanBarcode, Package } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { toast } from "sonner";
+import { compressImage } from "@/lib/compress-image";
+import { addServiceAttachment } from "@/features/vehicles/Actions/addServiceAttachment";
+import { addPartToServiceRecord } from "@/features/vehicles/Actions/addPartToServiceRecord";
+import { lookupPartByBarcode } from "@/features/inventory/Actions/lookupPartByBarcode";
+import { BarcodeScannerDialog } from "@/components/barcode-scanner-dialog";
+import { CreatePartDialog } from "./CreatePartDialog";
+import type { MyActiveJob } from "@/features/vehicles/Actions/getMyActiveJobs";
+
+interface MyActiveJobsProps {
+  jobs: MyActiveJob[];
+}
+
+const STATUS_ICON: Record<string, typeof Wrench> = {
+  "in-progress": Wrench,
+  pending: Clock,
+  "waiting-parts": Pause,
+};
+
+const STATUS_COLOR: Record<string, string> = {
+  "in-progress": "bg-blue-500/10 text-blue-600",
+  pending: "bg-amber-500/10 text-amber-600",
+  "waiting-parts": "bg-orange-500/10 text-orange-600",
+};
+
+export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
+  const t = useTranslations("dashboard.myJobs");
+  const router = useRouter();
+  const [uploading, setUploading] = useState<string | null>(null);
+  const [imageCounts, setImageCounts] = useState<Record<string, number>>(
+    () => Object.fromEntries(jobs.map((j) => [j.id, j.imageCount]))
+  );
+  const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
+
+  // Barcode scanner state
+  const [scannerOpen, setScannerOpen] = useState(false);
+  const [scanJobId, setScanJobId] = useState<string | null>(null);
+  const [createPartOpen, setCreatePartOpen] = useState(false);
+  const [pendingBarcode, setPendingBarcode] = useState("");
+  const [partCounts, setPartCounts] = useState<Record<string, number>>(
+    () => Object.fromEntries(jobs.map((j) => [j.id, j.partCount]))
+  );
+
+  if (jobs.length === 0) return null;
+
+  const handleCameraClick = (jobId: string) => {
+    fileInputRefs.current[jobId]?.click();
+  };
+
+  const handleScanClick = (jobId: string) => {
+    setScanJobId(jobId);
+    setScannerOpen(true);
+  };
+
+  const handleBarcodeScan = useCallback(async (barcode: string) => {
+    if (!scanJobId) return;
+    setScannerOpen(false);
+
+    const result = await lookupPartByBarcode(barcode);
+    if (result.success && result.data) {
+      const part = result.data;
+      const price = part.sellPrice > 0 ? part.sellPrice : part.unitCost;
+      const addResult = await addPartToServiceRecord({
+        serviceRecordId: scanJobId,
+        partNumber: part.partNumber || undefined,
+        name: part.name,
+        quantity: 1,
+        unitPrice: price,
+        total: price,
+        unitCost: part.unitCost,
+        inventoryPartId: part.id,
+      });
+      if (addResult.success) {
+        toast.success(t("partAdded", { name: part.name }));
+        setPartCounts((prev) => ({ ...prev, [scanJobId]: (prev[scanJobId] || 0) + 1 }));
+        router.refresh();
+      } else {
+        toast.error(addResult.error || t("partAddFailed"));
+      }
+    } else {
+      // Part not found — offer to create it
+      setPendingBarcode(barcode);
+      setCreatePartOpen(true);
+    }
+  }, [scanJobId, t, router]);
+
+  const handlePartCreated = useCallback(async (part: { id: string; name: string; partNumber: string | null; sellPrice: number; unitCost: number }) => {
+    if (!scanJobId) return;
+    setCreatePartOpen(false);
+
+    const price = part.sellPrice > 0 ? part.sellPrice : part.unitCost;
+    const addResult = await addPartToServiceRecord({
+      serviceRecordId: scanJobId,
+      partNumber: part.partNumber || undefined,
+      name: part.name,
+      quantity: 1,
+      unitPrice: price,
+      total: price,
+      unitCost: part.unitCost,
+      inventoryPartId: part.id,
+    });
+    if (addResult.success) {
+      toast.success(t("partAdded", { name: part.name }));
+      setPartCounts((prev) => ({ ...prev, [scanJobId]: (prev[scanJobId] || 0) + 1 }));
+      router.refresh();
+    } else {
+      toast.error(addResult.error || t("partAddFailed"));
+    }
+  }, [scanJobId, t, router]);
+
+  const handleFileSelect = async (jobId: string, files: FileList | null) => {
+    if (!files || files.length === 0) return;
+    setUploading(jobId);
+
+    let uploaded = 0;
+    for (let i = 0; i < files.length; i++) {
+      let file = files[i];
+      if (!file.type.startsWith("image/")) continue;
+
+      try {
+        file = await compressImage(file);
+        const formData = new FormData();
+        formData.append("file", file);
+
+        const res = await fetch("/api/protected/upload/service-files", {
+          method: "POST",
+          body: formData,
+        });
+
+        if (!res.ok) {
+          const err = await res.json();
+          toast.error(err.error || t("uploadFailed"));
+          continue;
+        }
+
+        const data = await res.json();
+        await addServiceAttachment({
+          serviceRecordId: jobId,
+          attachment: {
+            fileName: data.fileName,
+            fileUrl: data.url,
+            fileType: data.fileType,
+            fileSize: data.fileSize,
+            category: "image",
+            includeInInvoice: true,
+          },
+        });
+        uploaded++;
+      } catch {
+        toast.error(t("uploadFailed"));
+      }
+    }
+
+    if (uploaded > 0) {
+      toast.success(t("uploadSuccess", { count: uploaded }));
+      setImageCounts((prev) => ({
+        ...prev,
+        [jobId]: (prev[jobId] || 0) + uploaded,
+      }));
+      router.refresh();
+    }
+
+    setUploading(null);
+    const input = fileInputRefs.current[jobId];
+    if (input) input.value = "";
+  };
+
+  return (
+    <>
+      <Card className="border-0 shadow-sm">
+        <CardHeader className="pb-3">
+          <CardTitle className="flex items-center gap-2 text-base">
+            <Wrench className="h-4 w-4" />
+            {t("title")}
+          </CardTitle>
+          <p className="text-xs text-muted-foreground">{t("description")}</p>
+        </CardHeader>
+        <CardContent className="p-0">
+          <div className="divide-y">
+            {jobs.map((job) => {
+              const StatusIcon = STATUS_ICON[job.status] || Wrench;
+              const statusColor = STATUS_COLOR[job.status] || "bg-muted text-muted-foreground";
+              const isUploading = uploading === job.id;
+              const imgCount = imageCounts[job.id] || 0;
+              const prtCount = partCounts[job.id] || 0;
+
+              return (
+                <div
+                  key={job.id}
+                  className="px-5 py-3"
+                >
+                  <div className="flex items-center justify-between">
+                    <div
+                      className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
+                      onClick={() => router.push(`/vehicles/${job.vehicleId}/service/${job.id}`)}
+                    >
+                      <div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${statusColor}`}>
+                        <StatusIcon className="h-4 w-4" />
+                      </div>
+                      <div className="min-w-0">
+                        <p className="font-medium text-sm truncate">
+                          {job.title}
+                        </p>
+                        <p className="text-xs text-muted-foreground truncate">
+                          {job.vehicle.year} {job.vehicle.make} {job.vehicle.model}
+                          {job.vehicle.licensePlate && ` · ${job.vehicle.licensePlate}`}
+                        </p>
+                      </div>
+                    </div>
+                    {/* Desktop: inline buttons */}
+                    <div className="shrink-0 ml-3 hidden sm:flex items-center gap-2">
+                      <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
+                        {imgCount > 0 && (
+                          <span className="flex items-center gap-0.5">
+                            <ImageIcon className="h-3 w-3" />
+                            {imgCount}
+                          </span>
+                        )}
+                        {prtCount > 0 && (
+                          <span className="flex items-center gap-0.5">
+                            <Package className="h-3 w-3" />
+                            {prtCount}
+                          </span>
+                        )}
+                      </div>
+                      <Button
+                        variant="outline"
+                        size="icon"
+                        className="h-9 w-9"
+                        disabled={isUploading}
+                        onClick={() => handleCameraClick(job.id)}
+                        title={t("takePhoto")}
+                      >
+                        {isUploading ? (
+                          <Loader2 className="h-4 w-4 animate-spin" />
+                        ) : (
+                          <Camera className="h-4 w-4" />
+                        )}
+                      </Button>
+                      <Button
+                        variant="outline"
+                        size="icon"
+                        className="h-9 w-9"
+                        onClick={() => handleScanClick(job.id)}
+                        title={t("scanPart")}
+                      >
+                        <ScanBarcode className="h-4 w-4" />
+                      </Button>
+                    </div>
+                    {/* Mobile: counters only */}
+                    <div className="shrink-0 ml-3 flex sm:hidden items-center gap-1.5 text-xs text-muted-foreground">
+                      {imgCount > 0 && (
+                        <span className="flex items-center gap-0.5">
+                          <ImageIcon className="h-3 w-3" />
+                          {imgCount}
+                        </span>
+                      )}
+                      {prtCount > 0 && (
+                        <span className="flex items-center gap-0.5">
+                          <Package className="h-3 w-3" />
+                          {prtCount}
+                        </span>
+                      )}
+                    </div>
+                  </div>
+                  {/* Mobile: action buttons on second line */}
+                  <div className="flex gap-2 mt-2 sm:hidden">
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      className="flex-1 h-9"
+                      disabled={isUploading}
+                      onClick={() => handleCameraClick(job.id)}
+                    >
+                      {isUploading ? (
+                        <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
+                      ) : (
+                        <Camera className="mr-1.5 h-4 w-4" />
+                      )}
+                      {t("takePhoto")}
+                    </Button>
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      className="flex-1 h-9"
+                      onClick={() => handleScanClick(job.id)}
+                    >
+                      <ScanBarcode className="mr-1.5 h-4 w-4" />
+                      {t("scanPart")}
+                    </Button>
+                  </div>
+                  <input
+                    ref={(el) => { fileInputRefs.current[job.id] = el; }}
+                    type="file"
+                    accept="image/*"
+                    capture="environment"
+                    multiple
+                    className="hidden"
+                    onChange={(e) => handleFileSelect(job.id, e.target.files)}
+                  />
+                </div>
+              );
+            })}
+          </div>
+        </CardContent>
+      </Card>
+
+      <BarcodeScannerDialog
+        open={scannerOpen}
+        onOpenChange={setScannerOpen}
+        onScan={handleBarcodeScan}
+        title={t("scanPart")}
+      />
+
+      <CreatePartDialog
+        open={createPartOpen}
+        onOpenChange={setCreatePartOpen}
+        barcode={pendingBarcode}
+        onCreated={handlePartCreated}
+      />
+    </>
+  );
+}

+ 103 - 20
src/features/vehicles/Components/service-edit/ScheduleTimesSection.tsx

@@ -30,11 +30,19 @@ const HOUR_PRESETS = [1, 2, 4, 5, 7]
 interface Technician {
 interface Technician {
   id: string
   id: string
   name: string
   name: string
+  userId?: string | null
+}
+
+interface OrgMember {
+  id: string
+  name: string | null
+  email: string
 }
 }
 
 
 interface ScheduleTimesSectionProps {
 interface ScheduleTimesSectionProps {
   serviceRecordId: string
   serviceRecordId: string
   technicians?: Technician[]
   technicians?: Technician[]
+  orgMembers?: OrgMember[]
   initialStartDateTime?: string | null
   initialStartDateTime?: string | null
   initialEndDateTime?: string | null
   initialEndDateTime?: string | null
   initialTechnicianId?: string | null
   initialTechnicianId?: string | null
@@ -44,6 +52,7 @@ interface ScheduleTimesSectionProps {
 export function ScheduleTimesSection({
 export function ScheduleTimesSection({
   serviceRecordId,
   serviceRecordId,
   technicians: initialTechnicians = [],
   technicians: initialTechnicians = [],
+  orgMembers = [],
   initialStartDateTime,
   initialStartDateTime,
   initialEndDateTime,
   initialEndDateTime,
   initialTechnicianId,
   initialTechnicianId,
@@ -107,6 +116,25 @@ export function ScheduleTimesSection({
     }
     }
   }
   }
 
 
+  const handleMemberSelect = async (member: OrgMember) => {
+    // Check if a technician already exists for this user
+    const existing = technicians.find((t) => t.userId === member.id)
+    if (existing) {
+      handleTechSelect(existing.id)
+      return
+    }
+    setCreating(true)
+    const res = await createTechnician({ name: member.name!, userId: member.id })
+    setCreating(false)
+    if (res.success && res.data) {
+      const newTech = { id: res.data.id, name: res.data.name, userId: member.id }
+      setTechnicians((prev) => [...prev, newTech])
+      handleTechSelect(newTech.id)
+    } else {
+      toast.error(t('failedCreate'))
+    }
+  }
+
   const doCreateTechnician = async (name: string) => {
   const doCreateTechnician = async (name: string) => {
     if (!name.trim()) return
     if (!name.trim()) return
     setCreating(true)
     setCreating(true)
@@ -126,6 +154,15 @@ export function ScheduleTimesSection({
 
 
   const selectedTechName = technicians.find((t) => t.id === selectedTechId)?.name
   const selectedTechName = technicians.find((t) => t.id === selectedTechId)?.name
 
 
+  // Platform users that already have a linked technician
+  const linkedUserIds = new Set(technicians.filter((t) => t.userId).map((t) => t.userId!))
+  // Platform users without a technician record yet
+  const unlinkedMembers = orgMembers.filter((m) => m.name && !linkedUserIds.has(m.id))
+  // Technicians linked to platform users
+  const linkedTechnicians = technicians.filter((t) => t.userId)
+  // Custom technicians (not linked to any platform user)
+  const customTechnicians = technicians.filter((t) => !t.userId)
+
   const searchLower = techSearch.toLowerCase()
   const searchLower = techSearch.toLowerCase()
   const exactMatch = technicians.some((t) => t.name.toLowerCase() === searchLower)
   const exactMatch = technicians.some((t) => t.name.toLowerCase() === searchLower)
 
 
@@ -135,9 +172,19 @@ export function ScheduleTimesSection({
 
 
   return (
   return (
     <div className="rounded-lg border p-3 space-y-3">
     <div className="rounded-lg border p-3 space-y-3">
-      <div className="flex items-center gap-2">
-        <Clock className="h-4 w-4 text-muted-foreground" />
-        <h3 className="text-sm font-semibold">{t('title')}</h3>
+      <div className="flex items-center justify-between">
+        <div className="flex items-center gap-2">
+          <Clock className="h-4 w-4 text-muted-foreground" />
+          <h3 className="text-sm font-semibold">{t('title')}</h3>
+        </div>
+        <a
+          href="https://torqvoice.com/docs/configuration/work-orders/technician-assignment"
+          target="_blank"
+          rel="noopener noreferrer"
+          className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
+        >
+          {t('readMore')} →
+        </a>
       </div>
       </div>
 
 
       <div className="space-y-1">
       <div className="space-y-1">
@@ -165,23 +212,58 @@ export function ScheduleTimesSection({
               />
               />
               <CommandList className="max-h-60 overflow-y-auto">
               <CommandList className="max-h-60 overflow-y-auto">
                 <CommandEmpty className="p-0" />
                 <CommandEmpty className="p-0" />
-                <CommandGroup>
-                  {technicians.map((tech) => (
-                    <CommandItem
-                      key={tech.id}
-                      value={tech.name}
-                      onSelect={() => handleTechSelect(tech.id)}
-                    >
-                      <Check
-                        className={cn(
-                          'mr-2 h-4 w-4',
-                          selectedTechId === tech.id ? 'opacity-100' : 'opacity-0',
-                        )}
-                      />
-                      {tech.name}
-                    </CommandItem>
-                  ))}
-                </CommandGroup>
+                {/* Platform users (linked technicians + unlinked org members) */}
+                {(linkedTechnicians.length > 0 || unlinkedMembers.length > 0) && (
+                  <CommandGroup heading={t('platformUsers')}>
+                    {linkedTechnicians.map((tech) => (
+                      <CommandItem
+                        key={tech.id}
+                        value={tech.name}
+                        onSelect={() => handleTechSelect(tech.id)}
+                      >
+                        <Check
+                          className={cn(
+                            'mr-2 h-4 w-4',
+                            selectedTechId === tech.id ? 'opacity-100' : 'opacity-0',
+                          )}
+                        />
+                        {tech.name}
+                      </CommandItem>
+                    ))}
+                    {unlinkedMembers.map((member) => (
+                      <CommandItem
+                        key={`member-${member.id}`}
+                        value={member.name!}
+                        onSelect={() => handleMemberSelect(member)}
+                        disabled={creating}
+                      >
+                        <Check className="mr-2 h-4 w-4 opacity-0" />
+                        {member.name}
+                        <span className="ml-auto text-[10px] text-muted-foreground">{member.email}</span>
+                      </CommandItem>
+                    ))}
+                  </CommandGroup>
+                )}
+                {/* Custom technicians (not linked to platform users) */}
+                {customTechnicians.length > 0 && (
+                  <CommandGroup heading={t('customTechnicians')}>
+                    {customTechnicians.map((tech) => (
+                      <CommandItem
+                        key={tech.id}
+                        value={tech.name}
+                        onSelect={() => handleTechSelect(tech.id)}
+                      >
+                        <Check
+                          className={cn(
+                            'mr-2 h-4 w-4',
+                            selectedTechId === tech.id ? 'opacity-100' : 'opacity-0',
+                          )}
+                        />
+                        {tech.name}
+                      </CommandItem>
+                    ))}
+                  </CommandGroup>
+                )}
                 {techSearch.trim() && !exactMatch && (
                 {techSearch.trim() && !exactMatch && (
                   <CommandGroup>
                   <CommandGroup>
                     <CommandItem
                     <CommandItem
@@ -304,6 +386,7 @@ export function ScheduleTimesSection({
           ))}
           ))}
         </div>
         </div>
       </div>
       </div>
+
     </div>
     </div>
   )
   )
 }
 }

+ 4 - 1
src/features/vehicles/Components/service-page/DetailsRightColumn.tsx

@@ -9,7 +9,7 @@ import { revokePublicLink } from '@/features/vehicles/Actions/serviceActions'
 import type { useServiceFormState } from './useServiceFormState'
 import type { useServiceFormState } from './useServiceFormState'
 import type { useServiceActions } from './useServiceActions'
 import type { useServiceActions } from './useServiceActions'
 import type { ServiceDetail } from '../service-detail/types'
 import type { ServiceDetail } from '../service-detail/types'
-import type { BoardTechnicianOption } from './service-page-types'
+import type { BoardTechnicianOption, OrgMemberOption } from './service-page-types'
 
 
 interface DetailsRightColumnProps {
 interface DetailsRightColumnProps {
   formState: ReturnType<typeof useServiceFormState>
   formState: ReturnType<typeof useServiceFormState>
@@ -21,6 +21,7 @@ interface DetailsRightColumnProps {
   taxEnabled: boolean
   taxEnabled: boolean
   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[]
   boardTechnicians: BoardTechnicianOption[]
+  orgMembers?: OrgMemberOption[]
 }
 }
 
 
 export function DetailsRightColumn({
 export function DetailsRightColumn({
@@ -33,6 +34,7 @@ export function DetailsRightColumn({
   taxEnabled,
   taxEnabled,
   initialVehicle,
   initialVehicle,
   boardTechnicians,
   boardTechnicians,
+  orgMembers,
 }: DetailsRightColumnProps) {
 }: DetailsRightColumnProps) {
   const router = useRouter()
   const router = useRouter()
 
 
@@ -69,6 +71,7 @@ export function DetailsRightColumn({
       <ScheduleTimesSection
       <ScheduleTimesSection
         serviceRecordId={record.id}
         serviceRecordId={record.id}
         technicians={boardTechnicians}
         technicians={boardTechnicians}
+        orgMembers={orgMembers}
         initialStartDateTime={formState.initialData.startDateTime}
         initialStartDateTime={formState.initialData.startDateTime}
         initialEndDateTime={formState.initialData.endDateTime}
         initialEndDateTime={formState.initialData.endDateTime}
         initialTechnicianId={record.technicianId}
         initialTechnicianId={record.technicianId}

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

@@ -42,6 +42,7 @@ export function ServicePageClient({
   inventoryParts,
   inventoryParts,
   initialVehicle,
   initialVehicle,
   boardTechnicians = [],
   boardTechnicians = [],
+  orgMembers = [],
   currentUserName,
   currentUserName,
   imageAttachmentsForManager,
   imageAttachmentsForManager,
   videoAttachments,
   videoAttachments,
@@ -158,6 +159,7 @@ export function ServicePageClient({
                 taxEnabled={taxEnabled}
                 taxEnabled={taxEnabled}
                 initialVehicle={initialVehicle}
                 initialVehicle={initialVehicle}
                 boardTechnicians={boardTechnicians}
                 boardTechnicians={boardTechnicians}
+                orgMembers={orgMembers}
               />
               />
             }
             }
           />
           />

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

@@ -6,6 +6,13 @@ import type { LaborPresetOption } from '@/features/labor-presets/Components/Labo
 export interface BoardTechnicianOption {
 export interface BoardTechnicianOption {
   id: string
   id: string
   name: string
   name: string
+  userId?: string | null
+}
+
+export interface OrgMemberOption {
+  id: string
+  name: string | null
+  email: string
 }
 }
 
 
 export interface Attachment {
 export interface Attachment {
@@ -33,6 +40,7 @@ export interface ServicePageClientProps {
   inventoryParts: InventoryPartOption[]
   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[]
   boardTechnicians?: BoardTechnicianOption[]
+  orgMembers?: OrgMemberOption[]
   currentUserName: string
   currentUserName: string
   imageAttachmentsForManager: Attachment[]
   imageAttachmentsForManager: Attachment[]
   videoAttachments: Attachment[]
   videoAttachments: Attachment[]

+ 39 - 1
src/features/workboard/Actions/technicianActions.ts

@@ -16,6 +16,18 @@ export async function getTechnicians() {
       const technicians = await db.technician.findMany({
       const technicians = await db.technician.findMany({
         where: { organizationId, isActive: true },
         where: { organizationId, isActive: true },
         orderBy: { sortOrder: "asc" },
         orderBy: { sortOrder: "asc" },
+        select: {
+          id: true,
+          name: true,
+          color: true,
+          isActive: true,
+          sortOrder: true,
+          dailyCapacity: true,
+          userId: true,
+          organizationId: true,
+          createdAt: true,
+          updatedAt: true,
+        },
       });
       });
       return technicians;
       return technicians;
     },
     },
@@ -32,6 +44,14 @@ export async function createTechnician(input: unknown) {
     async ({ organizationId }) => {
     async ({ organizationId }) => {
       const data = createTechnicianSchema.parse(input);
       const data = createTechnicianSchema.parse(input);
 
 
+      // If userId provided, return existing technician instead of creating duplicate
+      if (data.userId) {
+        const existing = await db.technician.findFirst({
+          where: { userId: data.userId, organizationId },
+        });
+        if (existing) return existing;
+      }
+
       const maxOrder = await db.technician.aggregate({
       const maxOrder = await db.technician.aggregate({
         where: { organizationId },
         where: { organizationId },
         _max: { sortOrder: true },
         _max: { sortOrder: true },
@@ -41,7 +61,7 @@ export async function createTechnician(input: unknown) {
         data: {
         data: {
           name: data.name,
           name: data.name,
           color: data.color,
           color: data.color,
-          memberId: data.memberId || null,
+          userId: data.userId || null,
           sortOrder: (maxOrder._max.sortOrder ?? -1) + 1,
           sortOrder: (maxOrder._max.sortOrder ?? -1) + 1,
           organizationId,
           organizationId,
         },
         },
@@ -77,11 +97,29 @@ export async function updateTechnician(input: unknown) {
       const data = updateTechnicianSchema.parse(input);
       const data = updateTechnicianSchema.parse(input);
       const { id, ...updates } = data;
       const { id, ...updates } = data;
 
 
+      // Prevent linking a user that's already linked to another technician
+      if (updates.userId) {
+        const existing = await db.technician.findFirst({
+          where: { userId: updates.userId, organizationId, id: { not: id } },
+        });
+        if (existing) {
+          throw new Error(`This user is already linked to technician "${existing.name}"`);
+        }
+      }
+
       const technician = await db.technician.updateMany({
       const technician = await db.technician.updateMany({
         where: { id, organizationId },
         where: { id, organizationId },
         data: updates,
         data: updates,
       });
       });
 
 
+      // Sync denormalized techName on related service records when name changes
+      if (updates.name) {
+        await db.serviceRecord.updateMany({
+          where: { technicianId: id },
+          data: { techName: updates.name },
+        });
+      }
+
       notificationBus.emit("workboard", {
       notificationBus.emit("workboard", {
         type: "technician_updated",
         type: "technician_updated",
         organizationId,
         organizationId,

+ 7 - 7
src/features/workboard/Components/TechnicianDialog.tsx

@@ -49,7 +49,7 @@ export function TechnicianDialog({
 }) {
 }) {
   const [name, setName] = useState("");
   const [name, setName] = useState("");
   const [color, setColor] = useState(PRESET_COLORS[0]);
   const [color, setColor] = useState(PRESET_COLORS[0]);
-  const [memberId, setMemberId] = useState<string>("");
+  const [userId, setUserId] = useState<string>("");
   const [members, setMembers] = useState<OrgMember[]>([]);
   const [members, setMembers] = useState<OrgMember[]>([]);
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   const [deleting, setDeleting] = useState(false);
   const [deleting, setDeleting] = useState(false);
@@ -63,11 +63,11 @@ export function TechnicianDialog({
       if (technician) {
       if (technician) {
         setName(technician.name);
         setName(technician.name);
         setColor(technician.color);
         setColor(technician.color);
-        setMemberId(technician.memberId || "");
+        setUserId(technician.userId || "");
       } else {
       } else {
         setName("");
         setName("");
         setColor(PRESET_COLORS[Math.floor(Math.random() * PRESET_COLORS.length)]);
         setColor(PRESET_COLORS[Math.floor(Math.random() * PRESET_COLORS.length)]);
-        setMemberId("");
+        setUserId("");
       }
       }
       // Fetch org members
       // Fetch org members
       getOrgMembers().then((res) => {
       getOrgMembers().then((res) => {
@@ -86,7 +86,7 @@ export function TechnicianDialog({
         id: technician.id,
         id: technician.id,
         name: name.trim(),
         name: name.trim(),
         color,
         color,
-        memberId: memberId && memberId !== "none" ? memberId : null,
+        userId: userId && userId !== "none" ? userId : null,
       });
       });
       if (res.success) {
       if (res.success) {
         // Update store
         // Update store
@@ -94,7 +94,7 @@ export function TechnicianDialog({
         store.setTechnicians(
         store.setTechnicians(
           store.technicians.map((t) =>
           store.technicians.map((t) =>
             t.id === technician.id
             t.id === technician.id
-              ? { ...t, name: name.trim(), color, memberId: memberId || null }
+              ? { ...t, name: name.trim(), color, userId: userId || null }
               : t,
               : t,
           ),
           ),
         );
         );
@@ -104,7 +104,7 @@ export function TechnicianDialog({
       const res = await createTechnician({
       const res = await createTechnician({
         name: name.trim(),
         name: name.trim(),
         color,
         color,
-        memberId: memberId && memberId !== "none" ? memberId : undefined,
+        userId: userId && userId !== "none" ? userId : undefined,
       });
       });
       if (res.success && res.data) {
       if (res.success && res.data) {
         addTechnician(res.data as Technician);
         addTechnician(res.data as Technician);
@@ -155,7 +155,7 @@ export function TechnicianDialog({
 
 
           <div className="space-y-2">
           <div className="space-y-2">
             <Label htmlFor="tech-member">{t("linkMember")}</Label>
             <Label htmlFor="tech-member">{t("linkMember")}</Label>
-            <Select value={memberId} onValueChange={setMemberId}>
+            <Select value={userId} onValueChange={setUserId}>
               <SelectTrigger id="tech-member">
               <SelectTrigger id="tech-member">
                 <SelectValue placeholder={t("standalone")} />
                 <SelectValue placeholder={t("standalone")} />
               </SelectTrigger>
               </SelectTrigger>

+ 14 - 1
src/features/workboard/Components/UnassignedJobsPanel.tsx

@@ -4,9 +4,11 @@ import { useState } from "react";
 import { Search } from "lucide-react";
 import { Search } from "lucide-react";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
 import { ScrollArea } from "@/components/ui/scroll-area";
 import { ScrollArea } from "@/components/ui/scroll-area";
+import { useDroppable } from "@dnd-kit/core";
 import { useWorkBoardStore } from "../store/workboardStore";
 import { useWorkBoardStore } from "../store/workboardStore";
 import { UnassignedJobCard } from "./BoardJobCard";
 import { UnassignedJobCard } from "./BoardJobCard";
 import { useTranslations } from "next-intl";
 import { useTranslations } from "next-intl";
+import { cn } from "@/lib/utils";
 
 
 export function UnassignedJobsPanel() {
 export function UnassignedJobsPanel() {
   const serviceRecords = useWorkBoardStore((s) => s.unassignedServiceRecords);
   const serviceRecords = useWorkBoardStore((s) => s.unassignedServiceRecords);
@@ -14,6 +16,11 @@ export function UnassignedJobsPanel() {
   const [search, setSearch] = useState("");
   const [search, setSearch] = useState("");
   const t = useTranslations("workBoard.unassigned");
   const t = useTranslations("workBoard.unassigned");
 
 
+  const { isOver, setNodeRef } = useDroppable({
+    id: "unassigned-drop",
+    data: { unassigned: true },
+  });
+
   const lowerSearch = search.toLowerCase();
   const lowerSearch = search.toLowerCase();
 
 
   const filteredSR = serviceRecords.filter((sr) => {
   const filteredSR = serviceRecords.filter((sr) => {
@@ -39,7 +46,13 @@ export function UnassignedJobsPanel() {
   const totalCount = filteredSR.length + filteredInsp.length;
   const totalCount = filteredSR.length + filteredInsp.length;
 
 
   return (
   return (
-    <div className="flex w-64 shrink-0 flex-col rounded-lg border bg-muted/30">
+    <div
+      ref={setNodeRef}
+      className={cn(
+        "flex w-64 shrink-0 flex-col rounded-lg border bg-muted/30 transition-colors",
+        isOver && "ring-2 ring-primary/50 bg-primary/5"
+      )}
+    >
       <div className="border-b p-3">
       <div className="border-b p-3">
         <h3 className="mb-2 text-sm font-semibold">
         <h3 className="mb-2 text-sm font-semibold">
           {t("title", { count: totalCount })}
           {t("title", { count: totalCount })}

+ 45 - 7
src/features/workboard/Components/WorkBoardClient.tsx

@@ -13,6 +13,7 @@ import {
   assignTechnician,
   assignTechnician,
   moveJob,
   moveJob,
   unassignJob,
   unassignJob,
+  updateServiceTimes,
 } from "../Actions/boardActions";
 } from "../Actions/boardActions";
 import { WorkBoardToolbar, type BoardView } from "./WorkBoardToolbar";
 import { WorkBoardToolbar, type BoardView } from "./WorkBoardToolbar";
 import { DayTimeline } from "./DayTimeline";
 import { DayTimeline } from "./DayTimeline";
@@ -168,17 +169,54 @@ export function WorkBoardClient({
     setActiveDrag(null);
     setActiveDrag(null);
     const { active, over } = event;
     const { active, over } = event;
     if (!over) return;
     if (!over) return;
-    const overData = over.data.current as { technicianId: string; date: string } | undefined;
-    if (!overData?.technicianId) return;
+    const overData = over.data.current as { technicianId?: string; date?: string; unassigned?: boolean } | undefined;
+    if (!overData) return;
     const activeData = active.data.current;
     const activeData = active.data.current;
 
 
+    // Drop assigned job onto unassigned panel
+    if (overData.unassigned && activeData?.job && activeData.job.type) {
+      const job = activeData.job as WorkBoardJob;
+      if (!job.technicianId) return;
+      store.removeJob(job.id);
+      const res = await unassignJob({ id: job.id, type: job.type });
+      if (!res.success) { toast.error(t("failedRemove"), { description: res.error }); store.addJob(job); }
+      else { const unRes = await getUnassignedJobs(); if (unRes.success && unRes.data) store.setUnassigned(unRes.data.serviceRecords as Parameters<typeof store.setUnassigned>[0], unRes.data.inspections as Parameters<typeof store.setUnassigned>[1]); }
+      return;
+    }
+
+    if (!overData.technicianId) return;
+
     if (activeData?.job && activeData.job.type) {
     if (activeData?.job && activeData.job.type) {
       const job = activeData.job as WorkBoardJob;
       const job = activeData.job as WorkBoardJob;
-      if (job.technicianId === overData.technicianId) return;
-      store.optimisticMove(job.id, overData.technicianId);
-      const res = await moveJob({ id: job.id, technicianId: overData.technicianId, sortOrder: 0, type: job.type });
-      if (!res.success) { toast.error(t("failedMove"), { description: res.error }); store.optimisticMove(job.id, job.technicianId!); }
-      else if (res.data) store.updateJob(res.data as WorkBoardJob);
+      const dropDate = overData.date;
+      const jobDate = job.startDateTime ? job.startDateTime.split("T")[0] : null;
+      const sameTech = job.technicianId === overData.technicianId;
+      const sameDay = dropDate && jobDate && dropDate === jobDate;
+
+      if (sameTech && sameDay) return;
+
+      // Move to different tech if needed
+      if (!sameTech) {
+        store.optimisticMove(job.id, overData.technicianId);
+        const res = await moveJob({ id: job.id, technicianId: overData.technicianId, sortOrder: 0, type: job.type });
+        if (!res.success) { toast.error(t("failedMove"), { description: res.error }); store.optimisticMove(job.id, job.technicianId!); return; }
+        else if (res.data) store.updateJob(res.data as WorkBoardJob);
+      }
+
+      // Update dates if dropped on a different day
+      if (!sameDay && dropDate) {
+        const oldStart = job.startDateTime ? new Date(job.startDateTime) : null;
+        const oldEnd = job.endDateTime ? new Date(job.endDateTime) : null;
+        const duration = oldStart && oldEnd ? oldEnd.getTime() - oldStart.getTime() : 60 * 60 * 1000;
+        const timeOfDay = oldStart
+          ? `${String(oldStart.getHours()).padStart(2, "0")}:${String(oldStart.getMinutes()).padStart(2, "0")}`
+          : boardSettings.workDayStart;
+        const newStart = new Date(dropDate + "T" + timeOfDay + ":00");
+        const newEnd = new Date(newStart.getTime() + duration);
+        store.updateServiceTimes(job.id, newStart.toISOString(), newEnd.toISOString());
+        const timeRes = await updateServiceTimes({ id: job.id, startDateTime: newStart, endDateTime: newEnd });
+        if (!timeRes.success) { toast.error(t("failedMove"), { description: timeRes.error }); }
+      }
       return;
       return;
     }
     }
 
 

+ 2 - 2
src/features/workboard/Schema/workboardSchema.ts

@@ -6,7 +6,7 @@ export const createTechnicianSchema = z.object({
     .string()
     .string()
     .regex(/^#[0-9a-fA-F]{6}$/, "Must be a valid hex color")
     .regex(/^#[0-9a-fA-F]{6}$/, "Must be a valid hex color")
     .default("#3b82f6"),
     .default("#3b82f6"),
-  memberId: z.string().optional(),
+  userId: z.string().optional(),
 });
 });
 
 
 export const updateTechnicianSchema = z.object({
 export const updateTechnicianSchema = z.object({
@@ -19,7 +19,7 @@ export const updateTechnicianSchema = z.object({
   isActive: z.boolean().optional(),
   isActive: z.boolean().optional(),
   sortOrder: z.number().int().min(0).optional(),
   sortOrder: z.number().int().min(0).optional(),
   dailyCapacity: z.number().int().min(60).max(720).optional(),
   dailyCapacity: z.number().int().min(60).max(720).optional(),
-  memberId: z.string().nullable().optional(),
+  userId: z.string().nullable().optional(),
 });
 });
 
 
 export const assignTechnicianSchema = z.object({
 export const assignTechnicianSchema = z.object({

+ 1 - 1
src/features/workboard/store/workboardStore.ts

@@ -34,7 +34,7 @@ export type Technician = {
   isActive: boolean;
   isActive: boolean;
   sortOrder: number;
   sortOrder: number;
   dailyCapacity: number;
   dailyCapacity: number;
-  memberId: string | null;
+  userId: string | null;
   organizationId: string;
   organizationId: string;
 };
 };