Ver código fonte

Work order duration tracking and direct technician assignment (#38)

* workorder duration

* work orders

* tech

* translation

* translations

* basic info

* sidebar width

* lint

* migration

* migration

* work orders

* initdb

* migration init

* initdb

* migration

* new migration

* migration
Bernt Christian Egeland 6 meses atrás
pai
commit
62dbccef7a
100 arquivos alterados com 5506 adições e 2737 exclusões
  1. 11 8
      init-db.sh
  2. 21 0
      messages/de/service.json
  3. 7 1
      messages/de/settings.json
  4. 5 1
      messages/de/workBoard.json
  5. 21 0
      messages/en/service.json
  6. 22 2
      messages/en/settings.json
  7. 10 2
      messages/en/workBoard.json
  8. 21 0
      messages/es/service.json
  9. 7 1
      messages/es/settings.json
  10. 5 1
      messages/es/workBoard.json
  11. 21 0
      messages/fr/service.json
  12. 7 1
      messages/fr/settings.json
  13. 5 1
      messages/fr/workBoard.json
  14. 21 0
      messages/it/service.json
  15. 7 1
      messages/it/settings.json
  16. 5 1
      messages/it/workBoard.json
  17. 26 5
      messages/nb/service.json
  18. 22 2
      messages/nb/settings.json
  19. 10 2
      messages/nb/workBoard.json
  20. 21 0
      messages/nl/service.json
  21. 7 1
      messages/nl/settings.json
  22. 5 1
      messages/nl/workBoard.json
  23. 21 0
      messages/pl/service.json
  24. 7 1
      messages/pl/settings.json
  25. 5 1
      messages/pl/workBoard.json
  26. 21 0
      messages/pt-BR/service.json
  27. 7 1
      messages/pt-BR/settings.json
  28. 5 1
      messages/pt-BR/workBoard.json
  29. 21 0
      messages/tr/service.json
  30. 7 1
      messages/tr/settings.json
  31. 5 1
      messages/tr/workBoard.json
  32. 973 0
      prisma/migrations/0_init/migration.sql
  33. 0 640
      prisma/migrations/20260214135830_init/migration.sql
  34. 0 18
      prisma/migrations/20260215140404_2fa/migration.sql
  35. 0 26
      prisma/migrations/20260216075347_user_invitations/migration.sql
  36. 0 59
      prisma/migrations/20260216105753_recurring_invoices/migration.sql
  37. 0 5
      prisma/migrations/20260216171151_custom_roles/migration.sql
  38. 0 2
      prisma/migrations/20260216180745_organization_user_relation/migration.sql
  39. 0 2
      prisma/migrations/20260217113542_archives/migration.sql
  40. 0 2
      prisma/migrations/20260217115943_terms_of_service/migration.sql
  41. 0 2
      prisma/migrations/20260217163702_invoice_paid_status/migration.sql
  42. 0 2
      prisma/migrations/20260218071159_qoute_share_link/migration.sql
  43. 0 3
      prisma/migrations/20260219050044_predicted_maintenance/migration.sql
  44. 0 2
      prisma/migrations/20260219121039_sell_price/migration.sql
  45. 0 131
      prisma/migrations/20260220172149_dvi_inspection/migration.sql
  46. 0 2
      prisma/migrations/20260220174923_customer_quotation_interaction/migration.sql
  47. 0 21
      prisma/migrations/20260220182239_push_notifications/migration.sql
  48. 0 3
      prisma/migrations/20260221055301_user_last_seen/migration.sql
  49. 0 34
      prisma/migrations/20260221061849_sms_gateway/migration.sql
  50. 0 57
      prisma/migrations/20260223165833_job_board/migration.sql
  51. 0 82
      prisma/migrations/20260224102050_customer_portal/migration.sql
  52. 0 21
      prisma/migrations/20260226201811_qoute_attachments/migration.sql
  53. 0 34
      prisma/migrations/20260227185301_passkeys/migration.sql
  54. 0 9
      prisma/migrations/20260227191610_shared_pages_viewcounter/migration.sql
  55. 0 5
      prisma/migrations/20260302163120_exlcude_parts_option/migration.sql
  56. 37 0
      prisma/migrations/20260307232727_workorder_duration/migration.sql
  57. 50 65
      prisma/schema.prisma
  58. 225 0
      src/__tests__/features/workboard/datetime.test.ts
  59. 9 2
      src/app/(authenticated)/settings/workshop/page.tsx
  60. 209 9
      src/app/(authenticated)/settings/workshop/workshop-settings.tsx
  61. 2 1
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  62. 55 1
      src/app/(authenticated)/vehicles/[id]/service/new/page.tsx
  63. 13 7
      src/app/(authenticated)/work-board/page.tsx
  64. 32 14
      src/app/(presenter)/work-board/presenter/page.tsx
  65. 1 1
      src/components/page-header.tsx
  66. 252 0
      src/components/ui/context-menu.tsx
  67. 848 0
      src/components/ui/datetime-picker.tsx
  68. 63 0
      src/components/ui/slider.tsx
  69. 6 1
      src/components/vehicle-picker-dialog.tsx
  70. 3 0
      src/features/settings/Schema/settingsSchema.ts
  71. 40 9
      src/features/vehicles/Actions/createDraftServiceRecord.ts
  72. 0 71
      src/features/vehicles/Actions/serviceActions.ts
  73. 46 30
      src/features/vehicles/Components/service-detail/ServiceDetailContent.tsx
  74. 1 0
      src/features/vehicles/Components/service-detail/types.ts
  75. 32 165
      src/features/vehicles/Components/service-edit/BasicInfoSection.tsx
  76. 309 0
      src/features/vehicles/Components/service-edit/ScheduleTimesSection.tsx
  77. 2 0
      src/features/vehicles/Components/service-edit/form-types.ts
  78. 22 15
      src/features/vehicles/Components/service-page/ServicePageClient.tsx
  79. 3 3
      src/features/vehicles/Components/service-page/UnifiedServiceHeader.tsx
  80. 0 416
      src/features/workboard/Actions/boardActions.ts
  81. 405 0
      src/features/workboard/Actions/boardActions/assignments.ts
  82. 11 0
      src/features/workboard/Actions/boardActions/index.ts
  83. 26 0
      src/features/workboard/Actions/boardActions/queries.ts
  84. 99 0
      src/features/workboard/Actions/boardActions/scheduling.ts
  85. 24 0
      src/features/workboard/Actions/boardActions/types.ts
  86. 56 4
      src/features/workboard/Components/BoardDayCell.tsx
  87. 31 39
      src/features/workboard/Components/BoardJobCard.tsx
  88. 109 0
      src/features/workboard/Components/DayTimeline/TimelineCard.tsx
  89. 166 0
      src/features/workboard/Components/DayTimeline/TimelineColumn.tsx
  90. 236 0
      src/features/workboard/Components/DayTimeline/index.tsx
  91. 39 0
      src/features/workboard/Components/DayTimeline/utils.ts
  92. 57 0
      src/features/workboard/Components/DurationSlider.tsx
  93. 114 32
      src/features/workboard/Components/JobDetailPopover.tsx
  94. 15 23
      src/features/workboard/Components/PresenterDayView.tsx
  95. 14 29
      src/features/workboard/Components/PresenterKanbanView.tsx
  96. 174 0
      src/features/workboard/Components/PresenterTimeline.tsx
  97. 49 15
      src/features/workboard/Components/TechnicianRow.tsx
  98. 125 290
      src/features/workboard/Components/WorkBoardClient.tsx
  99. 69 292
      src/features/workboard/Components/WorkBoardPresenter.tsx
  100. 68 4
      src/features/workboard/Components/WorkBoardToolbar.tsx

+ 11 - 8
init-db.sh

@@ -6,19 +6,22 @@ trap 'echo "An error occurred. Exiting..."; exit 1' ERR
 cmd="$@"
 cmd="$@"
 
 
 echo "Waiting for PostgreSQL to be ready..."
 echo "Waiting for PostgreSQL to be ready..."
-until node -e "
-const { Client } = require('pg');
-const c = new Client({ connectionString: process.env.DATABASE_URL });
-c.connect().then(() => { c.end(); process.exit(0); }).catch(() => process.exit(1));
-" 2>/dev/null; do
+until npx prisma db execute --schema prisma/schema.prisma --stdin <<< "SELECT 1" 2>/dev/null; do
   echo "PostgreSQL is unavailable - sleeping 2s..."
   echo "PostgreSQL is unavailable - sleeping 2s..."
   sleep 2
   sleep 2
 done
 done
 echo "PostgreSQL is ready!"
 echo "PostgreSQL is ready!"
 
 
-echo "Applying database schema..."
-npx prisma db push --accept-data-loss
-echo "Schema applied successfully!"
+echo "Applying database migrations..."
+if ! npx prisma migrate deploy 2>/dev/null; then
+  # P3005: existing DB without migration history (from db push).
+  # Create empty migration table so migrate deploy can run.
+  # 0_init is fully idempotent — safe on any DB state.
+  echo "Existing database detected. Creating migration table..."
+  npx prisma db execute --schema prisma/schema.prisma --stdin <<< 'CREATE TABLE IF NOT EXISTS "_prisma_migrations" ("id" VARCHAR(36) PRIMARY KEY NOT NULL, "checksum" VARCHAR(64) NOT NULL, "finished_at" TIMESTAMPTZ, "migration_name" VARCHAR(255) NOT NULL, "logs" TEXT, "rolled_back_at" TIMESTAMPTZ, "started_at" TIMESTAMPTZ NOT NULL DEFAULT now(), "applied_steps_count" INTEGER NOT NULL DEFAULT 0)'
+  npx prisma migrate deploy
+fi
+echo "Migrations applied successfully!"
 
 
 echo "Starting application..."
 echo "Starting application..."
 exec $cmd
 exec $cmd

+ 21 - 0
messages/de/service.json

@@ -247,5 +247,26 @@
     "failedDelete": "Datei konnte nicht gelöscht werden",
     "failedDelete": "Datei konnte nicht gelöscht werden",
     "failedUpdate": "Aktualisierung fehlgeschlagen",
     "failedUpdate": "Aktualisierung fehlgeschlagen",
     "invoice": "Rechnung"
     "invoice": "Rechnung"
+  },
+  "schedule": {
+    "title": "Zeitplan",
+    "date": "Datum",
+    "startTime": "Startzeit",
+    "endTime": "Endzeit",
+    "technician": "Techniker",
+    "selectTechnician": "Techniker auswählen...",
+    "searchOrCreate": "Techniker suchen oder erstellen...",
+    "createTechnician": "\"{name}\" erstellen",
+    "creating": "Erstelle...",
+    "failedCreate": "Techniker konnte nicht erstellt werden",
+    "addNew": "+ Neu hinzufügen",
+    "add": "Hinzufügen",
+    "newTechPlaceholder": "Technikername...",
+    "assign": "Tafel zuweisen",
+    "assigned": "Der Arbeitstafel zugewiesen",
+    "failedAssign": "Zuweisung fehlgeschlagen",
+    "endBeforeStart": "Endzeit muss nach der Startzeit liegen",
+    "failedUpdate": "Zeitplan konnte nicht aktualisiert werden",
+    "splitCreated": "Auftrag erstreckt sich über mehrere Tage — Fortsetzung für den nächsten Tag erstellt"
   }
   }
 }
 }

+ 7 - 1
messages/de/settings.json

@@ -492,7 +492,13 @@
     "title": "Werkstatt-Standards",
     "title": "Werkstatt-Standards",
     "description": "Standardwerte beim Erstellen neuer Servicedaten.",
     "description": "Standardwerte beim Erstellen neuer Servicedaten.",
     "defaultTechnician": "Standard-Technikername",
     "defaultTechnician": "Standard-Technikername",
-    "technicianPlaceholder": "Max Mustermann",
+    "technicianPlaceholder": "Techniker suchen...",
+    "failedCreateTech": "Techniker konnte nicht erstellt werden",
+    "creating": "Wird erstellt...",
+    "createTechnician": "\"{name}\" erstellen",
+    "newTechPlaceholder": "Technikername",
+    "addTech": "Hinzufügen",
+    "addNewTech": "+ Neu hinzufügen",
     "defaultLaborRate": "Standard-Stundensatz",
     "defaultLaborRate": "Standard-Stundensatz",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
     "workingHours": "Arbeitszeiten",
     "workingHours": "Arbeitszeiten",

+ 5 - 1
messages/de/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Heute",
     "today": "Heute",
+    "day": "Tag",
+    "week": "Woche",
     "presenter": "Präsentator",
     "presenter": "Präsentator",
     "addTechnician": "Techniker hinzufügen"
     "addTechnician": "Techniker hinzufügen"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Fügen Sie einen Techniker hinzu, um mit der Planung von Arbeiten im Board zu beginnen.",
     "noTechniciansDescription": "Fügen Sie einen Techniker hinzu, um mit der Planung von Arbeiten im Board zu beginnen.",
     "failedMove": "Zuweisung konnte nicht verschoben werden",
     "failedMove": "Zuweisung konnte nicht verschoben werden",
     "failedAssign": "Auftrag konnte nicht zugewiesen werden",
     "failedAssign": "Auftrag konnte nicht zugewiesen werden",
-    "failedRemove": "Zuweisung konnte nicht entfernt werden"
+    "failedRemove": "Zuweisung konnte nicht entfernt werden",
+    "createNewWorkorder": "Neuen Arbeitsauftrag erstellen"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Nicht zugewiesene Aufträge ({count})",
     "title": "Nicht zugewiesene Aufträge ({count})",
@@ -51,6 +54,7 @@
     "status": "Status",
     "status": "Status",
     "week": "Woche",
     "week": "Woche",
     "today": "Heute",
     "today": "Heute",
+    "timeline": "Zeitleiste",
     "live": "Live",
     "live": "Live",
     "disconnected": "Live-Updates unterbrochen",
     "disconnected": "Live-Updates unterbrochen",
     "noTechnicians": "Keine Techniker konfiguriert",
     "noTechnicians": "Keine Techniker konfiguriert",

+ 21 - 0
messages/en/service.json

@@ -247,5 +247,26 @@
     "failedDelete": "Failed to delete file",
     "failedDelete": "Failed to delete file",
     "failedUpdate": "Failed to update",
     "failedUpdate": "Failed to update",
     "invoice": "Invoice"
     "invoice": "Invoice"
+  },
+  "schedule": {
+    "title": "Schedule",
+    "date": "Date",
+    "startTime": "Start Time",
+    "endTime": "End Time",
+    "technician": "Technician",
+    "selectTechnician": "Select technician...",
+    "searchOrCreate": "Search or create technician...",
+    "createTechnician": "Create \"{name}\"",
+    "creating": "Creating...",
+    "failedCreate": "Failed to create technician",
+    "addNew": "+ Add New",
+    "add": "Add",
+    "newTechPlaceholder": "Technician name...",
+    "assign": "Assign to Board",
+    "assigned": "Assigned to work board",
+    "failedAssign": "Failed to assign",
+    "endBeforeStart": "End time must be after start time",
+    "failedUpdate": "Failed to update schedule times",
+    "splitCreated": "Job spans multiple days — continuation created for next day"
   }
   }
 }
 }

+ 22 - 2
messages/en/settings.json

@@ -492,7 +492,13 @@
     "title": "Workshop Defaults",
     "title": "Workshop Defaults",
     "description": "Default values used when creating new service records.",
     "description": "Default values used when creating new service records.",
     "defaultTechnician": "Default Technician Name",
     "defaultTechnician": "Default Technician Name",
-    "technicianPlaceholder": "John Smith",
+    "technicianPlaceholder": "Search technician...",
+    "failedCreateTech": "Failed to create technician",
+    "creating": "Creating...",
+    "createTechnician": "Create \"{name}\"",
+    "newTechPlaceholder": "Technician name",
+    "addTech": "Add",
+    "addNewTech": "+ Add New",
     "defaultLaborRate": "Default Labor Rate (per hour)",
     "defaultLaborRate": "Default Labor Rate (per hour)",
     "laborRatePlaceholder": "75.00",
     "laborRatePlaceholder": "75.00",
     "workingHours": "Working Hours",
     "workingHours": "Working Hours",
@@ -509,7 +515,21 @@
     "miles": "miles (mi)",
     "miles": "miles (mi)",
     "gallons": "gallons (gal)",
     "gallons": "gallons (gal)",
     "saveWorkshop": "Save Workshop Settings",
     "saveWorkshop": "Save Workshop Settings",
-    "saved": "Workshop settings saved"
+    "saved": "Workshop settings saved",
+    "workBoardTitle": "Work Board Settings",
+    "workBoardDescription": "Configure the work board schedule and day view timeline.",
+    "weekStartDay": "Week Start Day",
+    "workDayStart": "Work Day Start",
+    "workDayEnd": "Work Day End",
+    "weekDays": {
+      "0": "Sunday",
+      "1": "Monday",
+      "2": "Tuesday",
+      "3": "Wednesday",
+      "4": "Thursday",
+      "5": "Friday",
+      "6": "Saturday"
+    }
   },
   },
   "maintenance": {
   "maintenance": {
     "title": "Predicted Maintenance",
     "title": "Predicted Maintenance",

+ 10 - 2
messages/en/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Today",
     "today": "Today",
+    "day": "Day",
+    "week": "Week",
     "presenter": "Presenter",
     "presenter": "Presenter",
     "addTechnician": "Add Technician"
     "addTechnician": "Add Technician"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Add a technician to start scheduling work on the board.",
     "noTechniciansDescription": "Add a technician to start scheduling work on the board.",
     "failedMove": "Failed to move assignment",
     "failedMove": "Failed to move assignment",
     "failedAssign": "Failed to assign job",
     "failedAssign": "Failed to assign job",
-    "failedRemove": "Failed to remove assignment"
+    "failedRemove": "Failed to remove assignment",
+    "createNewWorkorder": "Create new Workorder"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Unassigned Jobs ({count})",
     "title": "Unassigned Jobs ({count})",
@@ -43,7 +46,11 @@
   "jobDetail": {
   "jobDetail": {
     "viewDetails": "View Details",
     "viewDetails": "View Details",
     "remove": "Remove",
     "remove": "Remove",
-    "date": "Date: {date}"
+    "startTime": "Start Time",
+    "endTime": "End Time",
+    "failedDuration": "Failed to update duration",
+    "failedTimes": "Failed to update times",
+    "endBeforeStart": "End time must be after start time"
   },
   },
   "presenter": {
   "presenter": {
     "title": "Work Board",
     "title": "Work Board",
@@ -51,6 +58,7 @@
     "status": "Status",
     "status": "Status",
     "week": "Week",
     "week": "Week",
     "today": "Today",
     "today": "Today",
+    "timeline": "Timeline",
     "live": "Live",
     "live": "Live",
     "disconnected": "Live updates disconnected",
     "disconnected": "Live updates disconnected",
     "noTechnicians": "No technicians configured",
     "noTechnicians": "No technicians configured",

+ 21 - 0
messages/es/service.json

@@ -247,5 +247,26 @@
     "failedDelete": "Error al eliminar archivo",
     "failedDelete": "Error al eliminar archivo",
     "failedUpdate": "Error al actualizar",
     "failedUpdate": "Error al actualizar",
     "invoice": "Factura"
     "invoice": "Factura"
+  },
+  "schedule": {
+    "title": "Horario",
+    "date": "Fecha",
+    "startTime": "Hora de inicio",
+    "endTime": "Hora de fin",
+    "technician": "Técnico",
+    "selectTechnician": "Seleccionar técnico...",
+    "searchOrCreate": "Buscar o crear técnico...",
+    "createTechnician": "Crear \"{name}\"",
+    "creating": "Creando...",
+    "failedCreate": "Error al crear técnico",
+    "addNew": "+ Agregar nuevo",
+    "add": "Agregar",
+    "newTechPlaceholder": "Nombre del técnico...",
+    "assign": "Asignar al tablero",
+    "assigned": "Asignado al tablero de trabajo",
+    "failedAssign": "Error al asignar",
+    "endBeforeStart": "La hora de fin debe ser posterior a la hora de inicio",
+    "failedUpdate": "Error al actualizar el horario",
+    "splitCreated": "El trabajo abarca varios días — continuación creada para el día siguiente"
   }
   }
 }
 }

+ 7 - 1
messages/es/settings.json

@@ -492,7 +492,13 @@
     "title": "Valores predeterminados del taller",
     "title": "Valores predeterminados del taller",
     "description": "Valores predeterminados al crear nuevos registros de servicio.",
     "description": "Valores predeterminados al crear nuevos registros de servicio.",
     "defaultTechnician": "Nombre del tecnico predeterminado",
     "defaultTechnician": "Nombre del tecnico predeterminado",
-    "technicianPlaceholder": "Juan Perez",
+    "technicianPlaceholder": "Buscar técnico...",
+    "failedCreateTech": "Error al crear técnico",
+    "creating": "Creando...",
+    "createTechnician": "Crear \"{name}\"",
+    "newTechPlaceholder": "Nombre del técnico",
+    "addTech": "Agregar",
+    "addNewTech": "+ Agregar nuevo",
     "defaultLaborRate": "Tarifa de mano de obra predeterminada (por hora)",
     "defaultLaborRate": "Tarifa de mano de obra predeterminada (por hora)",
     "laborRatePlaceholder": "75.00",
     "laborRatePlaceholder": "75.00",
     "workingHours": "Horario de trabajo",
     "workingHours": "Horario de trabajo",

+ 5 - 1
messages/es/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Hoy",
     "today": "Hoy",
+    "day": "Día",
+    "week": "Semana",
     "presenter": "Presentador",
     "presenter": "Presentador",
     "addTechnician": "Agregar Técnico"
     "addTechnician": "Agregar Técnico"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Agregue un técnico para comenzar a programar trabajo en el tablero.",
     "noTechniciansDescription": "Agregue un técnico para comenzar a programar trabajo en el tablero.",
     "failedMove": "Error al mover asignación",
     "failedMove": "Error al mover asignación",
     "failedAssign": "Error al asignar trabajo",
     "failedAssign": "Error al asignar trabajo",
-    "failedRemove": "Error al eliminar asignación"
+    "failedRemove": "Error al eliminar asignación",
+    "createNewWorkorder": "Crear nueva orden de trabajo"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Trabajos Sin Asignar ({count})",
     "title": "Trabajos Sin Asignar ({count})",
@@ -51,6 +54,7 @@
     "status": "Estado",
     "status": "Estado",
     "week": "Semana",
     "week": "Semana",
     "today": "Hoy",
     "today": "Hoy",
+    "timeline": "Línea de tiempo",
     "live": "En Vivo",
     "live": "En Vivo",
     "disconnected": "Actualizaciones en vivo desconectadas",
     "disconnected": "Actualizaciones en vivo desconectadas",
     "noTechnicians": "Sin técnicos configurados",
     "noTechnicians": "Sin técnicos configurados",

+ 21 - 0
messages/fr/service.json

@@ -247,5 +247,26 @@
     "failedDelete": "Impossible de supprimer le fichier",
     "failedDelete": "Impossible de supprimer le fichier",
     "failedUpdate": "Impossible de mettre à jour",
     "failedUpdate": "Impossible de mettre à jour",
     "invoice": "Facture"
     "invoice": "Facture"
+  },
+  "schedule": {
+    "title": "Planification",
+    "date": "Date",
+    "startTime": "Heure de début",
+    "endTime": "Heure de fin",
+    "technician": "Technicien",
+    "selectTechnician": "Sélectionner un technicien...",
+    "searchOrCreate": "Rechercher ou créer un technicien...",
+    "createTechnician": "Créer \"{name}\"",
+    "creating": "Création...",
+    "failedCreate": "Impossible de créer le technicien",
+    "addNew": "+ Ajouter nouveau",
+    "add": "Ajouter",
+    "newTechPlaceholder": "Nom du technicien...",
+    "assign": "Assigner au tableau",
+    "assigned": "Assigné au tableau de travail",
+    "failedAssign": "Échec de l'assignation",
+    "endBeforeStart": "L'heure de fin doit être après l'heure de début",
+    "failedUpdate": "Impossible de mettre à jour le planning",
+    "splitCreated": "Le travail s'étend sur plusieurs jours — suite créée pour le jour suivant"
   }
   }
 }
 }

+ 7 - 1
messages/fr/settings.json

@@ -492,7 +492,13 @@
     "title": "Valeurs par defaut de l'atelier",
     "title": "Valeurs par defaut de l'atelier",
     "description": "Valeurs par defaut lors de la creation de nouveaux dossiers de service.",
     "description": "Valeurs par defaut lors de la creation de nouveaux dossiers de service.",
     "defaultTechnician": "Nom du technicien par defaut",
     "defaultTechnician": "Nom du technicien par defaut",
-    "technicianPlaceholder": "Jean Dupont",
+    "technicianPlaceholder": "Rechercher technicien...",
+    "failedCreateTech": "Échec de la création du technicien",
+    "creating": "Création...",
+    "createTechnician": "Créer \"{name}\"",
+    "newTechPlaceholder": "Nom du technicien",
+    "addTech": "Ajouter",
+    "addNewTech": "+ Ajouter nouveau",
     "defaultLaborRate": "Taux horaire par defaut",
     "defaultLaborRate": "Taux horaire par defaut",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
     "workingHours": "Heures de travail",
     "workingHours": "Heures de travail",

+ 5 - 1
messages/fr/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Aujourd'hui",
     "today": "Aujourd'hui",
+    "day": "Jour",
+    "week": "Semaine",
     "presenter": "Présentateur",
     "presenter": "Présentateur",
     "addTechnician": "Ajouter un Technicien"
     "addTechnician": "Ajouter un Technicien"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Ajoutez un technicien pour commencer à programmer le travail sur le tableau.",
     "noTechniciansDescription": "Ajoutez un technicien pour commencer à programmer le travail sur le tableau.",
     "failedMove": "Impossible de déplacer l'assignation",
     "failedMove": "Impossible de déplacer l'assignation",
     "failedAssign": "Impossible d'assigner le travail",
     "failedAssign": "Impossible d'assigner le travail",
-    "failedRemove": "Impossible de supprimer l'assignation"
+    "failedRemove": "Impossible de supprimer l'assignation",
+    "createNewWorkorder": "Créer un nouvel ordre de travail"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Travaux Non Assignés ({count})",
     "title": "Travaux Non Assignés ({count})",
@@ -51,6 +54,7 @@
     "status": "Statut",
     "status": "Statut",
     "week": "Semaine",
     "week": "Semaine",
     "today": "Aujourd'hui",
     "today": "Aujourd'hui",
+    "timeline": "Chronologie",
     "live": "En Direct",
     "live": "En Direct",
     "disconnected": "Mises à jour en direct déconnectées",
     "disconnected": "Mises à jour en direct déconnectées",
     "noTechnicians": "Aucun technicien configuré",
     "noTechnicians": "Aucun technicien configuré",

+ 21 - 0
messages/it/service.json

@@ -247,5 +247,26 @@
     "failedDelete": "Impossibile eliminare il file",
     "failedDelete": "Impossibile eliminare il file",
     "failedUpdate": "Impossibile aggiornare",
     "failedUpdate": "Impossibile aggiornare",
     "invoice": "Fattura"
     "invoice": "Fattura"
+  },
+  "schedule": {
+    "title": "Pianificazione",
+    "date": "Data",
+    "startTime": "Ora di inizio",
+    "endTime": "Ora di fine",
+    "technician": "Tecnico",
+    "selectTechnician": "Seleziona tecnico...",
+    "searchOrCreate": "Cerca o crea tecnico...",
+    "createTechnician": "Crea \"{name}\"",
+    "creating": "Creazione...",
+    "failedCreate": "Impossibile creare il tecnico",
+    "addNew": "+ Aggiungi nuovo",
+    "add": "Aggiungi",
+    "newTechPlaceholder": "Nome del tecnico...",
+    "assign": "Assegna alla bacheca",
+    "assigned": "Assegnato alla bacheca di lavoro",
+    "failedAssign": "Assegnazione fallita",
+    "endBeforeStart": "L'ora di fine deve essere dopo l'ora di inizio",
+    "failedUpdate": "Impossibile aggiornare la pianificazione",
+    "splitCreated": "Il lavoro si estende su più giorni — continuazione creata per il giorno successivo"
   }
   }
 }
 }

+ 7 - 1
messages/it/settings.json

@@ -492,7 +492,13 @@
     "title": "Predefiniti officina",
     "title": "Predefiniti officina",
     "description": "Valori predefiniti utilizzati quando si creano nuovi registri di servizio.",
     "description": "Valori predefiniti utilizzati quando si creano nuovi registri di servizio.",
     "defaultTechnician": "Nome tecnico predefinito",
     "defaultTechnician": "Nome tecnico predefinito",
-    "technicianPlaceholder": "Mario Rossi",
+    "technicianPlaceholder": "Cerca tecnico...",
+    "failedCreateTech": "Impossibile creare il tecnico",
+    "creating": "Creazione...",
+    "createTechnician": "Crea \"{name}\"",
+    "newTechPlaceholder": "Nome del tecnico",
+    "addTech": "Aggiungi",
+    "addNewTech": "+ Aggiungi nuovo",
     "defaultLaborRate": "Tariffa manodopera predefinita (all'ora)",
     "defaultLaborRate": "Tariffa manodopera predefinita (all'ora)",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
     "workingHours": "Orario di lavoro",
     "workingHours": "Orario di lavoro",

+ 5 - 1
messages/it/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Oggi",
     "today": "Oggi",
+    "day": "Giorno",
+    "week": "Settimana",
     "presenter": "Presentatore",
     "presenter": "Presentatore",
     "addTechnician": "Aggiungi Tecnico"
     "addTechnician": "Aggiungi Tecnico"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Aggiungi un tecnico per iniziare a programmare il lavoro sulla lavagna.",
     "noTechniciansDescription": "Aggiungi un tecnico per iniziare a programmare il lavoro sulla lavagna.",
     "failedMove": "Impossibile spostare l'incarico",
     "failedMove": "Impossibile spostare l'incarico",
     "failedAssign": "Impossibile assegnare il lavoro",
     "failedAssign": "Impossibile assegnare il lavoro",
-    "failedRemove": "Impossibile rimuovere l'incarico"
+    "failedRemove": "Impossibile rimuovere l'incarico",
+    "createNewWorkorder": "Crea nuovo ordine di lavoro"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Lavori Non Assegnati ({count})",
     "title": "Lavori Non Assegnati ({count})",
@@ -51,6 +54,7 @@
     "status": "Stato",
     "status": "Stato",
     "week": "Settimana",
     "week": "Settimana",
     "today": "Oggi",
     "today": "Oggi",
+    "timeline": "Cronologia",
     "live": "Dal Vivo",
     "live": "Dal Vivo",
     "disconnected": "Aggiornamenti dal vivo disconnessi",
     "disconnected": "Aggiornamenti dal vivo disconnessi",
     "noTechnicians": "Nessun tecnico configurato",
     "noTechnicians": "Nessun tecnico configurato",

+ 26 - 5
messages/nb/service.json

@@ -77,19 +77,19 @@
     "noInventory": "Ingen beholdningsdeler funnet."
     "noInventory": "Ingen beholdningsdeler funnet."
   },
   },
   "labor": {
   "labor": {
-    "title": "Arbeidskraft",
-    "addLabor": "Legg til arbeidskraft",
+    "title": "Arbeid",
+    "addLabor": "Legg til arbeid",
     "description": "Beskrivelse",
     "description": "Beskrivelse",
     "hours": "Timer",
     "hours": "Timer",
     "rate": "Sats ({currency}/time)",
     "rate": "Sats ({currency}/time)",
     "total": "Total",
     "total": "Total",
     "descriptionPlaceholder": "Beskrivelse *",
     "descriptionPlaceholder": "Beskrivelse *",
-    "subtotal": "Arbeidskraft delsum: {amount}"
+    "subtotal": "Arbeid delsum: {amount}"
   },
   },
   "totals": {
   "totals": {
     "title": "Totalt",
     "title": "Totalt",
     "parts": "Deler",
     "parts": "Deler",
-    "labor": "Arbeidskraft",
+    "labor": "Arbeid",
     "subtotal": "Delsum",
     "subtotal": "Delsum",
     "discount": "Rabatt",
     "discount": "Rabatt",
     "discountNone": "Ingen",
     "discountNone": "Ingen",
@@ -247,5 +247,26 @@
     "failedDelete": "Kunne ikke slette fil",
     "failedDelete": "Kunne ikke slette fil",
     "failedUpdate": "Kunne ikke oppdatere",
     "failedUpdate": "Kunne ikke oppdatere",
     "invoice": "Faktura"
     "invoice": "Faktura"
+  },
+  "schedule": {
+    "title": "Tidsplan",
+    "date": "Dato",
+    "startTime": "Starttid",
+    "endTime": "Sluttid",
+    "technician": "Tekniker",
+    "selectTechnician": "Velg tekniker...",
+    "searchOrCreate": "Søk eller opprett tekniker...",
+    "createTechnician": "Opprett \"{name}\"",
+    "creating": "Oppretter...",
+    "failedCreate": "Kunne ikke opprette tekniker",
+    "addNew": "+ Legg til ny",
+    "add": "Legg til",
+    "newTechPlaceholder": "Teknikernavn...",
+    "assign": "Tilordne til tavle",
+    "assigned": "Tilordnet arbeidstavle",
+    "failedAssign": "Kunne ikke tilordne",
+    "endBeforeStart": "Sluttid må være etter starttid",
+    "failedUpdate": "Kunne ikke oppdatere tidsplan",
+    "splitCreated": "Jobben går over flere dager — fortsettelse opprettet for neste dag"
   }
   }
-}
+}

+ 22 - 2
messages/nb/settings.json

@@ -492,7 +492,13 @@
     "title": "Verkstedstandarder",
     "title": "Verkstedstandarder",
     "description": "Standardverdier ved opprettelse av nye servicedata.",
     "description": "Standardverdier ved opprettelse av nye servicedata.",
     "defaultTechnician": "Standard teknikernavn",
     "defaultTechnician": "Standard teknikernavn",
-    "technicianPlaceholder": "Ola Nordmann",
+    "technicianPlaceholder": "Søk tekniker...",
+    "failedCreateTech": "Kunne ikke opprette tekniker",
+    "creating": "Oppretter...",
+    "createTechnician": "Opprett \"{name}\"",
+    "newTechPlaceholder": "Teknikernavn",
+    "addTech": "Legg til",
+    "addNewTech": "+ Legg til ny",
     "defaultLaborRate": "Standard timesats",
     "defaultLaborRate": "Standard timesats",
     "laborRatePlaceholder": "750",
     "laborRatePlaceholder": "750",
     "workingHours": "Arbeidstider",
     "workingHours": "Arbeidstider",
@@ -509,7 +515,21 @@
     "miles": "miles (mi)",
     "miles": "miles (mi)",
     "gallons": "gallons (gal)",
     "gallons": "gallons (gal)",
     "saveWorkshop": "Lagre verkstedinnstillinger",
     "saveWorkshop": "Lagre verkstedinnstillinger",
-    "saved": "Verkstedinnstillinger lagret"
+    "saved": "Verkstedinnstillinger lagret",
+    "workBoardTitle": "Arbeidstavle-innstillinger",
+    "workBoardDescription": "Konfigurer arbeidstavlens tidsplan og dagvisning.",
+    "weekStartDay": "Ukens startdag",
+    "workDayStart": "Arbeidsdagen starter",
+    "workDayEnd": "Arbeidsdagen slutter",
+    "weekDays": {
+      "0": "Søndag",
+      "1": "Mandag",
+      "2": "Tirsdag",
+      "3": "Onsdag",
+      "4": "Torsdag",
+      "5": "Fredag",
+      "6": "Lørdag"
+    }
   },
   },
   "maintenance": {
   "maintenance": {
     "title": "Predikert vedlikehold",
     "title": "Predikert vedlikehold",

+ 10 - 2
messages/nb/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "I dag",
     "today": "I dag",
+    "day": "Dag",
+    "week": "Uke",
     "presenter": "Presentatør",
     "presenter": "Presentatør",
     "addTechnician": "Legg til tekniker"
     "addTechnician": "Legg til tekniker"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Legg til en tekniker for å begynne å planlegge arbeid på tavlen.",
     "noTechniciansDescription": "Legg til en tekniker for å begynne å planlegge arbeid på tavlen.",
     "failedMove": "Kunne ikke flytte tilordning",
     "failedMove": "Kunne ikke flytte tilordning",
     "failedAssign": "Kunne ikke tilordne jobb",
     "failedAssign": "Kunne ikke tilordne jobb",
-    "failedRemove": "Kunne ikke fjerne tilordning"
+    "failedRemove": "Kunne ikke fjerne tilordning",
+    "createNewWorkorder": "Opprett ny arbeidsordre"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Utildelte jobber ({count})",
     "title": "Utildelte jobber ({count})",
@@ -43,7 +46,11 @@
   "jobDetail": {
   "jobDetail": {
     "viewDetails": "Vis detaljer",
     "viewDetails": "Vis detaljer",
     "remove": "Fjern",
     "remove": "Fjern",
-    "date": "Dato: {date}"
+    "startTime": "Starttid",
+    "endTime": "Sluttid",
+    "failedDuration": "Kunne ikke oppdatere varighet",
+    "failedTimes": "Kunne ikke oppdatere tider",
+    "endBeforeStart": "Sluttid må være etter starttid"
   },
   },
   "presenter": {
   "presenter": {
     "title": "Arbeidstavle",
     "title": "Arbeidstavle",
@@ -51,6 +58,7 @@
     "status": "Status",
     "status": "Status",
     "week": "Uke",
     "week": "Uke",
     "today": "I dag",
     "today": "I dag",
+    "timeline": "Tidslinje",
     "live": "Direkte",
     "live": "Direkte",
     "disconnected": "Direkteoppdateringer frakoblet",
     "disconnected": "Direkteoppdateringer frakoblet",
     "noTechnicians": "Ingen teknikere konfigurert",
     "noTechnicians": "Ingen teknikere konfigurert",

+ 21 - 0
messages/nl/service.json

@@ -247,5 +247,26 @@
     "failedDelete": "Bestand verwijderen mislukt",
     "failedDelete": "Bestand verwijderen mislukt",
     "failedUpdate": "Bijwerken mislukt",
     "failedUpdate": "Bijwerken mislukt",
     "invoice": "Factuur"
     "invoice": "Factuur"
+  },
+  "schedule": {
+    "title": "Planning",
+    "date": "Datum",
+    "startTime": "Starttijd",
+    "endTime": "Eindtijd",
+    "technician": "Technicus",
+    "selectTechnician": "Selecteer technicus...",
+    "searchOrCreate": "Zoek of maak technicus aan...",
+    "createTechnician": "\"{name}\" aanmaken",
+    "creating": "Aanmaken...",
+    "failedCreate": "Technicus aanmaken mislukt",
+    "addNew": "+ Nieuwe toevoegen",
+    "add": "Toevoegen",
+    "newTechPlaceholder": "Naam technicus...",
+    "assign": "Toewijzen aan bord",
+    "assigned": "Toegewezen aan werkbord",
+    "failedAssign": "Toewijzing mislukt",
+    "endBeforeStart": "Eindtijd moet na de starttijd zijn",
+    "failedUpdate": "Planning bijwerken mislukt",
+    "splitCreated": "Taak beslaat meerdere dagen — vervolg aangemaakt voor de volgende dag"
   }
   }
 }
 }

+ 7 - 1
messages/nl/settings.json

@@ -362,7 +362,13 @@
   "workshop": {
   "workshop": {
     "title": "Werkplaatsstandaarden",
     "title": "Werkplaatsstandaarden",
     "description": "Standaardwaarden bij het aanmaken van nieuwe servicerecords.",
     "description": "Standaardwaarden bij het aanmaken van nieuwe servicerecords.",
-    "defaultTechnician": "Standaard monteursnaam", "technicianPlaceholder": "Jan Jansen",
+    "defaultTechnician": "Standaard monteursnaam", "technicianPlaceholder": "Zoek monteur...",
+    "failedCreateTech": "Kan monteur niet aanmaken",
+    "creating": "Aanmaken...",
+    "createTechnician": "\"{name}\" aanmaken",
+    "newTechPlaceholder": "Naam monteur",
+    "addTech": "Toevoegen",
+    "addNewTech": "+ Nieuw toevoegen",
     "defaultLaborRate": "Standaard uurtarief", "laborRatePlaceholder": "75,00",
     "defaultLaborRate": "Standaard uurtarief", "laborRatePlaceholder": "75,00",
     "workingHours": "Werkuren", "workingHoursPlaceholder": "Ma-Vr 8:00 - 17:00",
     "workingHours": "Werkuren", "workingHoursPlaceholder": "Ma-Vr 8:00 - 17:00",
     "unitsTitle": "Eenheden", "unitsDescription": "Kies tussen metrisch (km, liters) en imperiaal (mijlen, gallons).",
     "unitsTitle": "Eenheden", "unitsDescription": "Kies tussen metrisch (km, liters) en imperiaal (mijlen, gallons).",

+ 5 - 1
messages/nl/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Vandaag",
     "today": "Vandaag",
+    "day": "Dag",
+    "week": "Week",
     "presenter": "Presentator",
     "presenter": "Presentator",
     "addTechnician": "Technicus toevoegen"
     "addTechnician": "Technicus toevoegen"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Voeg een technicus toe om werk op het bord in te plannen.",
     "noTechniciansDescription": "Voeg een technicus toe om werk op het bord in te plannen.",
     "failedMove": "Toewijzing verplaatsen mislukt",
     "failedMove": "Toewijzing verplaatsen mislukt",
     "failedAssign": "Taak toewijzen mislukt",
     "failedAssign": "Taak toewijzen mislukt",
-    "failedRemove": "Toewijzing verwijderen mislukt"
+    "failedRemove": "Toewijzing verwijderen mislukt",
+    "createNewWorkorder": "Nieuwe werkorder aanmaken"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Niet-toegewezen taken ({count})",
     "title": "Niet-toegewezen taken ({count})",
@@ -51,6 +54,7 @@
     "status": "Status",
     "status": "Status",
     "week": "Week",
     "week": "Week",
     "today": "Vandaag",
     "today": "Vandaag",
+    "timeline": "Tijdlijn",
     "live": "Live",
     "live": "Live",
     "disconnected": "Live-updates verbroken",
     "disconnected": "Live-updates verbroken",
     "noTechnicians": "Geen technici geconfigureerd",
     "noTechnicians": "Geen technici geconfigureerd",

+ 21 - 0
messages/pl/service.json

@@ -247,5 +247,26 @@
     "failedDelete": "Nie udało się usunąć pliku",
     "failedDelete": "Nie udało się usunąć pliku",
     "failedUpdate": "Nie udało się zaktualizować",
     "failedUpdate": "Nie udało się zaktualizować",
     "invoice": "Faktura"
     "invoice": "Faktura"
+  },
+  "schedule": {
+    "title": "Harmonogram",
+    "date": "Data",
+    "startTime": "Czas rozpoczęcia",
+    "endTime": "Czas zakończenia",
+    "technician": "Technik",
+    "selectTechnician": "Wybierz technika...",
+    "searchOrCreate": "Szukaj lub utwórz technika...",
+    "createTechnician": "Utwórz \"{name}\"",
+    "creating": "Tworzenie...",
+    "failedCreate": "Nie udało się utworzyć technika",
+    "addNew": "+ Dodaj nowego",
+    "add": "Dodaj",
+    "newTechPlaceholder": "Nazwa technika...",
+    "assign": "Przypisz do tablicy",
+    "assigned": "Przypisano do tablicy roboczej",
+    "failedAssign": "Przypisanie nie powiodło się",
+    "endBeforeStart": "Czas zakończenia musi być po czasie rozpoczęcia",
+    "failedUpdate": "Nie udało się zaktualizować harmonogramu",
+    "splitCreated": "Zadanie obejmuje wiele dni — kontynuacja utworzona na następny dzień"
   }
   }
 }
 }

+ 7 - 1
messages/pl/settings.json

@@ -468,7 +468,13 @@
     "title": "Domyslne warsztatu",
     "title": "Domyslne warsztatu",
     "description": "Domyslne wartosci przy tworzeniu nowych rekordow serwisowych.",
     "description": "Domyslne wartosci przy tworzeniu nowych rekordow serwisowych.",
     "defaultTechnician": "Domyslna nazwa technika",
     "defaultTechnician": "Domyslna nazwa technika",
-    "technicianPlaceholder": "Jan Kowalski",
+    "technicianPlaceholder": "Szukaj technika...",
+    "failedCreateTech": "Nie udało się utworzyć technika",
+    "creating": "Tworzenie...",
+    "createTechnician": "Utwórz \"{name}\"",
+    "newTechPlaceholder": "Nazwa technika",
+    "addTech": "Dodaj",
+    "addNewTech": "+ Dodaj nowego",
     "defaultLaborRate": "Domyslna stawka za godzine",
     "defaultLaborRate": "Domyslna stawka za godzine",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
     "workingHours": "Godziny pracy",
     "workingHours": "Godziny pracy",

+ 5 - 1
messages/pl/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Dzisiaj",
     "today": "Dzisiaj",
+    "day": "Dzień",
+    "week": "Tydzień",
     "presenter": "Prezenter",
     "presenter": "Prezenter",
     "addTechnician": "Dodaj Technika"
     "addTechnician": "Dodaj Technika"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Dodaj technika, aby rozpocząć planowanie pracy na tablicy.",
     "noTechniciansDescription": "Dodaj technika, aby rozpocząć planowanie pracy na tablicy.",
     "failedMove": "Nie udało się przenieść przydzielenia",
     "failedMove": "Nie udało się przenieść przydzielenia",
     "failedAssign": "Nie udało się przydzielić zadania",
     "failedAssign": "Nie udało się przydzielić zadania",
-    "failedRemove": "Nie udało się usunąć przydzielenia"
+    "failedRemove": "Nie udało się usunąć przydzielenia",
+    "createNewWorkorder": "Utwórz nowe zlecenie pracy"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Nieprzyzdzielone Zadania ({count})",
     "title": "Nieprzyzdzielone Zadania ({count})",
@@ -51,6 +54,7 @@
     "status": "Status",
     "status": "Status",
     "week": "Tydzień",
     "week": "Tydzień",
     "today": "Dzisiaj",
     "today": "Dzisiaj",
+    "timeline": "Oś czasu",
     "live": "Na Żywo",
     "live": "Na Żywo",
     "disconnected": "Aktualizacje na żywo rozłączone",
     "disconnected": "Aktualizacje na żywo rozłączone",
     "noTechnicians": "Brak skonfigurowanych techników",
     "noTechnicians": "Brak skonfigurowanych techników",

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

@@ -247,5 +247,26 @@
     "failedDelete": "Falha ao deletar arquivo",
     "failedDelete": "Falha ao deletar arquivo",
     "failedUpdate": "Falha ao atualizar",
     "failedUpdate": "Falha ao atualizar",
     "invoice": "Fatura"
     "invoice": "Fatura"
+  },
+  "schedule": {
+    "title": "Agendamento",
+    "date": "Data",
+    "startTime": "Hora de início",
+    "endTime": "Hora de término",
+    "technician": "Técnico",
+    "selectTechnician": "Selecionar técnico...",
+    "searchOrCreate": "Buscar ou criar técnico...",
+    "createTechnician": "Criar \"{name}\"",
+    "creating": "Criando...",
+    "failedCreate": "Falha ao criar técnico",
+    "addNew": "+ Adicionar novo",
+    "add": "Adicionar",
+    "newTechPlaceholder": "Nome do técnico...",
+    "assign": "Atribuir ao quadro",
+    "assigned": "Atribuído ao quadro de trabalho",
+    "failedAssign": "Falha ao atribuir",
+    "endBeforeStart": "A hora de término deve ser após a hora de início",
+    "failedUpdate": "Falha ao atualizar o agendamento",
+    "splitCreated": "O trabalho abrange vários dias — continuação criada para o dia seguinte"
   }
   }
 }
 }

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

@@ -492,7 +492,13 @@
     "title": "Padroes da oficina",
     "title": "Padroes da oficina",
     "description": "Valores padrao ao criar novos registros de servico.",
     "description": "Valores padrao ao criar novos registros de servico.",
     "defaultTechnician": "Nome do tecnico padrao",
     "defaultTechnician": "Nome do tecnico padrao",
-    "technicianPlaceholder": "Joao Silva",
+    "technicianPlaceholder": "Buscar técnico...",
+    "failedCreateTech": "Falha ao criar técnico",
+    "creating": "Criando...",
+    "createTechnician": "Criar \"{name}\"",
+    "newTechPlaceholder": "Nome do técnico",
+    "addTech": "Adicionar",
+    "addNewTech": "+ Adicionar novo",
     "defaultLaborRate": "Taxa de mao de obra padrao (por hora)",
     "defaultLaborRate": "Taxa de mao de obra padrao (por hora)",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
     "workingHours": "Horario de funcionamento",
     "workingHours": "Horario de funcionamento",

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

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Hoje",
     "today": "Hoje",
+    "day": "Dia",
+    "week": "Semana",
     "presenter": "Apresentador",
     "presenter": "Apresentador",
     "addTechnician": "Adicionar Técnico"
     "addTechnician": "Adicionar Técnico"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Adicione um técnico para começar a agendar trabalho no quadro.",
     "noTechniciansDescription": "Adicione um técnico para começar a agendar trabalho no quadro.",
     "failedMove": "Falha ao mover atribuição",
     "failedMove": "Falha ao mover atribuição",
     "failedAssign": "Falha ao atribuir trabalho",
     "failedAssign": "Falha ao atribuir trabalho",
-    "failedRemove": "Falha ao remover atribuição"
+    "failedRemove": "Falha ao remover atribuição",
+    "createNewWorkorder": "Criar nova ordem de serviço"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Trabalhos Não Atribuídos ({count})",
     "title": "Trabalhos Não Atribuídos ({count})",
@@ -51,6 +54,7 @@
     "status": "Status",
     "status": "Status",
     "week": "Semana",
     "week": "Semana",
     "today": "Hoje",
     "today": "Hoje",
+    "timeline": "Linha do tempo",
     "live": "Ao Vivo",
     "live": "Ao Vivo",
     "disconnected": "Atualizações ao vivo desconectadas",
     "disconnected": "Atualizações ao vivo desconectadas",
     "noTechnicians": "Nenhum técnico configurado",
     "noTechnicians": "Nenhum técnico configurado",

+ 21 - 0
messages/tr/service.json

@@ -247,5 +247,26 @@
     "failedDelete": "Dosya silinemedi",
     "failedDelete": "Dosya silinemedi",
     "failedUpdate": "Güncellenemedi",
     "failedUpdate": "Güncellenemedi",
     "invoice": "Fatura"
     "invoice": "Fatura"
+  },
+  "schedule": {
+    "title": "Zamanlama",
+    "date": "Tarih",
+    "startTime": "Başlangıç Saati",
+    "endTime": "Bitiş Saati",
+    "technician": "Teknisyen",
+    "selectTechnician": "Teknisyen seçin...",
+    "searchOrCreate": "Teknisyen ara veya oluştur...",
+    "createTechnician": "\"{name}\" oluştur",
+    "creating": "Oluşturuluyor...",
+    "failedCreate": "Teknisyen oluşturulamadı",
+    "addNew": "+ Yeni Ekle",
+    "add": "Ekle",
+    "newTechPlaceholder": "Teknisyen adı...",
+    "assign": "Panoya ata",
+    "assigned": "Çalışma panosuna atandı",
+    "failedAssign": "Atama başarısız",
+    "endBeforeStart": "Bitiş saati başlangıç saatinden sonra olmalıdır",
+    "failedUpdate": "Zamanlama güncellenemedi",
+    "splitCreated": "İş birden fazla güne yayılıyor — sonraki gün için devam oluşturuldu"
   }
   }
 }
 }

+ 7 - 1
messages/tr/settings.json

@@ -492,7 +492,13 @@
     "title": "Atölye Varsayılanları",
     "title": "Atölye Varsayılanları",
     "description": "Yeni servis kayıtları oluşturulurken kullanılan varsayılan değerler.",
     "description": "Yeni servis kayıtları oluşturulurken kullanılan varsayılan değerler.",
     "defaultTechnician": "Varsayılan Teknisyen Adı",
     "defaultTechnician": "Varsayılan Teknisyen Adı",
-    "technicianPlaceholder": "Ahmet Yılmaz",
+    "technicianPlaceholder": "Teknisyen ara...",
+    "failedCreateTech": "Teknisyen oluşturulamadı",
+    "creating": "Oluşturuluyor...",
+    "createTechnician": "\"{name}\" oluştur",
+    "newTechPlaceholder": "Teknisyen adı",
+    "addTech": "Ekle",
+    "addNewTech": "+ Yeni ekle",
     "defaultLaborRate": "Varsayılan İşçilik Ücreti (saat başı)",
     "defaultLaborRate": "Varsayılan İşçilik Ücreti (saat başı)",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
     "workingHours": "Çalışma Saatleri",
     "workingHours": "Çalışma Saatleri",

+ 5 - 1
messages/tr/workBoard.json

@@ -4,6 +4,8 @@
   },
   },
   "toolbar": {
   "toolbar": {
     "today": "Bugün",
     "today": "Bugün",
+    "day": "Gün",
+    "week": "Hafta",
     "presenter": "Sunucu",
     "presenter": "Sunucu",
     "addTechnician": "Teknisyen Ekle"
     "addTechnician": "Teknisyen Ekle"
   },
   },
@@ -21,7 +23,8 @@
     "noTechniciansDescription": "Panoda iş planlamaya başlamak için bir teknisyen ekleyin.",
     "noTechniciansDescription": "Panoda iş planlamaya başlamak için bir teknisyen ekleyin.",
     "failedMove": "Atama taşınamadı",
     "failedMove": "Atama taşınamadı",
     "failedAssign": "İş atanmadı",
     "failedAssign": "İş atanmadı",
-    "failedRemove": "Atama kaldırılamadı"
+    "failedRemove": "Atama kaldırılamadı",
+    "createNewWorkorder": "Yeni iş emri oluştur"
   },
   },
   "unassigned": {
   "unassigned": {
     "title": "Atanmamış İşler ({count})",
     "title": "Atanmamış İşler ({count})",
@@ -51,6 +54,7 @@
     "status": "Durum",
     "status": "Durum",
     "week": "Hafta",
     "week": "Hafta",
     "today": "Bugün",
     "today": "Bugün",
+    "timeline": "Zaman çizelgesi",
     "live": "Canlı",
     "live": "Canlı",
     "disconnected": "Canlı güncellemeler kesildi",
     "disconnected": "Canlı güncellemeler kesildi",
     "noTechnicians": "Hiçbir teknisyen yapılandırılmadı",
     "noTechnicians": "Hiçbir teknisyen yapılandırılmadı",

+ 973 - 0
prisma/migrations/0_init/migration.sql

@@ -0,0 +1,973 @@
+-- Idempotent baseline migration: creates all tables, indexes, and foreign keys
+-- Safe to run on both fresh and existing databases
+
+CREATE SCHEMA IF NOT EXISTS "public";
+
+-- CreateTable
+CREATE TABLE IF NOT EXISTS "users" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "email" TEXT NOT NULL,
+    "emailVerified" BOOLEAN NOT NULL DEFAULT false,
+    "image" TEXT,
+    "isSuperAdmin" BOOLEAN NOT NULL DEFAULT false,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "twoFactorEnabled" BOOLEAN NOT NULL DEFAULT false,
+    "termsAcceptedAt" TIMESTAMP(3),
+    "lastLogin" TIMESTAMP(3),
+    "lastSeen" TIMESTAMP(3),
+    CONSTRAINT "users_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "app_settings" (
+    "id" TEXT NOT NULL,
+    "key" TEXT NOT NULL,
+    "value" TEXT NOT NULL,
+    "userId" TEXT NOT NULL,
+    "organizationId" TEXT,
+    CONSTRAINT "app_settings_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "sessions" (
+    "id" TEXT NOT NULL,
+    "expiresAt" TIMESTAMP(3) NOT NULL,
+    "token" TEXT NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "ipAddress" TEXT,
+    "userAgent" TEXT,
+    "userId" TEXT NOT NULL,
+    CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "accounts" (
+    "id" TEXT NOT NULL,
+    "accountId" TEXT NOT NULL,
+    "providerId" TEXT NOT NULL,
+    "userId" TEXT NOT NULL,
+    "accessToken" TEXT,
+    "refreshToken" TEXT,
+    "idToken" TEXT,
+    "accessTokenExpiresAt" TIMESTAMP(3),
+    "refreshTokenExpiresAt" TIMESTAMP(3),
+    "scope" TEXT,
+    "password" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "verifications" (
+    "id" TEXT NOT NULL,
+    "identifier" TEXT NOT NULL,
+    "value" TEXT NOT NULL,
+    "expiresAt" TIMESTAMP(3) NOT NULL,
+    "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3),
+    CONSTRAINT "verifications_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "two_factor" (
+    "id" TEXT NOT NULL,
+    "secret" TEXT NOT NULL,
+    "backupCodes" TEXT NOT NULL,
+    "userId" TEXT NOT NULL,
+    CONSTRAINT "two_factor_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "passkeys" (
+    "id" TEXT NOT NULL,
+    "name" TEXT,
+    "publicKey" TEXT NOT NULL,
+    "userId" TEXT NOT NULL,
+    "credentialID" TEXT NOT NULL,
+    "counter" INTEGER NOT NULL,
+    "deviceType" TEXT NOT NULL,
+    "backedUp" BOOLEAN NOT NULL,
+    "transports" TEXT,
+    "aaguid" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    CONSTRAINT "passkeys_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "vehicles" (
+    "id" TEXT NOT NULL,
+    "make" TEXT NOT NULL,
+    "model" TEXT NOT NULL,
+    "year" INTEGER NOT NULL,
+    "vin" TEXT,
+    "licensePlate" TEXT,
+    "color" TEXT,
+    "mileage" INTEGER NOT NULL DEFAULT 0,
+    "fuelType" TEXT DEFAULT 'gasoline',
+    "transmission" TEXT DEFAULT 'automatic',
+    "engineSize" TEXT,
+    "purchaseDate" TIMESTAMP(3),
+    "purchasePrice" DOUBLE PRECISION,
+    "imageUrl" TEXT,
+    "isArchived" BOOLEAN NOT NULL DEFAULT false,
+    "archiveReason" TEXT,
+    "maintenanceDismissed" BOOLEAN NOT NULL DEFAULT false,
+    "maintenanceDismissedAt" TIMESTAMP(3),
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "userId" TEXT NOT NULL,
+    "organizationId" TEXT,
+    "customerId" TEXT,
+    CONSTRAINT "vehicles_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "service_records" (
+    "id" TEXT NOT NULL,
+    "title" TEXT NOT NULL,
+    "description" TEXT,
+    "type" TEXT NOT NULL DEFAULT 'maintenance',
+    "status" TEXT NOT NULL DEFAULT 'completed',
+    "cost" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "mileage" INTEGER,
+    "serviceDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "shopName" TEXT,
+    "techName" TEXT,
+    "parts" TEXT,
+    "laborHours" DOUBLE PRECISION,
+    "diagnosticNotes" TEXT,
+    "invoiceNotes" TEXT,
+    "subtotal" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "taxAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "totalAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "invoiceNumber" TEXT,
+    "discountType" TEXT,
+    "discountValue" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "discountAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "manuallyPaid" BOOLEAN NOT NULL DEFAULT false,
+    "publicToken" TEXT,
+    "sharedAt" TIMESTAMP(3),
+    "viewCount" INTEGER NOT NULL DEFAULT 0,
+    "lastViewedAt" TIMESTAMP(3),
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    CONSTRAINT "service_records_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "notes" (
+    "id" TEXT NOT NULL,
+    "title" TEXT NOT NULL,
+    "content" TEXT NOT NULL,
+    "isPinned" BOOLEAN NOT NULL DEFAULT false,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    CONSTRAINT "notes_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "fuel_logs" (
+    "id" TEXT NOT NULL,
+    "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "mileage" INTEGER NOT NULL,
+    "gallons" DOUBLE PRECISION NOT NULL,
+    "pricePerGallon" DOUBLE PRECISION NOT NULL,
+    "totalCost" DOUBLE PRECISION NOT NULL,
+    "isFillUp" BOOLEAN NOT NULL DEFAULT true,
+    "station" TEXT,
+    "notes" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    CONSTRAINT "fuel_logs_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "reminders" (
+    "id" TEXT NOT NULL,
+    "title" TEXT NOT NULL,
+    "description" TEXT,
+    "dueDate" TIMESTAMP(3),
+    "dueMileage" INTEGER,
+    "isCompleted" BOOLEAN NOT NULL DEFAULT false,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    CONSTRAINT "reminders_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "customers" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "email" TEXT,
+    "phone" TEXT,
+    "address" TEXT,
+    "company" TEXT,
+    "notes" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "userId" TEXT NOT NULL,
+    "organizationId" TEXT,
+    CONSTRAINT "customers_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "payments" (
+    "id" TEXT NOT NULL,
+    "amount" DOUBLE PRECISION NOT NULL,
+    "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "method" TEXT NOT NULL DEFAULT 'other',
+    "note" TEXT,
+    "provider" TEXT,
+    "externalId" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "serviceRecordId" TEXT NOT NULL,
+    CONSTRAINT "payments_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "service_attachments" (
+    "id" TEXT NOT NULL,
+    "fileName" TEXT NOT NULL,
+    "fileUrl" TEXT NOT NULL,
+    "fileType" TEXT NOT NULL,
+    "fileSize" INTEGER NOT NULL,
+    "category" TEXT NOT NULL DEFAULT 'diagnostic',
+    "description" TEXT,
+    "includeInInvoice" BOOLEAN NOT NULL DEFAULT true,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "serviceRecordId" TEXT NOT NULL,
+    CONSTRAINT "service_attachments_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "service_parts" (
+    "id" TEXT NOT NULL,
+    "partNumber" TEXT,
+    "name" TEXT NOT NULL,
+    "quantity" DOUBLE PRECISION NOT NULL DEFAULT 1,
+    "unitPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "total" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "serviceRecordId" TEXT NOT NULL,
+    CONSTRAINT "service_parts_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "service_labor" (
+    "id" TEXT NOT NULL,
+    "description" TEXT NOT NULL,
+    "hours" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "rate" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "total" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "serviceRecordId" TEXT NOT NULL,
+    CONSTRAINT "service_labor_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "inventory_parts" (
+    "id" TEXT NOT NULL,
+    "partNumber" TEXT,
+    "name" TEXT NOT NULL,
+    "description" TEXT,
+    "category" TEXT,
+    "quantity" INTEGER NOT NULL DEFAULT 0,
+    "minQuantity" INTEGER NOT NULL DEFAULT 0,
+    "unitCost" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "sellPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "supplier" TEXT,
+    "supplierPhone" TEXT,
+    "supplierEmail" TEXT,
+    "supplierUrl" TEXT,
+    "imageUrl" TEXT,
+    "location" TEXT,
+    "isArchived" BOOLEAN NOT NULL DEFAULT false,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "userId" TEXT NOT NULL,
+    "organizationId" TEXT,
+    CONSTRAINT "inventory_parts_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "quotes" (
+    "id" TEXT NOT NULL,
+    "quoteNumber" TEXT,
+    "title" TEXT NOT NULL,
+    "description" TEXT,
+    "status" TEXT NOT NULL DEFAULT 'draft',
+    "validUntil" TIMESTAMP(3),
+    "subtotal" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "taxAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "discountType" TEXT,
+    "discountValue" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "discountAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "totalAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "notes" TEXT,
+    "publicToken" TEXT,
+    "sharedAt" TIMESTAMP(3),
+    "viewCount" INTEGER NOT NULL DEFAULT 0,
+    "lastViewedAt" TIMESTAMP(3),
+    "customerMessage" TEXT,
+    "convertedToId" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "userId" TEXT NOT NULL,
+    "organizationId" TEXT,
+    "customerId" TEXT,
+    "vehicleId" TEXT,
+    "inspectionId" TEXT,
+    CONSTRAINT "quotes_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "quote_attachments" (
+    "id" TEXT NOT NULL,
+    "fileName" TEXT NOT NULL,
+    "fileUrl" TEXT NOT NULL,
+    "fileType" TEXT NOT NULL,
+    "fileSize" INTEGER NOT NULL,
+    "category" TEXT NOT NULL DEFAULT 'image',
+    "description" TEXT,
+    "includeInInvoice" BOOLEAN NOT NULL DEFAULT true,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "quoteId" TEXT NOT NULL,
+    CONSTRAINT "quote_attachments_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "quote_parts" (
+    "id" TEXT NOT NULL,
+    "partNumber" TEXT,
+    "name" TEXT NOT NULL,
+    "quantity" DOUBLE PRECISION NOT NULL DEFAULT 1,
+    "unitPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "total" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "excluded" BOOLEAN NOT NULL DEFAULT false,
+    "quoteId" TEXT NOT NULL,
+    CONSTRAINT "quote_parts_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "quote_labor" (
+    "id" TEXT NOT NULL,
+    "description" TEXT NOT NULL,
+    "hours" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "rate" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "total" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "excluded" BOOLEAN NOT NULL DEFAULT false,
+    "quoteId" TEXT NOT NULL,
+    CONSTRAINT "quote_labor_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "custom_field_definitions" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "label" TEXT NOT NULL,
+    "fieldType" TEXT NOT NULL DEFAULT 'text',
+    "options" TEXT,
+    "required" BOOLEAN NOT NULL DEFAULT false,
+    "entityType" TEXT NOT NULL,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "isActive" BOOLEAN NOT NULL DEFAULT true,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "userId" TEXT NOT NULL,
+    "organizationId" TEXT,
+    CONSTRAINT "custom_field_definitions_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "custom_field_values" (
+    "id" TEXT NOT NULL,
+    "value" TEXT NOT NULL,
+    "entityId" TEXT NOT NULL,
+    "entityType" TEXT NOT NULL,
+    "fieldId" TEXT NOT NULL,
+    CONSTRAINT "custom_field_values_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "organizations" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "portalSlug" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "organization_members" (
+    "id" TEXT NOT NULL,
+    "role" TEXT NOT NULL DEFAULT 'member',
+    "userId" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    "roleId" TEXT,
+    CONSTRAINT "organization_members_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "roles" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "isAdmin" BOOLEAN NOT NULL DEFAULT false,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "permissions" (
+    "id" TEXT NOT NULL,
+    "action" TEXT NOT NULL,
+    "subject" TEXT NOT NULL,
+    "roleId" TEXT NOT NULL,
+    CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "system_settings" (
+    "id" TEXT NOT NULL,
+    "key" TEXT NOT NULL,
+    "value" TEXT NOT NULL,
+    CONSTRAINT "system_settings_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "team_invitations" (
+    "id" TEXT NOT NULL,
+    "email" TEXT NOT NULL,
+    "role" TEXT NOT NULL DEFAULT 'member',
+    "token" TEXT NOT NULL,
+    "status" TEXT NOT NULL DEFAULT 'pending',
+    "expiresAt" TIMESTAMP(3) NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "organizationId" TEXT NOT NULL,
+    "invitedById" TEXT NOT NULL,
+    "roleId" TEXT,
+    CONSTRAINT "team_invitations_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "subscription_plans" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "stripePriceId" TEXT,
+    "price" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "interval" TEXT NOT NULL DEFAULT 'month',
+    "maxMembers" INTEGER NOT NULL DEFAULT 1,
+    "features" TEXT,
+    "isActive" BOOLEAN NOT NULL DEFAULT true,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    CONSTRAINT "subscription_plans_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "subscriptions" (
+    "id" TEXT NOT NULL,
+    "status" TEXT NOT NULL DEFAULT 'active',
+    "stripeSubscriptionId" TEXT,
+    "stripeCustomerId" TEXT,
+    "currentPeriodStart" TIMESTAMP(3),
+    "currentPeriodEnd" TIMESTAMP(3),
+    "cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    "planId" TEXT NOT NULL,
+    CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "recurring_invoices" (
+    "id" TEXT NOT NULL,
+    "title" TEXT NOT NULL,
+    "description" TEXT,
+    "frequency" TEXT NOT NULL,
+    "nextRunDate" TIMESTAMP(3) NOT NULL,
+    "endDate" TIMESTAMP(3),
+    "isActive" BOOLEAN NOT NULL DEFAULT true,
+    "lastRunAt" TIMESTAMP(3),
+    "runCount" INTEGER NOT NULL DEFAULT 0,
+    "type" TEXT NOT NULL DEFAULT 'maintenance',
+    "cost" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "invoiceNotes" TEXT,
+    "vehicleId" TEXT NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    CONSTRAINT "recurring_invoices_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "recurring_parts" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "partNumber" TEXT,
+    "quantity" INTEGER NOT NULL DEFAULT 1,
+    "unitPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "recurringInvoiceId" TEXT NOT NULL,
+    CONSTRAINT "recurring_parts_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "recurring_labor" (
+    "id" TEXT NOT NULL,
+    "description" TEXT NOT NULL,
+    "hours" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "rate" DOUBLE PRECISION NOT NULL DEFAULT 0,
+    "recurringInvoiceId" TEXT NOT NULL,
+    CONSTRAINT "recurring_labor_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "inspection_templates" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "description" TEXT,
+    "isDefault" BOOLEAN NOT NULL DEFAULT false,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    CONSTRAINT "inspection_templates_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "inspection_template_sections" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "templateId" TEXT NOT NULL,
+    CONSTRAINT "inspection_template_sections_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "inspection_template_items" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "sectionId" TEXT NOT NULL,
+    CONSTRAINT "inspection_template_items_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "inspections" (
+    "id" TEXT NOT NULL,
+    "status" TEXT NOT NULL DEFAULT 'in_progress',
+    "mileage" INTEGER,
+    "notes" TEXT,
+    "publicToken" TEXT,
+    "completedAt" TIMESTAMP(3),
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    "templateId" TEXT NOT NULL,
+    "technicianId" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    CONSTRAINT "inspections_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "inspection_items" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "section" TEXT NOT NULL,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "condition" TEXT NOT NULL DEFAULT 'not_inspected',
+    "notes" TEXT,
+    "imageUrls" TEXT[] DEFAULT ARRAY[]::TEXT[],
+    "inspectionId" TEXT NOT NULL,
+    CONSTRAINT "inspection_items_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "inspection_quote_requests" (
+    "id" TEXT NOT NULL,
+    "status" TEXT NOT NULL DEFAULT 'pending',
+    "message" TEXT,
+    "selectedItemIds" TEXT[],
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "inspectionId" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    CONSTRAINT "inspection_quote_requests_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "notifications" (
+    "id" TEXT NOT NULL,
+    "type" TEXT NOT NULL,
+    "title" TEXT NOT NULL,
+    "message" TEXT NOT NULL,
+    "entityType" TEXT NOT NULL,
+    "entityId" TEXT NOT NULL,
+    "entityUrl" TEXT NOT NULL,
+    "read" BOOLEAN NOT NULL DEFAULT false,
+    "organizationId" TEXT NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    CONSTRAINT "notifications_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "sms_messages" (
+    "id" TEXT NOT NULL,
+    "direction" TEXT NOT NULL,
+    "fromNumber" TEXT NOT NULL,
+    "toNumber" TEXT NOT NULL,
+    "body" TEXT NOT NULL,
+    "status" TEXT NOT NULL DEFAULT 'queued',
+    "providerMsgId" TEXT,
+    "errorMessage" TEXT,
+    "relatedEntityType" TEXT,
+    "relatedEntityId" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    "customerId" TEXT,
+    CONSTRAINT "sms_messages_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "technicians" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "color" TEXT NOT NULL DEFAULT '#3b82f6',
+    "isActive" BOOLEAN NOT NULL DEFAULT true,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "memberId" TEXT,
+    "organizationId" TEXT NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    CONSTRAINT "technicians_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "board_assignments" (
+    "id" TEXT NOT NULL,
+    "date" DATE NOT NULL,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "notes" TEXT,
+    "technicianId" TEXT NOT NULL,
+    "serviceRecordId" TEXT,
+    "inspectionId" TEXT,
+    "organizationId" TEXT NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    CONSTRAINT "board_assignments_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "customer_sessions" (
+    "id" TEXT NOT NULL,
+    "token" TEXT NOT NULL,
+    "expiresAt" TIMESTAMP(3) NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "customerId" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    CONSTRAINT "customer_sessions_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "customer_magic_links" (
+    "id" TEXT NOT NULL,
+    "token" TEXT NOT NULL,
+    "email" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    "expiresAt" TIMESTAMP(3) NOT NULL,
+    "usedAt" TIMESTAMP(3),
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    CONSTRAINT "customer_magic_links_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE IF NOT EXISTS "service_requests" (
+    "id" TEXT NOT NULL,
+    "description" TEXT NOT NULL,
+    "preferredDate" TIMESTAMP(3),
+    "status" TEXT NOT NULL DEFAULT 'pending',
+    "adminNotes" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "customerId" TEXT NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    CONSTRAINT "service_requests_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex (idempotent)
+CREATE UNIQUE INDEX IF NOT EXISTS "users_email_key" ON "users"("email");
+CREATE INDEX IF NOT EXISTS "app_settings_organizationId_idx" ON "app_settings"("organizationId");
+CREATE UNIQUE INDEX IF NOT EXISTS "app_settings_organizationId_key_key" ON "app_settings"("organizationId", "key");
+CREATE UNIQUE INDEX IF NOT EXISTS "sessions_token_key" ON "sessions"("token");
+CREATE INDEX IF NOT EXISTS "sessions_userId_idx" ON "sessions"("userId");
+CREATE INDEX IF NOT EXISTS "accounts_userId_idx" ON "accounts"("userId");
+CREATE UNIQUE INDEX IF NOT EXISTS "verifications_identifier_key" ON "verifications"("identifier");
+CREATE UNIQUE INDEX IF NOT EXISTS "two_factor_userId_key" ON "two_factor"("userId");
+CREATE UNIQUE INDEX IF NOT EXISTS "passkeys_credentialID_key" ON "passkeys"("credentialID");
+CREATE INDEX IF NOT EXISTS "passkeys_userId_idx" ON "passkeys"("userId");
+CREATE INDEX IF NOT EXISTS "vehicles_organizationId_idx" ON "vehicles"("organizationId");
+CREATE UNIQUE INDEX IF NOT EXISTS "service_records_publicToken_key" ON "service_records"("publicToken");
+CREATE INDEX IF NOT EXISTS "service_records_vehicleId_idx" ON "service_records"("vehicleId");
+CREATE INDEX IF NOT EXISTS "notes_vehicleId_idx" ON "notes"("vehicleId");
+CREATE INDEX IF NOT EXISTS "fuel_logs_vehicleId_idx" ON "fuel_logs"("vehicleId");
+CREATE INDEX IF NOT EXISTS "reminders_vehicleId_idx" ON "reminders"("vehicleId");
+CREATE INDEX IF NOT EXISTS "customers_organizationId_idx" ON "customers"("organizationId");
+CREATE INDEX IF NOT EXISTS "payments_serviceRecordId_idx" ON "payments"("serviceRecordId");
+CREATE INDEX IF NOT EXISTS "service_attachments_serviceRecordId_idx" ON "service_attachments"("serviceRecordId");
+CREATE INDEX IF NOT EXISTS "service_parts_serviceRecordId_idx" ON "service_parts"("serviceRecordId");
+CREATE INDEX IF NOT EXISTS "service_labor_serviceRecordId_idx" ON "service_labor"("serviceRecordId");
+CREATE INDEX IF NOT EXISTS "inventory_parts_organizationId_idx" ON "inventory_parts"("organizationId");
+CREATE INDEX IF NOT EXISTS "quotes_organizationId_idx" ON "quotes"("organizationId");
+CREATE INDEX IF NOT EXISTS "quote_attachments_quoteId_idx" ON "quote_attachments"("quoteId");
+CREATE INDEX IF NOT EXISTS "quote_parts_quoteId_idx" ON "quote_parts"("quoteId");
+CREATE INDEX IF NOT EXISTS "quote_labor_quoteId_idx" ON "quote_labor"("quoteId");
+CREATE INDEX IF NOT EXISTS "custom_field_definitions_organizationId_idx" ON "custom_field_definitions"("organizationId");
+CREATE UNIQUE INDEX IF NOT EXISTS "custom_field_definitions_organizationId_name_entityType_key" ON "custom_field_definitions"("organizationId", "name", "entityType");
+CREATE INDEX IF NOT EXISTS "custom_field_values_fieldId_idx" ON "custom_field_values"("fieldId");
+CREATE UNIQUE INDEX IF NOT EXISTS "custom_field_values_fieldId_entityId_key" ON "custom_field_values"("fieldId", "entityId");
+CREATE UNIQUE INDEX IF NOT EXISTS "organizations_portalSlug_key" ON "organizations"("portalSlug");
+CREATE UNIQUE INDEX IF NOT EXISTS "organization_members_userId_organizationId_key" ON "organization_members"("userId", "organizationId");
+CREATE UNIQUE INDEX IF NOT EXISTS "roles_organizationId_name_key" ON "roles"("organizationId", "name");
+CREATE INDEX IF NOT EXISTS "permissions_roleId_idx" ON "permissions"("roleId");
+CREATE UNIQUE INDEX IF NOT EXISTS "permissions_roleId_action_subject_key" ON "permissions"("roleId", "action", "subject");
+CREATE UNIQUE INDEX IF NOT EXISTS "system_settings_key_key" ON "system_settings"("key");
+CREATE UNIQUE INDEX IF NOT EXISTS "team_invitations_token_key" ON "team_invitations"("token");
+CREATE INDEX IF NOT EXISTS "team_invitations_token_idx" ON "team_invitations"("token");
+CREATE UNIQUE INDEX IF NOT EXISTS "team_invitations_email_organizationId_key" ON "team_invitations"("email", "organizationId");
+CREATE UNIQUE INDEX IF NOT EXISTS "subscription_plans_stripePriceId_key" ON "subscription_plans"("stripePriceId");
+CREATE UNIQUE INDEX IF NOT EXISTS "subscriptions_stripeSubscriptionId_key" ON "subscriptions"("stripeSubscriptionId");
+CREATE UNIQUE INDEX IF NOT EXISTS "subscriptions_organizationId_key" ON "subscriptions"("organizationId");
+CREATE INDEX IF NOT EXISTS "recurring_invoices_vehicleId_idx" ON "recurring_invoices"("vehicleId");
+CREATE INDEX IF NOT EXISTS "recurring_invoices_nextRunDate_isActive_idx" ON "recurring_invoices"("nextRunDate", "isActive");
+CREATE INDEX IF NOT EXISTS "inspection_templates_organizationId_idx" ON "inspection_templates"("organizationId");
+CREATE INDEX IF NOT EXISTS "inspection_template_sections_templateId_idx" ON "inspection_template_sections"("templateId");
+CREATE INDEX IF NOT EXISTS "inspection_template_items_sectionId_idx" ON "inspection_template_items"("sectionId");
+CREATE UNIQUE INDEX IF NOT EXISTS "inspections_publicToken_key" ON "inspections"("publicToken");
+CREATE INDEX IF NOT EXISTS "inspections_vehicleId_idx" ON "inspections"("vehicleId");
+CREATE INDEX IF NOT EXISTS "inspections_organizationId_idx" ON "inspections"("organizationId");
+CREATE INDEX IF NOT EXISTS "inspection_items_inspectionId_idx" ON "inspection_items"("inspectionId");
+CREATE INDEX IF NOT EXISTS "inspection_quote_requests_organizationId_idx" ON "inspection_quote_requests"("organizationId");
+CREATE INDEX IF NOT EXISTS "inspection_quote_requests_inspectionId_idx" ON "inspection_quote_requests"("inspectionId");
+CREATE INDEX IF NOT EXISTS "notifications_organizationId_createdAt_idx" ON "notifications"("organizationId", "createdAt" DESC);
+CREATE INDEX IF NOT EXISTS "notifications_organizationId_read_idx" ON "notifications"("organizationId", "read");
+CREATE INDEX IF NOT EXISTS "sms_messages_organizationId_customerId_createdAt_idx" ON "sms_messages"("organizationId", "customerId", "createdAt" DESC);
+CREATE INDEX IF NOT EXISTS "sms_messages_organizationId_createdAt_idx" ON "sms_messages"("organizationId", "createdAt" DESC);
+CREATE INDEX IF NOT EXISTS "sms_messages_toNumber_organizationId_idx" ON "sms_messages"("toNumber", "organizationId");
+CREATE INDEX IF NOT EXISTS "technicians_organizationId_idx" ON "technicians"("organizationId");
+CREATE INDEX IF NOT EXISTS "board_assignments_organizationId_date_idx" ON "board_assignments"("organizationId", "date");
+CREATE INDEX IF NOT EXISTS "board_assignments_technicianId_date_idx" ON "board_assignments"("technicianId", "date");
+CREATE UNIQUE INDEX IF NOT EXISTS "board_assignments_serviceRecordId_date_key" ON "board_assignments"("serviceRecordId", "date");
+CREATE UNIQUE INDEX IF NOT EXISTS "board_assignments_inspectionId_date_key" ON "board_assignments"("inspectionId", "date");
+CREATE UNIQUE INDEX IF NOT EXISTS "customer_sessions_token_key" ON "customer_sessions"("token");
+CREATE INDEX IF NOT EXISTS "customer_sessions_token_idx" ON "customer_sessions"("token");
+CREATE INDEX IF NOT EXISTS "customer_sessions_customerId_organizationId_idx" ON "customer_sessions"("customerId", "organizationId");
+CREATE UNIQUE INDEX IF NOT EXISTS "customer_magic_links_token_key" ON "customer_magic_links"("token");
+CREATE INDEX IF NOT EXISTS "customer_magic_links_token_idx" ON "customer_magic_links"("token");
+CREATE INDEX IF NOT EXISTS "service_requests_organizationId_status_idx" ON "service_requests"("organizationId", "status");
+CREATE INDEX IF NOT EXISTS "service_requests_customerId_idx" ON "service_requests"("customerId");
+
+-- AddForeignKey (idempotent: drop if exists, then add)
+DO $$ BEGIN
+  ALTER TABLE "app_settings" ADD CONSTRAINT "app_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "app_settings" ADD CONSTRAINT "app_settings_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "passkeys" ADD CONSTRAINT "passkeys_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "service_records" ADD CONSTRAINT "service_records_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "notes" ADD CONSTRAINT "notes_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "fuel_logs" ADD CONSTRAINT "fuel_logs_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "reminders" ADD CONSTRAINT "reminders_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "customers" ADD CONSTRAINT "customers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "customers" ADD CONSTRAINT "customers_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "payments" ADD CONSTRAINT "payments_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "service_attachments" ADD CONSTRAINT "service_attachments_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "service_parts" ADD CONSTRAINT "service_parts_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "service_labor" ADD CONSTRAINT "service_labor_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inventory_parts" ADD CONSTRAINT "inventory_parts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inventory_parts" ADD CONSTRAINT "inventory_parts_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "quotes" ADD CONSTRAINT "quotes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "quotes" ADD CONSTRAINT "quotes_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "quotes" ADD CONSTRAINT "quotes_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "quotes" ADD CONSTRAINT "quotes_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "quotes" ADD CONSTRAINT "quotes_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "quote_attachments" ADD CONSTRAINT "quote_attachments_quoteId_fkey" FOREIGN KEY ("quoteId") REFERENCES "quotes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "quote_parts" ADD CONSTRAINT "quote_parts_quoteId_fkey" FOREIGN KEY ("quoteId") REFERENCES "quotes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "quote_labor" ADD CONSTRAINT "quote_labor_quoteId_fkey" FOREIGN KEY ("quoteId") REFERENCES "quotes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "custom_field_definitions" ADD CONSTRAINT "custom_field_definitions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "custom_field_definitions" ADD CONSTRAINT "custom_field_definitions_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "custom_field_values" ADD CONSTRAINT "custom_field_values_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "custom_field_definitions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "roles" ADD CONSTRAINT "roles_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "permissions" ADD CONSTRAINT "permissions_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "team_invitations" ADD CONSTRAINT "team_invitations_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "team_invitations" ADD CONSTRAINT "team_invitations_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_planId_fkey" FOREIGN KEY ("planId") REFERENCES "subscription_plans"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "recurring_invoices" ADD CONSTRAINT "recurring_invoices_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "recurring_parts" ADD CONSTRAINT "recurring_parts_recurringInvoiceId_fkey" FOREIGN KEY ("recurringInvoiceId") REFERENCES "recurring_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "recurring_labor" ADD CONSTRAINT "recurring_labor_recurringInvoiceId_fkey" FOREIGN KEY ("recurringInvoiceId") REFERENCES "recurring_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inspection_templates" ADD CONSTRAINT "inspection_templates_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inspection_template_sections" ADD CONSTRAINT "inspection_template_sections_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "inspection_templates"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inspection_template_items" ADD CONSTRAINT "inspection_template_items_sectionId_fkey" FOREIGN KEY ("sectionId") REFERENCES "inspection_template_sections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inspections" ADD CONSTRAINT "inspections_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inspections" ADD CONSTRAINT "inspections_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "inspection_templates"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inspection_items" ADD CONSTRAINT "inspection_items_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "inspection_quote_requests" ADD CONSTRAINT "inspection_quote_requests_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "sms_messages" ADD CONSTRAINT "sms_messages_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "sms_messages" ADD CONSTRAINT "sms_messages_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "technicians" ADD CONSTRAINT "technicians_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "board_assignments" ADD CONSTRAINT "board_assignments_technicianId_fkey" FOREIGN KEY ("technicianId") REFERENCES "technicians"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "board_assignments" ADD CONSTRAINT "board_assignments_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "board_assignments" ADD CONSTRAINT "board_assignments_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "customer_sessions" ADD CONSTRAINT "customer_sessions_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "service_requests" ADD CONSTRAINT "service_requests_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN
+  ALTER TABLE "service_requests" ADD CONSTRAINT "service_requests_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;

+ 0 - 640
prisma/migrations/20260214135830_init/migration.sql

@@ -1,640 +0,0 @@
--- CreateTable
-CREATE TABLE "users" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "email" TEXT NOT NULL,
-    "emailVerified" BOOLEAN NOT NULL DEFAULT false,
-    "image" TEXT,
-    "isSuperAdmin" BOOLEAN NOT NULL DEFAULT false,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-
-    CONSTRAINT "users_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "app_settings" (
-    "id" TEXT NOT NULL,
-    "key" TEXT NOT NULL,
-    "value" TEXT NOT NULL,
-    "userId" TEXT NOT NULL,
-    "organizationId" TEXT,
-
-    CONSTRAINT "app_settings_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "sessions" (
-    "id" TEXT NOT NULL,
-    "expiresAt" TIMESTAMP(3) NOT NULL,
-    "token" TEXT NOT NULL,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "ipAddress" TEXT,
-    "userAgent" TEXT,
-    "userId" TEXT NOT NULL,
-
-    CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "accounts" (
-    "id" TEXT NOT NULL,
-    "accountId" TEXT NOT NULL,
-    "providerId" TEXT NOT NULL,
-    "userId" TEXT NOT NULL,
-    "accessToken" TEXT,
-    "refreshToken" TEXT,
-    "idToken" TEXT,
-    "accessTokenExpiresAt" TIMESTAMP(3),
-    "refreshTokenExpiresAt" TIMESTAMP(3),
-    "scope" TEXT,
-    "password" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-
-    CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "verifications" (
-    "id" TEXT NOT NULL,
-    "identifier" TEXT NOT NULL,
-    "value" TEXT NOT NULL,
-    "expiresAt" TIMESTAMP(3) NOT NULL,
-    "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3),
-
-    CONSTRAINT "verifications_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "vehicles" (
-    "id" TEXT NOT NULL,
-    "make" TEXT NOT NULL,
-    "model" TEXT NOT NULL,
-    "year" INTEGER NOT NULL,
-    "vin" TEXT,
-    "licensePlate" TEXT,
-    "color" TEXT,
-    "mileage" INTEGER NOT NULL DEFAULT 0,
-    "fuelType" TEXT DEFAULT 'gasoline',
-    "transmission" TEXT DEFAULT 'automatic',
-    "engineSize" TEXT,
-    "purchaseDate" TIMESTAMP(3),
-    "purchasePrice" DOUBLE PRECISION,
-    "imageUrl" TEXT,
-    "isArchived" BOOLEAN NOT NULL DEFAULT false,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "userId" TEXT NOT NULL,
-    "organizationId" TEXT,
-    "customerId" TEXT,
-
-    CONSTRAINT "vehicles_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "service_records" (
-    "id" TEXT NOT NULL,
-    "title" TEXT NOT NULL,
-    "description" TEXT,
-    "type" TEXT NOT NULL DEFAULT 'maintenance',
-    "status" TEXT NOT NULL DEFAULT 'completed',
-    "cost" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "mileage" INTEGER,
-    "serviceDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "shopName" TEXT,
-    "techName" TEXT,
-    "parts" TEXT,
-    "laborHours" DOUBLE PRECISION,
-    "diagnosticNotes" TEXT,
-    "invoiceNotes" TEXT,
-    "subtotal" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "taxAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "totalAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "invoiceNumber" TEXT,
-    "discountType" TEXT,
-    "discountValue" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "discountAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "publicToken" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "vehicleId" TEXT NOT NULL,
-
-    CONSTRAINT "service_records_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "notes" (
-    "id" TEXT NOT NULL,
-    "title" TEXT NOT NULL,
-    "content" TEXT NOT NULL,
-    "isPinned" BOOLEAN NOT NULL DEFAULT false,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "vehicleId" TEXT NOT NULL,
-
-    CONSTRAINT "notes_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "fuel_logs" (
-    "id" TEXT NOT NULL,
-    "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "mileage" INTEGER NOT NULL,
-    "gallons" DOUBLE PRECISION NOT NULL,
-    "pricePerGallon" DOUBLE PRECISION NOT NULL,
-    "totalCost" DOUBLE PRECISION NOT NULL,
-    "isFillUp" BOOLEAN NOT NULL DEFAULT true,
-    "station" TEXT,
-    "notes" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "vehicleId" TEXT NOT NULL,
-
-    CONSTRAINT "fuel_logs_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "reminders" (
-    "id" TEXT NOT NULL,
-    "title" TEXT NOT NULL,
-    "description" TEXT,
-    "dueDate" TIMESTAMP(3),
-    "dueMileage" INTEGER,
-    "isCompleted" BOOLEAN NOT NULL DEFAULT false,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "vehicleId" TEXT NOT NULL,
-
-    CONSTRAINT "reminders_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "customers" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "email" TEXT,
-    "phone" TEXT,
-    "address" TEXT,
-    "company" TEXT,
-    "notes" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "userId" TEXT NOT NULL,
-    "organizationId" TEXT,
-
-    CONSTRAINT "customers_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "payments" (
-    "id" TEXT NOT NULL,
-    "amount" DOUBLE PRECISION NOT NULL,
-    "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "method" TEXT NOT NULL DEFAULT 'other',
-    "note" TEXT,
-    "provider" TEXT,
-    "externalId" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "serviceRecordId" TEXT NOT NULL,
-
-    CONSTRAINT "payments_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "service_attachments" (
-    "id" TEXT NOT NULL,
-    "fileName" TEXT NOT NULL,
-    "fileUrl" TEXT NOT NULL,
-    "fileType" TEXT NOT NULL,
-    "fileSize" INTEGER NOT NULL,
-    "category" TEXT NOT NULL DEFAULT 'diagnostic',
-    "description" TEXT,
-    "includeInInvoice" BOOLEAN NOT NULL DEFAULT true,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "serviceRecordId" TEXT NOT NULL,
-
-    CONSTRAINT "service_attachments_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "service_parts" (
-    "id" TEXT NOT NULL,
-    "partNumber" TEXT,
-    "name" TEXT NOT NULL,
-    "quantity" DOUBLE PRECISION NOT NULL DEFAULT 1,
-    "unitPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "total" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "serviceRecordId" TEXT NOT NULL,
-
-    CONSTRAINT "service_parts_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "service_labor" (
-    "id" TEXT NOT NULL,
-    "description" TEXT NOT NULL,
-    "hours" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "rate" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "total" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "serviceRecordId" TEXT NOT NULL,
-
-    CONSTRAINT "service_labor_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "inventory_parts" (
-    "id" TEXT NOT NULL,
-    "partNumber" TEXT,
-    "name" TEXT NOT NULL,
-    "description" TEXT,
-    "category" TEXT,
-    "quantity" INTEGER NOT NULL DEFAULT 0,
-    "minQuantity" INTEGER NOT NULL DEFAULT 0,
-    "unitCost" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "supplier" TEXT,
-    "supplierPhone" TEXT,
-    "supplierEmail" TEXT,
-    "supplierUrl" TEXT,
-    "imageUrl" TEXT,
-    "location" TEXT,
-    "isArchived" BOOLEAN NOT NULL DEFAULT false,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "userId" TEXT NOT NULL,
-    "organizationId" TEXT,
-
-    CONSTRAINT "inventory_parts_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "quotes" (
-    "id" TEXT NOT NULL,
-    "quoteNumber" TEXT,
-    "title" TEXT NOT NULL,
-    "description" TEXT,
-    "status" TEXT NOT NULL DEFAULT 'draft',
-    "validUntil" TIMESTAMP(3),
-    "subtotal" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "taxAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "discountType" TEXT,
-    "discountValue" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "discountAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "totalAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "notes" TEXT,
-    "convertedToId" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "userId" TEXT NOT NULL,
-    "organizationId" TEXT,
-    "customerId" TEXT,
-    "vehicleId" TEXT,
-
-    CONSTRAINT "quotes_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "quote_parts" (
-    "id" TEXT NOT NULL,
-    "partNumber" TEXT,
-    "name" TEXT NOT NULL,
-    "quantity" DOUBLE PRECISION NOT NULL DEFAULT 1,
-    "unitPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "total" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "quoteId" TEXT NOT NULL,
-
-    CONSTRAINT "quote_parts_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "quote_labor" (
-    "id" TEXT NOT NULL,
-    "description" TEXT NOT NULL,
-    "hours" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "rate" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "total" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "quoteId" TEXT NOT NULL,
-
-    CONSTRAINT "quote_labor_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "custom_field_definitions" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "label" TEXT NOT NULL,
-    "fieldType" TEXT NOT NULL DEFAULT 'text',
-    "options" TEXT,
-    "required" BOOLEAN NOT NULL DEFAULT false,
-    "entityType" TEXT NOT NULL,
-    "sortOrder" INTEGER NOT NULL DEFAULT 0,
-    "isActive" BOOLEAN NOT NULL DEFAULT true,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "userId" TEXT NOT NULL,
-    "organizationId" TEXT,
-
-    CONSTRAINT "custom_field_definitions_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "custom_field_values" (
-    "id" TEXT NOT NULL,
-    "value" TEXT NOT NULL,
-    "entityId" TEXT NOT NULL,
-    "entityType" TEXT NOT NULL,
-    "fieldId" TEXT NOT NULL,
-
-    CONSTRAINT "custom_field_values_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "organizations" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-
-    CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "organization_members" (
-    "id" TEXT NOT NULL,
-    "role" TEXT NOT NULL DEFAULT 'member',
-    "userId" TEXT NOT NULL,
-    "organizationId" TEXT NOT NULL,
-    "roleId" TEXT,
-
-    CONSTRAINT "organization_members_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "roles" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "isAdmin" BOOLEAN NOT NULL DEFAULT false,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "organizationId" TEXT NOT NULL,
-
-    CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "permissions" (
-    "id" TEXT NOT NULL,
-    "action" TEXT NOT NULL,
-    "subject" TEXT NOT NULL,
-    "roleId" TEXT NOT NULL,
-
-    CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "system_settings" (
-    "id" TEXT NOT NULL,
-    "key" TEXT NOT NULL,
-    "value" TEXT NOT NULL,
-
-    CONSTRAINT "system_settings_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "subscription_plans" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "stripePriceId" TEXT,
-    "price" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "interval" TEXT NOT NULL DEFAULT 'month',
-    "maxMembers" INTEGER NOT NULL DEFAULT 1,
-    "features" TEXT,
-    "isActive" BOOLEAN NOT NULL DEFAULT true,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-
-    CONSTRAINT "subscription_plans_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "subscriptions" (
-    "id" TEXT NOT NULL,
-    "status" TEXT NOT NULL DEFAULT 'active',
-    "stripeSubscriptionId" TEXT,
-    "stripeCustomerId" TEXT,
-    "currentPeriodStart" TIMESTAMP(3),
-    "currentPeriodEnd" TIMESTAMP(3),
-    "cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "organizationId" TEXT NOT NULL,
-    "planId" TEXT NOT NULL,
-
-    CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-
--- CreateIndex
-CREATE INDEX "app_settings_organizationId_idx" ON "app_settings"("organizationId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "app_settings_organizationId_key_key" ON "app_settings"("organizationId", "key");
-
--- CreateIndex
-CREATE UNIQUE INDEX "sessions_token_key" ON "sessions"("token");
-
--- CreateIndex
-CREATE INDEX "sessions_userId_idx" ON "sessions"("userId");
-
--- CreateIndex
-CREATE INDEX "accounts_userId_idx" ON "accounts"("userId");
-
--- CreateIndex
-CREATE INDEX "vehicles_organizationId_idx" ON "vehicles"("organizationId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "service_records_publicToken_key" ON "service_records"("publicToken");
-
--- CreateIndex
-CREATE INDEX "service_records_vehicleId_idx" ON "service_records"("vehicleId");
-
--- CreateIndex
-CREATE INDEX "notes_vehicleId_idx" ON "notes"("vehicleId");
-
--- CreateIndex
-CREATE INDEX "fuel_logs_vehicleId_idx" ON "fuel_logs"("vehicleId");
-
--- CreateIndex
-CREATE INDEX "reminders_vehicleId_idx" ON "reminders"("vehicleId");
-
--- CreateIndex
-CREATE INDEX "customers_organizationId_idx" ON "customers"("organizationId");
-
--- CreateIndex
-CREATE INDEX "payments_serviceRecordId_idx" ON "payments"("serviceRecordId");
-
--- CreateIndex
-CREATE INDEX "service_attachments_serviceRecordId_idx" ON "service_attachments"("serviceRecordId");
-
--- CreateIndex
-CREATE INDEX "service_parts_serviceRecordId_idx" ON "service_parts"("serviceRecordId");
-
--- CreateIndex
-CREATE INDEX "service_labor_serviceRecordId_idx" ON "service_labor"("serviceRecordId");
-
--- CreateIndex
-CREATE INDEX "inventory_parts_organizationId_idx" ON "inventory_parts"("organizationId");
-
--- CreateIndex
-CREATE INDEX "quotes_organizationId_idx" ON "quotes"("organizationId");
-
--- CreateIndex
-CREATE INDEX "quote_parts_quoteId_idx" ON "quote_parts"("quoteId");
-
--- CreateIndex
-CREATE INDEX "quote_labor_quoteId_idx" ON "quote_labor"("quoteId");
-
--- CreateIndex
-CREATE INDEX "custom_field_definitions_organizationId_idx" ON "custom_field_definitions"("organizationId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "custom_field_definitions_organizationId_name_entityType_key" ON "custom_field_definitions"("organizationId", "name", "entityType");
-
--- CreateIndex
-CREATE INDEX "custom_field_values_fieldId_idx" ON "custom_field_values"("fieldId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "custom_field_values_fieldId_entityId_key" ON "custom_field_values"("fieldId", "entityId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "organization_members_userId_organizationId_key" ON "organization_members"("userId", "organizationId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "roles_organizationId_name_key" ON "roles"("organizationId", "name");
-
--- CreateIndex
-CREATE INDEX "permissions_roleId_idx" ON "permissions"("roleId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "permissions_roleId_action_subject_key" ON "permissions"("roleId", "action", "subject");
-
--- CreateIndex
-CREATE UNIQUE INDEX "system_settings_key_key" ON "system_settings"("key");
-
--- CreateIndex
-CREATE UNIQUE INDEX "subscription_plans_stripePriceId_key" ON "subscription_plans"("stripePriceId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "subscriptions_stripeSubscriptionId_key" ON "subscriptions"("stripeSubscriptionId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "subscriptions_organizationId_key" ON "subscriptions"("organizationId");
-
--- AddForeignKey
-ALTER TABLE "app_settings" ADD CONSTRAINT "app_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "app_settings" ADD CONSTRAINT "app_settings_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "service_records" ADD CONSTRAINT "service_records_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "notes" ADD CONSTRAINT "notes_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "fuel_logs" ADD CONSTRAINT "fuel_logs_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "reminders" ADD CONSTRAINT "reminders_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "customers" ADD CONSTRAINT "customers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "customers" ADD CONSTRAINT "customers_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "payments" ADD CONSTRAINT "payments_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "service_attachments" ADD CONSTRAINT "service_attachments_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "service_parts" ADD CONSTRAINT "service_parts_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "service_labor" ADD CONSTRAINT "service_labor_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inventory_parts" ADD CONSTRAINT "inventory_parts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inventory_parts" ADD CONSTRAINT "inventory_parts_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "quotes" ADD CONSTRAINT "quotes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "quotes" ADD CONSTRAINT "quotes_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "quotes" ADD CONSTRAINT "quotes_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "quotes" ADD CONSTRAINT "quotes_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "quote_parts" ADD CONSTRAINT "quote_parts_quoteId_fkey" FOREIGN KEY ("quoteId") REFERENCES "quotes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "quote_labor" ADD CONSTRAINT "quote_labor_quoteId_fkey" FOREIGN KEY ("quoteId") REFERENCES "quotes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "custom_field_definitions" ADD CONSTRAINT "custom_field_definitions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "custom_field_definitions" ADD CONSTRAINT "custom_field_definitions_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "custom_field_values" ADD CONSTRAINT "custom_field_values_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "custom_field_definitions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "roles" ADD CONSTRAINT "roles_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "permissions" ADD CONSTRAINT "permissions_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_planId_fkey" FOREIGN KEY ("planId") REFERENCES "subscription_plans"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

+ 0 - 18
prisma/migrations/20260215140404_2fa/migration.sql

@@ -1,18 +0,0 @@
--- AlterTable
-ALTER TABLE "users" ADD COLUMN     "twoFactorEnabled" BOOLEAN NOT NULL DEFAULT false;
-
--- CreateTable
-CREATE TABLE "two_factor" (
-    "id" TEXT NOT NULL,
-    "secret" TEXT NOT NULL,
-    "backupCodes" TEXT NOT NULL,
-    "userId" TEXT NOT NULL,
-
-    CONSTRAINT "two_factor_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "two_factor_userId_key" ON "two_factor"("userId");
-
--- AddForeignKey
-ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 26
prisma/migrations/20260216075347_user_invitations/migration.sql

@@ -1,26 +0,0 @@
--- CreateTable
-CREATE TABLE "team_invitations" (
-    "id" TEXT NOT NULL,
-    "email" TEXT NOT NULL,
-    "role" TEXT NOT NULL DEFAULT 'member',
-    "token" TEXT NOT NULL,
-    "status" TEXT NOT NULL DEFAULT 'pending',
-    "expiresAt" TIMESTAMP(3) NOT NULL,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "organizationId" TEXT NOT NULL,
-    "invitedById" TEXT NOT NULL,
-
-    CONSTRAINT "team_invitations_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "team_invitations_token_key" ON "team_invitations"("token");
-
--- CreateIndex
-CREATE INDEX "team_invitations_token_idx" ON "team_invitations"("token");
-
--- CreateIndex
-CREATE UNIQUE INDEX "team_invitations_email_organizationId_key" ON "team_invitations"("email", "organizationId");
-
--- AddForeignKey
-ALTER TABLE "team_invitations" ADD CONSTRAINT "team_invitations_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 59
prisma/migrations/20260216105753_recurring_invoices/migration.sql

@@ -1,59 +0,0 @@
--- CreateTable
-CREATE TABLE "recurring_invoices" (
-    "id" TEXT NOT NULL,
-    "title" TEXT NOT NULL,
-    "description" TEXT,
-    "frequency" TEXT NOT NULL,
-    "nextRunDate" TIMESTAMP(3) NOT NULL,
-    "endDate" TIMESTAMP(3),
-    "isActive" BOOLEAN NOT NULL DEFAULT true,
-    "lastRunAt" TIMESTAMP(3),
-    "runCount" INTEGER NOT NULL DEFAULT 0,
-    "type" TEXT NOT NULL DEFAULT 'maintenance',
-    "cost" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "invoiceNotes" TEXT,
-    "vehicleId" TEXT NOT NULL,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-
-    CONSTRAINT "recurring_invoices_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "recurring_parts" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "partNumber" TEXT,
-    "quantity" INTEGER NOT NULL DEFAULT 1,
-    "unitPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "recurringInvoiceId" TEXT NOT NULL,
-
-    CONSTRAINT "recurring_parts_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "recurring_labor" (
-    "id" TEXT NOT NULL,
-    "description" TEXT NOT NULL,
-    "hours" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "rate" DOUBLE PRECISION NOT NULL DEFAULT 0,
-    "recurringInvoiceId" TEXT NOT NULL,
-
-    CONSTRAINT "recurring_labor_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "recurring_invoices_vehicleId_idx" ON "recurring_invoices"("vehicleId");
-
--- CreateIndex
-CREATE INDEX "recurring_invoices_nextRunDate_isActive_idx" ON "recurring_invoices"("nextRunDate", "isActive");
-
--- AddForeignKey
-ALTER TABLE "recurring_invoices" ADD CONSTRAINT "recurring_invoices_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "recurring_parts" ADD CONSTRAINT "recurring_parts_recurringInvoiceId_fkey" FOREIGN KEY ("recurringInvoiceId") REFERENCES "recurring_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "recurring_labor" ADD CONSTRAINT "recurring_labor_recurringInvoiceId_fkey" FOREIGN KEY ("recurringInvoiceId") REFERENCES "recurring_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 5
prisma/migrations/20260216171151_custom_roles/migration.sql

@@ -1,5 +0,0 @@
--- AlterTable
-ALTER TABLE "team_invitations" ADD COLUMN     "roleId" TEXT;
-
--- AddForeignKey
-ALTER TABLE "team_invitations" ADD CONSTRAINT "team_invitations_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 0 - 2
prisma/migrations/20260216180745_organization_user_relation/migration.sql

@@ -1,2 +0,0 @@
--- AddForeignKey
-ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 2
prisma/migrations/20260217113542_archives/migration.sql

@@ -1,2 +0,0 @@
--- AlterTable
-ALTER TABLE "vehicles" ADD COLUMN     "archiveReason" TEXT;

+ 0 - 2
prisma/migrations/20260217115943_terms_of_service/migration.sql

@@ -1,2 +0,0 @@
--- AlterTable
-ALTER TABLE "users" ADD COLUMN     "termsAcceptedAt" TIMESTAMP(3);

+ 0 - 2
prisma/migrations/20260217163702_invoice_paid_status/migration.sql

@@ -1,2 +0,0 @@
--- AlterTable
-ALTER TABLE "service_records" ADD COLUMN     "manuallyPaid" BOOLEAN NOT NULL DEFAULT false;

+ 0 - 2
prisma/migrations/20260218071159_qoute_share_link/migration.sql

@@ -1,2 +0,0 @@
--- AlterTable
-ALTER TABLE "quotes" ADD COLUMN     "publicToken" TEXT;

+ 0 - 3
prisma/migrations/20260219050044_predicted_maintenance/migration.sql

@@ -1,3 +0,0 @@
--- AlterTable
-ALTER TABLE "vehicles" ADD COLUMN     "maintenanceDismissed" BOOLEAN NOT NULL DEFAULT false,
-ADD COLUMN     "maintenanceDismissedAt" TIMESTAMP(3);

+ 0 - 2
prisma/migrations/20260219121039_sell_price/migration.sql

@@ -1,2 +0,0 @@
--- AlterTable
-ALTER TABLE "inventory_parts" ADD COLUMN     "sellPrice" DOUBLE PRECISION NOT NULL DEFAULT 0;

+ 0 - 131
prisma/migrations/20260220172149_dvi_inspection/migration.sql

@@ -1,131 +0,0 @@
--- AlterTable
-ALTER TABLE "quotes" ADD COLUMN     "inspectionId" TEXT;
-
--- CreateTable
-CREATE TABLE "inspection_templates" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "description" TEXT,
-    "isDefault" BOOLEAN NOT NULL DEFAULT false,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "organizationId" TEXT NOT NULL,
-
-    CONSTRAINT "inspection_templates_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "inspection_template_sections" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "sortOrder" INTEGER NOT NULL DEFAULT 0,
-    "templateId" TEXT NOT NULL,
-
-    CONSTRAINT "inspection_template_sections_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "inspection_template_items" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "sortOrder" INTEGER NOT NULL DEFAULT 0,
-    "sectionId" TEXT NOT NULL,
-
-    CONSTRAINT "inspection_template_items_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "inspections" (
-    "id" TEXT NOT NULL,
-    "status" TEXT NOT NULL DEFAULT 'in_progress',
-    "mileage" INTEGER,
-    "notes" TEXT,
-    "publicToken" TEXT,
-    "completedAt" TIMESTAMP(3),
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "vehicleId" TEXT NOT NULL,
-    "templateId" TEXT NOT NULL,
-    "technicianId" TEXT NOT NULL,
-    "organizationId" TEXT NOT NULL,
-
-    CONSTRAINT "inspections_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "inspection_items" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "section" TEXT NOT NULL,
-    "sortOrder" INTEGER NOT NULL DEFAULT 0,
-    "condition" TEXT NOT NULL DEFAULT 'not_inspected',
-    "notes" TEXT,
-    "imageUrls" TEXT[] DEFAULT ARRAY[]::TEXT[],
-    "inspectionId" TEXT NOT NULL,
-
-    CONSTRAINT "inspection_items_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "inspection_quote_requests" (
-    "id" TEXT NOT NULL,
-    "status" TEXT NOT NULL DEFAULT 'pending',
-    "message" TEXT,
-    "selectedItemIds" TEXT[],
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "inspectionId" TEXT NOT NULL,
-    "organizationId" TEXT NOT NULL,
-
-    CONSTRAINT "inspection_quote_requests_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "inspection_templates_organizationId_idx" ON "inspection_templates"("organizationId");
-
--- CreateIndex
-CREATE INDEX "inspection_template_sections_templateId_idx" ON "inspection_template_sections"("templateId");
-
--- CreateIndex
-CREATE INDEX "inspection_template_items_sectionId_idx" ON "inspection_template_items"("sectionId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "inspections_publicToken_key" ON "inspections"("publicToken");
-
--- CreateIndex
-CREATE INDEX "inspections_vehicleId_idx" ON "inspections"("vehicleId");
-
--- CreateIndex
-CREATE INDEX "inspections_organizationId_idx" ON "inspections"("organizationId");
-
--- CreateIndex
-CREATE INDEX "inspection_items_inspectionId_idx" ON "inspection_items"("inspectionId");
-
--- CreateIndex
-CREATE INDEX "inspection_quote_requests_organizationId_idx" ON "inspection_quote_requests"("organizationId");
-
--- CreateIndex
-CREATE INDEX "inspection_quote_requests_inspectionId_idx" ON "inspection_quote_requests"("inspectionId");
-
--- AddForeignKey
-ALTER TABLE "quotes" ADD CONSTRAINT "quotes_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inspection_templates" ADD CONSTRAINT "inspection_templates_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inspection_template_sections" ADD CONSTRAINT "inspection_template_sections_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "inspection_templates"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inspection_template_items" ADD CONSTRAINT "inspection_template_items_sectionId_fkey" FOREIGN KEY ("sectionId") REFERENCES "inspection_template_sections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inspections" ADD CONSTRAINT "inspections_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inspections" ADD CONSTRAINT "inspections_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "inspection_templates"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inspection_items" ADD CONSTRAINT "inspection_items_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "inspection_quote_requests" ADD CONSTRAINT "inspection_quote_requests_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 2
prisma/migrations/20260220174923_customer_quotation_interaction/migration.sql

@@ -1,2 +0,0 @@
--- AlterTable
-ALTER TABLE "quotes" ADD COLUMN     "customerMessage" TEXT;

+ 0 - 21
prisma/migrations/20260220182239_push_notifications/migration.sql

@@ -1,21 +0,0 @@
--- CreateTable
-CREATE TABLE "notifications" (
-    "id" TEXT NOT NULL,
-    "type" TEXT NOT NULL,
-    "title" TEXT NOT NULL,
-    "message" TEXT NOT NULL,
-    "entityType" TEXT NOT NULL,
-    "entityId" TEXT NOT NULL,
-    "entityUrl" TEXT NOT NULL,
-    "read" BOOLEAN NOT NULL DEFAULT false,
-    "organizationId" TEXT NOT NULL,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
-    CONSTRAINT "notifications_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "notifications_organizationId_createdAt_idx" ON "notifications"("organizationId", "createdAt" DESC);
-
--- CreateIndex
-CREATE INDEX "notifications_organizationId_read_idx" ON "notifications"("organizationId", "read");

+ 0 - 3
prisma/migrations/20260221055301_user_last_seen/migration.sql

@@ -1,3 +0,0 @@
--- AlterTable
-ALTER TABLE "users" ADD COLUMN     "lastLogin" TIMESTAMP(3),
-ADD COLUMN     "lastSeen" TIMESTAMP(3);

+ 0 - 34
prisma/migrations/20260221061849_sms_gateway/migration.sql

@@ -1,34 +0,0 @@
--- CreateTable
-CREATE TABLE "sms_messages" (
-    "id" TEXT NOT NULL,
-    "direction" TEXT NOT NULL,
-    "fromNumber" TEXT NOT NULL,
-    "toNumber" TEXT NOT NULL,
-    "body" TEXT NOT NULL,
-    "status" TEXT NOT NULL DEFAULT 'queued',
-    "providerMsgId" TEXT,
-    "errorMessage" TEXT,
-    "relatedEntityType" TEXT,
-    "relatedEntityId" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "organizationId" TEXT NOT NULL,
-    "customerId" TEXT,
-
-    CONSTRAINT "sms_messages_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "sms_messages_organizationId_customerId_createdAt_idx" ON "sms_messages"("organizationId", "customerId", "createdAt" DESC);
-
--- CreateIndex
-CREATE INDEX "sms_messages_organizationId_createdAt_idx" ON "sms_messages"("organizationId", "createdAt" DESC);
-
--- CreateIndex
-CREATE INDEX "sms_messages_toNumber_organizationId_idx" ON "sms_messages"("toNumber", "organizationId");
-
--- AddForeignKey
-ALTER TABLE "sms_messages" ADD CONSTRAINT "sms_messages_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "sms_messages" ADD CONSTRAINT "sms_messages_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 0 - 57
prisma/migrations/20260223165833_job_board/migration.sql

@@ -1,57 +0,0 @@
--- CreateTable
-CREATE TABLE "technicians" (
-    "id" TEXT NOT NULL,
-    "name" TEXT NOT NULL,
-    "color" TEXT NOT NULL DEFAULT '#3b82f6',
-    "isActive" BOOLEAN NOT NULL DEFAULT true,
-    "sortOrder" INTEGER NOT NULL DEFAULT 0,
-    "memberId" TEXT,
-    "organizationId" TEXT NOT NULL,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-
-    CONSTRAINT "technicians_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "board_assignments" (
-    "id" TEXT NOT NULL,
-    "date" DATE NOT NULL,
-    "sortOrder" INTEGER NOT NULL DEFAULT 0,
-    "notes" TEXT,
-    "technicianId" TEXT NOT NULL,
-    "serviceRecordId" TEXT,
-    "inspectionId" TEXT,
-    "organizationId" TEXT NOT NULL,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-
-    CONSTRAINT "board_assignments_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "technicians_organizationId_idx" ON "technicians"("organizationId");
-
--- CreateIndex
-CREATE INDEX "board_assignments_organizationId_date_idx" ON "board_assignments"("organizationId", "date");
-
--- CreateIndex
-CREATE INDEX "board_assignments_technicianId_date_idx" ON "board_assignments"("technicianId", "date");
-
--- CreateIndex
-CREATE UNIQUE INDEX "board_assignments_serviceRecordId_date_key" ON "board_assignments"("serviceRecordId", "date");
-
--- CreateIndex
-CREATE UNIQUE INDEX "board_assignments_inspectionId_date_key" ON "board_assignments"("inspectionId", "date");
-
--- AddForeignKey
-ALTER TABLE "technicians" ADD CONSTRAINT "technicians_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "board_assignments" ADD CONSTRAINT "board_assignments_technicianId_fkey" FOREIGN KEY ("technicianId") REFERENCES "technicians"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "board_assignments" ADD CONSTRAINT "board_assignments_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "board_assignments" ADD CONSTRAINT "board_assignments_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 82
prisma/migrations/20260224102050_customer_portal/migration.sql

@@ -1,82 +0,0 @@
-/*
-  Warnings:
-
-  - A unique constraint covering the columns `[portalSlug]` on the table `organizations` will be added. If there are existing duplicate values, this will fail.
-
-*/
--- AlterTable
-ALTER TABLE "organizations" ADD COLUMN     "portalSlug" TEXT;
-
--- CreateTable
-CREATE TABLE "customer_sessions" (
-    "id" TEXT NOT NULL,
-    "token" TEXT NOT NULL,
-    "expiresAt" TIMESTAMP(3) NOT NULL,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "customerId" TEXT NOT NULL,
-    "organizationId" TEXT NOT NULL,
-
-    CONSTRAINT "customer_sessions_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "customer_magic_links" (
-    "id" TEXT NOT NULL,
-    "token" TEXT NOT NULL,
-    "email" TEXT NOT NULL,
-    "organizationId" TEXT NOT NULL,
-    "expiresAt" TIMESTAMP(3) NOT NULL,
-    "usedAt" TIMESTAMP(3),
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
-    CONSTRAINT "customer_magic_links_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "service_requests" (
-    "id" TEXT NOT NULL,
-    "description" TEXT NOT NULL,
-    "preferredDate" TIMESTAMP(3),
-    "status" TEXT NOT NULL DEFAULT 'pending',
-    "adminNotes" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL,
-    "customerId" TEXT NOT NULL,
-    "vehicleId" TEXT NOT NULL,
-    "organizationId" TEXT NOT NULL,
-
-    CONSTRAINT "service_requests_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "customer_sessions_token_key" ON "customer_sessions"("token");
-
--- CreateIndex
-CREATE INDEX "customer_sessions_token_idx" ON "customer_sessions"("token");
-
--- CreateIndex
-CREATE INDEX "customer_sessions_customerId_organizationId_idx" ON "customer_sessions"("customerId", "organizationId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "customer_magic_links_token_key" ON "customer_magic_links"("token");
-
--- CreateIndex
-CREATE INDEX "customer_magic_links_token_idx" ON "customer_magic_links"("token");
-
--- CreateIndex
-CREATE INDEX "service_requests_organizationId_status_idx" ON "service_requests"("organizationId", "status");
-
--- CreateIndex
-CREATE INDEX "service_requests_customerId_idx" ON "service_requests"("customerId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "organizations_portalSlug_key" ON "organizations"("portalSlug");
-
--- AddForeignKey
-ALTER TABLE "customer_sessions" ADD CONSTRAINT "customer_sessions_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "service_requests" ADD CONSTRAINT "service_requests_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "service_requests" ADD CONSTRAINT "service_requests_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 21
prisma/migrations/20260226201811_qoute_attachments/migration.sql

@@ -1,21 +0,0 @@
--- CreateTable
-CREATE TABLE "quote_attachments" (
-    "id" TEXT NOT NULL,
-    "fileName" TEXT NOT NULL,
-    "fileUrl" TEXT NOT NULL,
-    "fileType" TEXT NOT NULL,
-    "fileSize" INTEGER NOT NULL,
-    "category" TEXT NOT NULL DEFAULT 'image',
-    "description" TEXT,
-    "includeInInvoice" BOOLEAN NOT NULL DEFAULT true,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "quoteId" TEXT NOT NULL,
-
-    CONSTRAINT "quote_attachments_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "quote_attachments_quoteId_idx" ON "quote_attachments"("quoteId");
-
--- AddForeignKey
-ALTER TABLE "quote_attachments" ADD CONSTRAINT "quote_attachments_quoteId_fkey" FOREIGN KEY ("quoteId") REFERENCES "quotes"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 34
prisma/migrations/20260227185301_passkeys/migration.sql

@@ -1,34 +0,0 @@
-/*
-  Warnings:
-
-  - A unique constraint covering the columns `[identifier]` on the table `verifications` will be added. If there are existing duplicate values, this will fail.
-
-*/
--- CreateTable
-CREATE TABLE "passkeys" (
-    "id" TEXT NOT NULL,
-    "name" TEXT,
-    "publicKey" TEXT NOT NULL,
-    "userId" TEXT NOT NULL,
-    "credentialID" TEXT NOT NULL,
-    "counter" INTEGER NOT NULL,
-    "deviceType" TEXT NOT NULL,
-    "backedUp" BOOLEAN NOT NULL,
-    "transports" TEXT,
-    "aaguid" TEXT,
-    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
-    CONSTRAINT "passkeys_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "passkeys_credentialID_key" ON "passkeys"("credentialID");
-
--- CreateIndex
-CREATE INDEX "passkeys_userId_idx" ON "passkeys"("userId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "verifications_identifier_key" ON "verifications"("identifier");
-
--- AddForeignKey
-ALTER TABLE "passkeys" ADD CONSTRAINT "passkeys_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 0 - 9
prisma/migrations/20260227191610_shared_pages_viewcounter/migration.sql

@@ -1,9 +0,0 @@
--- AlterTable
-ALTER TABLE "quotes" ADD COLUMN     "lastViewedAt" TIMESTAMP(3),
-ADD COLUMN     "sharedAt" TIMESTAMP(3),
-ADD COLUMN     "viewCount" INTEGER NOT NULL DEFAULT 0;
-
--- AlterTable
-ALTER TABLE "service_records" ADD COLUMN     "lastViewedAt" TIMESTAMP(3),
-ADD COLUMN     "sharedAt" TIMESTAMP(3),
-ADD COLUMN     "viewCount" INTEGER NOT NULL DEFAULT 0;

+ 0 - 5
prisma/migrations/20260302163120_exlcude_parts_option/migration.sql

@@ -1,5 +0,0 @@
--- AlterTable
-ALTER TABLE "quote_labor" ADD COLUMN     "excluded" BOOLEAN NOT NULL DEFAULT false;
-
--- AlterTable
-ALTER TABLE "quote_parts" ADD COLUMN     "excluded" BOOLEAN NOT NULL DEFAULT false;

+ 37 - 0
prisma/migrations/20260307232727_workorder_duration/migration.sql

@@ -0,0 +1,37 @@
+-- DropForeignKey (idempotent)
+ALTER TABLE "board_assignments" DROP CONSTRAINT IF EXISTS "board_assignments_inspectionId_fkey";
+ALTER TABLE "board_assignments" DROP CONSTRAINT IF EXISTS "board_assignments_serviceRecordId_fkey";
+ALTER TABLE "board_assignments" DROP CONSTRAINT IF EXISTS "board_assignments_technicianId_fkey";
+
+-- AlterTable inspections (idempotent)
+ALTER TABLE "inspections" ADD COLUMN IF NOT EXISTS "endDateTime" TIMESTAMP(3);
+ALTER TABLE "inspections" ADD COLUMN IF NOT EXISTS "sortOrder" INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE "inspections" ADD COLUMN IF NOT EXISTS "startDateTime" TIMESTAMP(3);
+ALTER TABLE "inspections" ALTER COLUMN "technicianId" DROP NOT NULL;
+
+-- AlterTable service_records (idempotent)
+ALTER TABLE "service_records" ADD COLUMN IF NOT EXISTS "endDateTime" TIMESTAMP(3);
+ALTER TABLE "service_records" ADD COLUMN IF NOT EXISTS "sortOrder" INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE "service_records" ADD COLUMN IF NOT EXISTS "startDateTime" TIMESTAMP(3);
+ALTER TABLE "service_records" ADD COLUMN IF NOT EXISTS "technicianId" TEXT;
+
+-- AlterTable technicians (idempotent)
+ALTER TABLE "technicians" ADD COLUMN IF NOT EXISTS "dailyCapacity" INTEGER NOT NULL DEFAULT 480;
+
+-- DropTable (idempotent)
+DROP TABLE IF EXISTS "board_assignments";
+
+-- CreateIndex (idempotent)
+CREATE INDEX IF NOT EXISTS "inspections_technicianId_idx" ON "inspections"("technicianId");
+CREATE INDEX IF NOT EXISTS "service_records_technicianId_idx" ON "service_records"("technicianId");
+
+-- Clean up orphaned technicianId references before adding foreign keys
+UPDATE "service_records" SET "technicianId" = NULL WHERE "technicianId" IS NOT NULL AND "technicianId" NOT IN (SELECT id FROM "technicians");
+UPDATE "inspections" SET "technicianId" = NULL WHERE "technicianId" IS NOT NULL AND "technicianId" NOT IN (SELECT id FROM "technicians");
+
+-- AddForeignKey (idempotent)
+ALTER TABLE "service_records" DROP CONSTRAINT IF EXISTS "service_records_technicianId_fkey";
+ALTER TABLE "service_records" ADD CONSTRAINT "service_records_technicianId_fkey" FOREIGN KEY ("technicianId") REFERENCES "technicians"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+ALTER TABLE "inspections" DROP CONSTRAINT IF EXISTS "inspections_technicianId_fkey";
+ALTER TABLE "inspections" ADD CONSTRAINT "inspections_technicianId_fkey" FOREIGN KEY ("technicianId") REFERENCES "technicians"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 50 - 65
prisma/schema.prisma

@@ -91,7 +91,7 @@ model Account {
 
 
 model Verification {
 model Verification {
   id         String    @id @default(cuid())
   id         String    @id @default(cuid())
-  identifier String   @unique
+  identifier String    @unique
   value      String
   value      String
   expiresAt  DateTime
   expiresAt  DateTime
   createdAt  DateTime? @default(now())
   createdAt  DateTime? @default(now())
@@ -173,29 +173,31 @@ model Vehicle {
 }
 }
 
 
 model ServiceRecord {
 model ServiceRecord {
-  id              String   @id @default(cuid())
+  id              String    @id @default(cuid())
   title           String
   title           String
   description     String?
   description     String?
-  type            String   @default("maintenance")
-  status          String   @default("completed")
-  cost            Float    @default(0)
+  type            String    @default("maintenance")
+  status          String    @default("completed")
+  cost            Float     @default(0)
   mileage         Int?
   mileage         Int?
-  serviceDate     DateTime @default(now())
+  serviceDate     DateTime  @default(now())
+  startDateTime   DateTime?
+  endDateTime     DateTime?
   shopName        String?
   shopName        String?
   techName        String?
   techName        String?
   parts           String?
   parts           String?
   laborHours      Float?
   laborHours      Float?
   diagnosticNotes String?
   diagnosticNotes String?
   invoiceNotes    String?
   invoiceNotes    String?
-  subtotal        Float    @default(0)
-  taxRate         Float    @default(0)
-  taxAmount       Float    @default(0)
-  totalAmount     Float    @default(0)
+  subtotal        Float     @default(0)
+  taxRate         Float     @default(0)
+  taxAmount       Float     @default(0)
+  totalAmount     Float     @default(0)
   invoiceNumber   String?
   invoiceNumber   String?
   discountType    String?
   discountType    String?
-  discountValue   Float    @default(0)
-  discountAmount  Float    @default(0)
-  manuallyPaid    Boolean  @default(false)
+  discountValue   Float     @default(0)
+  discountAmount  Float     @default(0)
+  manuallyPaid    Boolean   @default(false)
   publicToken     String?   @unique
   publicToken     String?   @unique
   sharedAt        DateTime?
   sharedAt        DateTime?
   viewCount       Int       @default(0)
   viewCount       Int       @default(0)
@@ -206,13 +208,17 @@ model ServiceRecord {
   vehicleId String
   vehicleId String
   vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
   vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
 
 
-  partItems        ServicePart[]
-  laborItems       ServiceLabor[]
-  attachments      ServiceAttachment[]
-  payments         Payment[]
-  boardAssignments BoardAssignment[]
+  technicianId String?
+  technician   Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
+  sortOrder    Int         @default(0)
+
+  partItems   ServicePart[]
+  laborItems  ServiceLabor[]
+  attachments ServiceAttachment[]
+  payments    Payment[]
 
 
   @@index([vehicleId])
   @@index([vehicleId])
+  @@index([technicianId])
   @@map("service_records")
   @@map("service_records")
 }
 }
 
 
@@ -449,7 +455,7 @@ model QuoteAttachment {
   createdAt        DateTime @default(now())
   createdAt        DateTime @default(now())
 
 
   quoteId String
   quoteId String
-  quote   Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
+  quote   Quote  @relation(fields: [quoteId], references: [id], onDelete: Cascade)
 
 
   @@index([quoteId])
   @@index([quoteId])
   @@map("quote_attachments")
   @@map("quote_attachments")
@@ -765,14 +771,16 @@ model InspectionTemplateItem {
 }
 }
 
 
 model Inspection {
 model Inspection {
-  id          String    @id @default(cuid())
-  status      String    @default("in_progress")
-  mileage     Int?
-  notes       String?
-  publicToken String?   @unique
-  completedAt DateTime?
-  createdAt   DateTime  @default(now())
-  updatedAt   DateTime  @updatedAt
+  id            String    @id @default(cuid())
+  status        String    @default("in_progress")
+  mileage       Int?
+  notes         String?
+  startDateTime DateTime?
+  endDateTime   DateTime?
+  publicToken   String?   @unique
+  completedAt   DateTime?
+  createdAt     DateTime  @default(now())
+  updatedAt     DateTime  @updatedAt
 
 
   vehicleId String
   vehicleId String
   vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
   vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
@@ -780,17 +788,19 @@ model Inspection {
   templateId String
   templateId String
   template   InspectionTemplate @relation(fields: [templateId], references: [id])
   template   InspectionTemplate @relation(fields: [templateId], references: [id])
 
 
-  technicianId String
+  technicianId String?
+  technician   Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
+  sortOrder    Int         @default(0)
 
 
   organizationId String
   organizationId String
 
 
-  items            InspectionItem[]
-  quoteRequests    InspectionQuoteRequest[]
-  quotes           Quote[]
-  boardAssignments BoardAssignment[]
+  items         InspectionItem[]
+  quoteRequests InspectionQuoteRequest[]
+  quotes        Quote[]
 
 
   @@index([vehicleId])
   @@index([vehicleId])
   @@index([organizationId])
   @@index([organizationId])
+  @@index([technicianId])
   @@map("inspections")
   @@map("inspections")
 }
 }
 
 
@@ -871,18 +881,20 @@ model SmsMessage {
 }
 }
 
 
 model Technician {
 model Technician {
-  id        String  @id @default(cuid())
-  name      String
-  color     String  @default("#3b82f6")
-  isActive  Boolean @default(true)
-  sortOrder Int     @default(0)
+  id            String  @id @default(cuid())
+  name          String
+  color         String  @default("#3b82f6")
+  isActive      Boolean @default(true)
+  sortOrder     Int     @default(0)
+  dailyCapacity Int     @default(480)
 
 
   memberId String?
   memberId String?
 
 
   organizationId String
   organizationId String
   organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
   organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
 
 
-  assignments BoardAssignment[]
+  serviceRecords ServiceRecord[]
+  inspections    Inspection[]
 
 
   createdAt DateTime @default(now())
   createdAt DateTime @default(now())
   updatedAt DateTime @updatedAt
   updatedAt DateTime @updatedAt
@@ -891,33 +903,6 @@ model Technician {
   @@map("technicians")
   @@map("technicians")
 }
 }
 
 
-model BoardAssignment {
-  id        String   @id @default(cuid())
-  date      DateTime @db.Date
-  sortOrder Int      @default(0)
-  notes     String?
-
-  technicianId String
-  technician   Technician @relation(fields: [technicianId], references: [id], onDelete: Cascade)
-
-  serviceRecordId String?
-  serviceRecord   ServiceRecord? @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
-
-  inspectionId String?
-  inspection   Inspection? @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
-
-  organizationId String
-
-  createdAt DateTime @default(now())
-  updatedAt DateTime @updatedAt
-
-  @@unique([serviceRecordId, date])
-  @@unique([inspectionId, date])
-  @@index([organizationId, date])
-  @@index([technicianId, date])
-  @@map("board_assignments")
-}
-
 model CustomerSession {
 model CustomerSession {
   id        String   @id @default(cuid())
   id        String   @id @default(cuid())
   token     String   @unique
   token     String   @unique

+ 225 - 0
src/__tests__/features/workboard/datetime.test.ts

@@ -0,0 +1,225 @@
+import { describe, it, expect } from "vitest";
+import {
+  getTimeOfDay,
+  timeToMinutes,
+  minutesToTime,
+  isSameDay,
+  dateOverlaps,
+  clampToDay,
+  getJobDateRange,
+  getDurationMinutes,
+  jobOverlapsDate,
+} from "@/features/workboard/utils/datetime";
+import type { WorkBoardJob } from "@/features/workboard/Actions/boardActions";
+
+function makeJob(
+  startDateTime: string | null,
+  endDateTime: string | null,
+): WorkBoardJob {
+  return {
+    id: "a1",
+    type: "serviceRecord",
+    technicianId: "t1",
+    sortOrder: 0,
+    title: "Test",
+    status: "pending",
+    startDateTime,
+    endDateTime,
+    vehicle: null,
+  };
+}
+
+describe("getTimeOfDay", () => {
+  it("formats midnight as 00:00", () => {
+    expect(getTimeOfDay(new Date(2026, 2, 17, 0, 0))).toBe("00:00");
+  });
+
+  it("formats morning time with zero-padding", () => {
+    expect(getTimeOfDay(new Date(2026, 2, 17, 7, 5))).toBe("07:05");
+  });
+
+  it("formats afternoon time", () => {
+    expect(getTimeOfDay(new Date(2026, 2, 17, 14, 30))).toBe("14:30");
+  });
+
+  it("ignores seconds", () => {
+    expect(getTimeOfDay(new Date(2026, 2, 17, 12, 0, 45))).toBe("12:00");
+  });
+});
+
+describe("timeToMinutes", () => {
+  it("converts 00:00 to 0", () => {
+    expect(timeToMinutes("00:00")).toBe(0);
+  });
+
+  it("converts 07:00 to 420", () => {
+    expect(timeToMinutes("07:00")).toBe(420);
+  });
+
+  it("converts 15:00 to 900", () => {
+    expect(timeToMinutes("15:00")).toBe(900);
+  });
+
+  it("converts 23:59 to 1439", () => {
+    expect(timeToMinutes("23:59")).toBe(1439);
+  });
+
+  it("converts 12:30 to 750", () => {
+    expect(timeToMinutes("12:30")).toBe(750);
+  });
+});
+
+describe("minutesToTime", () => {
+  it("converts 0 to 00:00", () => {
+    expect(minutesToTime(0)).toBe("00:00");
+  });
+
+  it("converts 420 to 07:00", () => {
+    expect(minutesToTime(420)).toBe("07:00");
+  });
+
+  it("converts 900 to 15:00", () => {
+    expect(minutesToTime(900)).toBe("15:00");
+  });
+
+  it("converts 1439 to 23:59", () => {
+    expect(minutesToTime(1439)).toBe("23:59");
+  });
+
+  it("clamps negative values to 00:00", () => {
+    expect(minutesToTime(-10)).toBe("00:00");
+  });
+
+  it("clamps values above 1439 to 23:59", () => {
+    expect(minutesToTime(2000)).toBe("23:59");
+  });
+});
+
+describe("isSameDay", () => {
+  it("returns true for same day different times", () => {
+    expect(isSameDay(new Date(2026, 2, 17, 8, 0), new Date(2026, 2, 17, 15, 30))).toBe(true);
+  });
+
+  it("returns false for adjacent days", () => {
+    expect(isSameDay(new Date(2026, 2, 17, 23, 59), new Date(2026, 2, 18, 0, 0))).toBe(false);
+  });
+
+  it("returns false for different months", () => {
+    expect(isSameDay(new Date(2026, 2, 17), new Date(2026, 3, 17))).toBe(false);
+  });
+});
+
+describe("dateOverlaps", () => {
+  it("returns true when ranges overlap", () => {
+    const start = new Date(2026, 2, 17, 8, 0);
+    const end = new Date(2026, 2, 17, 16, 0);
+    const dayStart = new Date(2026, 2, 17, 0, 0);
+    const dayEnd = new Date(2026, 2, 17, 23, 59);
+    expect(dateOverlaps(start, end, dayStart, dayEnd)).toBe(true);
+  });
+
+  it("returns false when ranges do not overlap", () => {
+    const start = new Date(2026, 2, 18, 8, 0);
+    const end = new Date(2026, 2, 18, 16, 0);
+    const dayStart = new Date(2026, 2, 17, 0, 0);
+    const dayEnd = new Date(2026, 2, 17, 23, 59);
+    expect(dateOverlaps(start, end, dayStart, dayEnd)).toBe(false);
+  });
+
+  it("returns true for multi-day range overlapping a day", () => {
+    const start = new Date(2026, 2, 16, 14, 0);
+    const end = new Date(2026, 2, 18, 10, 0);
+    const dayStart = new Date(2026, 2, 17, 0, 0);
+    const dayEnd = new Date(2026, 2, 17, 23, 59);
+    expect(dateOverlaps(start, end, dayStart, dayEnd)).toBe(true);
+  });
+});
+
+describe("clampToDay", () => {
+  it("clamps a range that extends beyond day boundaries", () => {
+    const dayStart = new Date(2026, 2, 17, 7, 0);
+    const dayEnd = new Date(2026, 2, 17, 15, 0);
+    const result = clampToDay(
+      new Date(2026, 2, 16, 14, 0),
+      new Date(2026, 2, 18, 10, 0),
+      dayStart,
+      dayEnd,
+    );
+    expect(result.start.getTime()).toBe(dayStart.getTime());
+    expect(result.end.getTime()).toBe(dayEnd.getTime());
+  });
+
+  it("preserves range within day boundaries", () => {
+    const start = new Date(2026, 2, 17, 9, 0);
+    const end = new Date(2026, 2, 17, 12, 0);
+    const result = clampToDay(start, end, new Date(2026, 2, 17, 7, 0), new Date(2026, 2, 17, 15, 0));
+    expect(result.start.getTime()).toBe(start.getTime());
+    expect(result.end.getTime()).toBe(end.getTime());
+  });
+});
+
+describe("getJobDateRange", () => {
+  it("extracts dates from job", () => {
+    const j = makeJob("2026-03-17T08:00:00.000Z", "2026-03-17T16:00:00.000Z");
+    const { start, end } = getJobDateRange(j);
+    expect(start).toBeInstanceOf(Date);
+    expect(end).toBeInstanceOf(Date);
+  });
+
+  it("returns null when no dates", () => {
+    const j = makeJob(null, null);
+    const { start, end } = getJobDateRange(j);
+    expect(start).toBeNull();
+    expect(end).toBeNull();
+  });
+});
+
+describe("getDurationMinutes", () => {
+  it("computes 60 minutes for 1 hour", () => {
+    const start = new Date(2026, 2, 17, 8, 0);
+    const end = new Date(2026, 2, 17, 9, 0);
+    expect(getDurationMinutes(start, end)).toBe(60);
+  });
+
+  it("computes multi-day duration", () => {
+    const start = new Date(2026, 2, 17, 14, 0);
+    const end = new Date(2026, 2, 18, 10, 0);
+    expect(getDurationMinutes(start, end)).toBe(20 * 60); // 20 hours
+  });
+});
+
+describe("jobOverlapsDate", () => {
+  it("returns true when job spans the date", () => {
+    const j = makeJob("2026-03-17T08:00:00.000Z", "2026-03-17T16:00:00.000Z");
+    expect(jobOverlapsDate(j, "2026-03-17")).toBe(true);
+  });
+
+  it("returns false when job is on different date", () => {
+    const j = makeJob("2026-03-18T08:00:00.000Z", "2026-03-18T16:00:00.000Z");
+    expect(jobOverlapsDate(j, "2026-03-17")).toBe(false);
+  });
+
+  it("returns true for multi-day job overlapping the date", () => {
+    const j = makeJob("2026-03-16T14:00:00.000Z", "2026-03-18T10:00:00.000Z");
+    expect(jobOverlapsDate(j, "2026-03-17")).toBe(true);
+  });
+
+  it("returns false when no dates on job", () => {
+    const j = makeJob(null, null);
+    expect(jobOverlapsDate(j, "2026-03-17")).toBe(false);
+  });
+});
+
+describe("roundtrip: timeToMinutes <-> minutesToTime", () => {
+  it("preserves value through roundtrip", () => {
+    for (const mins of [0, 60, 420, 750, 900, 1439]) {
+      expect(timeToMinutes(minutesToTime(mins))).toBe(mins);
+    }
+  });
+
+  it("preserves time string through roundtrip", () => {
+    for (const time of ["00:00", "07:00", "12:30", "15:00", "23:59"]) {
+      expect(minutesToTime(timeToMinutes(time))).toBe(time);
+    }
+  });
+});

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

@@ -1,9 +1,16 @@
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
+import { getTechnicians } from "@/features/workboard/Actions/technicianActions";
 import { WorkshopSettings } from "./workshop-settings";
 import { WorkshopSettings } from "./workshop-settings";
 
 
 export default async function WorkshopSettingsPage() {
 export default async function WorkshopSettingsPage() {
-  const result = await getSettings();
+  const [result, techResult] = await Promise.all([
+    getSettings(),
+    getTechnicians(),
+  ]);
   const settings = result.success && result.data ? result.data : {};
   const settings = result.success && result.data ? result.data : {};
+  const technicians = techResult.success && techResult.data
+    ? techResult.data.map((t) => ({ id: t.id, name: t.name }))
+    : [];
 
 
-  return <WorkshopSettings settings={settings} />;
+  return <WorkshopSettings settings={settings} technicians={technicians} />;
 }
 }

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

@@ -8,10 +8,20 @@ import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Label } from "@/components/ui/label";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Separator } from "@/components/ui/separator";
 import { Separator } from "@/components/ui/separator";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+  CommandSeparator,
+} from "@/components/ui/command";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { setSettings } from "@/features/settings/Actions/settingsActions";
 import { setSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
-import { Loader2, Ruler, Save, Wrench } from "lucide-react";
+import { Loader2, Ruler, Save, Wrench, CalendarDays, Check, ChevronsUpDown, Plus } from "lucide-react";
 import {
 import {
   Select,
   Select,
   SelectContent,
   SelectContent,
@@ -19,9 +29,16 @@ import {
   SelectTrigger,
   SelectTrigger,
   SelectValue,
   SelectValue,
 } from "@/components/ui/select";
 } from "@/components/ui/select";
+import { createTechnician } from "@/features/workboard/Actions/technicianActions";
+import { cn } from "@/lib/utils";
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 
 
-export function WorkshopSettings({ settings }: { settings: Record<string, string> }) {
+interface TechnicianOption {
+  id: string
+  name: string
+}
+
+export function WorkshopSettings({ settings, technicians: initialTechnicians = [] }: { settings: Record<string, string>; technicians?: TechnicianOption[] }) {
   const router = useRouter();
   const router = useRouter();
   const t = useTranslations('settings');
   const t = useTranslations('settings');
   const [saving, setSaving] = useState(false);
   const [saving, setSaving] = useState(false);
@@ -29,6 +46,12 @@ export function WorkshopSettings({ settings }: { settings: Record<string, string
   const [defaultTechnician, setDefaultTechnician] = useState(
   const [defaultTechnician, setDefaultTechnician] = useState(
     settings[SETTING_KEYS.DEFAULT_TECHNICIAN] || ""
     settings[SETTING_KEYS.DEFAULT_TECHNICIAN] || ""
   );
   );
+  const [technicians, setTechnicians] = useState<TechnicianOption[]>(initialTechnicians);
+  const [techOpen, setTechOpen] = useState(false);
+  const [techSearch, setTechSearch] = useState('');
+  const [creatingTech, setCreatingTech] = useState(false);
+  const [showNewInput, setShowNewInput] = useState(false);
+  const [newTechName, setNewTechName] = useState('');
   const [defaultLaborRate, setDefaultLaborRate] = useState(
   const [defaultLaborRate, setDefaultLaborRate] = useState(
     settings[SETTING_KEYS.DEFAULT_LABOR_RATE] || ""
     settings[SETTING_KEYS.DEFAULT_LABOR_RATE] || ""
   );
   );
@@ -38,6 +61,40 @@ export function WorkshopSettings({ settings }: { settings: Record<string, string
   const [unitSystem, setUnitSystem] = useState(
   const [unitSystem, setUnitSystem] = useState(
     settings[SETTING_KEYS.UNIT_SYSTEM] || "imperial"
     settings[SETTING_KEYS.UNIT_SYSTEM] || "imperial"
   );
   );
+  const [weekStartDay, setWeekStartDay] = useState(
+    settings[SETTING_KEYS.WORKBOARD_WEEK_START_DAY] || "1"
+  );
+  const [workDayStart, setWorkDayStart] = useState(
+    settings[SETTING_KEYS.WORKBOARD_WORK_DAY_START] || "07:00"
+  );
+  const [workDayEnd, setWorkDayEnd] = useState(
+    settings[SETTING_KEYS.WORKBOARD_WORK_DAY_END] || "15:00"
+  );
+
+  const handleTechSelect = (techName: string) => {
+    setDefaultTechnician(techName);
+    setTechOpen(false);
+  };
+
+  const doCreateTechnician = async (name: string) => {
+    if (!name.trim()) return;
+    setCreatingTech(true);
+    const res = await createTechnician({ name: name.trim() });
+    setCreatingTech(false);
+    if (res.success && res.data) {
+      const newTech = { id: res.data.id, name: res.data.name };
+      setTechnicians((prev) => [...prev, newTech]);
+      setTechSearch('');
+      setNewTechName('');
+      setShowNewInput(false);
+      handleTechSelect(newTech.name);
+    } else {
+      toast.error(t('workshop.failedCreateTech'));
+    }
+  };
+
+  const searchLower = techSearch.toLowerCase();
+  const exactMatch = technicians.some((tech) => tech.name.toLowerCase() === searchLower);
 
 
   const handleSave = async () => {
   const handleSave = async () => {
     setSaving(true);
     setSaving(true);
@@ -46,6 +103,9 @@ export function WorkshopSettings({ settings }: { settings: Record<string, string
       [SETTING_KEYS.DEFAULT_LABOR_RATE]: defaultLaborRate,
       [SETTING_KEYS.DEFAULT_LABOR_RATE]: defaultLaborRate,
       [SETTING_KEYS.WORKING_HOURS]: workingHours,
       [SETTING_KEYS.WORKING_HOURS]: workingHours,
       [SETTING_KEYS.UNIT_SYSTEM]: unitSystem,
       [SETTING_KEYS.UNIT_SYSTEM]: unitSystem,
+      [SETTING_KEYS.WORKBOARD_WEEK_START_DAY]: weekStartDay,
+      [SETTING_KEYS.WORKBOARD_WORK_DAY_START]: workDayStart,
+      [SETTING_KEYS.WORKBOARD_WORK_DAY_END]: workDayEnd,
     });
     });
     setSaving(false);
     setSaving(false);
     router.refresh();
     router.refresh();
@@ -68,13 +128,105 @@ export function WorkshopSettings({ settings }: { settings: Record<string, string
 
 
           <div className="grid gap-4 sm:grid-cols-2">
           <div className="grid gap-4 sm:grid-cols-2">
             <div className="space-y-2">
             <div className="space-y-2">
-              <Label htmlFor="defaultTechnician">{t('workshop.defaultTechnician')}</Label>
-              <Input
-                id="defaultTechnician"
-                placeholder={t('workshop.technicianPlaceholder')}
-                value={defaultTechnician}
-                onChange={(e) => setDefaultTechnician(e.target.value)}
-              />
+              <Label>{t('workshop.defaultTechnician')}</Label>
+              <Popover open={techOpen} onOpenChange={setTechOpen} modal={true}>
+                <PopoverTrigger asChild>
+                  <Button
+                    variant="outline"
+                    role="combobox"
+                    aria-expanded={techOpen}
+                    className="w-full justify-between font-normal"
+                  >
+                    <span className="truncate">
+                      {defaultTechnician || t('workshop.technicianPlaceholder')}
+                    </span>
+                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                  </Button>
+                </PopoverTrigger>
+                <PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
+                  <Command shouldFilter={true}>
+                    <CommandInput
+                      placeholder={t('workshop.technicianPlaceholder')}
+                      value={techSearch}
+                      onValueChange={setTechSearch}
+                    />
+                    <CommandList className="max-h-60 overflow-y-auto">
+                      <CommandEmpty className="p-0" />
+                      <CommandGroup>
+                        {technicians.map((tech) => (
+                          <CommandItem
+                            key={tech.id}
+                            value={tech.name}
+                            onSelect={() => handleTechSelect(tech.name)}
+                          >
+                            <Check
+                              className={cn(
+                                'mr-2 h-4 w-4',
+                                defaultTechnician === tech.name ? 'opacity-100' : 'opacity-0',
+                              )}
+                            />
+                            {tech.name}
+                          </CommandItem>
+                        ))}
+                      </CommandGroup>
+                      {techSearch.trim() && !exactMatch && (
+                        <CommandGroup>
+                          <CommandItem
+                            value={`__create__${techSearch}`}
+                            onSelect={() => doCreateTechnician(techSearch)}
+                            disabled={creatingTech}
+                          >
+                            <Plus className="mr-2 h-4 w-4" />
+                            {creatingTech ? t('workshop.creating') : t('workshop.createTechnician', { name: techSearch.trim() })}
+                          </CommandItem>
+                        </CommandGroup>
+                      )}
+                      <CommandSeparator />
+                      <CommandGroup>
+                        {showNewInput ? (
+                          <div className="flex items-center gap-1.5 px-2 py-1.5" onKeyDown={(e) => e.stopPropagation()}>
+                            <Input
+                              autoFocus
+                              placeholder={t('workshop.newTechPlaceholder')}
+                              value={newTechName}
+                              onChange={(e) => setNewTechName(e.target.value)}
+                              onKeyDown={(e) => {
+                                if (e.key === 'Enter') {
+                                  e.preventDefault();
+                                  doCreateTechnician(newTechName);
+                                }
+                                if (e.key === 'Escape') {
+                                  setShowNewInput(false);
+                                  setNewTechName('');
+                                }
+                              }}
+                              className="h-7 text-sm"
+                              disabled={creatingTech}
+                            />
+                            <Button
+                              size="sm"
+                              variant="ghost"
+                              className="h-7 px-2 shrink-0"
+                              disabled={creatingTech || !newTechName.trim()}
+                              onClick={() => doCreateTechnician(newTechName)}
+                            >
+                              {creatingTech ? t('workshop.creating') : t('workshop.addTech')}
+                            </Button>
+                          </div>
+                        ) : (
+                          <CommandItem
+                            value="__add_new__"
+                            onSelect={() => setShowNewInput(true)}
+                          >
+                            <Plus className="mr-2 h-4 w-4" />
+                            {t('workshop.addNewTech')}
+                          </CommandItem>
+                        )}
+                      </CommandGroup>
+                    </CommandList>
+                  </Command>
+                </PopoverContent>
+              </Popover>
             </div>
             </div>
             <div className="space-y-2">
             <div className="space-y-2">
               <Label htmlFor="defaultLaborRate">{t('workshop.defaultLaborRate')}</Label>
               <Label htmlFor="defaultLaborRate">{t('workshop.defaultLaborRate')}</Label>
@@ -137,6 +289,54 @@ export function WorkshopSettings({ settings }: { settings: Record<string, string
             </div>
             </div>
           </div>
           </div>
 
 
+          <Separator />
+
+          <div className="space-y-4">
+            <div className="flex flex-row items-center gap-3">
+              <CalendarDays className="h-5 w-5 text-muted-foreground" />
+              <h3 className="text-lg font-semibold">{t('workshop.workBoardTitle')}</h3>
+            </div>
+            <p className="text-sm text-muted-foreground">
+              {t('workshop.workBoardDescription')}
+            </p>
+
+            <div className="grid gap-4 sm:grid-cols-3">
+              <div className="space-y-2">
+                <Label htmlFor="weekStartDay">{t('workshop.weekStartDay')}</Label>
+                <Select value={weekStartDay} onValueChange={setWeekStartDay}>
+                  <SelectTrigger>
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {[0, 1, 2, 3, 4, 5, 6].map((d) => (
+                      <SelectItem key={d} value={String(d)}>
+                        {t(`workshop.weekDays.${d}`)}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+              <div className="space-y-2">
+                <Label htmlFor="workDayStart">{t('workshop.workDayStart')}</Label>
+                <Input
+                  id="workDayStart"
+                  type="time"
+                  value={workDayStart}
+                  onChange={(e) => setWorkDayStart(e.target.value)}
+                />
+              </div>
+              <div className="space-y-2">
+                <Label htmlFor="workDayEnd">{t('workshop.workDayEnd')}</Label>
+                <Input
+                  id="workDayEnd"
+                  type="time"
+                  value={workDayEnd}
+                  onChange={(e) => setWorkDayEnd(e.target.value)}
+                />
+              </div>
+            </div>
+          </div>
+
           <SaveButton>
           <SaveButton>
             <Separator />
             <Separator />
             <div className="flex items-center gap-3">
             <div className="flex items-center gap-3">

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

@@ -108,6 +108,8 @@ export default async function ServiceDetailPage({
     status: record.status,
     status: record.status,
     mileage: record.mileage,
     mileage: record.mileage,
     serviceDate: new Date(record.serviceDate).toISOString().split("T")[0],
     serviceDate: new Date(record.serviceDate).toISOString().split("T")[0],
+    startDateTime: record.startDateTime?.toISOString() ?? null,
+    endDateTime: record.endDateTime?.toISOString() ?? null,
     techName: record.techName || "",
     techName: record.techName || "",
     diagnosticNotes: record.diagnosticNotes || "",
     diagnosticNotes: record.diagnosticNotes || "",
     invoiceNotes: record.invoiceNotes || "",
     invoiceNotes: record.invoiceNotes || "",
@@ -165,7 +167,6 @@ export default async function ServiceDetailPage({
         initialData={initialData}
         initialData={initialData}
         inventoryParts={inventoryParts}
         inventoryParts={inventoryParts}
         vehicles={vehicles}
         vehicles={vehicles}
-        teamMembers={teamMembers}
         boardTechnicians={boardTechnicians}
         boardTechnicians={boardTechnicians}
         currentUserName={currentUserName}
         currentUserName={currentUserName}
         imageAttachmentsForManager={imageAttachmentsForManager}
         imageAttachmentsForManager={imageAttachmentsForManager}

+ 55 - 1
src/app/(authenticated)/vehicles/[id]/service/new/page.tsx

@@ -1,13 +1,67 @@
 import { redirect } from "next/navigation";
 import { redirect } from "next/navigation";
 import { createDraftServiceRecord } from "@/features/vehicles/Actions/createDraftServiceRecord";
 import { createDraftServiceRecord } from "@/features/vehicles/Actions/createDraftServiceRecord";
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 
 export default async function NewServicePage({
 export default async function NewServicePage({
   params,
   params,
+  searchParams,
 }: {
 }: {
   params: Promise<{ id: string }>;
   params: Promise<{ id: string }>;
+  searchParams: Promise<Record<string, string | undefined>>;
 }) {
 }) {
   const { id } = await params;
   const { id } = await params;
-  const result = await createDraftServiceRecord(id);
+  const query = await searchParams;
+
+  // Guard: if a pending draft already exists for this vehicle (created within the last 5 seconds),
+  // reuse it instead of creating a duplicate. This prevents double-creation from
+  // Next.js Server Component re-renders.
+  const existingResult = await withAuth(
+    async ({ organizationId }) => {
+      const fiveSecondsAgo = new Date(Date.now() - 5000);
+      return db.serviceRecord.findFirst({
+        where: {
+          vehicleId: id,
+          vehicle: { organizationId },
+          status: "pending",
+          title: "New Service Record",
+          createdAt: { gte: fiveSecondsAgo },
+        },
+        select: { id: true },
+        orderBy: { createdAt: "desc" },
+      });
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.CREATE, subject: PermissionSubject.SERVICES },
+      ],
+    },
+  );
+
+  if (existingResult.success && existingResult.data?.id) {
+    redirect(`/vehicles/${id}/service/${existingResult.data.id}`);
+  }
+
+  // If coming from work board context menu, use the provided date
+  const boardDate = query.boardDate;
+  const boardStart = query.boardStart;
+  const boardEnd = query.boardEnd;
+
+  // Build startDateTime/endDateTime from board context
+  let startDateTime: Date | undefined;
+  let endDateTime: Date | undefined;
+  if (boardDate) {
+    startDateTime = boardStart
+      ? new Date(`${boardDate}T${boardStart}:00`)
+      : new Date(`${boardDate}T08:00:00`);
+    endDateTime = boardEnd
+      ? new Date(`${boardDate}T${boardEnd}:00`)
+      : new Date(startDateTime.getTime() + 3600000);
+  }
+
+  const boardTechId = query.boardTech;
+  const result = await createDraftServiceRecord(id, startDateTime, endDateTime, boardTechId);
 
 
   if (result.success && result.data?.id) {
   if (result.success && result.data?.id) {
     redirect(`/vehicles/${id}/service/${result.data.id}`);
     redirect(`/vehicles/${id}/service/${result.data.id}`);

+ 13 - 7
src/app/(authenticated)/work-board/page.tsx

@@ -1,14 +1,14 @@
 import { getTranslations } from "next-intl/server";
 import { getTranslations } from "next-intl/server";
 import { PageHeader } from "@/components/page-header";
 import { PageHeader } from "@/components/page-header";
 import { getTechnicians } from "@/features/workboard/Actions/technicianActions";
 import { getTechnicians } from "@/features/workboard/Actions/technicianActions";
-import { getBoardAssignments, getUnassignedJobs } from "@/features/workboard/Actions/boardActions";
+import { getBoardJobs, getUnassignedJobs, getWorkBoardSettings } from "@/features/workboard/Actions/boardActions";
 import { WorkBoardClient } from "@/features/workboard/Components/WorkBoardClient";
 import { WorkBoardClient } from "@/features/workboard/Components/WorkBoardClient";
 
 
-function getMonday(date: Date): string {
+function getWeekStart(date: Date, weekStartDay: number): string {
   const d = new Date(date);
   const d = new Date(date);
-  const day = d.getDay();
-  const diff = d.getDate() - day + (day === 0 ? -6 : 1);
-  d.setDate(diff);
+  const currentDay = d.getDay();
+  const diff = (currentDay - weekStartDay + 7) % 7;
+  d.setDate(d.getDate() - diff);
   const y = d.getFullYear();
   const y = d.getFullYear();
   const m = String(d.getMonth() + 1).padStart(2, "0");
   const m = String(d.getMonth() + 1).padStart(2, "0");
   const dd = String(d.getDate()).padStart(2, "0");
   const dd = String(d.getDate()).padStart(2, "0");
@@ -16,11 +16,16 @@ function getMonday(date: Date): string {
 }
 }
 
 
 export default async function WorkBoardPage() {
 export default async function WorkBoardPage() {
-  const weekStart = getMonday(new Date());
+  const settingsResult = await getWorkBoardSettings();
+  const boardSettings = settingsResult.success && settingsResult.data
+    ? settingsResult.data
+    : { weekStartDay: 1, workDayStart: "07:00", workDayEnd: "15:00" };
+
+  const weekStart = getWeekStart(new Date(), boardSettings.weekStartDay);
 
 
   const [techResult, assignResult, unassignedResult] = await Promise.all([
   const [techResult, assignResult, unassignedResult] = await Promise.all([
     getTechnicians(),
     getTechnicians(),
-    getBoardAssignments(weekStart),
+    getBoardJobs(weekStart),
     getUnassignedJobs(),
     getUnassignedJobs(),
   ]);
   ]);
 
 
@@ -52,6 +57,7 @@ export default async function WorkBoardPage() {
           initialAssignments={assignments as Parameters<typeof WorkBoardClient>[0]["initialAssignments"]}
           initialAssignments={assignments as Parameters<typeof WorkBoardClient>[0]["initialAssignments"]}
           initialUnassigned={unassigned}
           initialUnassigned={unassigned}
           initialWeekStart={weekStart}
           initialWeekStart={weekStart}
+          boardSettings={boardSettings}
         />
         />
       </div>
       </div>
     </>
     </>

+ 32 - 14
src/app/(presenter)/work-board/presenter/page.tsx

@@ -1,8 +1,11 @@
+import { Suspense } from "react";
 import { getTranslations } from "next-intl/server";
 import { getTranslations } from "next-intl/server";
 import { getTechnicians } from "@/features/workboard/Actions/technicianActions";
 import { getTechnicians } from "@/features/workboard/Actions/technicianActions";
-import { getBoardAssignments } from "@/features/workboard/Actions/boardActions";
+import { getBoardJobs, getWorkBoardSettings } from "@/features/workboard/Actions/boardActions";
 import { WorkBoardPresenter } from "@/features/workboard/Components/WorkBoardPresenter";
 import { WorkBoardPresenter } from "@/features/workboard/Components/WorkBoardPresenter";
 
 
+export const dynamic = "force-dynamic";
+
 function getMonday(date: Date): string {
 function getMonday(date: Date): string {
   const d = new Date(date);
   const d = new Date(date);
   const day = d.getDay();
   const day = d.getDay();
@@ -14,14 +17,25 @@ function getMonday(date: Date): string {
   return `${y}-${m}-${dd}`;
   return `${y}-${m}-${dd}`;
 }
 }
 
 
-export default async function WorkBoardPresenterPage() {
-  const weekStart = getMonday(new Date());
+export default async function WorkBoardPresenterPage({
+  searchParams,
+}: {
+  searchParams: Promise<{ date?: string }>;
+}) {
+  const params = await searchParams;
+  const baseDate = params.date ? new Date(params.date + "T12:00:00") : new Date();
+  const weekStart = getMonday(baseDate);
 
 
-  const [techResult, assignResult] = await Promise.all([
+  const [techResult, assignResult, settingsResult] = await Promise.all([
     getTechnicians(),
     getTechnicians(),
-    getBoardAssignments(weekStart),
+    getBoardJobs(weekStart),
+    getWorkBoardSettings(),
   ]);
   ]);
 
 
+  const boardSettings = settingsResult.success && settingsResult.data
+    ? settingsResult.data
+    : { weekStartDay: 1, workDayStart: "07:00", workDayEnd: "15:00" };
+
   if (!techResult.success) {
   if (!techResult.success) {
     return (
     return (
       <div className="flex h-screen items-center justify-center">
       <div className="flex h-screen items-center justify-center">
@@ -37,14 +51,18 @@ export default async function WorkBoardPresenterPage() {
     assignResult.success && assignResult.data ? assignResult.data : [];
     assignResult.success && assignResult.data ? assignResult.data : [];
 
 
   return (
   return (
-    <WorkBoardPresenter
-      initialTechnicians={
-        technicians as Parameters<typeof WorkBoardPresenter>[0]["initialTechnicians"]
-      }
-      initialAssignments={
-        assignments as Parameters<typeof WorkBoardPresenter>[0]["initialAssignments"]
-      }
-      initialWeekStart={weekStart}
-    />
+    <Suspense>
+      <WorkBoardPresenter
+        initialTechnicians={
+          technicians as Parameters<typeof WorkBoardPresenter>[0]["initialTechnicians"]
+        }
+        initialAssignments={
+          assignments as Parameters<typeof WorkBoardPresenter>[0]["initialAssignments"]
+        }
+        initialWeekStart={weekStart}
+        workDayStart={boardSettings.workDayStart}
+        workDayEnd={boardSettings.workDayEnd}
+      />
+    </Suspense>
   );
   );
 }
 }

+ 1 - 1
src/components/page-header.tsx

@@ -159,7 +159,7 @@ export function PageHeader() {
         <SearchTrigger />
         <SearchTrigger />
         <NewWorkOrderButton />
         <NewWorkOrderButton />
         {showWhiteLabelCta && (
         {showWhiteLabelCta && (
-          <Button asChild variant="outline" size="sm">
+          <Button asChild variant="outline" size="sm" className="hidden sm:inline-flex">
             <Link href="/settings/license">
             <Link href="/settings/license">
               <Zap className="mr-2 h-3 w-3" />
               <Zap className="mr-2 h-3 w-3" />
               {tn('purchaseWhiteLabel')}
               {tn('purchaseWhiteLabel')}

+ 252 - 0
src/components/ui/context-menu.tsx

@@ -0,0 +1,252 @@
+"use client"
+
+import * as React from "react"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+import { ContextMenu as ContextMenuPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function ContextMenu({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
+  return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
+}
+
+function ContextMenuTrigger({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
+  return (
+    <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
+  )
+}
+
+function ContextMenuGroup({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
+  return (
+    <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
+  )
+}
+
+function ContextMenuPortal({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
+  return (
+    <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
+  )
+}
+
+function ContextMenuSub({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
+  return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
+}
+
+function ContextMenuRadioGroup({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
+  return (
+    <ContextMenuPrimitive.RadioGroup
+      data-slot="context-menu-radio-group"
+      {...props}
+    />
+  )
+}
+
+function ContextMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
+  inset?: boolean
+}) {
+  return (
+    <ContextMenuPrimitive.SubTrigger
+      data-slot="context-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto" />
+    </ContextMenuPrimitive.SubTrigger>
+  )
+}
+
+function ContextMenuSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
+  return (
+    <ContextMenuPrimitive.SubContent
+      data-slot="context-menu-sub-content"
+      className={cn(
+        "z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function ContextMenuContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
+  return (
+    <ContextMenuPrimitive.Portal>
+      <ContextMenuPrimitive.Content
+        data-slot="context-menu-content"
+        className={cn(
+          "z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
+          className
+        )}
+        {...props}
+      />
+    </ContextMenuPrimitive.Portal>
+  )
+}
+
+function ContextMenuItem({
+  className,
+  inset,
+  variant = "default",
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
+  inset?: boolean
+  variant?: "default" | "destructive"
+}) {
+  return (
+    <ContextMenuPrimitive.Item
+      data-slot="context-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function ContextMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
+  return (
+    <ContextMenuPrimitive.CheckboxItem
+      data-slot="context-menu-checkbox-item"
+      className={cn(
+        "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <ContextMenuPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </ContextMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </ContextMenuPrimitive.CheckboxItem>
+  )
+}
+
+function ContextMenuRadioItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
+  return (
+    <ContextMenuPrimitive.RadioItem
+      data-slot="context-menu-radio-item"
+      className={cn(
+        "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <ContextMenuPrimitive.ItemIndicator>
+          <CircleIcon className="size-2 fill-current" />
+        </ContextMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </ContextMenuPrimitive.RadioItem>
+  )
+}
+
+function ContextMenuLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
+  inset?: boolean
+}) {
+  return (
+    <ContextMenuPrimitive.Label
+      data-slot="context-menu-label"
+      data-inset={inset}
+      className={cn(
+        "px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function ContextMenuSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
+  return (
+    <ContextMenuPrimitive.Separator
+      data-slot="context-menu-separator"
+      className={cn("-mx-1 my-1 h-px bg-border", className)}
+      {...props}
+    />
+  )
+}
+
+function ContextMenuShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="context-menu-shortcut"
+      className={cn(
+        "ml-auto text-xs tracking-widest text-muted-foreground",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export {
+  ContextMenu,
+  ContextMenuTrigger,
+  ContextMenuContent,
+  ContextMenuItem,
+  ContextMenuCheckboxItem,
+  ContextMenuRadioItem,
+  ContextMenuLabel,
+  ContextMenuSeparator,
+  ContextMenuShortcut,
+  ContextMenuGroup,
+  ContextMenuPortal,
+  ContextMenuSub,
+  ContextMenuSubContent,
+  ContextMenuSubTrigger,
+  ContextMenuRadioGroup,
+}

+ 848 - 0
src/components/ui/datetime-picker.tsx

@@ -0,0 +1,848 @@
+"use client";
+
+import { Button, buttonVariants } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
+import { add, format } from "date-fns";
+import { type Locale, enUS } from "date-fns/locale";
+import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, Clock } from "lucide-react";
+import * as React from "react";
+import { useImperativeHandle, useRef } from "react";
+
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import { DayPicker, type DayPickerProps } from "react-day-picker";
+
+// ---------- UTILITY FUNCTIONS ----------
+
+function isValidHour(value: string) {
+  return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value);
+}
+
+function isValid12Hour(value: string) {
+  return /^(0[1-9]|1[0-2])$/.test(value);
+}
+
+function isValidMinuteOrSecond(value: string) {
+  return /^[0-5][0-9]$/.test(value);
+}
+
+type GetValidNumberConfig = { max: number; min?: number; loop?: boolean };
+
+function getValidNumber(
+  value: string,
+  { max, min = 0, loop = false }: GetValidNumberConfig,
+) {
+  let numericValue = parseInt(value, 10);
+
+  if (!Number.isNaN(numericValue)) {
+    if (!loop) {
+      if (numericValue > max) numericValue = max;
+      if (numericValue < min) numericValue = min;
+    } else {
+      if (numericValue > max) numericValue = min;
+      if (numericValue < min) numericValue = max;
+    }
+    return numericValue.toString().padStart(2, "0");
+  }
+  return "00";
+}
+
+function getValidHour(value: string) {
+  if (isValidHour(value)) return value;
+  return getValidNumber(value, { max: 23 });
+}
+
+function getValid12Hour(value: string) {
+  if (isValid12Hour(value)) return value;
+  return getValidNumber(value, { min: 1, max: 12 });
+}
+
+function getValidMinuteOrSecond(value: string) {
+  if (isValidMinuteOrSecond(value)) return value;
+  return getValidNumber(value, { max: 59 });
+}
+
+type GetValidArrowNumberConfig = {
+  min: number;
+  max: number;
+  step: number;
+};
+
+function getValidArrowNumber(
+  value: string,
+  { min, max, step }: GetValidArrowNumberConfig,
+) {
+  let numericValue = parseInt(value, 10);
+  if (!Number.isNaN(numericValue)) {
+    numericValue += step;
+    return getValidNumber(String(numericValue), { min, max, loop: true });
+  }
+  return "00";
+}
+
+function getValidArrowHour(value: string, step: number) {
+  return getValidArrowNumber(value, { min: 0, max: 23, step });
+}
+
+function getValidArrow12Hour(value: string, step: number) {
+  return getValidArrowNumber(value, { min: 1, max: 12, step });
+}
+
+function getValidArrowMinuteOrSecond(value: string, step: number) {
+  return getValidArrowNumber(value, { min: 0, max: 59, step });
+}
+
+function setMinutes(date: Date, value: string) {
+  const minutes = getValidMinuteOrSecond(value);
+  date.setMinutes(parseInt(minutes, 10));
+  return date;
+}
+
+function setSeconds(date: Date, value: string) {
+  const seconds = getValidMinuteOrSecond(value);
+  date.setSeconds(parseInt(seconds, 10));
+  return date;
+}
+
+function setHours(date: Date, value: string) {
+  const hours = getValidHour(value);
+  date.setHours(parseInt(hours, 10));
+  return date;
+}
+
+type TimePickerType = "minutes" | "seconds" | "hours" | "12hours";
+type Period = "AM" | "PM";
+
+function set12Hours(date: Date, value: string, period: Period) {
+  const hours = parseInt(getValid12Hour(value), 10);
+  const convertedHours = convert12HourTo24Hour(hours, period);
+  date.setHours(convertedHours);
+  return date;
+}
+
+function setDateByType(
+  date: Date,
+  value: string,
+  type: TimePickerType,
+  period?: Period,
+) {
+  switch (type) {
+    case "minutes":
+      return setMinutes(date, value);
+    case "seconds":
+      return setSeconds(date, value);
+    case "hours":
+      return setHours(date, value);
+    case "12hours": {
+      if (!period) return date;
+      return set12Hours(date, value, period);
+    }
+    default:
+      return date;
+  }
+}
+
+function getDateByType(date: Date | null, type: TimePickerType) {
+  if (!date) return "00";
+  switch (type) {
+    case "minutes":
+      return getValidMinuteOrSecond(String(date.getMinutes()));
+    case "seconds":
+      return getValidMinuteOrSecond(String(date.getSeconds()));
+    case "hours":
+      return getValidHour(String(date.getHours()));
+    case "12hours":
+      return getValid12Hour(String(display12HourValue(date.getHours())));
+    default:
+      return "00";
+  }
+}
+
+function getArrowByType(value: string, step: number, type: TimePickerType) {
+  switch (type) {
+    case "minutes":
+      return getValidArrowMinuteOrSecond(value, step);
+    case "seconds":
+      return getValidArrowMinuteOrSecond(value, step);
+    case "hours":
+      return getValidArrowHour(value, step);
+    case "12hours":
+      return getValidArrow12Hour(value, step);
+    default:
+      return "00";
+  }
+}
+
+function convert12HourTo24Hour(hour: number, period: Period) {
+  if (period === "PM") {
+    if (hour <= 11) return hour + 12;
+    return hour;
+  }
+  if (period === "AM") {
+    if (hour === 12) return 0;
+    return hour;
+  }
+  return hour;
+}
+
+function display12HourValue(hours: number) {
+  if (hours === 0 || hours === 12) return "12";
+  if (hours >= 22) return `${hours - 12}`;
+  if (hours % 12 > 9) return `${hours}`;
+  return `0${hours % 12}`;
+}
+
+function genMonths(
+  locale: Pick<Locale, "options" | "localize" | "formatLong">,
+) {
+  return Array.from({ length: 12 }, (_, i) => ({
+    value: i,
+    label: format(new Date(2021, i), "MMMM", { locale }),
+  }));
+}
+
+function genYears(yearRange = 50) {
+  const today = new Date();
+  return Array.from({ length: yearRange * 2 + 1 }, (_, i) => ({
+    value: today.getFullYear() - yearRange + i,
+    label: (today.getFullYear() - yearRange + i).toString(),
+  }));
+}
+
+// ---------- CALENDAR COMPONENT ----------
+
+function Calendar({
+  className,
+  classNames,
+  showOutsideDays = true,
+  yearRange = 50,
+  ...props
+}: DayPickerProps & { yearRange?: number }) {
+  const MONTHS = React.useMemo(() => {
+    let locale: Pick<Locale, "options" | "localize" | "formatLong"> = enUS;
+    const { options, localize, formatLong } = props.locale || {};
+    if (options && localize && formatLong) {
+      locale = { options, localize, formatLong };
+    }
+    return genMonths(locale);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const YEARS = React.useMemo(() => genYears(yearRange), [yearRange]);
+
+  const disableLeftNavigation = () => {
+    const today = new Date();
+    const startDate = new Date(today.getFullYear() - yearRange, 0, 1);
+    if (props.month) {
+      return (
+        props.month.getMonth() === startDate.getMonth() &&
+        props.month.getFullYear() === startDate.getFullYear()
+      );
+    }
+    return false;
+  };
+
+  const disableRightNavigation = () => {
+    const today = new Date();
+    const endDate = new Date(today.getFullYear() + yearRange, 11, 31);
+    if (props.month) {
+      return (
+        props.month.getMonth() === endDate.getMonth() &&
+        props.month.getFullYear() === endDate.getFullYear()
+      );
+    }
+    return false;
+  };
+
+  return (
+    <DayPicker
+      showOutsideDays={showOutsideDays}
+      className={cn("p-3", className)}
+      classNames={{
+        months:
+          "flex flex-col sm:flex-row space-y-4 sm:space-y-0 justify-center",
+        month: "flex flex-col items-center space-y-4",
+        month_caption: "flex justify-center pt-1 relative items-center",
+        caption_label: "text-sm font-medium",
+        nav: "space-x-1 flex items-center",
+        button_previous: cn(
+          buttonVariants({ variant: "outline" }),
+          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute left-5 top-5",
+          disableLeftNavigation() && "pointer-events-none",
+        ),
+        button_next: cn(
+          buttonVariants({ variant: "outline" }),
+          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute right-5 top-5",
+          disableRightNavigation() && "pointer-events-none",
+        ),
+        month_grid: "w-full border-collapse space-y-1",
+        weekdays: cn("flex", props.showWeekNumber && "justify-end"),
+        weekday:
+          "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
+        week: "flex w-full mt-2",
+        day: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20 rounded-1",
+        day_button: cn(
+          buttonVariants({ variant: "ghost" }),
+          "h-9 w-9 p-0 font-normal aria-selected:opacity-100 rounded-l-md rounded-r-md",
+        ),
+        range_end: "day-range-end",
+        selected:
+          "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground rounded-l-md rounded-r-md",
+        today: "bg-accent text-accent-foreground",
+        outside:
+          "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
+        disabled: "text-muted-foreground opacity-50",
+        range_middle:
+          "aria-selected:bg-accent aria-selected:text-accent-foreground",
+        hidden: "invisible",
+        ...classNames,
+      }}
+      components={{
+        Chevron: ({ orientation }) =>
+          orientation === "left" ? (
+            <ChevronLeft className="h-4 w-4" />
+          ) : (
+            <ChevronRight className="h-4 w-4" />
+          ),
+        MonthCaption: ({ calendarMonth }) => {
+          return (
+            <div className="inline-flex gap-2">
+              <Select
+                defaultValue={calendarMonth.date.getMonth().toString()}
+                onValueChange={(value) => {
+                  const newDate = new Date(calendarMonth.date);
+                  newDate.setMonth(Number.parseInt(value, 10));
+                  props.onMonthChange?.(newDate);
+                }}
+              >
+                <SelectTrigger className="focus:bg-accent focus:text-accent-foreground w-fit gap-1 border-none p-0">
+                  <SelectValue />
+                </SelectTrigger>
+                <SelectContent>
+                  {MONTHS.map((month) => (
+                    <SelectItem
+                      key={month.value}
+                      value={month.value.toString()}
+                    >
+                      {month.label}
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+              <Select
+                defaultValue={calendarMonth.date.getFullYear().toString()}
+                onValueChange={(value) => {
+                  const newDate = new Date(calendarMonth.date);
+                  newDate.setFullYear(Number.parseInt(value, 10));
+                  props.onMonthChange?.(newDate);
+                }}
+              >
+                <SelectTrigger className="focus:bg-accent focus:text-accent-foreground w-fit gap-1 border-none p-0">
+                  <SelectValue />
+                </SelectTrigger>
+                <SelectContent>
+                  {YEARS.map((year) => (
+                    <SelectItem
+                      key={year.value}
+                      value={year.value.toString()}
+                    >
+                      {year.label}
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+            </div>
+          );
+        },
+      }}
+      {...props}
+    />
+  );
+}
+
+Calendar.displayName = "Calendar";
+
+// ---------- TIME PERIOD SELECT COMPONENT ----------
+
+interface PeriodSelectorProps {
+  period: Period;
+  setPeriod?: (m: Period) => void;
+  date?: Date | null;
+  onDateChange?: (date: Date | undefined) => void;
+  onRightFocus?: () => void;
+  onLeftFocus?: () => void;
+}
+
+const TimePeriodSelect = React.forwardRef<
+  HTMLButtonElement,
+  PeriodSelectorProps
+>(({ period, setPeriod, date, onDateChange, onLeftFocus, onRightFocus }, ref) => {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
+    if (e.key === "ArrowRight") onRightFocus?.();
+    if (e.key === "ArrowLeft") onLeftFocus?.();
+  };
+
+  const handleValueChange = (value: Period) => {
+    setPeriod?.(value);
+
+    if (date) {
+      const tempDate = new Date(date);
+      const hours = display12HourValue(date.getHours());
+      onDateChange?.(
+        setDateByType(
+          tempDate,
+          hours.toString(),
+          "12hours",
+          period === "AM" ? "PM" : "AM",
+        ),
+      );
+    }
+  };
+
+  return (
+    <div className="flex h-10 items-center">
+      <Select
+        defaultValue={period}
+        onValueChange={(value: string) => handleValueChange(value as Period)}
+      >
+        <SelectTrigger
+          ref={ref}
+          className="focus:bg-accent focus:text-accent-foreground w-[65px]"
+          onKeyDown={handleKeyDown}
+        >
+          <SelectValue />
+        </SelectTrigger>
+        <SelectContent>
+          <SelectItem value="AM">AM</SelectItem>
+          <SelectItem value="PM">PM</SelectItem>
+        </SelectContent>
+      </Select>
+    </div>
+  );
+});
+
+TimePeriodSelect.displayName = "TimePeriodSelect";
+
+// ---------- TIME PICKER INPUT COMPONENT ----------
+
+interface TimePickerInputProps
+  extends React.InputHTMLAttributes<HTMLInputElement> {
+  picker: TimePickerType;
+  date?: Date | null;
+  onDateChange?: (date: Date | undefined) => void;
+  period?: Period;
+  onRightFocus?: () => void;
+  onLeftFocus?: () => void;
+}
+
+const TimePickerInput = React.forwardRef<HTMLInputElement, TimePickerInputProps>(
+  (
+    {
+      className,
+      type = "tel",
+      value,
+      id,
+      name,
+      date = new Date(new Date().setHours(0, 0, 0, 0)),
+      onDateChange,
+      onChange,
+      onKeyDown,
+      picker,
+      period,
+      onLeftFocus,
+      onRightFocus,
+      ...props
+    },
+    ref,
+  ) => {
+    const [flag, setFlag] = React.useState<boolean>(false);
+    const [prevIntKey, setPrevIntKey] = React.useState<string>("0");
+
+    React.useEffect(() => {
+      if (flag) {
+        const timer = setTimeout(() => {
+          setFlag(false);
+        }, 2000);
+
+        return () => clearTimeout(timer);
+      }
+    }, [flag]);
+
+    const calculatedValue = React.useMemo(() => {
+      return getDateByType(date, picker);
+    }, [date, picker]);
+
+    const calculateNewValue = (key: string) => {
+      if (picker === "12hours") {
+        if (
+          flag &&
+          calculatedValue.slice(1, 2) === "1" &&
+          prevIntKey === "0"
+        )
+          return `0${key}`;
+      }
+
+      return !flag ? `0${key}` : calculatedValue.slice(1, 2) + key;
+    };
+
+    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+      if (e.key === "Tab") return;
+      e.preventDefault();
+      if (e.key === "ArrowRight") onRightFocus?.();
+      if (e.key === "ArrowLeft") onLeftFocus?.();
+      if (["ArrowUp", "ArrowDown"].includes(e.key)) {
+        const step = e.key === "ArrowUp" ? 1 : -1;
+        const newValue = getArrowByType(calculatedValue, step, picker);
+        if (flag) setFlag(false);
+        const tempDate = date ? new Date(date) : new Date();
+        onDateChange?.(setDateByType(tempDate, newValue, picker, period));
+      }
+      if (e.key >= "0" && e.key <= "9") {
+        if (picker === "12hours") setPrevIntKey(e.key);
+
+        const newValue = calculateNewValue(e.key);
+        if (flag) onRightFocus?.();
+        setFlag((prev) => !prev);
+        const tempDate = date ? new Date(date) : new Date();
+        onDateChange?.(setDateByType(tempDate, newValue, picker, period));
+      }
+    };
+
+    return (
+      <Input
+        ref={ref}
+        id={id || picker}
+        name={name || picker}
+        className={cn(
+          "focus:bg-accent focus:text-accent-foreground w-[48px] text-center font-mono text-base tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none",
+          className,
+        )}
+        value={value || calculatedValue}
+        onChange={(e) => {
+          e.preventDefault();
+          onChange?.(e);
+        }}
+        type={type}
+        inputMode="decimal"
+        onKeyDown={(e) => {
+          onKeyDown?.(e);
+          handleKeyDown(e);
+        }}
+        {...props}
+      />
+    );
+  },
+);
+
+TimePickerInput.displayName = "TimePickerInput";
+
+// ---------- TIME PICKER COMPONENT ----------
+
+interface TimePickerProps {
+  date?: Date | null;
+  onChange?: (date: Date | undefined) => void;
+  hourCycle?: 12 | 24;
+  granularity?: Granularity;
+}
+
+interface TimePickerRef {
+  minuteRef: HTMLInputElement | null;
+  hourRef: HTMLInputElement | null;
+  secondRef: HTMLInputElement | null;
+}
+
+type Granularity = "day" | "hour" | "minute" | "second";
+
+const TimePicker = React.forwardRef<TimePickerRef, TimePickerProps>(
+  ({ date, onChange, hourCycle = 24, granularity = "second" }, ref) => {
+    const minuteRef = React.useRef<HTMLInputElement>(null);
+    const hourRef = React.useRef<HTMLInputElement>(null);
+    const secondRef = React.useRef<HTMLInputElement>(null);
+    const periodRef = React.useRef<HTMLButtonElement>(null);
+    const [period, setPeriod] = React.useState<Period>(
+      date && date.getHours() >= 12 ? "PM" : "AM",
+    );
+
+    useImperativeHandle(
+      ref,
+      () => ({
+        minuteRef: minuteRef.current,
+        hourRef: hourRef.current,
+        secondRef: secondRef.current,
+        periodRef: periodRef.current,
+      }),
+      [],
+    );
+
+    return (
+      <div className="flex items-center justify-center gap-2">
+        <label
+          htmlFor="datetime-picker-hour-input"
+          className="cursor-pointer"
+        >
+          <Clock className="mr-2 h-4 w-4" />
+        </label>
+        <TimePickerInput
+          picker={hourCycle === 24 ? "hours" : "12hours"}
+          date={date}
+          id="datetime-picker-hour-input"
+          onDateChange={onChange}
+          ref={hourRef}
+          period={period}
+          onRightFocus={() => minuteRef?.current?.focus()}
+        />
+        {(granularity === "minute" || granularity === "second") && (
+          <>
+            :
+            <TimePickerInput
+              picker="minutes"
+              date={date}
+              onDateChange={onChange}
+              ref={minuteRef}
+              onLeftFocus={() => hourRef?.current?.focus()}
+              onRightFocus={() => secondRef?.current?.focus()}
+            />
+          </>
+        )}
+        {granularity === "second" && (
+          <>
+            :
+            <TimePickerInput
+              picker="seconds"
+              date={date}
+              onDateChange={onChange}
+              ref={secondRef}
+              onLeftFocus={() => minuteRef?.current?.focus()}
+              onRightFocus={() => periodRef?.current?.focus()}
+            />
+          </>
+        )}
+        {hourCycle === 12 && (
+          <div className="grid gap-1 text-center">
+            <TimePeriodSelect
+              period={period}
+              setPeriod={setPeriod}
+              date={date}
+              onDateChange={(date) => {
+                onChange?.(date);
+                if (date && date?.getHours() >= 12) {
+                  setPeriod("PM");
+                } else {
+                  setPeriod("AM");
+                }
+              }}
+              ref={periodRef}
+              onLeftFocus={() => secondRef?.current?.focus()}
+            />
+          </div>
+        )}
+      </div>
+    );
+  },
+);
+
+TimePicker.displayName = "TimePicker";
+
+// ---------- DATETIME PICKER COMPONENT ----------
+
+type DateTimePickerProps = {
+  value?: Date;
+  onChange?: (date: Date | undefined) => void;
+  onMonthChange?: (date: Date | undefined) => void;
+  disabled?: boolean;
+  hourCycle?: 12 | 24;
+  placeholder?: string;
+  yearRange?: number;
+  displayFormat?: { hour24?: string; hour12?: string };
+  granularity?: Granularity;
+  className?: string;
+  defaultPopupValue?: Date;
+} & Pick<
+  DayPickerProps,
+  "locale" | "weekStartsOn" | "showWeekNumber" | "showOutsideDays"
+>;
+
+type DateTimePickerRef = {
+  value?: Date;
+} & Omit<HTMLButtonElement, "value">;
+
+const DateTimePicker = React.forwardRef<
+  Partial<DateTimePickerRef>,
+  DateTimePickerProps
+>(
+  (
+    {
+      locale = enUS,
+      defaultPopupValue = new Date(new Date().setHours(0, 0, 0, 0)),
+      value,
+      onChange,
+      onMonthChange,
+      hourCycle = 24,
+      yearRange = 50,
+      disabled = false,
+      displayFormat,
+      granularity = "second",
+      placeholder = "Pick a date",
+      className,
+      ...props
+    },
+    ref,
+  ) => {
+    const [month, setMonth] = React.useState<Date>(
+      value ?? defaultPopupValue,
+    );
+    const buttonRef = useRef<HTMLButtonElement>(null);
+    const [displayDate, setDisplayDate] = React.useState<Date | undefined>(
+      value ?? undefined,
+    );
+    onMonthChange ||= onChange;
+
+    React.useEffect(() => {
+      setDisplayDate(value);
+    }, [value]);
+
+    const handleMonthChange = (newDay: Date | undefined) => {
+      if (!newDay) return;
+      if (!defaultPopupValue) {
+        newDay.setHours(
+          month?.getHours() ?? 0,
+          month?.getMinutes() ?? 0,
+          month?.getSeconds() ?? 0,
+        );
+        onMonthChange?.(newDay);
+        setMonth(newDay);
+        return;
+      }
+      const diff = newDay.getTime() - defaultPopupValue.getTime();
+      const diffInDays = diff / (1000 * 60 * 60 * 24);
+      const newDateFull = add(defaultPopupValue, {
+        days: Math.ceil(diffInDays),
+      });
+      newDateFull.setHours(
+        month?.getHours() ?? 0,
+        month?.getMinutes() ?? 0,
+        month?.getSeconds() ?? 0,
+      );
+      onMonthChange?.(newDateFull);
+      setMonth(newDateFull);
+    };
+
+    const onSelect = (newDay?: Date) => {
+      if (!newDay) return;
+      onChange?.(newDay);
+      setMonth(newDay);
+      setDisplayDate(newDay);
+    };
+
+    useImperativeHandle(
+      ref,
+      () => ({
+        ...buttonRef.current,
+        value: displayDate,
+      }),
+      [displayDate],
+    );
+
+    const initHourFormat = {
+      hour24:
+        displayFormat?.hour24 ??
+        `PPP HH:mm${!granularity || granularity === "second" ? ":ss" : ""}`,
+      hour12:
+        displayFormat?.hour12 ??
+        `PP hh:mm${!granularity || granularity === "second" ? ":ss" : ""} b`,
+    };
+
+    let loc = enUS;
+    const { options, localize, formatLong } = locale;
+    if (options && localize && formatLong) {
+      loc = {
+        ...enUS,
+        options,
+        localize,
+        formatLong,
+      };
+    }
+
+    return (
+      <Popover>
+        <PopoverTrigger asChild disabled={disabled}>
+          <Button
+            variant="outline"
+            className={cn(
+              "w-full justify-start text-left font-normal",
+              !displayDate && "text-muted-foreground",
+              className,
+            )}
+            ref={buttonRef}
+          >
+            <CalendarIcon className="mr-2 h-4 w-4" />
+            {displayDate ? (
+              format(
+                displayDate,
+                hourCycle === 24
+                  ? initHourFormat.hour24
+                  : initHourFormat.hour12,
+                { locale: loc },
+              )
+            ) : (
+              <span>{placeholder}</span>
+            )}
+          </Button>
+        </PopoverTrigger>
+        <PopoverContent className="w-auto p-0">
+          <Calendar
+            mode="single"
+            selected={displayDate}
+            month={month}
+            onSelect={(newDate) => {
+              if (newDate) {
+                newDate.setHours(
+                  month?.getHours() ?? 0,
+                  month?.getMinutes() ?? 0,
+                  month?.getSeconds() ?? 0,
+                );
+                onSelect(newDate);
+              }
+            }}
+            onMonthChange={handleMonthChange}
+            yearRange={yearRange}
+            locale={locale}
+            {...props}
+          />
+          {granularity !== "day" && (
+            <div className="border-border border-t p-3">
+              <TimePicker
+                onChange={(value) => {
+                  onChange?.(value);
+                  setDisplayDate(value);
+                  if (value) {
+                    setMonth(value);
+                  }
+                }}
+                date={month}
+                hourCycle={hourCycle}
+                granularity={granularity}
+              />
+            </div>
+          )}
+        </PopoverContent>
+      </Popover>
+    );
+  },
+);
+
+DateTimePicker.displayName = "DateTimePicker";
+
+export { DateTimePicker, TimePickerInput, TimePicker };
+export type { TimePickerType, DateTimePickerProps, DateTimePickerRef, Granularity };

+ 63 - 0
src/components/ui/slider.tsx

@@ -0,0 +1,63 @@
+"use client"
+
+import * as React from "react"
+import { Slider as SliderPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Slider({
+  className,
+  defaultValue,
+  value,
+  min = 0,
+  max = 100,
+  ...props
+}: React.ComponentProps<typeof SliderPrimitive.Root>) {
+  const _values = React.useMemo(
+    () =>
+      Array.isArray(value)
+        ? value
+        : Array.isArray(defaultValue)
+          ? defaultValue
+          : [min, max],
+    [value, defaultValue, min, max]
+  )
+
+  return (
+    <SliderPrimitive.Root
+      data-slot="slider"
+      defaultValue={defaultValue}
+      value={value}
+      min={min}
+      max={max}
+      className={cn(
+        "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
+        className
+      )}
+      {...props}
+    >
+      <SliderPrimitive.Track
+        data-slot="slider-track"
+        className={cn(
+          "relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
+        )}
+      >
+        <SliderPrimitive.Range
+          data-slot="slider-range"
+          className={cn(
+            "absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
+          )}
+        />
+      </SliderPrimitive.Track>
+      {Array.from({ length: _values.length }, (_, index) => (
+        <SliderPrimitive.Thumb
+          data-slot="slider-thumb"
+          key={index}
+          className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
+        />
+      ))}
+    </SliderPrimitive.Root>
+  )
+}
+
+export { Slider }

+ 6 - 1
src/components/vehicle-picker-dialog.tsx

@@ -52,6 +52,7 @@ interface VehiclePickerDialogProps {
   vehicles: Vehicle[];
   vehicles: Vehicle[];
   customers: Customer[];
   customers: Customer[];
   title?: string;
   title?: string;
+  redirectQuery?: Record<string, string>;
 }
 }
 
 
 export function VehiclePickerDialog({
 export function VehiclePickerDialog({
@@ -60,6 +61,7 @@ export function VehiclePickerDialog({
   vehicles,
   vehicles,
   customers,
   customers,
   title,
   title,
+  redirectQuery,
 }: VehiclePickerDialogProps) {
 }: VehiclePickerDialogProps) {
   const router = useRouter();
   const router = useRouter();
   const t = useTranslations("workOrders.vehiclePicker");
   const t = useTranslations("workOrders.vehiclePicker");
@@ -107,7 +109,10 @@ export function VehiclePickerDialog({
   const handleSelect = (vehicleId: string) => {
   const handleSelect = (vehicleId: string) => {
     onOpenChange(false);
     onOpenChange(false);
     resetState();
     resetState();
-    router.push(`/vehicles/${vehicleId}/service/new`);
+    const qs = redirectQuery
+      ? "?" + new URLSearchParams(redirectQuery).toString()
+      : "";
+    router.push(`/vehicles/${vehicleId}/service/new${qs}`);
   };
   };
 
 
   const handleOpenChange = (nextOpen: boolean) => {
   const handleOpenChange = (nextOpen: boolean) => {

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

@@ -70,6 +70,9 @@ export const SETTING_KEYS = {
   SMS_TEMPLATE_STATUS_COMPLETED: "sms.template.statusCompleted",
   SMS_TEMPLATE_STATUS_COMPLETED: "sms.template.statusCompleted",
   SMS_TEMPLATE_PAYMENT_RECEIVED: "sms.template.paymentReceived",
   SMS_TEMPLATE_PAYMENT_RECEIVED: "sms.template.paymentReceived",
   PORTAL_ENABLED: "portal.enabled",
   PORTAL_ENABLED: "portal.enabled",
+  WORKBOARD_WEEK_START_DAY: "workboard.weekStartDay",
+  WORKBOARD_WORK_DAY_START: "workboard.workDayStart",
+  WORKBOARD_WORK_DAY_END: "workboard.workDayEnd",
 } as const;
 } as const;
 
 
 export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];
 export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];

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

@@ -4,7 +4,12 @@ import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 
-export async function createDraftServiceRecord(vehicleId: string, serviceDate?: Date) {
+export async function createDraftServiceRecord(
+  vehicleId: string,
+  startDateTime?: Date,
+  endDateTime?: Date,
+  technicianId?: string,
+) {
   return withAuth(
   return withAuth(
     async ({ organizationId, userId }) => {
     async ({ organizationId, userId }) => {
       const vehicle = await db.vehicle.findFirst({
       const vehicle = await db.vehicle.findFirst({
@@ -12,13 +17,16 @@ export async function createDraftServiceRecord(vehicleId: string, serviceDate?:
       });
       });
       if (!vehicle) throw new Error("Vehicle not found");
       if (!vehicle) throw new Error("Vehicle not found");
 
 
-      // Auto-populate shop name and invoice number
       const [settings, org, currentUser] = await Promise.all([
       const [settings, org, currentUser] = await Promise.all([
         db.appSetting.findMany({
         db.appSetting.findMany({
           where: {
           where: {
             organizationId,
             organizationId,
             key: {
             key: {
-              in: ["workshop.invoicePrefix", "workshop.invoiceStartNumber"],
+              in: [
+                "workshop.invoicePrefix",
+                "workshop.invoiceStartNumber",
+                "workshop.defaultTechnician",
+              ],
             },
             },
           },
           },
         }),
         }),
@@ -35,19 +43,39 @@ export async function createDraftServiceRecord(vehicleId: string, serviceDate?:
       for (const s of settings) settingsMap[s.key] = s.value;
       for (const s of settings) settingsMap[s.key] = s.value;
 
 
       const shopName = org?.name || undefined;
       const shopName = org?.name || undefined;
-      const techName = currentUser?.name || undefined;
+      let techName = currentUser?.name || undefined;
+
+      // If no technician specified, look up the default technician from settings
+      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 a technician is resolved (explicit or default), use their name
+      if (resolvedTechId) {
+        const tech = await db.technician.findFirst({
+          where: { id: resolvedTechId, organizationId },
+          select: { name: true },
+        });
+        if (tech) techName = tech.name;
+      }
 
 
-      // Resolve invoice prefix
       const rawPrefix = settingsMap["workshop.invoicePrefix"] || "{year}-";
       const rawPrefix = settingsMap["workshop.invoicePrefix"] || "{year}-";
       const now = new Date();
       const now = new Date();
       const prefix = rawPrefix
       const prefix = rawPrefix
         .replace("{year}", now.getFullYear().toString())
         .replace("{year}", now.getFullYear().toString())
         .replace("{month}", String(now.getMonth() + 1).padStart(2, "0"));
         .replace("{month}", String(now.getMonth() + 1).padStart(2, "0"));
 
 
-      // Generate sequential invoice number
       const startNumber = parseInt(
       const startNumber = parseInt(
         settingsMap["workshop.invoiceStartNumber"] || "0",
         settingsMap["workshop.invoiceStartNumber"] || "0",
-        10
+        10,
       );
       );
       const lastRecord = await db.serviceRecord.findFirst({
       const lastRecord = await db.serviceRecord.findFirst({
         where: { vehicle: { organizationId } },
         where: { vehicle: { organizationId } },
@@ -79,8 +107,11 @@ export async function createDraftServiceRecord(vehicleId: string, serviceDate?:
           vehicleId,
           vehicleId,
           shopName,
           shopName,
           techName,
           techName,
+          technicianId: resolvedTechId || undefined,
           invoiceNumber,
           invoiceNumber,
-          serviceDate: serviceDate ?? now,
+          serviceDate: startDateTime ?? now,
+          startDateTime: startDateTime ?? now,
+          endDateTime: endDateTime ?? new Date((startDateTime ?? now).getTime() + 3600000),
         },
         },
       });
       });
 
 
@@ -93,6 +124,6 @@ export async function createDraftServiceRecord(vehicleId: string, serviceDate?:
           subject: PermissionSubject.SERVICES,
           subject: PermissionSubject.SERVICES,
         },
         },
       ],
       ],
-    }
+    },
   );
   );
 }
 }

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

@@ -456,77 +456,6 @@ export async function updateServiceRecord(input: unknown) {
       });
       });
     }
     }
 
 
-    // Sync board assignment when techName or serviceDate changes
-    const techChanged = recordData.techName !== undefined && recordData.techName !== existing.techName;
-    const dateChanged = recordData.serviceDate !== undefined && recordData.serviceDate !== existing.serviceDate.toISOString().split("T")[0];
-
-    if (techChanged || dateChanged) {
-      const boardAssignment = await db.boardAssignment.findFirst({
-        where: { serviceRecordId: id, organizationId },
-      });
-      if (boardAssignment) {
-        // Build update payload
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        const updateData: any = {};
-
-        if (dateChanged && recordData.serviceDate) {
-          updateData.date = new Date(recordData.serviceDate);
-        }
-
-        if (techChanged && recordData.techName) {
-          const matchingTech = await db.technician.findFirst({
-            where: { organizationId, name: recordData.techName, isActive: true },
-          });
-          if (matchingTech) {
-            updateData.technicianId = matchingTech.id;
-          }
-        }
-
-        if (Object.keys(updateData).length > 0) {
-          const updated = await db.boardAssignment.update({
-            where: { id: boardAssignment.id },
-            data: updateData,
-            include: {
-              technician: true,
-              serviceRecord: {
-                select: {
-                  id: true, title: true, status: true,
-                  vehicle: {
-                    select: { id: true, make: true, model: true, year: true, licensePlate: true },
-                  },
-                },
-              },
-              inspection: {
-                select: {
-                  id: true, status: true,
-                  vehicle: {
-                    select: { id: true, make: true, model: true, year: true, licensePlate: true },
-                  },
-                  template: { select: { name: true } },
-                },
-              },
-            },
-          });
-          notificationBus.emit("workboard", {
-            type: "assignment_moved",
-            organizationId,
-            assignment: {
-              ...updated,
-              date: updated.date.toISOString().split("T")[0],
-              createdAt: updated.createdAt.toISOString(),
-              updatedAt: updated.updatedAt.toISOString(),
-              technician: {
-                ...updated.technician,
-                createdAt: updated.technician.createdAt.toISOString(),
-                updatedAt: updated.technician.updatedAt.toISOString(),
-              },
-            },
-          });
-          revalidatePath("/work-board");
-        }
-      }
-    }
-
     revalidatePath("/");
     revalidatePath("/");
     revalidatePath(`/vehicles/${existing.vehicleId}`);
     revalidatePath(`/vehicles/${existing.vehicleId}`);
     revalidatePath(`/vehicles/${existing.vehicleId}/service/${id}`);
     revalidatePath(`/vehicles/${existing.vehicleId}/service/${id}`);

+ 46 - 30
src/features/vehicles/Components/service-detail/ServiceDetailContent.tsx

@@ -1,21 +1,9 @@
 'use client'
 'use client'
 
 
 import { useEffect, useState } from 'react'
 import { useEffect, useState } from 'react'
-import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'
 
 
-const LG_BREAKPOINT = 1024
-
-function useIsLargeScreen() {
-  const [isLarge, setIsLarge] = useState(false)
-  useEffect(() => {
-    const mql = window.matchMedia(`(min-width: ${LG_BREAKPOINT}px)`)
-    const onChange = () => setIsLarge(mql.matches)
-    mql.addEventListener('change', onChange)
-    setIsLarge(mql.matches)
-    return () => mql.removeEventListener('change', onChange)
-  }, [])
-  return isLarge
-}
+const SIDEBAR_MIN_W = 280
+const SIDEBAR_DEFAULT_PCT = 0.22 // 22% of viewport
 
 
 interface ServiceDetailContentProps {
 interface ServiceDetailContentProps {
   leftColumn: React.ReactNode
   leftColumn: React.ReactNode
@@ -23,32 +11,60 @@ interface ServiceDetailContentProps {
 }
 }
 
 
 export function ServiceDetailContent({ leftColumn, rightColumn }: ServiceDetailContentProps) {
 export function ServiceDetailContent({ leftColumn, rightColumn }: ServiceDetailContentProps) {
-  const isLarge = useIsLargeScreen()
+  const [sidebarWidth, setSidebarWidth] = useState(() =>
+    typeof window !== 'undefined' ? Math.max(SIDEBAR_MIN_W, window.innerWidth * SIDEBAR_DEFAULT_PCT) : 380
+  )
+  const [isDragging, setIsDragging] = useState(false)
+
+  useEffect(() => {
+    if (!isDragging) return
+    const onMouseMove = (e: MouseEvent) => {
+      const newWidth = window.innerWidth - e.clientX
+      setSidebarWidth(Math.max(SIDEBAR_MIN_W, Math.min(newWidth, window.innerWidth * 0.6)))
+    }
+    const onMouseUp = () => setIsDragging(false)
+    document.body.style.cursor = 'col-resize'
+    document.body.style.userSelect = 'none'
+    window.addEventListener('mousemove', onMouseMove)
+    window.addEventListener('mouseup', onMouseUp)
+    return () => {
+      document.body.style.cursor = ''
+      document.body.style.userSelect = ''
+      window.removeEventListener('mousemove', onMouseMove)
+      window.removeEventListener('mouseup', onMouseUp)
+    }
+  }, [isDragging])
 
 
-  if (!isLarge) {
-    return (
-      <div className="flex-1 overflow-y-auto overscroll-contain p-4">
+  return (
+    <>
+      {/* Mobile: stacked layout */}
+      <div className="flex-1 overflow-y-auto overscroll-contain p-4 lg:hidden">
         <div className="space-y-3 pb-40">
         <div className="space-y-3 pb-40">
           {leftColumn}
           {leftColumn}
           {rightColumn}
           {rightColumn}
         </div>
         </div>
       </div>
       </div>
-    )
-  }
 
 
-  return (
-    <ResizablePanelGroup orientation="horizontal" className="flex-1 overflow-hidden">
-      <ResizablePanel defaultSize={75} minSize={40}>
-        <div className="h-full overflow-y-auto overscroll-contain p-4 pr-2">
+      {/* Desktop: side-by-side with resizable sidebar */}
+      <div className="hidden lg:flex flex-1 overflow-hidden">
+        <div className="flex-1 min-w-0 overflow-y-auto overscroll-contain p-4 pr-2">
           <div className="space-y-3 pb-40">{leftColumn}</div>
           <div className="space-y-3 pb-40">{leftColumn}</div>
         </div>
         </div>
-      </ResizablePanel>
-      <ResizableHandle withHandle />
-      <ResizablePanel defaultSize={25} minSize={15}>
-        <div className="h-full overflow-y-auto overscroll-contain p-4 pl-2">
+        <div
+          className="w-1.5 shrink-0 cursor-col-resize bg-border hover:bg-primary/30 transition-colors relative group"
+          onMouseDown={() => setIsDragging(true)}
+        >
+          <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-8 w-4 flex items-center justify-center rounded-sm border bg-background shadow-sm">
+            <svg width="6" height="14" viewBox="0 0 6 14" className="text-muted-foreground"><path d="M1 0v14M5 0v14" stroke="currentColor" strokeWidth="1" /></svg>
+          </div>
+        </div>
+        <div
+          className="shrink-0 overflow-y-auto overscroll-contain p-4 pl-2"
+          style={{ width: sidebarWidth }}
+        >
           <div className="space-y-3 pb-40">{rightColumn}</div>
           <div className="space-y-3 pb-40">{rightColumn}</div>
         </div>
         </div>
-      </ResizablePanel>
-    </ResizablePanelGroup>
+      </div>
+    </>
   )
   )
 }
 }

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

@@ -27,6 +27,7 @@ export interface ServiceDetail {
   serviceDate: Date;
   serviceDate: Date;
   shopName: string | null;
   shopName: string | null;
   techName: string | null;
   techName: string | null;
+  technicianId: string | null;
   parts: string | null;
   parts: string | null;
   laborHours: number | null;
   laborHours: number | null;
   diagnosticNotes: string | null;
   diagnosticNotes: string | null;

+ 32 - 165
src/features/vehicles/Components/service-edit/BasicInfoSection.tsx

@@ -1,6 +1,6 @@
 'use client'
 'use client'
 
 
-import { useMemo, useState } from 'react'
+import { useMemo } from 'react'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
@@ -23,12 +23,7 @@ import {
 } from '@/components/ui/command'
 } from '@/components/ui/command'
 import Link from 'next/link'
 import Link from 'next/link'
 import { Check, ChevronsUpDown, ExternalLink } from 'lucide-react'
 import { Check, ChevronsUpDown, ExternalLink } from 'lucide-react'
-import type { InitialData, VehicleOption, TeamMemberOption } from './form-types'
-
-export interface BoardTechnicianOption {
-  id: string
-  name: string
-}
+import type { InitialData, VehicleOption } from './form-types'
 
 
 interface CustomerInfo {
 interface CustomerInfo {
   id: string
   id: string
@@ -50,11 +45,6 @@ interface BasicInfoSectionProps {
   status: string
   status: string
   setStatus: (status: string) => void
   setStatus: (status: string) => void
   techName: string
   techName: string
-  setTechName: (name: string) => void
-  techOpen: boolean
-  setTechOpen: (open: boolean) => void
-  teamMembers: TeamMemberOption[]
-  boardTechnicians?: BoardTechnicianOption[]
   customer?: CustomerInfo | null
   customer?: CustomerInfo | null
 }
 }
 
 
@@ -71,11 +61,6 @@ export function BasicInfoSection({
   status,
   status,
   setStatus,
   setStatus,
   techName,
   techName,
-  setTechName,
-  techOpen,
-  setTechOpen,
-  teamMembers,
-  boardTechnicians = [],
   customer,
   customer,
 }: BasicInfoSectionProps) {
 }: BasicInfoSectionProps) {
   const t = useTranslations('service.basicInfo')
   const t = useTranslations('service.basicInfo')
@@ -178,20 +163,18 @@ export function BasicInfoSection({
         />
         />
       </div>
       </div>
 
 
+      {/* Hidden serviceDate — kept for form submission, visible scheduling is in ScheduleTimesSection */}
+      <input
+        type="hidden"
+        name="serviceDate"
+        value={initialData.serviceDate || new Date().toISOString().split('T')[0]}
+      />
+
       <div className="grid grid-cols-2 gap-2">
       <div className="grid grid-cols-2 gap-2">
-        <div className="space-y-1">
-          <Label htmlFor="serviceDate" className="text-xs">{t('date')}</Label>
-          <Input
-            id="serviceDate"
-            name="serviceDate"
-            type="date"
-            defaultValue={initialData.serviceDate || new Date().toISOString().split('T')[0]}
-          />
-        </div>
         <div className="space-y-1">
         <div className="space-y-1">
           <Label className="text-xs">{t('type')}</Label>
           <Label className="text-xs">{t('type')}</Label>
           <Select value={type} onValueChange={setType}>
           <Select value={type} onValueChange={setType}>
-            <SelectTrigger>
+            <SelectTrigger className="w-full">
               <SelectValue />
               <SelectValue />
             </SelectTrigger>
             </SelectTrigger>
             <SelectContent>
             <SelectContent>
@@ -202,6 +185,20 @@ export function BasicInfoSection({
             </SelectContent>
             </SelectContent>
           </Select>
           </Select>
         </div>
         </div>
+        <div className="space-y-1">
+          <Label className="text-xs">{t('status')}</Label>
+          <Select value={status} onValueChange={setStatus}>
+            <SelectTrigger className="w-full">
+              <SelectValue />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="pending">{t('statusOptions.pending')}</SelectItem>
+              <SelectItem value="in-progress">{t('statusOptions.in_progress')}</SelectItem>
+              <SelectItem value="waiting-parts">{t('statusOptions.waiting_parts')}</SelectItem>
+              <SelectItem value="completed">{t('statusOptions.completed')}</SelectItem>
+            </SelectContent>
+          </Select>
+        </div>
       </div>
       </div>
 
 
       <div className="grid grid-cols-2 gap-2">
       <div className="grid grid-cols-2 gap-2">
@@ -216,148 +213,18 @@ export function BasicInfoSection({
           />
           />
         </div>
         </div>
         <div className="space-y-1">
         <div className="space-y-1">
-          <Label className="text-xs">{t('status')}</Label>
-          <Select value={status} onValueChange={setStatus}>
-            <SelectTrigger>
-              <SelectValue />
-            </SelectTrigger>
-            <SelectContent>
-              <SelectItem value="pending">{t('statusOptions.pending')}</SelectItem>
-              <SelectItem value="in-progress">{t('statusOptions.in_progress')}</SelectItem>
-              <SelectItem value="waiting-parts">{t('statusOptions.waiting_parts')}</SelectItem>
-              <SelectItem value="completed">{t('statusOptions.completed')}</SelectItem>
-            </SelectContent>
-          </Select>
+          <Label htmlFor="invoiceNumber" className="text-xs">{t('invoiceNumber')}</Label>
+          <Input
+            id="invoiceNumber"
+            name="invoiceNumber"
+            placeholder="2026-1001"
+            defaultValue={initialData.invoiceNumber || ''}
+          />
         </div>
         </div>
       </div>
       </div>
 
 
-      <TechnicianPicker
-        techName={techName}
-        setTechName={setTechName}
-        techOpen={techOpen}
-        setTechOpen={setTechOpen}
-        boardTechnicians={boardTechnicians}
-        teamMembers={teamMembers}
-      />
-
-      <div className="space-y-1">
-        <Label htmlFor="invoiceNumber" className="text-xs">{t('invoiceNumber')}</Label>
-        <Input
-          id="invoiceNumber"
-          name="invoiceNumber"
-          placeholder="2026-1001"
-          defaultValue={initialData.invoiceNumber || ''}
-        />
-      </div>
+      <input type="hidden" name="techName" value={techName} />
     </div>
     </div>
   )
   )
 }
 }
 
 
-function TechnicianPicker({
-  techName,
-  setTechName,
-  techOpen,
-  setTechOpen,
-  boardTechnicians,
-  teamMembers,
-}: {
-  techName: string
-  setTechName: (name: string) => void
-  techOpen: boolean
-  setTechOpen: (open: boolean) => void
-  boardTechnicians: BoardTechnicianOption[]
-  teamMembers: TeamMemberOption[]
-}) {
-  const t = useTranslations('service.basicInfo.techPicker')
-  const [search, setSearch] = useState('')
-
-  return (
-    <div className="space-y-1">
-      <Label className="text-xs">Technician</Label>
-      <Popover
-        open={techOpen}
-        onOpenChange={(open) => {
-          setTechOpen(open)
-          if (open) setSearch('')
-        }}
-      >
-        <PopoverTrigger asChild>
-          <Button
-            variant="outline"
-            role="combobox"
-            aria-expanded={techOpen}
-            className="w-full justify-between font-normal"
-          >
-            {techName || <span className="text-muted-foreground">{t('placeholder')}</span>}
-            <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-          </Button>
-        </PopoverTrigger>
-        <PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
-          <Command>
-            <CommandInput
-              placeholder={t('searchPlaceholder')}
-              value={search}
-              onValueChange={setSearch}
-            />
-            <CommandList>
-              <CommandEmpty>
-                {search ? (
-                  <button
-                    type="button"
-                    className="w-full px-2 py-1.5 text-sm text-left"
-                    onClick={() => {
-                      setTechName(search)
-                      setTechOpen(false)
-                    }}
-                  >
-                    {t('useCustom', { search })}
-                  </button>
-                ) : (
-                  t('typePrompt')
-                )}
-              </CommandEmpty>
-              {boardTechnicians.length > 0 && (
-                <CommandGroup heading={t('boardTechnicians')}>
-                  {boardTechnicians.map((tech) => (
-                    <CommandItem
-                      key={tech.id}
-                      value={tech.name}
-                      onSelect={() => {
-                        setTechName(tech.name)
-                        setTechOpen(false)
-                      }}
-                    >
-                      <Check
-                        className={`mr-2 h-4 w-4 ${techName === tech.name ? 'opacity-100' : 'opacity-0'}`}
-                      />
-                      {tech.name}
-                    </CommandItem>
-                  ))}
-                </CommandGroup>
-              )}
-              {teamMembers.length > 0 && (
-                <CommandGroup heading={t('teamMembers')}>
-                  {teamMembers.map((member) => (
-                    <CommandItem
-                      key={member.id}
-                      value={member.name}
-                      onSelect={() => {
-                        setTechName(member.name)
-                        setTechOpen(false)
-                      }}
-                    >
-                      <Check
-                        className={`mr-2 h-4 w-4 ${techName === member.name ? 'opacity-100' : 'opacity-0'}`}
-                      />
-                      {member.name}
-                    </CommandItem>
-                  ))}
-                </CommandGroup>
-              )}
-            </CommandList>
-          </Command>
-        </PopoverContent>
-      </Popover>
-    </div>
-  )
-}

+ 309 - 0
src/features/vehicles/Components/service-edit/ScheduleTimesSection.tsx

@@ -0,0 +1,309 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { useTranslations } from 'next-intl'
+import { Label } from '@/components/ui/label'
+import { Button } from '@/components/ui/button'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+  CommandSeparator,
+} from '@/components/ui/command'
+import { Input } from '@/components/ui/input'
+import { Check, ChevronsUpDown, Clock, Plus } from 'lucide-react'
+import { toast } from 'sonner'
+import { DateTimePicker } from '@/components/ui/datetime-picker'
+import {
+  assignTechnician,
+  updateServiceTimes,
+} from '@/features/workboard/Actions/boardActions'
+import { createTechnician } from '@/features/workboard/Actions/technicianActions'
+import { cn } from '@/lib/utils'
+
+const HOUR_PRESETS = [1, 2, 4, 5, 7]
+
+interface Technician {
+  id: string
+  name: string
+}
+
+interface ScheduleTimesSectionProps {
+  serviceRecordId: string
+  technicians?: Technician[]
+  initialStartDateTime?: string | null
+  initialEndDateTime?: string | null
+  initialTechnicianId?: string | null
+  onSaved?: () => void
+}
+
+export function ScheduleTimesSection({
+  serviceRecordId,
+  technicians: initialTechnicians = [],
+  initialStartDateTime,
+  initialEndDateTime,
+  initialTechnicianId,
+  onSaved,
+}: ScheduleTimesSectionProps) {
+  const t = useTranslations('service.schedule')
+  const [selectedTechId, setSelectedTechId] = useState(initialTechnicianId || '')
+  const [techOpen, setTechOpen] = useState(false)
+  const [technicians, setTechnicians] = useState<Technician[]>(initialTechnicians)
+  const [techSearch, setTechSearch] = useState('')
+  const [creating, setCreating] = useState(false)
+  const [showNewInput, setShowNewInput] = useState(false)
+  const [mounted, setMounted] = useState(false)
+  useEffect(() => { setMounted(true) }, [])
+  const [newTechName, setNewTechName] = useState('')
+
+  const [startDateTime, setStartDateTime] = useState<Date | undefined>(
+    initialStartDateTime ? new Date(initialStartDateTime) : new Date(),
+  )
+  const [endDateTime, setEndDateTime] = useState<Date | undefined>(
+    initialEndDateTime ? new Date(initialEndDateTime) : new Date(Date.now() + 3600000),
+  )
+
+  const saveTimes = async (start: Date, end: Date) => {
+    if (end <= start) {
+      toast.error(t('endBeforeStart'))
+      return
+    }
+    const res = await updateServiceTimes({
+      id: serviceRecordId,
+      startDateTime: start,
+      endDateTime: end,
+    })
+    if (res.success) {
+      onSaved?.()
+    } else {
+      toast.error(res.error || t('failedUpdate'))
+    }
+  }
+
+  const handlePreset = (hours: number) => {
+    if (!startDateTime) return
+    const newEnd = new Date(startDateTime.getTime() + hours * 3600000)
+    setEndDateTime(newEnd)
+    saveTimes(startDateTime, newEnd)
+  }
+
+  const handleTechSelect = async (techId: string) => {
+    setSelectedTechId(techId)
+    setTechOpen(false)
+    const res = await assignTechnician({
+      id: serviceRecordId,
+      technicianId: techId,
+      type: 'serviceRecord',
+    })
+    if (res.success) {
+      onSaved?.()
+    } else {
+      toast.error(t('failedAssign'))
+      setSelectedTechId(initialTechnicianId || '')
+    }
+  }
+
+  const doCreateTechnician = async (name: string) => {
+    if (!name.trim()) return
+    setCreating(true)
+    const res = await createTechnician({ name: name.trim() })
+    setCreating(false)
+    if (res.success && res.data) {
+      const newTech = { id: res.data.id, name: res.data.name }
+      setTechnicians((prev) => [...prev, newTech])
+      setTechSearch('')
+      setNewTechName('')
+      setShowNewInput(false)
+      handleTechSelect(newTech.id)
+    } else {
+      toast.error(t('failedCreate'))
+    }
+  }
+
+  const selectedTechName = technicians.find((t) => t.id === selectedTechId)?.name
+
+  const searchLower = techSearch.toLowerCase()
+  const exactMatch = technicians.some((t) => t.name.toLowerCase() === searchLower)
+
+  const currentHours = startDateTime && endDateTime
+    ? Math.round((endDateTime.getTime() - startDateTime.getTime()) / 3600000)
+    : null
+
+  return (
+    <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>
+
+      <div className="space-y-1">
+        <Label className="text-xs">{t('technician')}</Label>
+        <Popover open={techOpen} onOpenChange={setTechOpen} modal={true}>
+          <PopoverTrigger asChild>
+            <Button
+              variant="outline"
+              role="combobox"
+              aria-expanded={techOpen}
+              className="w-full justify-between font-normal"
+            >
+              <span className="truncate">
+                {selectedTechName || t('selectTechnician')}
+              </span>
+              <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+            </Button>
+          </PopoverTrigger>
+          <PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
+            <Command shouldFilter={true}>
+              <CommandInput
+                placeholder={t('searchOrCreate')}
+                value={techSearch}
+                onValueChange={setTechSearch}
+              />
+              <CommandList className="max-h-60 overflow-y-auto">
+                <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>
+                {techSearch.trim() && !exactMatch && (
+                  <CommandGroup>
+                    <CommandItem
+                      value={`__create__${techSearch}`}
+                      onSelect={() => doCreateTechnician(techSearch)}
+                      disabled={creating}
+                    >
+                      <Plus className="mr-2 h-4 w-4" />
+                      {creating ? t('creating') : t('createTechnician', { name: techSearch.trim() })}
+                    </CommandItem>
+                  </CommandGroup>
+                )}
+                <CommandSeparator />
+                <CommandGroup>
+                  {showNewInput ? (
+                    <div className="flex items-center gap-1.5 px-2 py-1.5" onKeyDown={(e) => e.stopPropagation()}>
+                      <Input
+                        autoFocus
+                        placeholder={t('newTechPlaceholder')}
+                        value={newTechName}
+                        onChange={(e) => setNewTechName(e.target.value)}
+                        onKeyDown={(e) => {
+                          if (e.key === 'Enter') {
+                            e.preventDefault()
+                            doCreateTechnician(newTechName)
+                          }
+                          if (e.key === 'Escape') {
+                            setShowNewInput(false)
+                            setNewTechName('')
+                          }
+                        }}
+                        className="h-7 text-sm"
+                        disabled={creating}
+                      />
+                      <Button
+                        size="sm"
+                        variant="ghost"
+                        className="h-7 px-2 shrink-0"
+                        disabled={creating || !newTechName.trim()}
+                        onClick={() => doCreateTechnician(newTechName)}
+                      >
+                        {creating ? t('creating') : t('add')}
+                      </Button>
+                    </div>
+                  ) : (
+                    <CommandItem
+                      value="__add_new__"
+                      onSelect={() => setShowNewInput(true)}
+                    >
+                      <Plus className="mr-2 h-4 w-4" />
+                      {t('addNew')}
+                    </CommandItem>
+                  )}
+                </CommandGroup>
+              </CommandList>
+            </Command>
+          </PopoverContent>
+        </Popover>
+      </div>
+
+      <div className="space-y-1.5">
+        <Label className="text-xs">{t('startTime')}</Label>
+        {mounted ? (
+          <DateTimePicker
+            value={startDateTime}
+            onChange={(d) => {
+              setStartDateTime(d);
+              if (d) {
+                const newEnd = new Date(d.getTime() + 3600000);
+                setEndDateTime(newEnd);
+                saveTimes(d, newEnd);
+              }
+            }}
+            granularity="minute"
+            hourCycle={24}
+            placeholder={t('startTime')}
+            displayFormat={{ hour24: "PPP HH:mm" }}
+          />
+        ) : (
+          <div className="h-9 rounded-md border" />
+        )}
+      </div>
+
+      <div className="space-y-1.5">
+        <Label className="text-xs">{t('endTime')}</Label>
+        {mounted ? (
+          <DateTimePicker
+            value={endDateTime}
+            onChange={(d) => {
+              setEndDateTime(d);
+              if (d && startDateTime) saveTimes(startDateTime, d);
+            }}
+            granularity="minute"
+            hourCycle={24}
+            placeholder={t('endTime')}
+            displayFormat={{ hour24: "PPP HH:mm" }}
+          />
+        ) : (
+          <div className="h-9 rounded-md border" />
+        )}
+      </div>
+
+      <div className="space-y-1">
+        <span className="text-xs font-medium text-muted-foreground">Duration presets</span>
+        <div className="flex flex-wrap gap-1">
+          {HOUR_PRESETS.map((h) => (
+            <button
+              key={h}
+              type="button"
+              onClick={() => handlePreset(h)}
+              className={cn(
+                "rounded-md border px-2.5 py-1 text-xs font-medium transition-colors",
+                currentHours === h
+                  ? "border-primary bg-primary text-primary-foreground"
+                  : "border-border bg-background text-foreground hover:bg-muted",
+              )}
+            >
+              {h}h
+            </button>
+          ))}
+        </div>
+      </div>
+    </div>
+  )
+}

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

@@ -22,6 +22,8 @@ export interface InitialData {
   status: string
   status: string
   mileage: number | null
   mileage: number | null
   serviceDate: string
   serviceDate: string
+  startDateTime?: string | null
+  endDateTime?: string | null
   techName: string
   techName: string
   diagnosticNotes: string
   diagnosticNotes: string
   invoiceNotes: string
   invoiceNotes: string

+ 22 - 15
src/features/vehicles/Components/service-page/ServicePageClient.tsx

@@ -11,8 +11,11 @@ import { sendInvoiceEmail } from '@/features/email/Actions/emailActions'
 import { SendEmailDialog } from '@/features/email/Components/SendEmailDialog'
 import { SendEmailDialog } from '@/features/email/Components/SendEmailDialog'
 import type { ServicePartInput, ServiceLaborInput } from '@/features/vehicles/Schema/serviceSchema'
 import type { ServicePartInput, ServiceLaborInput } from '@/features/vehicles/Schema/serviceSchema'
 import type { ServiceDetail } from '../service-detail/types'
 import type { ServiceDetail } from '../service-detail/types'
-import type { InitialData, InventoryPartOption, VehicleOption, TeamMemberOption } from '../service-edit/form-types'
-import type { BoardTechnicianOption } from '../service-edit/BasicInfoSection'
+import type { InitialData, InventoryPartOption, VehicleOption } from '../service-edit/form-types'
+export interface BoardTechnicianOption {
+  id: string
+  name: string
+}
 
 
 import { ServiceDetailContent } from '../service-detail/ServiceDetailContent'
 import { ServiceDetailContent } from '../service-detail/ServiceDetailContent'
 import { InvoiceSummary } from '../service-detail/InvoiceSummary'
 import { InvoiceSummary } from '../service-detail/InvoiceSummary'
@@ -27,6 +30,7 @@ import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SMS_TEMPLATE_DEFAULTS, interpolateSmsTemplate } from '@/lib/sms-templates'
 import { SMS_TEMPLATE_DEFAULTS, interpolateSmsTemplate } from '@/lib/sms-templates'
 
 
 import { BasicInfoSection } from '../service-edit/BasicInfoSection'
 import { BasicInfoSection } from '../service-edit/BasicInfoSection'
+import { ScheduleTimesSection } from '../service-edit/ScheduleTimesSection'
 import { PartsEditor } from '../service-edit/PartsEditor'
 import { PartsEditor } from '../service-edit/PartsEditor'
 import { LaborEditor } from '../service-edit/LaborEditor'
 import { LaborEditor } from '../service-edit/LaborEditor'
 import { TotalsSection } from '../service-edit/TotalsSection'
 import { TotalsSection } from '../service-edit/TotalsSection'
@@ -65,7 +69,6 @@ export interface ServicePageClientProps {
   initialData: InitialData
   initialData: InitialData
   inventoryParts: InventoryPartOption[]
   inventoryParts: InventoryPartOption[]
   vehicles: VehicleOption[]
   vehicles: VehicleOption[]
-  teamMembers: TeamMemberOption[]
   boardTechnicians?: BoardTechnicianOption[]
   boardTechnicians?: BoardTechnicianOption[]
   currentUserName: string
   currentUserName: string
   // Media data
   // Media data
@@ -91,7 +94,6 @@ export function ServicePageClient({
   initialData,
   initialData,
   inventoryParts,
   inventoryParts,
   vehicles,
   vehicles,
-  teamMembers,
   boardTechnicians = [],
   boardTechnicians = [],
   currentUserName,
   currentUserName,
   imageAttachmentsForManager,
   imageAttachmentsForManager,
@@ -117,8 +119,7 @@ export function ServicePageClient({
   const [loading, setLoading] = useState(false)
   const [loading, setLoading] = useState(false)
   const [selectedVehicleId, setSelectedVehicleId] = useState(vehicleId)
   const [selectedVehicleId, setSelectedVehicleId] = useState(vehicleId)
   const [vehicleOpen, setVehicleOpen] = useState(false)
   const [vehicleOpen, setVehicleOpen] = useState(false)
-  const [techName, setTechName] = useState(initialData.techName || currentUserName)
-  const [techOpen, setTechOpen] = useState(false)
+  const [techName] = useState(initialData.techName || currentUserName)
   const [type, setType] = useState(initialData.type || 'maintenance')
   const [type, setType] = useState(initialData.type || 'maintenance')
   const [status, setStatus] = useState(initialData.status || 'completed')
   const [status, setStatus] = useState(initialData.status || 'completed')
   const [partItems, setPartItems] = useState<ServicePartInput[]>(initialData.partItems || [])
   const [partItems, setPartItems] = useState<ServicePartInput[]>(initialData.partItems || [])
@@ -146,6 +147,12 @@ export function ServicePageClient({
   const isSavingRef = useRef(false)
   const isSavingRef = useRef(false)
   const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
   const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
 
 
+  const flashSaved = useCallback(() => {
+    if (savedTimerRef.current) clearTimeout(savedTimerRef.current)
+    setShowSaved(true)
+    savedTimerRef.current = setTimeout(() => setShowSaved(false), 2000)
+  }, [])
+
   const markDirty = useCallback(() => {
   const markDirty = useCallback(() => {
     setHasUnsavedChanges(true)
     setHasUnsavedChanges(true)
     if (autosaveTimer.current) clearTimeout(autosaveTimer.current)
     if (autosaveTimer.current) clearTimeout(autosaveTimer.current)
@@ -268,7 +275,6 @@ export function ServicePageClient({
   const dirtySetTaxRate = useCallback((v: number) => { setTaxRate(v); markDirty() }, [markDirty])
   const dirtySetTaxRate = useCallback((v: number) => { setTaxRate(v); markDirty() }, [markDirty])
   const dirtySetType = useCallback((v: string) => { setType(v); markDirty() }, [markDirty])
   const dirtySetType = useCallback((v: string) => { setType(v); markDirty() }, [markDirty])
   const dirtySetStatus = useCallback((v: string) => { setStatus(v); markDirty() }, [markDirty])
   const dirtySetStatus = useCallback((v: string) => { setStatus(v); markDirty() }, [markDirty])
-  const dirtySetTechName = useCallback((v: string) => { setTechName(v); markDirty() }, [markDirty])
   const dirtySetSelectedVehicleId = useCallback((v: string) => { setSelectedVehicleId(v); markDirty() }, [markDirty])
   const dirtySetSelectedVehicleId = useCallback((v: string) => { setSelectedVehicleId(v); markDirty() }, [markDirty])
 
 
   // Form submit
   // Form submit
@@ -309,9 +315,7 @@ export function ServicePageClient({
 
 
     if (result.success) {
     if (result.success) {
       setHasUnsavedChanges(false)
       setHasUnsavedChanges(false)
-      if (savedTimerRef.current) clearTimeout(savedTimerRef.current)
-      setShowSaved(true)
-      savedTimerRef.current = setTimeout(() => setShowSaved(false), 2000)
+      flashSaved()
       if (selectedVehicleId !== vehicleId) {
       if (selectedVehicleId !== vehicleId) {
         router.push(`/vehicles/${selectedVehicleId}/service/${initialData.id}`)
         router.push(`/vehicles/${selectedVehicleId}/service/${initialData.id}`)
       } else {
       } else {
@@ -550,13 +554,16 @@ export function ServicePageClient({
         status={status}
         status={status}
         setStatus={dirtySetStatus}
         setStatus={dirtySetStatus}
         techName={techName}
         techName={techName}
-        setTechName={dirtySetTechName}
-        techOpen={techOpen}
-        setTechOpen={setTechOpen}
-        teamMembers={teamMembers}
-        boardTechnicians={boardTechnicians}
         customer={record.vehicle.customer}
         customer={record.vehicle.customer}
       />
       />
+      <ScheduleTimesSection
+        serviceRecordId={record.id}
+        technicians={boardTechnicians}
+        initialStartDateTime={initialData.startDateTime}
+        initialEndDateTime={initialData.endDateTime}
+        initialTechnicianId={record.technicianId}
+        onSaved={flashSaved}
+      />
       <TotalsSection
       <TotalsSection
         partsSubtotal={partsSubtotal}
         partsSubtotal={partsSubtotal}
         laborSubtotal={laborSubtotal}
         laborSubtotal={laborSubtotal}

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

@@ -74,14 +74,14 @@ export function UnifiedServiceHeader({
 
 
   return (
   return (
     <div className="shrink-0 border-b bg-background px-4 pt-2 pb-0">
     <div className="shrink-0 border-b bg-background px-4 pt-2 pb-0">
-      <div className="mb-2 flex items-center justify-between">
+      <div className="mb-2 flex flex-wrap items-center justify-between gap-2">
         <Link
         <Link
           href={`/vehicles/${vehicleId}`}
           href={`/vehicles/${vehicleId}`}
           className="flex min-w-0 items-center gap-3 text-foreground transition-colors hover:text-muted-foreground"
           className="flex min-w-0 items-center gap-3 text-foreground transition-colors hover:text-muted-foreground"
         >
         >
           <ArrowLeft className="h-4 w-4 shrink-0" />
           <ArrowLeft className="h-4 w-4 shrink-0" />
           <div className="min-w-0">
           <div className="min-w-0">
-            <div className="flex items-center gap-2">
+            <div className="flex flex-wrap items-center gap-1.5">
               <Badge
               <Badge
                 variant="outline"
                 variant="outline"
                 className={`shrink-0 text-xs ${paymentStatusColors[paymentStatus] || ''}`}
                 className={`shrink-0 text-xs ${paymentStatusColors[paymentStatus] || ''}`}
@@ -99,7 +99,7 @@ export function UnifiedServiceHeader({
             <p className="truncate text-xs text-muted-foreground">{vehicleName}</p>
             <p className="truncate text-xs text-muted-foreground">{vehicleName}</p>
           </div>
           </div>
         </Link>
         </Link>
-        <div className="flex shrink-0 items-center gap-2">
+        <div className="flex shrink-0 flex-wrap items-center gap-2">
           {activeTab === 'details' && (
           {activeTab === 'details' && (
             <>
             <>
               {hasUnsavedChanges && (
               {hasUnsavedChanges && (

+ 0 - 416
src/features/workboard/Actions/boardActions.ts

@@ -1,416 +0,0 @@
-"use server";
-
-import { db } from "@/lib/db";
-import { withAuth } from "@/lib/with-auth";
-import { PermissionAction, PermissionSubject } from "@/lib/permissions";
-import { revalidatePath } from "next/cache";
-import { notificationBus } from "@/lib/notification-bus";
-import {
-  createBoardAssignmentSchema,
-  moveAssignmentSchema,
-  removeAssignmentSchema,
-} from "../Schema/workboardSchema";
-
-export type BoardAssignmentWithJob = {
-  id: string;
-  date: string;
-  sortOrder: number;
-  notes: string | null;
-  technicianId: string;
-  serviceRecordId: string | null;
-  inspectionId: string | null;
-  organizationId: string;
-  serviceRecord?: {
-    id: string;
-    title: string;
-    status: string;
-    vehicle: {
-      id: string;
-      make: string;
-      model: string;
-      year: number;
-      licensePlate: string | null;
-    };
-  } | null;
-  inspection?: {
-    id: string;
-    status: string;
-    vehicle: {
-      id: string;
-      make: string;
-      model: string;
-      year: number;
-      licensePlate: string | null;
-    };
-    template: { name: string };
-  } | null;
-};
-
-export async function getBoardAssignments(weekStart: string) {
-  return withAuth(
-    async ({ organizationId }) => {
-      const start = new Date(weekStart);
-      const end = new Date(start);
-      end.setDate(end.getDate() + 7);
-
-      const assignments = await db.boardAssignment.findMany({
-        where: {
-          organizationId,
-          date: { gte: start, lt: end },
-        },
-        include: {
-          technician: true,
-          serviceRecord: {
-            select: {
-              id: true,
-              title: true,
-              status: true,
-              vehicle: {
-                select: {
-                  id: true,
-                  make: true,
-                  model: true,
-                  year: true,
-                  licensePlate: true,
-                },
-              },
-            },
-          },
-          inspection: {
-            select: {
-              id: true,
-              status: true,
-              vehicle: {
-                select: {
-                  id: true,
-                  make: true,
-                  model: true,
-                  year: true,
-                  licensePlate: true,
-                },
-              },
-              template: { select: { name: true } },
-            },
-          },
-        },
-        orderBy: { sortOrder: "asc" },
-      });
-
-      // Serialize dates to strings for client
-      return assignments.map((a) => ({
-        ...a,
-        date: a.date.toISOString().split("T")[0],
-        createdAt: a.createdAt.toISOString(),
-        updatedAt: a.updatedAt.toISOString(),
-        technician: {
-          ...a.technician,
-          createdAt: a.technician.createdAt.toISOString(),
-          updatedAt: a.technician.updatedAt.toISOString(),
-        },
-      }));
-    },
-    {
-      requiredPermissions: [
-        { action: PermissionAction.READ, subject: PermissionSubject.WORK_BOARD },
-      ],
-    },
-  );
-}
-
-export async function getUnassignedJobs() {
-  return withAuth(
-    async ({ organizationId }) => {
-      // Get IDs of service records that have any board assignment
-      const assignedServiceRecordIds = (
-        await db.boardAssignment.findMany({
-          where: { organizationId, serviceRecordId: { not: null } },
-          select: { serviceRecordId: true },
-          distinct: ["serviceRecordId"],
-        })
-      ).map((a) => a.serviceRecordId!);
-
-      const assignedInspectionIds = (
-        await db.boardAssignment.findMany({
-          where: { organizationId, inspectionId: { not: null } },
-          select: { inspectionId: true },
-          distinct: ["inspectionId"],
-        })
-      ).map((a) => a.inspectionId!);
-
-      const [serviceRecords, inspections] = await Promise.all([
-        db.serviceRecord.findMany({
-          where: {
-            vehicle: { organizationId },
-            status: { in: ["pending", "in-progress", "waiting-parts", "scheduled"] },
-            id: { notIn: assignedServiceRecordIds },
-          },
-          select: {
-            id: true,
-            title: true,
-            status: true,
-            vehicle: {
-              select: {
-                id: true,
-                make: true,
-                model: true,
-                year: true,
-                licensePlate: true,
-              },
-            },
-          },
-          orderBy: { createdAt: "desc" },
-          take: 100,
-        }),
-        db.inspection.findMany({
-          where: {
-            organizationId,
-            status: { in: ["in_progress", "pending"] },
-            id: { notIn: assignedInspectionIds },
-          },
-          select: {
-            id: true,
-            status: true,
-            vehicle: {
-              select: {
-                id: true,
-                make: true,
-                model: true,
-                year: true,
-                licensePlate: true,
-              },
-            },
-            template: { select: { name: true } },
-          },
-          orderBy: { createdAt: "desc" },
-          take: 100,
-        }),
-      ]);
-
-      return { serviceRecords, inspections };
-    },
-    {
-      requiredPermissions: [
-        { action: PermissionAction.READ, subject: PermissionSubject.WORK_BOARD },
-      ],
-    },
-  );
-}
-
-export async function createBoardAssignment(input: unknown) {
-  return withAuth(
-    async ({ organizationId }) => {
-      const data = createBoardAssignmentSchema.parse(input);
-
-      // Verify the technician belongs to this org
-      const tech = await db.technician.findFirst({
-        where: { id: data.technicianId, organizationId },
-      });
-      if (!tech) throw new Error("Technician not found");
-
-      if (!data.serviceRecordId && !data.inspectionId) {
-        throw new Error("Must provide serviceRecordId or inspectionId");
-      }
-
-      const assignment = await db.boardAssignment.create({
-        data: {
-          date: new Date(data.date),
-          sortOrder: data.sortOrder,
-          notes: data.notes,
-          technicianId: data.technicianId,
-          serviceRecordId: data.serviceRecordId || null,
-          inspectionId: data.inspectionId || null,
-          organizationId,
-        },
-        include: {
-          technician: true,
-          serviceRecord: {
-            select: {
-              id: true,
-              title: true,
-              status: true,
-              vehicle: {
-                select: { id: true, make: true, model: true, year: true, licensePlate: true },
-              },
-            },
-          },
-          inspection: {
-            select: {
-              id: true,
-              status: true,
-              vehicle: {
-                select: { id: true, make: true, model: true, year: true, licensePlate: true },
-              },
-              template: { select: { name: true } },
-            },
-          },
-        },
-      });
-
-      // Sync service record: techName + serviceDate from board
-      if (assignment.serviceRecordId) {
-        await db.serviceRecord.update({
-          where: { id: assignment.serviceRecordId },
-          data: { techName: tech.name, serviceDate: new Date(data.date) },
-        });
-      }
-
-      notificationBus.emit("workboard", {
-        type: "assignment_created",
-        organizationId,
-        assignment: {
-          ...assignment,
-          date: assignment.date.toISOString().split("T")[0],
-          createdAt: assignment.createdAt.toISOString(),
-          updatedAt: assignment.updatedAt.toISOString(),
-          technician: {
-            ...assignment.technician,
-            createdAt: assignment.technician.createdAt.toISOString(),
-            updatedAt: assignment.technician.updatedAt.toISOString(),
-          },
-        },
-      });
-
-      revalidatePath("/work-board");
-      return {
-        ...assignment,
-        date: assignment.date.toISOString().split("T")[0],
-        createdAt: assignment.createdAt.toISOString(),
-        updatedAt: assignment.updatedAt.toISOString(),
-        technician: {
-          ...assignment.technician,
-          createdAt: assignment.technician.createdAt.toISOString(),
-          updatedAt: assignment.technician.updatedAt.toISOString(),
-        },
-      };
-    },
-    {
-      requiredPermissions: [
-        { action: PermissionAction.CREATE, subject: PermissionSubject.WORK_BOARD },
-      ],
-    },
-  );
-}
-
-export async function moveAssignment(input: unknown) {
-  return withAuth(
-    async ({ organizationId }) => {
-      const data = moveAssignmentSchema.parse(input);
-
-      const assignment = await db.boardAssignment.findFirst({
-        where: { id: data.id, organizationId },
-      });
-      if (!assignment) throw new Error("Assignment not found");
-
-      const updated = await db.boardAssignment.update({
-        where: { id: data.id },
-        data: {
-          technicianId: data.technicianId,
-          date: new Date(data.date),
-          sortOrder: data.sortOrder,
-        },
-        include: {
-          technician: true,
-          serviceRecord: {
-            select: {
-              id: true,
-              title: true,
-              status: true,
-              vehicle: {
-                select: { id: true, make: true, model: true, year: true, licensePlate: true },
-              },
-            },
-          },
-          inspection: {
-            select: {
-              id: true,
-              status: true,
-              vehicle: {
-                select: { id: true, make: true, model: true, year: true, licensePlate: true },
-              },
-              template: { select: { name: true } },
-            },
-          },
-        },
-      });
-
-      // Sync service record: techName + serviceDate from board
-      if (updated.serviceRecordId) {
-        await db.serviceRecord.update({
-          where: { id: updated.serviceRecordId },
-          data: { techName: updated.technician.name, serviceDate: new Date(data.date) },
-        });
-      }
-
-      notificationBus.emit("workboard", {
-        type: "assignment_moved",
-        organizationId,
-        assignment: {
-          ...updated,
-          date: updated.date.toISOString().split("T")[0],
-          createdAt: updated.createdAt.toISOString(),
-          updatedAt: updated.updatedAt.toISOString(),
-          technician: {
-            ...updated.technician,
-            createdAt: updated.technician.createdAt.toISOString(),
-            updatedAt: updated.technician.updatedAt.toISOString(),
-          },
-        },
-      });
-
-      revalidatePath("/work-board");
-      return {
-        ...updated,
-        date: updated.date.toISOString().split("T")[0],
-        createdAt: updated.createdAt.toISOString(),
-        updatedAt: updated.updatedAt.toISOString(),
-        technician: {
-          ...updated.technician,
-          createdAt: updated.technician.createdAt.toISOString(),
-          updatedAt: updated.technician.updatedAt.toISOString(),
-        },
-      };
-    },
-    {
-      requiredPermissions: [
-        { action: PermissionAction.UPDATE, subject: PermissionSubject.WORK_BOARD },
-      ],
-    },
-  );
-}
-
-export async function removeAssignment(input: unknown) {
-  return withAuth(
-    async ({ organizationId }) => {
-      const data = removeAssignmentSchema.parse(input);
-
-      const assignment = await db.boardAssignment.findFirst({
-        where: { id: data.id, organizationId },
-      });
-      if (!assignment) throw new Error("Assignment not found");
-
-      // Clear techName on the linked service record
-      if (assignment.serviceRecordId) {
-        await db.serviceRecord.update({
-          where: { id: assignment.serviceRecordId },
-          data: { techName: null },
-        });
-      }
-
-      await db.boardAssignment.delete({ where: { id: data.id } });
-
-      notificationBus.emit("workboard", {
-        type: "assignment_removed",
-        organizationId,
-        assignmentId: data.id,
-      });
-
-      revalidatePath("/work-board");
-      return { success: true };
-    },
-    {
-      requiredPermissions: [
-        { action: PermissionAction.DELETE, subject: PermissionSubject.WORK_BOARD },
-      ],
-    },
-  );
-}

+ 405 - 0
src/features/workboard/Actions/boardActions/assignments.ts

@@ -0,0 +1,405 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { revalidatePath } from "next/cache";
+import { notificationBus } from "@/lib/notification-bus";
+import {
+  assignTechnicianSchema,
+  moveJobSchema,
+  unassignJobSchema,
+} from "../../Schema/workboardSchema";
+import type { WorkBoardJob, WorkBoardSettings } from "./types";
+
+function serviceRecordToJob(sr: {
+  id: string;
+  title: string;
+  status: string;
+  startDateTime: Date | null;
+  endDateTime: Date | null;
+  technicianId: string | null;
+  sortOrder: number;
+  vehicle: {
+    id: string;
+    make: string;
+    model: string;
+    year: number;
+    licensePlate: string | null;
+  };
+}): WorkBoardJob {
+  return {
+    id: sr.id,
+    type: "serviceRecord",
+    technicianId: sr.technicianId,
+    sortOrder: sr.sortOrder,
+    title: sr.title,
+    status: sr.status,
+    startDateTime: sr.startDateTime?.toISOString() ?? null,
+    endDateTime: sr.endDateTime?.toISOString() ?? null,
+    vehicle: sr.vehicle,
+  };
+}
+
+function inspectionToJob(insp: {
+  id: string;
+  status: string;
+  startDateTime: Date | null;
+  endDateTime: Date | null;
+  technicianId: string | null;
+  sortOrder: number;
+  vehicle: {
+    id: string;
+    make: string;
+    model: string;
+    year: number;
+    licensePlate: string | null;
+  };
+  template: { name: string };
+}): WorkBoardJob {
+  return {
+    id: insp.id,
+    type: "inspection",
+    technicianId: insp.technicianId,
+    sortOrder: insp.sortOrder,
+    title: insp.template.name,
+    status: insp.status,
+    startDateTime: insp.startDateTime?.toISOString() ?? null,
+    endDateTime: insp.endDateTime?.toISOString() ?? null,
+    vehicle: insp.vehicle,
+    templateName: insp.template.name,
+  };
+}
+
+const VEHICLE_SELECT = {
+  id: true,
+  make: true,
+  model: true,
+  year: true,
+  licensePlate: true,
+} as const;
+
+export async function getBoardJobs(weekStart: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const start = new Date(weekStart);
+      const end = new Date(start);
+      end.setDate(end.getDate() + 7);
+
+      const [serviceRecords, inspections] = await Promise.all([
+        db.serviceRecord.findMany({
+          where: {
+            vehicle: { organizationId },
+            technicianId: { not: null },
+            OR: [
+              { startDateTime: { gte: start, lt: end } },
+              { endDateTime: { gt: start, lte: end } },
+              { startDateTime: { lte: start }, endDateTime: { gte: end } },
+              { startDateTime: null },
+            ],
+          },
+          select: {
+            id: true,
+            title: true,
+            status: true,
+            startDateTime: true,
+            endDateTime: true,
+            technicianId: true,
+            sortOrder: true,
+            vehicle: { select: VEHICLE_SELECT },
+          },
+          orderBy: { sortOrder: "asc" },
+        }),
+        db.inspection.findMany({
+          where: {
+            organizationId,
+            technicianId: { not: null },
+            OR: [
+              { startDateTime: { gte: start, lt: end } },
+              { endDateTime: { gt: start, lte: end } },
+              { startDateTime: { lte: start }, endDateTime: { gte: end } },
+              { startDateTime: null },
+            ],
+          },
+          select: {
+            id: true,
+            status: true,
+            startDateTime: true,
+            endDateTime: true,
+            technicianId: true,
+            sortOrder: true,
+            vehicle: { select: VEHICLE_SELECT },
+            template: { select: { name: true } },
+          },
+          orderBy: { sortOrder: "asc" },
+        }),
+      ]);
+
+      const jobs: WorkBoardJob[] = [
+        ...serviceRecords.map(serviceRecordToJob),
+        ...inspections.map(inspectionToJob),
+      ];
+      jobs.sort((a, b) => a.sortOrder - b.sortOrder);
+
+      return jobs;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}
+
+export async function getUnassignedJobs() {
+  return withAuth(
+    async ({ organizationId }) => {
+      const [serviceRecords, inspections] = await Promise.all([
+        db.serviceRecord.findMany({
+          where: {
+            vehicle: { organizationId },
+            technicianId: null,
+            status: { in: ["pending", "in-progress", "waiting-parts", "scheduled"] },
+          },
+          select: {
+            id: true,
+            title: true,
+            status: true,
+            vehicle: { select: VEHICLE_SELECT },
+          },
+          orderBy: { createdAt: "desc" },
+          take: 100,
+        }),
+        db.inspection.findMany({
+          where: {
+            organizationId,
+            technicianId: null,
+            status: { in: ["in_progress", "pending"] },
+          },
+          select: {
+            id: true,
+            status: true,
+            vehicle: { select: VEHICLE_SELECT },
+            template: { select: { name: true } },
+          },
+          orderBy: { createdAt: "desc" },
+          take: 100,
+        }),
+      ]);
+
+      return { serviceRecords, inspections };
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}
+
+export async function assignTechnician(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const data = assignTechnicianSchema.parse(input);
+
+      const tech = await db.technician.findFirst({
+        where: { id: data.technicianId, organizationId },
+      });
+      if (!tech) throw new Error("Technician not found");
+
+      let job: WorkBoardJob;
+
+      if (data.type === "serviceRecord") {
+        const sr = await db.serviceRecord.update({
+          where: { id: data.id },
+          data: { technicianId: data.technicianId, techName: tech.name },
+          select: {
+            id: true,
+            title: true,
+            status: true,
+            startDateTime: true,
+            endDateTime: true,
+            technicianId: true,
+            sortOrder: true,
+            vehicle: { select: VEHICLE_SELECT },
+          },
+        });
+        job = serviceRecordToJob(sr);
+      } else {
+        const insp = await db.inspection.update({
+          where: { id: data.id },
+          data: { technicianId: data.technicianId },
+          select: {
+            id: true,
+            status: true,
+            startDateTime: true,
+            endDateTime: true,
+            technicianId: true,
+            sortOrder: true,
+            vehicle: { select: VEHICLE_SELECT },
+            template: { select: { name: true } },
+          },
+        });
+        job = inspectionToJob(insp);
+      }
+
+      notificationBus.emit("workboard", {
+        type: "job_assigned",
+        organizationId,
+        job,
+      });
+
+      revalidatePath("/work-board");
+      return job;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.CREATE, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}
+
+export async function moveJob(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const data = moveJobSchema.parse(input);
+
+      const tech = await db.technician.findFirst({
+        where: { id: data.technicianId, organizationId },
+      });
+      if (!tech) throw new Error("Technician not found");
+
+      let job: WorkBoardJob;
+
+      if (data.type === "serviceRecord") {
+        const sr = await db.serviceRecord.update({
+          where: { id: data.id },
+          data: {
+            technicianId: data.technicianId,
+            sortOrder: data.sortOrder,
+            techName: tech.name,
+          },
+          select: {
+            id: true,
+            title: true,
+            status: true,
+            startDateTime: true,
+            endDateTime: true,
+            technicianId: true,
+            sortOrder: true,
+            vehicle: { select: VEHICLE_SELECT },
+          },
+        });
+        job = serviceRecordToJob(sr);
+      } else {
+        const insp = await db.inspection.update({
+          where: { id: data.id },
+          data: {
+            technicianId: data.technicianId,
+            sortOrder: data.sortOrder,
+          },
+          select: {
+            id: true,
+            status: true,
+            startDateTime: true,
+            endDateTime: true,
+            technicianId: true,
+            sortOrder: true,
+            vehicle: { select: VEHICLE_SELECT },
+            template: { select: { name: true } },
+          },
+        });
+        job = inspectionToJob(insp);
+      }
+
+      notificationBus.emit("workboard", {
+        type: "job_moved",
+        organizationId,
+        job,
+      });
+
+      revalidatePath("/work-board");
+      return job;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}
+
+export async function unassignJob(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const data = unassignJobSchema.parse(input);
+
+      if (data.type === "serviceRecord") {
+        const sr = await db.serviceRecord.findFirst({
+          where: { id: data.id, vehicle: { organizationId } },
+        });
+        if (!sr) throw new Error("Service record not found");
+
+        await db.serviceRecord.update({
+          where: { id: data.id },
+          data: { technicianId: null, techName: null, sortOrder: 0 },
+        });
+      } else {
+        const insp = await db.inspection.findFirst({
+          where: { id: data.id, organizationId },
+        });
+        if (!insp) throw new Error("Inspection not found");
+
+        await db.inspection.update({
+          where: { id: data.id },
+          data: { technicianId: null, sortOrder: 0 },
+        });
+      }
+
+      notificationBus.emit("workboard", {
+        type: "job_unassigned",
+        organizationId,
+        jobId: data.id,
+        jobType: data.type,
+      });
+
+      revalidatePath("/work-board");
+      return { success: true };
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.DELETE, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}
+
+export async function getWorkBoardSettings() {
+  return withAuth(
+    async ({ organizationId }) => {
+      const settings = await db.appSetting.findMany({
+        where: {
+          organizationId,
+          key: { in: ["workboard.weekStartDay", "workboard.workDayStart", "workboard.workDayEnd"] },
+        },
+      });
+
+      const map: Record<string, string> = {};
+      for (const s of settings) {
+        map[s.key] = s.value;
+      }
+
+      return {
+        weekStartDay: parseInt(map["workboard.weekStartDay"] || "1", 10),
+        workDayStart: map["workboard.workDayStart"] || "07:00",
+        workDayEnd: map["workboard.workDayEnd"] || "15:00",
+      } as WorkBoardSettings;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}

+ 11 - 0
src/features/workboard/Actions/boardActions/index.ts

@@ -0,0 +1,11 @@
+export type { WorkBoardJob, WorkBoardSettings } from "./types";
+export { getWorkBoardSettings } from "./assignments";
+export {
+  getBoardJobs,
+  getUnassignedJobs,
+  assignTechnician,
+  moveJob,
+  unassignJob,
+} from "./assignments";
+export { updateServiceTimes, updateInspectionTimes } from "./scheduling";
+export { getServiceRecordTechnician } from "./queries";

+ 26 - 0
src/features/workboard/Actions/boardActions/queries.ts

@@ -0,0 +1,26 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function getServiceRecordTechnician(serviceRecordId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const sr = await db.serviceRecord.findFirst({
+        where: { id: serviceRecordId, vehicle: { organizationId } },
+        select: {
+          technicianId: true,
+          technician: { select: { id: true, name: true } },
+        },
+      });
+      if (!sr || !sr.technician) return null;
+      return { technician: sr.technician };
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}

+ 99 - 0
src/features/workboard/Actions/boardActions/scheduling.ts

@@ -0,0 +1,99 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { revalidatePath } from "next/cache";
+import { notificationBus } from "@/lib/notification-bus";
+import { updateServiceTimesSchema } from "../../Schema/workboardSchema";
+
+export async function updateServiceTimes(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const data = updateServiceTimesSchema.parse(input);
+
+      if (data.endDateTime <= data.startDateTime) {
+        throw new Error("End time must be after start time");
+      }
+
+      const record = await db.serviceRecord.findFirst({
+        where: { id: data.id, vehicle: { organizationId } },
+      });
+      if (!record) throw new Error("Service record not found");
+
+      await db.serviceRecord.update({
+        where: { id: data.id },
+        data: {
+          startDateTime: data.startDateTime,
+          endDateTime: data.endDateTime,
+        },
+      });
+
+      notificationBus.emit("workboard", {
+        type: "service_times_updated",
+        organizationId,
+        serviceRecordId: data.id,
+        startDateTime: data.startDateTime.toISOString(),
+        endDateTime: data.endDateTime.toISOString(),
+      });
+
+      revalidatePath("/work-board");
+      revalidatePath("/vehicles");
+      return {
+        id: data.id,
+        startDateTime: data.startDateTime.toISOString(),
+        endDateTime: data.endDateTime.toISOString(),
+      };
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}
+
+export async function updateInspectionTimes(input: unknown) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const data = updateServiceTimesSchema.parse(input);
+
+      if (data.endDateTime <= data.startDateTime) {
+        throw new Error("End time must be after start time");
+      }
+
+      const inspection = await db.inspection.findFirst({
+        where: { id: data.id, organizationId },
+      });
+      if (!inspection) throw new Error("Inspection not found");
+
+      await db.inspection.update({
+        where: { id: data.id },
+        data: {
+          startDateTime: data.startDateTime,
+          endDateTime: data.endDateTime,
+        },
+      });
+
+      notificationBus.emit("workboard", {
+        type: "inspection_times_updated",
+        organizationId,
+        inspectionId: data.id,
+        startDateTime: data.startDateTime.toISOString(),
+        endDateTime: data.endDateTime.toISOString(),
+      });
+
+      revalidatePath("/work-board");
+      return {
+        id: data.id,
+        startDateTime: data.startDateTime.toISOString(),
+        endDateTime: data.endDateTime.toISOString(),
+      };
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.WORK_BOARD },
+      ],
+    },
+  );
+}

+ 24 - 0
src/features/workboard/Actions/boardActions/types.ts

@@ -0,0 +1,24 @@
+export type WorkBoardJob = {
+  id: string;
+  type: "serviceRecord" | "inspection";
+  technicianId: string | null;
+  sortOrder: number;
+  title: string;
+  status: string;
+  startDateTime: string | null;
+  endDateTime: string | null;
+  vehicle: {
+    id: string;
+    make: string;
+    model: string;
+    year: number;
+    licensePlate: string | null;
+  } | null;
+  templateName?: string;
+};
+
+export type WorkBoardSettings = {
+  weekStartDay: number;
+  workDayStart: string;
+  workDayEnd: string;
+};

+ 56 - 4
src/features/workboard/Components/BoardDayCell.tsx

@@ -2,19 +2,68 @@
 
 
 import { useDroppable } from "@dnd-kit/core";
 import { useDroppable } from "@dnd-kit/core";
 import { cn } from "@/lib/utils";
 import { cn } from "@/lib/utils";
-import type { BoardAssignmentWithJob } from "../Actions/boardActions";
+import type { WorkBoardJob } from "../Actions/boardActions";
 import { BoardJobCard } from "./BoardJobCard";
 import { BoardJobCard } from "./BoardJobCard";
+import { formatDuration } from "./DurationSlider";
+import { getJobDateRange, getDurationMinutes } from "../utils/datetime";
+
+export function CapacityBar({
+  bookedMinutes,
+  capacityMinutes,
+}: {
+  bookedMinutes: number;
+  capacityMinutes: number;
+}) {
+  if (bookedMinutes === 0 && capacityMinutes === 0) return null;
+
+  const pct = capacityMinutes > 0 ? Math.min((bookedMinutes / capacityMinutes) * 100, 100) : 0;
+  const isOver = bookedMinutes > capacityMinutes;
+
+  return (
+    <div className="mt-auto space-y-0.5 border-t pt-1">
+      <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
+        <div
+          className={cn(
+            "h-full rounded-full transition-all",
+            isOver
+              ? "bg-red-500"
+              : pct >= 75
+                ? "bg-amber-500"
+                : "bg-emerald-500",
+          )}
+          style={{ width: `${Math.min(pct, 100)}%` }}
+        />
+      </div>
+      <p className={cn(
+        "text-center text-[10px] font-medium",
+        isOver ? "text-red-500" : "text-muted-foreground",
+      )}>
+        {formatDuration(bookedMinutes)} / {formatDuration(capacityMinutes)}
+      </p>
+    </div>
+  );
+}
+
+function getBookedMinutes(jobs: WorkBoardJob[]): number {
+  return jobs.reduce((sum, a) => {
+    const { start, end } = getJobDateRange(a);
+    if (start && end) return sum + getDurationMinutes(start, end);
+    return sum;
+  }, 0);
+}
 
 
 export function BoardDayCell({
 export function BoardDayCell({
   technicianId,
   technicianId,
   date,
   date,
   assignments,
   assignments,
+  dailyCapacity,
   onCardClick,
   onCardClick,
 }: {
 }: {
   technicianId: string;
   technicianId: string;
   date: string;
   date: string;
-  assignments: BoardAssignmentWithJob[];
-  onCardClick: (assignment: BoardAssignmentWithJob) => void;
+  assignments: WorkBoardJob[];
+  dailyCapacity: number;
+  onCardClick: (job: WorkBoardJob) => void;
 }) {
 }) {
   const droppableId = `${technicianId}::${date}`;
   const droppableId = `${technicianId}::${date}`;
   const { isOver, setNodeRef } = useDroppable({
   const { isOver, setNodeRef } = useDroppable({
@@ -26,6 +75,8 @@ export function BoardDayCell({
   const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
   const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
   const isToday = date === today;
   const isToday = date === today;
 
 
+  const bookedMinutes = getBookedMinutes(assignments);
+
   return (
   return (
     <div
     <div
       ref={setNodeRef}
       ref={setNodeRef}
@@ -38,10 +89,11 @@ export function BoardDayCell({
       {assignments.map((a) => (
       {assignments.map((a) => (
         <BoardJobCard
         <BoardJobCard
           key={a.id}
           key={a.id}
-          assignment={a}
+          job={a}
           onClick={() => onCardClick(a)}
           onClick={() => onCardClick(a)}
         />
         />
       ))}
       ))}
+      <CapacityBar bookedMinutes={bookedMinutes} capacityMinutes={dailyCapacity} />
     </div>
     </div>
   );
   );
 }
 }

+ 31 - 39
src/features/workboard/Components/BoardJobCard.tsx

@@ -2,20 +2,22 @@
 
 
 import { useDraggable } from "@dnd-kit/core";
 import { useDraggable } from "@dnd-kit/core";
 import { CSS } from "@dnd-kit/utilities";
 import { CSS } from "@dnd-kit/utilities";
-import { GripVertical, Wrench, ClipboardCheck } from "lucide-react";
-import type { BoardAssignmentWithJob } from "../Actions/boardActions";
+import { Clock, GripVertical, Wrench, ClipboardCheck } from "lucide-react";
+import type { WorkBoardJob } from "../Actions/boardActions";
+import { getJobDateRange, getDurationMinutes } from "../utils/datetime";
+import { formatDuration } from "./DurationSlider";
 
 
 export function BoardJobCard({
 export function BoardJobCard({
-  assignment,
+  job,
   onClick,
   onClick,
 }: {
 }: {
-  assignment: BoardAssignmentWithJob;
+  job: WorkBoardJob;
   onClick?: () => void;
   onClick?: () => void;
 }) {
 }) {
   const { attributes, listeners, setNodeRef, transform, isDragging } =
   const { attributes, listeners, setNodeRef, transform, isDragging } =
     useDraggable({
     useDraggable({
-      id: assignment.id,
-      data: { assignment },
+      id: job.id,
+      data: { job },
     });
     });
 
 
   const style = transform
   const style = transform
@@ -26,16 +28,10 @@ export function BoardJobCard({
       }
       }
     : undefined;
     : undefined;
 
 
-  const isServiceRecord = !!assignment.serviceRecordId;
-  const vehicle = isServiceRecord
-    ? assignment.serviceRecord?.vehicle
-    : assignment.inspection?.vehicle;
-  const title = isServiceRecord
-    ? assignment.serviceRecord?.title
-    : assignment.inspection?.template?.name;
-  const status = isServiceRecord
-    ? assignment.serviceRecord?.status
-    : assignment.inspection?.status;
+  const isServiceRecord = job.type === "serviceRecord";
+
+  const { start, end } = getJobDateRange(job);
+  const durationMins = start && end ? getDurationMinutes(start, end) : null;
 
 
   return (
   return (
     <div
     <div
@@ -59,19 +55,27 @@ export function BoardJobCard({
           ) : (
           ) : (
             <ClipboardCheck className="h-3 w-3 shrink-0 text-green-500" />
             <ClipboardCheck className="h-3 w-3 shrink-0 text-green-500" />
           )}
           )}
-          <span className="truncate font-medium">{title}</span>
+          <span className="truncate font-medium">{job.title}</span>
         </div>
         </div>
-        {vehicle && (
+        {job.vehicle && (
           <p className="truncate text-muted-foreground">
           <p className="truncate text-muted-foreground">
-            {vehicle.year} {vehicle.make} {vehicle.model}
-            {vehicle.licensePlate ? ` · ${vehicle.licensePlate}` : ""}
+            {job.vehicle.year} {job.vehicle.make} {job.vehicle.model}
+            {job.vehicle.licensePlate ? ` · ${job.vehicle.licensePlate}` : ""}
           </p>
           </p>
         )}
         )}
-        {status && (
-          <span className="inline-block rounded bg-muted px-1 py-0.5 text-[10px] capitalize">
-            {status.replace(/_/g, " ")}
-          </span>
-        )}
+        <div className="flex items-center gap-1.5">
+          {job.status && (
+            <span className="inline-block rounded bg-muted px-1 py-0.5 text-[10px] capitalize">
+              {job.status.replace(/_/g, " ")}
+            </span>
+          )}
+          {durationMins != null && durationMins > 0 && (
+            <span className="inline-flex items-center gap-0.5 rounded bg-blue-500/10 px-1 py-0.5 text-[10px] text-blue-600 dark:text-blue-400">
+              <Clock className="h-2.5 w-2.5" />
+              {formatDuration(durationMins)}
+            </span>
+          )}
+        </div>
       </div>
       </div>
     </div>
     </div>
   );
   );
@@ -86,24 +90,12 @@ export function UnassignedJobCard({
         id: string;
         id: string;
         title: string;
         title: string;
         status: string;
         status: string;
-        vehicle: {
-          id: string;
-          make: string;
-          model: string;
-          year: number;
-          licensePlate: string | null;
-        };
+        vehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null };
       }
       }
     | {
     | {
         id: string;
         id: string;
         status: string;
         status: string;
-        vehicle: {
-          id: string;
-          make: string;
-          model: string;
-          year: number;
-          licensePlate: string | null;
-        };
+        vehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null };
         template: { name: string };
         template: { name: string };
       };
       };
   type: "serviceRecord" | "inspection";
   type: "serviceRecord" | "inspection";

+ 109 - 0
src/features/workboard/Components/DayTimeline/TimelineCard.tsx

@@ -0,0 +1,109 @@
+"use client";
+
+import { useRef } from "react";
+import { cn } from "@/lib/utils";
+import { Wrench, ClipboardCheck } from "lucide-react";
+import type { WorkBoardJob } from "../../Actions/boardActions";
+import { minutesToTime } from "./utils";
+
+export function JobBar({
+  job,
+  leftPct,
+  widthPct,
+  colorClass,
+  unscheduled,
+  isDragging,
+  startMins,
+  endMins,
+  onClick,
+  onMoveStart,
+  onResizeStartLeft,
+  onResizeStartRight,
+}: {
+  job: WorkBoardJob;
+  leftPct: number;
+  widthPct: number;
+  colorClass: string;
+  unscheduled?: boolean;
+  isDragging?: boolean;
+  startMins: number;
+  endMins: number;
+  onClick: () => void;
+  onMoveStart: (e: React.MouseEvent) => void;
+  onResizeStartLeft: (e: React.MouseEvent) => void;
+  onResizeStartRight: (e: React.MouseEvent) => void;
+}) {
+  const isServiceRecord = job.type === "serviceRecord";
+
+  const mouseDownPos = useRef<{ x: number; y: number } | null>(null);
+
+  return (
+    <div
+      className={cn(
+        "absolute top-1 bottom-1 z-[1] flex items-center overflow-hidden rounded shadow-sm select-none group",
+        colorClass,
+        unscheduled && "border border-dashed border-current/30 opacity-70",
+        isDragging && "opacity-80 ring-2 ring-primary z-[3]",
+      )}
+      style={{ left: `${leftPct}%`, width: `${widthPct}%` }}
+      title={`${minutesToTime(startMins)}–${minutesToTime(endMins)} · ${job.title}${job.vehicle ? ` - ${job.vehicle.year} ${job.vehicle.make} ${job.vehicle.model}` : ""}`}
+      onMouseDown={(e) => {
+        if (e.button !== 0) return;
+        mouseDownPos.current = { x: e.clientX, y: e.clientY };
+        const onUp = (upEvent: MouseEvent) => {
+          window.removeEventListener("mouseup", onUp);
+          if (!mouseDownPos.current) return;
+          const dist = Math.abs(upEvent.clientX - mouseDownPos.current.x) + Math.abs(upEvent.clientY - mouseDownPos.current.y);
+          if (dist < 5) onClick();
+          mouseDownPos.current = null;
+        };
+        window.addEventListener("mouseup", onUp);
+        onMoveStart(e);
+      }}
+    >
+      <div
+        className="absolute left-0 top-0 bottom-0 w-2 cursor-col-resize z-[2] opacity-0 group-hover:opacity-100 bg-black/10 hover:bg-black/20 rounded-l transition-opacity"
+        onMouseDown={(e) => { e.stopPropagation(); if (e.button === 0) onResizeStartLeft(e); }}
+      />
+      <div className="flex items-center gap-1 px-3 min-w-0 cursor-grab active:cursor-grabbing text-[11px] font-medium">
+        {isServiceRecord ? <Wrench className="h-3 w-3 shrink-0" /> : <ClipboardCheck className="h-3 w-3 shrink-0" />}
+        {isDragging ? (
+          <span className="font-semibold whitespace-nowrap">{minutesToTime(startMins)} – {minutesToTime(endMins)}</span>
+        ) : (
+          <span className="truncate">
+            {job.title}
+            {job.vehicle ? ` - ${job.vehicle.licensePlate || `${job.vehicle.make} ${job.vehicle.model}`}` : ""}
+          </span>
+        )}
+      </div>
+      <div
+        className="absolute right-0 top-0 bottom-0 w-2 cursor-col-resize z-[2] opacity-0 group-hover:opacity-100 bg-black/10 hover:bg-black/20 rounded-r transition-opacity"
+        onMouseDown={(e) => { e.stopPropagation(); if (e.button === 0) onResizeStartRight(e); }}
+      />
+    </div>
+  );
+}
+
+export function CurrentTimeIndicator({
+  dayStartMins,
+  totalMinutes,
+  date,
+}: {
+  dayStartMins: number;
+  totalMinutes: number;
+  date: string;
+}) {
+  const now = new Date();
+  const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
+  if (date !== today) return null;
+
+  const nowMins = now.getHours() * 60 + now.getMinutes();
+  const leftPct = ((nowMins - dayStartMins) / totalMinutes) * 100;
+  if (leftPct < 0 || leftPct > 100) return null;
+
+  return (
+    <div className="absolute top-0 bottom-0 z-10 w-0.5 bg-red-500 pointer-events-none" style={{ left: `${leftPct}%` }}>
+      <div className="absolute -top-1 -left-1 h-2.5 w-2.5 rounded-full bg-red-500" />
+    </div>
+  );
+}

+ 166 - 0
src/features/workboard/Components/DayTimeline/TimelineColumn.tsx

@@ -0,0 +1,166 @@
+"use client";
+
+import { useRef, useCallback } from "react";
+import { useDroppable } from "@dnd-kit/core";
+import { cn } from "@/lib/utils";
+import type { WorkBoardJob } from "../../Actions/boardActions";
+import type { Technician } from "../../store/workboardStore";
+import { formatDuration } from "../DurationSlider";
+import { getJobDateRange, getDurationMinutes } from "../../utils/datetime";
+import { getResolvedRange, BAR_COLORS, minutesToTime } from "./utils";
+import { JobBar, CurrentTimeIndicator } from "./TimelineCard";
+import type { DragState, DragOverride, DragMode } from "./index";
+import {
+  ContextMenu,
+  ContextMenuContent,
+  ContextMenuItem,
+  ContextMenuTrigger,
+} from "@/components/ui/context-menu";
+import { Plus } from "lucide-react";
+import { useTranslations } from "next-intl";
+
+export function TechTimelineRow({
+  tech,
+  date,
+  dayAssignments,
+  dayStartMins,
+  dayEndMins,
+  totalMinutes,
+  hourSlots,
+  drag,
+  dragOverride,
+  startDrag,
+  onCardClick,
+  onTechClick,
+  onCreateWorkOrder,
+  cancelPendingDrag,
+  registerRef,
+}: {
+  tech: Technician;
+  date: string;
+  dayAssignments: WorkBoardJob[];
+  dayStartMins: number;
+  dayEndMins: number;
+  totalMinutes: number;
+  hourSlots: string[];
+  drag: DragState | null;
+  dragOverride: DragOverride | null;
+  startDrag: (e: React.MouseEvent, a: WorkBoardJob, mode: DragMode, el: HTMLElement) => void;
+  onCardClick: (a: WorkBoardJob) => void;
+  onTechClick?: (t: Technician) => void;
+  onCreateWorkOrder?: (techId: string, startTime: string, endTime: string) => void;
+  cancelPendingDrag: () => void;
+  registerRef: (el: HTMLDivElement | null) => void;
+}) {
+  const t = useTranslations("workBoard.board");
+  const timelineRef = useRef<HTMLDivElement>(null);
+  const contextClickTimeRef = useRef<number>(dayStartMins);
+  const techJobs = dayAssignments.filter((a) => a.technicianId === tech.id);
+
+  const bookedMinutes = techJobs.reduce((sum, a) => {
+    const { start, end } = getJobDateRange(a);
+    return start && end ? sum + getDurationMinutes(start, end) : sum;
+  }, 0);
+
+  const isDropTarget = drag && dragOverride && dragOverride.techId === tech.id && drag.origTechId !== tech.id;
+  const droppableId = `${tech.id}::${date}`;
+  const { isOver, setNodeRef } = useDroppable({ id: droppableId, data: { technicianId: tech.id, date } });
+  const rowRef = useCallback((el: HTMLDivElement | null) => { setNodeRef(el); registerRef(el); }, [setNodeRef, registerRef]);
+
+  const ghostBar = drag && dragOverride && dragOverride.techId === tech.id && drag.origTechId !== tech.id ? dragOverride : null;
+
+  return (
+    <div ref={rowRef} className={cn("flex border-b last:border-b-0 transition-colors", (isOver || isDropTarget) && "bg-primary/5")}>
+      <button type="button" className="flex w-[160px] shrink-0 flex-col justify-center gap-1 border-r p-2 text-left transition-colors hover:bg-muted/50" onClick={() => onTechClick?.(tech)}>
+        <div className="flex items-center gap-2">
+          <div className="h-3 w-3 shrink-0 rounded-full" style={{ backgroundColor: tech.color }} />
+          <span className="text-sm font-medium leading-tight">{tech.name}</span>
+        </div>
+        <span className="text-[10px] text-muted-foreground">
+          {formatDuration(bookedMinutes)} / {formatDuration(tech.dailyCapacity)}
+        </span>
+      </button>
+
+      <ContextMenu>
+        <ContextMenuTrigger asChild>
+          <div
+            ref={timelineRef}
+            className="relative flex-1"
+            style={{ minHeight: "56px" }}
+            onContextMenu={(e) => {
+              if (timelineRef.current) {
+                const rect = timelineRef.current.getBoundingClientRect();
+                const xPct = (e.clientX - rect.left) / rect.width;
+                contextClickTimeRef.current = Math.round((dayStartMins + xPct * totalMinutes) / 5) * 5;
+              }
+            }}
+          >
+            <div className="absolute inset-0 flex">
+              {hourSlots.map((slot) => (
+                <div key={slot} className="flex-1 border-r border-dashed border-border/50 transition-colors duration-150 hover:bg-muted/40" />
+              ))}
+            </div>
+            <CurrentTimeIndicator dayStartMins={dayStartMins} totalMinutes={totalMinutes} date={date} />
+
+            {techJobs.map((job, jobIdx) => {
+              const isDragged = drag?.assignmentId === job.id;
+              const resolved = getResolvedRange(job.startDateTime ?? null, job.endDateTime ?? null, dayStartMins);
+              let { startMins, endMins } = resolved;
+
+              if (isDragged && dragOverride) {
+                if (dragOverride.techId !== tech.id) return null;
+                startMins = dragOverride.startMins;
+                endMins = dragOverride.endMins;
+              }
+
+              const leftPct = ((startMins - dayStartMins) / totalMinutes) * 100;
+              const widthPct = ((endMins - startMins) / totalMinutes) * 100;
+
+              return (
+                <JobBar
+                  key={job.id}
+                  job={job}
+                  leftPct={Math.max(leftPct, 0)}
+                  widthPct={Math.max(widthPct, 1)}
+                  colorClass={BAR_COLORS[jobIdx % BAR_COLORS.length]}
+                  unscheduled={resolved.isUnscheduled && !isDragged}
+                  isDragging={isDragged}
+                  startMins={startMins}
+                  endMins={endMins}
+                  onClick={() => { cancelPendingDrag(); if (!isDragged) onCardClick(job); }}
+                  onMoveStart={(e) => { if (timelineRef.current) startDrag(e, job, "move", timelineRef.current); }}
+                  onResizeStartLeft={(e) => { if (timelineRef.current) startDrag(e, job, "resize-start", timelineRef.current); }}
+                  onResizeStartRight={(e) => { if (timelineRef.current) startDrag(e, job, "resize-end", timelineRef.current); }}
+                />
+              );
+            })}
+
+            {ghostBar && drag && (
+              <div
+                className={cn("absolute top-1 bottom-1 z-[2] rounded shadow-sm border-2 border-dashed pointer-events-none", "border-primary bg-primary/10")}
+                style={{
+                  left: `${Math.max(((ghostBar.startMins - dayStartMins) / totalMinutes) * 100, 0)}%`,
+                  width: `${Math.max(((ghostBar.endMins - ghostBar.startMins) / totalMinutes) * 100, 1)}%`,
+                }}
+              >
+                <div className="flex items-center justify-center h-full text-[11px] font-semibold text-primary whitespace-nowrap">
+                  {minutesToTime(ghostBar.startMins)} – {minutesToTime(ghostBar.endMins)}
+                </div>
+              </div>
+            )}
+          </div>
+        </ContextMenuTrigger>
+        <ContextMenuContent>
+          <ContextMenuItem onClick={() => {
+            const startMins = contextClickTimeRef.current ?? dayStartMins;
+            const endMins = Math.min(startMins + 60, dayStartMins + totalMinutes);
+            onCreateWorkOrder?.(tech.id, minutesToTime(startMins), minutesToTime(endMins));
+          }}>
+            <Plus className="mr-2 h-4 w-4" />
+            {t("createNewWorkorder")}
+          </ContextMenuItem>
+        </ContextMenuContent>
+      </ContextMenu>
+    </div>
+  );
+}

+ 236 - 0
src/features/workboard/Components/DayTimeline/index.tsx

@@ -0,0 +1,236 @@
+"use client";
+
+import { useRef, useMemo, useState, useCallback, useEffect } from "react";
+import type { WorkBoardJob } from "../../Actions/boardActions";
+import type { Technician } from "../../store/workboardStore";
+import { useWorkBoardStore } from "../../store/workboardStore";
+import { jobOverlapsDate } from "../../utils/datetime";
+import { timeToMinutes, minutesToTime } from "./utils";
+import { TechTimelineRow } from "./TimelineColumn";
+
+const SNAP_MINUTES = 5;
+
+function snapMinutes(mins: number): number {
+  return Math.round(mins / SNAP_MINUTES) * SNAP_MINUTES;
+}
+
+function getHourSlots(startTime: string, endTime: string): string[] {
+  const startMins = timeToMinutes(startTime);
+  const endMins = timeToMinutes(endTime);
+  const slots: string[] = [];
+  for (let m = startMins; m < endMins; m += 60) {
+    slots.push(minutesToTime(m));
+  }
+  return slots;
+}
+
+type DragMode = "move" | "resize-start" | "resize-end";
+
+type DragState = {
+  assignmentId: string;
+  mode: DragMode;
+  origStartMins: number;
+  origEndMins: number;
+  origTechId: string;
+  anchorX: number;
+  anchorY: number;
+  timelineWidth: number;
+};
+
+type DragOverride = {
+  startMins: number;
+  endMins: number;
+  techId: string;
+};
+
+export { type DragMode, type DragState, type DragOverride };
+
+export function DayTimeline({
+  date,
+  technicians,
+  assignments,
+  workDayStart,
+  workDayEnd,
+  onCardClick,
+  onTechClick,
+  onCreateWorkOrder,
+}: {
+  date: string;
+  technicians: Technician[];
+  assignments: WorkBoardJob[];
+  workDayStart: string;
+  workDayEnd: string;
+  onCardClick: (job: WorkBoardJob) => void;
+  onTechClick?: (technician: Technician) => void;
+  onCreateWorkOrder?: (techId: string, startTime: string, endTime: string) => void;
+}) {
+  const store = useWorkBoardStore();
+  const dayStartMins = timeToMinutes(workDayStart);
+  const baseEndMins = timeToMinutes(workDayEnd);
+
+  const dayAssignments = useMemo(
+    () => assignments.filter((a) => jobOverlapsDate(a, date)),
+    [assignments, date],
+  );
+
+  const dayEndMins = useMemo(() => {
+    let max = baseEndMins;
+    for (const a of dayAssignments) {
+      const endStr = a.endDateTime;
+      if (endStr) {
+        const end = new Date(endStr);
+        const endMins = end.getHours() * 60 + end.getMinutes();
+        if (endMins > max) max = endMins;
+      }
+    }
+    return max > baseEndMins ? Math.ceil(max / 60) * 60 : baseEndMins;
+  }, [dayAssignments, baseEndMins]);
+
+  const totalMinutes = dayEndMins - dayStartMins;
+  const hourSlots = useMemo(
+    () => getHourSlots(workDayStart, minutesToTime(dayEndMins)),
+    [workDayStart, dayEndMins],
+  );
+
+  const techRowRefs = useRef<Map<string, HTMLDivElement>>(new Map());
+  const [drag, setDrag] = useState<DragState | null>(null);
+  const [dragOverride, setDragOverride] = useState<DragOverride | null>(null);
+  const dragOverrideRef = useRef<DragOverride | null>(null);
+  const dragRef = useRef<DragState | null>(null);
+  const pendingDragRef = useRef<DragState | null>(null);
+  const [hasPending, setHasPending] = useState(false);
+
+  const cancelPendingDrag = useCallback(() => {
+    pendingDragRef.current = null;
+    dragRef.current = null;
+    setHasPending(false);
+    setDrag(null);
+    setDragOverride(null);
+    dragOverrideRef.current = null;
+  }, []);
+
+  const getTechAtY = useCallback((clientY: number): string | null => {
+    for (const [techId, el] of techRowRefs.current) {
+      const rect = el.getBoundingClientRect();
+      if (clientY >= rect.top && clientY <= rect.bottom) return techId;
+    }
+    return null;
+  }, []);
+
+  const handleMouseMove = useCallback(
+    (e: MouseEvent) => {
+      const pending = pendingDragRef.current;
+      if (pending && !dragRef.current) {
+        const dist = Math.abs(e.clientX - pending.anchorX) + Math.abs(e.clientY - pending.anchorY);
+        if (dist < 5) return;
+        dragRef.current = pending;
+        pendingDragRef.current = null;
+        setHasPending(false);
+        setDrag(pending);
+      }
+      const d = dragRef.current;
+      if (!d) return;
+      const dx = e.clientX - d.anchorX;
+      const dMins = snapMinutes((dx / d.timelineWidth) * totalMinutes);
+      let newStart = d.origStartMins, newEnd = d.origEndMins;
+      if (d.mode === "move") { newStart += dMins; newEnd += dMins; }
+      else if (d.mode === "resize-start") { newStart = Math.max(dayStartMins, Math.min(d.origStartMins + dMins, d.origEndMins - SNAP_MINUTES)); }
+      else { newEnd = Math.min(dayEndMins, Math.max(d.origEndMins + dMins, d.origStartMins + SNAP_MINUTES)); }
+      let targetTech = d.origTechId;
+      if (d.mode === "move") { const h = getTechAtY(e.clientY); if (h) targetTech = h; }
+      if (newStart < dayStartMins) { if (d.mode === "move") newEnd += dayStartMins - newStart; newStart = dayStartMins; }
+      if (newEnd > dayEndMins) { if (d.mode === "move") newStart -= newEnd - dayEndMins; newEnd = dayEndMins; }
+      const override: DragOverride = { startMins: newStart, endMins: newEnd, techId: targetTech };
+      dragOverrideRef.current = override;
+      setDragOverride(override);
+    },
+    [totalMinutes, dayStartMins, dayEndMins, getTechAtY],
+  );
+
+  const handleMouseUp = useCallback(async () => {
+    if (pendingDragRef.current) { pendingDragRef.current = null; setHasPending(false); return; }
+    const d = dragRef.current;
+    const override = dragOverrideRef.current;
+    setDrag(null); setDragOverride(null); dragRef.current = null; dragOverrideRef.current = null;
+    if (!d || !override) return;
+    if (override.startMins === d.origStartMins && override.endMins === d.origEndMins && override.techId === d.origTechId) return;
+
+    const newStartDate = new Date(date + "T00:00:00");
+    newStartDate.setMinutes(override.startMins);
+    const newEndDate = new Date(date + "T00:00:00");
+    newEndDate.setMinutes(override.endMins);
+
+    // Find the job to get its type
+    const job = store.jobs.find((x) => x.id === d.assignmentId);
+
+    if (override.techId !== d.origTechId) {
+      store.optimisticMove(d.assignmentId, override.techId);
+      store.updateServiceTimes(d.assignmentId, newStartDate.toISOString(), newEndDate.toISOString());
+      const { moveJob, updateServiceTimes: updateST } = await import("../../Actions/boardActions");
+      const res = await moveJob({ id: d.assignmentId, technicianId: override.techId, sortOrder: 0, type: job?.type ?? "serviceRecord" });
+      if (!res.success) { store.optimisticMove(d.assignmentId, d.origTechId); } else if (res.data) { store.updateJob(res.data as WorkBoardJob); }
+      await updateST({ id: d.assignmentId, startDateTime: newStartDate, endDateTime: newEndDate });
+    } else {
+      store.updateServiceTimes(d.assignmentId, newStartDate.toISOString(), newEndDate.toISOString());
+      const { updateServiceTimes: updateST } = await import("../../Actions/boardActions");
+      await updateST({ id: d.assignmentId, startDateTime: newStartDate, endDateTime: newEndDate });
+    }
+  }, [store, date]);
+
+  useEffect(() => {
+    if (!drag && !hasPending) return;
+    window.addEventListener("mousemove", handleMouseMove);
+    window.addEventListener("mouseup", handleMouseUp);
+    return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); };
+  }, [drag, hasPending, handleMouseMove, handleMouseUp]);
+
+  const startDrag = (e: React.MouseEvent, job: WorkBoardJob, mode: DragMode, timelineEl: HTMLElement) => {
+    e.preventDefault(); e.stopPropagation();
+    const startStr = job.startDateTime;
+    const endStr = job.endDateTime;
+    let startMins = dayStartMins, endMins = dayStartMins + 60;
+    if (startStr) { const d = new Date(startStr); startMins = d.getHours() * 60 + d.getMinutes(); }
+    if (endStr) { const d = new Date(endStr); endMins = d.getHours() * 60 + d.getMinutes(); }
+    pendingDragRef.current = { assignmentId: job.id, mode, origStartMins: startMins, origEndMins: endMins, origTechId: job.technicianId!, anchorX: e.clientX, anchorY: e.clientY, timelineWidth: timelineEl.getBoundingClientRect().width };
+    setHasPending(true);
+  };
+
+  return (
+    <div className="flex flex-1 flex-col overflow-auto">
+      <div className="min-w-[900px]">
+        <div className="flex border-b">
+          <div className="w-[160px] shrink-0 border-r p-2">
+            <span className="text-xs font-medium text-muted-foreground">Technician</span>
+          </div>
+          <div className="relative flex-1">
+            <div className="flex">
+              {hourSlots.map((slot) => (
+                <div key={slot} className="flex-1 border-r px-1 py-2 text-center text-[11px] font-medium text-muted-foreground">{slot}</div>
+              ))}
+            </div>
+          </div>
+        </div>
+        {technicians.map((tech) => (
+          <TechTimelineRow
+            key={tech.id}
+            tech={tech}
+            date={date}
+            dayAssignments={dayAssignments}
+            dayStartMins={dayStartMins}
+            dayEndMins={dayEndMins}
+            totalMinutes={totalMinutes}
+            hourSlots={hourSlots}
+            drag={drag}
+            dragOverride={dragOverride}
+            startDrag={startDrag}
+            onCardClick={onCardClick}
+            onTechClick={onTechClick}
+            onCreateWorkOrder={onCreateWorkOrder}
+            cancelPendingDrag={cancelPendingDrag}
+            registerRef={(el) => { if (el) techRowRefs.current.set(tech.id, el); else techRowRefs.current.delete(tech.id); }}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}

+ 39 - 0
src/features/workboard/Components/DayTimeline/utils.ts

@@ -0,0 +1,39 @@
+export function timeToMinutes(time: string): number {
+  const [h, m] = time.split(":").map(Number);
+  return h * 60 + m;
+}
+
+export function minutesToTime(mins: number): string {
+  const clamped = Math.max(0, Math.min(1439, mins));
+  const h = Math.floor(clamped / 60);
+  const m = clamped % 60;
+  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
+}
+
+export function getResolvedRange(
+  startDateTime: string | null | undefined,
+  endDateTime: string | null | undefined,
+  dayStartMins: number,
+): { startMins: number; endMins: number; isUnscheduled: boolean } {
+  if (startDateTime && endDateTime) {
+    const s = new Date(startDateTime);
+    const e = new Date(endDateTime);
+    return {
+      startMins: s.getHours() * 60 + s.getMinutes(),
+      endMins: e.getHours() * 60 + e.getMinutes(),
+      isUnscheduled: false,
+    };
+  }
+  return { startMins: dayStartMins, endMins: dayStartMins + 60, isUnscheduled: true };
+}
+
+export const BAR_COLORS = [
+  "bg-sky-400/80 text-sky-950 dark:bg-sky-600/80 dark:text-sky-50",
+  "bg-emerald-400/80 text-emerald-950 dark:bg-emerald-600/80 dark:text-emerald-50",
+  "bg-amber-400/80 text-amber-950 dark:bg-amber-600/80 dark:text-amber-50",
+  "bg-violet-400/80 text-violet-950 dark:bg-violet-600/80 dark:text-violet-50",
+  "bg-rose-400/80 text-rose-950 dark:bg-rose-600/80 dark:text-rose-50",
+  "bg-cyan-400/80 text-cyan-950 dark:bg-cyan-600/80 dark:text-cyan-50",
+  "bg-lime-400/80 text-lime-950 dark:bg-lime-600/80 dark:text-lime-50",
+  "bg-orange-400/80 text-orange-950 dark:bg-orange-600/80 dark:text-orange-50",
+];

+ 57 - 0
src/features/workboard/Components/DurationSlider.tsx

@@ -0,0 +1,57 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { X } from "lucide-react";
+
+const HOUR_PRESETS = [1, 2, 4, 5, 7];
+
+export function formatDuration(minutes: number): string {
+  if (minutes < 60) return `${minutes}min`;
+  const h = Math.floor(minutes / 60);
+  const m = minutes % 60;
+  if (m === 0) return `${h}h`;
+  return `${h}h ${m}min`;
+}
+
+interface DurationPickerProps {
+  value: number | null;
+  onChange: (hours: number | null) => void;
+  className?: string;
+}
+
+export function DurationPicker({ value, onChange, className }: DurationPickerProps) {
+  return (
+    <div className={cn("space-y-1.5", className)}>
+      <div className="flex items-center justify-between">
+        <span className="text-xs font-medium text-muted-foreground">Estimated hours</span>
+        {value !== null && (
+          <button
+            type="button"
+            onClick={() => onChange(null)}
+            className="flex items-center gap-0.5 rounded px-1 py-0.5 text-[10px] text-muted-foreground hover:bg-muted hover:text-foreground"
+          >
+            <X className="h-3 w-3" />
+            Clear
+          </button>
+        )}
+      </div>
+      <div className="flex flex-wrap gap-1">
+        {HOUR_PRESETS.map((hours) => (
+          <button
+            key={hours}
+            type="button"
+            onClick={() => onChange(hours)}
+            className={cn(
+              "rounded-md border px-2.5 py-1 text-xs font-medium transition-colors",
+              value === hours
+                ? "border-primary bg-primary text-primary-foreground"
+                : "border-border bg-background text-foreground hover:bg-muted",
+            )}
+          >
+            {hours}h
+          </button>
+        ))}
+      </div>
+    </div>
+  );
+}

+ 114 - 32
src/features/workboard/Components/JobDetailPopover.tsx

@@ -1,5 +1,6 @@
 "use client";
 "use client";
 
 
+import { useState, useEffect, useCallback } from "react";
 import Link from "next/link";
 import Link from "next/link";
 import {
 import {
   Dialog,
   Dialog,
@@ -9,36 +10,92 @@ import {
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
+import { Label } from "@/components/ui/label";
 import { Wrench, ClipboardCheck, ExternalLink, Trash2 } from "lucide-react";
 import { Wrench, ClipboardCheck, ExternalLink, Trash2 } from "lucide-react";
-import type { BoardAssignmentWithJob } from "../Actions/boardActions";
+import type { WorkBoardJob } from "../Actions/boardActions";
+import { updateServiceTimes, updateInspectionTimes } from "../Actions/boardActions";
+import { useWorkBoardStore } from "../store/workboardStore";
+import { DurationPicker } from "./DurationSlider";
+import { DateTimePicker } from "@/components/ui/datetime-picker";
 import { useTranslations } from "next-intl";
 import { useTranslations } from "next-intl";
+import { toast } from "sonner";
+import { getJobDateRange } from "../utils/datetime";
 
 
 export function JobDetailPopover({
 export function JobDetailPopover({
-  assignment,
+  job,
   open,
   open,
   onOpenChange,
   onOpenChange,
   onRemove,
   onRemove,
 }: {
 }: {
-  assignment: BoardAssignmentWithJob;
+  job: WorkBoardJob;
   open: boolean;
   open: boolean;
   onOpenChange: (open: boolean) => void;
   onOpenChange: (open: boolean) => void;
   onRemove: () => void;
   onRemove: () => void;
 }) {
 }) {
   const t = useTranslations("workBoard.jobDetail");
   const t = useTranslations("workBoard.jobDetail");
-  const isServiceRecord = !!assignment.serviceRecordId;
-  const vehicle = isServiceRecord
-    ? assignment.serviceRecord?.vehicle
-    : assignment.inspection?.vehicle;
-  const title = isServiceRecord
-    ? assignment.serviceRecord?.title
-    : assignment.inspection?.template?.name;
-  const status = isServiceRecord
-    ? assignment.serviceRecord?.status
-    : assignment.inspection?.status;
+  const store = useWorkBoardStore();
+
+  const liveJob =
+    store.jobs.find((j) => j.id === job.id) ?? job;
+
+  const { start: initStart, end: initEnd } = getJobDateRange(liveJob);
+  const [startDate, setStartDate] = useState<Date | undefined>(initStart ?? undefined);
+  const [endDate, setEndDate] = useState<Date | undefined>(initEnd ?? undefined);
+
+  useEffect(() => {
+    const { start, end } = getJobDateRange(liveJob);
+    setStartDate(start ?? undefined);
+    setEndDate(end ?? undefined);
+  }, [liveJob.id, liveJob.startDateTime, liveJob.endDateTime]);
+
+  const isServiceRecord = liveJob.type === "serviceRecord";
 
 
   const detailUrl = isServiceRecord
   const detailUrl = isServiceRecord
-    ? `/vehicles/${vehicle?.id}/service/${assignment.serviceRecordId}`
-    : `/inspections/${assignment.inspectionId}`;
+    ? `/vehicles/${liveJob.vehicle?.id}/service/${liveJob.id}`
+    : `/inspections/${liveJob.id}`;
+
+  const saveTimes = useCallback(
+    async (start: Date | undefined, end: Date | undefined) => {
+      if (!start || !end) return;
+      if (end <= start) {
+        toast.error(t("endBeforeStart"));
+        return;
+      }
+
+      store.updateServiceTimes(
+        job.id,
+        start.toISOString(),
+        end.toISOString(),
+      );
+
+      const action = isServiceRecord ? updateServiceTimes : updateInspectionTimes;
+      const res = await action({
+        id: job.id,
+        startDateTime: start,
+        endDateTime: end,
+      });
+      if (!res.success) {
+        const { start: origStart, end: origEnd } = getJobDateRange(job);
+        if (origStart && origEnd) {
+          store.updateServiceTimes(job.id, origStart.toISOString(), origEnd.toISOString());
+        }
+        toast.error(t("failedTimes"));
+      }
+    },
+    [job, store, t, isServiceRecord],
+  );
+
+  const handleDurationChange = (hours: number | null) => {
+    if (hours && startDate) {
+      const newEnd = new Date(startDate.getTime() + hours * 3600000);
+      setEndDate(newEnd);
+      saveTimes(startDate, newEnd);
+    }
+  };
+
+  const currentHours = startDate && endDate
+    ? Math.round((endDate.getTime() - startDate.getTime()) / 3600000)
+    : null;
 
 
   return (
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
     <Dialog open={open} onOpenChange={onOpenChange}>
@@ -50,30 +107,59 @@ export function JobDetailPopover({
             ) : (
             ) : (
               <ClipboardCheck className="h-4 w-4 shrink-0 text-green-500" />
               <ClipboardCheck className="h-4 w-4 shrink-0 text-green-500" />
             )}
             )}
-            {title}
+            {liveJob.title}
           </DialogTitle>
           </DialogTitle>
         </DialogHeader>
         </DialogHeader>
         <div className="space-y-3">
         <div className="space-y-3">
-          {vehicle && (
+          {liveJob.vehicle && (
             <p className="text-sm text-muted-foreground">
             <p className="text-sm text-muted-foreground">
-              {vehicle.year} {vehicle.make} {vehicle.model}
-              {vehicle.licensePlate ? ` · ${vehicle.licensePlate}` : ""}
+              {liveJob.vehicle.year} {liveJob.vehicle.make} {liveJob.vehicle.model}
+              {liveJob.vehicle.licensePlate ? ` · ${liveJob.vehicle.licensePlate}` : ""}
             </p>
             </p>
           )}
           )}
 
 
-          {status && (
+          {liveJob.status && (
             <Badge variant="secondary" className="capitalize">
             <Badge variant="secondary" className="capitalize">
-              {status.replace(/_/g, " ")}
+              {liveJob.status.replace(/_/g, " ")}
             </Badge>
             </Badge>
           )}
           )}
 
 
-          {assignment.notes && (
-            <p className="text-sm text-muted-foreground">{assignment.notes}</p>
-          )}
+          <DurationPicker
+            value={currentHours}
+            onChange={handleDurationChange}
+          />
 
 
-          <p className="text-xs text-muted-foreground">
-            {t("date", { date: assignment.date })}
-          </p>
+          <div className="space-y-1.5">
+            <Label className="text-xs">{t("startTime")}</Label>
+            <DateTimePicker
+              value={startDate}
+              onChange={(d) => {
+                setStartDate(d);
+                const newEnd = d ? new Date(d.getTime() + 3600000) : endDate;
+                setEndDate(newEnd);
+                saveTimes(d, newEnd);
+              }}
+              granularity="minute"
+              hourCycle={24}
+              placeholder={t("startTime")}
+              displayFormat={{ hour24: "PPP HH:mm" }}
+            />
+          </div>
+
+          <div className="space-y-1.5">
+            <Label className="text-xs">{t("endTime")}</Label>
+            <DateTimePicker
+              value={endDate}
+              onChange={(d) => {
+                setEndDate(d);
+                saveTimes(startDate, d);
+              }}
+              granularity="minute"
+              hourCycle={24}
+              placeholder={t("endTime")}
+              displayFormat={{ hour24: "PPP HH:mm" }}
+            />
+          </div>
 
 
           <div className="flex gap-2">
           <div className="flex gap-2">
             <Button asChild variant="outline" size="sm" className="flex-1">
             <Button asChild variant="outline" size="sm" className="flex-1">
@@ -82,11 +168,7 @@ export function JobDetailPopover({
                 {t("viewDetails")}
                 {t("viewDetails")}
               </Link>
               </Link>
             </Button>
             </Button>
-            <Button
-              variant="destructive"
-              size="sm"
-              onClick={onRemove}
-            >
+            <Button variant="destructive" size="sm" onClick={onRemove}>
               <Trash2 className="mr-1.5 h-3.5 w-3.5" />
               <Trash2 className="mr-1.5 h-3.5 w-3.5" />
               {t("remove")}
               {t("remove")}
             </Button>
             </Button>

+ 15 - 23
src/features/workboard/Components/PresenterDayView.tsx

@@ -2,18 +2,12 @@
 
 
 import { Wrench, ClipboardCheck } from 'lucide-react'
 import { Wrench, ClipboardCheck } from 'lucide-react'
 import type { Technician } from '../store/workboardStore'
 import type { Technician } from '../store/workboardStore'
-import type { BoardAssignmentWithJob } from '../Actions/boardActions'
+import type { WorkBoardJob } from '../Actions/boardActions'
+import { jobOverlapsDate } from '../utils/datetime'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 
 
-function DayJobCard({ assignment }: { assignment: BoardAssignmentWithJob }) {
-  const isServiceRecord = !!assignment.serviceRecordId
-  const vehicle = isServiceRecord
-    ? assignment.serviceRecord?.vehicle
-    : assignment.inspection?.vehicle
-  const title = isServiceRecord
-    ? assignment.serviceRecord?.title
-    : assignment.inspection?.template?.name
-  const status = isServiceRecord ? assignment.serviceRecord?.status : assignment.inspection?.status
+function DayJobCard({ job }: { job: WorkBoardJob }) {
+  const isServiceRecord = job.type === 'serviceRecord'
 
 
   return (
   return (
     <div className="flex items-start gap-3 rounded-lg border bg-card p-3 shadow-sm">
     <div className="flex items-start gap-3 rounded-lg border bg-card p-3 shadow-sm">
@@ -25,17 +19,17 @@ function DayJobCard({ assignment }: { assignment: BoardAssignmentWithJob }) {
         )}
         )}
       </div>
       </div>
       <div className="min-w-0 flex-1">
       <div className="min-w-0 flex-1">
-        <p className="truncate text-base font-semibold">{title}</p>
-        {vehicle && (
+        <p className="truncate text-base font-semibold">{job.title}</p>
+        {job.vehicle && (
           <p className="truncate text-sm text-muted-foreground">
           <p className="truncate text-sm text-muted-foreground">
-            {vehicle.year} {vehicle.make} {vehicle.model}
-            {vehicle.licensePlate ? ` · ${vehicle.licensePlate}` : ''}
+            {job.vehicle.year} {job.vehicle.make} {job.vehicle.model}
+            {job.vehicle.licensePlate ? ` · ${job.vehicle.licensePlate}` : ''}
           </p>
           </p>
         )}
         )}
       </div>
       </div>
-      {status && (
+      {job.status && (
         <span className="shrink-0 rounded-full bg-muted px-2.5 py-1 text-xs font-medium capitalize">
         <span className="shrink-0 rounded-full bg-muted px-2.5 py-1 text-xs font-medium capitalize">
-          {status.replace(/_/g, ' ')}
+          {job.status.replace(/_/g, ' ')}
         </span>
         </span>
       )}
       )}
     </div>
     </div>
@@ -49,10 +43,10 @@ export function PresenterDayView({
 }: {
 }: {
   date: string
   date: string
   technicians: Technician[]
   technicians: Technician[]
-  assignments: BoardAssignmentWithJob[]
+  assignments: WorkBoardJob[]
 }) {
 }) {
   const t = useTranslations('workBoard.presenter')
   const t = useTranslations('workBoard.presenter')
-  const dayAssignments = assignments.filter((a) => a.date === date)
+  const dayAssignments = assignments.filter((a) => jobOverlapsDate(a, date))
 
 
   if (technicians.length === 0) {
   if (technicians.length === 0) {
     return (
     return (
@@ -72,7 +66,6 @@ export function PresenterDayView({
 
 
           return (
           return (
             <div key={tech.id} className="flex gap-4 p-4">
             <div key={tech.id} className="flex gap-4 p-4">
-              {/* Tech label */}
               <div className="flex w-36 shrink-0 items-start gap-2 pt-1">
               <div className="flex w-36 shrink-0 items-start gap-2 pt-1">
                 <div
                 <div
                   className="mt-1 h-3.5 w-3.5 shrink-0 rounded-full"
                   className="mt-1 h-3.5 w-3.5 shrink-0 rounded-full"
@@ -86,14 +79,13 @@ export function PresenterDayView({
                 </div>
                 </div>
               </div>
               </div>
 
 
-              {/* Jobs */}
               <div className="flex flex-1 flex-wrap gap-2">
               <div className="flex flex-1 flex-wrap gap-2">
                 {techJobs.length === 0 && (
                 {techJobs.length === 0 && (
                   <p className="py-2 text-sm text-muted-foreground/60">{t('noJobsToday')}</p>
                   <p className="py-2 text-sm text-muted-foreground/60">{t('noJobsToday')}</p>
                 )}
                 )}
-                {techJobs.map((assignment) => (
-                  <div key={assignment.id} className="w-72">
-                    <DayJobCard assignment={assignment} />
+                {techJobs.map((job) => (
+                  <div key={job.id} className="w-72">
+                    <DayJobCard job={job} />
                   </div>
                   </div>
                 ))}
                 ))}
               </div>
               </div>

+ 14 - 29
src/features/workboard/Components/PresenterKanbanView.tsx

@@ -2,7 +2,8 @@
 
 
 import { Wrench, ClipboardCheck } from 'lucide-react'
 import { Wrench, ClipboardCheck } from 'lucide-react'
 import type { Technician } from '../store/workboardStore'
 import type { Technician } from '../store/workboardStore'
-import type { BoardAssignmentWithJob } from '../Actions/boardActions'
+import type { WorkBoardJob } from '../Actions/boardActions'
+import { jobOverlapsDate } from '../utils/datetime'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 
 
 const STATUS_COLUMNS = [
 const STATUS_COLUMNS = [
@@ -12,20 +13,8 @@ const STATUS_COLUMNS = [
   { key: 'completed', i18nKey: 'completed', color: 'bg-emerald-500' },
   { key: 'completed', i18nKey: 'completed', color: 'bg-emerald-500' },
 ] as const
 ] as const
 
 
-function getAssignmentStatus(a: BoardAssignmentWithJob): string {
-  if (a.serviceRecordId) return a.serviceRecord?.status ?? 'pending'
-  if (a.inspectionId) return a.inspection?.status ?? 'pending'
-  return 'pending'
-}
-
-function KanbanCard({ assignment }: { assignment: BoardAssignmentWithJob }) {
-  const isServiceRecord = !!assignment.serviceRecordId
-  const vehicle = isServiceRecord
-    ? assignment.serviceRecord?.vehicle
-    : assignment.inspection?.vehicle
-  const title = isServiceRecord
-    ? assignment.serviceRecord?.title
-    : assignment.inspection?.template?.name
+function KanbanCard({ job }: { job: WorkBoardJob }) {
+  const isServiceRecord = job.type === 'serviceRecord'
 
 
   return (
   return (
     <div className="rounded-lg border bg-card p-2.5 shadow-sm">
     <div className="rounded-lg border bg-card p-2.5 shadow-sm">
@@ -35,12 +24,12 @@ function KanbanCard({ assignment }: { assignment: BoardAssignmentWithJob }) {
         ) : (
         ) : (
           <ClipboardCheck className="h-4 w-4 shrink-0 text-green-500" />
           <ClipboardCheck className="h-4 w-4 shrink-0 text-green-500" />
         )}
         )}
-        <span className="truncate text-sm font-semibold">{title}</span>
+        <span className="truncate text-sm font-semibold">{job.title}</span>
       </div>
       </div>
-      {vehicle && (
+      {job.vehicle && (
         <p className="mt-1 truncate text-sm text-muted-foreground">
         <p className="mt-1 truncate text-sm text-muted-foreground">
-          {vehicle.year} {vehicle.make} {vehicle.model}
-          {vehicle.licensePlate ? ` · ${vehicle.licensePlate}` : ''}
+          {job.vehicle.year} {job.vehicle.make} {job.vehicle.model}
+          {job.vehicle.licensePlate ? ` · ${job.vehicle.licensePlate}` : ''}
         </p>
         </p>
       )}
       )}
     </div>
     </div>
@@ -54,10 +43,10 @@ export function PresenterKanbanView({
 }: {
 }: {
   date: string
   date: string
   technicians: Technician[]
   technicians: Technician[]
-  assignments: BoardAssignmentWithJob[]
+  assignments: WorkBoardJob[]
 }) {
 }) {
   const t = useTranslations('workBoard.presenter')
   const t = useTranslations('workBoard.presenter')
-  const dayAssignments = assignments.filter((a) => a.date === date)
+  const dayAssignments = assignments.filter((a) => jobOverlapsDate(a, date))
 
 
   if (technicians.length === 0) {
   if (technicians.length === 0) {
     return (
     return (
@@ -76,10 +65,9 @@ export function PresenterKanbanView({
           gridTemplateRows: `auto ${technicians.length > 0 ? `repeat(${technicians.length}, 1fr)` : '1fr'}`,
           gridTemplateRows: `auto ${technicians.length > 0 ? `repeat(${technicians.length}, 1fr)` : '1fr'}`,
         }}
         }}
       >
       >
-        {/* Status header row */}
         <div className="border-b p-2" />
         <div className="border-b p-2" />
         {STATUS_COLUMNS.map((col) => {
         {STATUS_COLUMNS.map((col) => {
-          const count = dayAssignments.filter((a) => getAssignmentStatus(a) === col.key).length
+          const count = dayAssignments.filter((a) => a.status === col.key).length
           return (
           return (
             <div key={col.key} className="flex items-center justify-center gap-2 border-b border-l p-2">
             <div key={col.key} className="flex items-center justify-center gap-2 border-b border-l p-2">
               <div className={`h-2.5 w-2.5 rounded-full ${col.color}`} />
               <div className={`h-2.5 w-2.5 rounded-full ${col.color}`} />
@@ -89,12 +77,10 @@ export function PresenterKanbanView({
           )
           )
         })}
         })}
 
 
-        {/* Tech rows */}
         {technicians.map((tech) => {
         {technicians.map((tech) => {
           const techJobs = dayAssignments.filter((a) => a.technicianId === tech.id)
           const techJobs = dayAssignments.filter((a) => a.technicianId === tech.id)
           return (
           return (
             <div key={tech.id} className="contents">
             <div key={tech.id} className="contents">
-              {/* Tech name */}
               <div className="flex items-center gap-2 border-b p-2">
               <div className="flex items-center gap-2 border-b p-2">
                 <div
                 <div
                   className="h-3 w-3 shrink-0 rounded-full"
                   className="h-3 w-3 shrink-0 rounded-full"
@@ -108,15 +94,14 @@ export function PresenterKanbanView({
                 </div>
                 </div>
               </div>
               </div>
 
 
-              {/* Status columns */}
               {STATUS_COLUMNS.map((col) => {
               {STATUS_COLUMNS.map((col) => {
                 const cellJobs = techJobs
                 const cellJobs = techJobs
-                  .filter((a) => getAssignmentStatus(a) === col.key)
+                  .filter((a) => a.status === col.key)
                   .sort((a, b) => a.sortOrder - b.sortOrder)
                   .sort((a, b) => a.sortOrder - b.sortOrder)
                 return (
                 return (
                   <div key={`${tech.id}-${col.key}`} className="space-y-1.5 overflow-y-auto border-b border-l p-1.5">
                   <div key={`${tech.id}-${col.key}`} className="space-y-1.5 overflow-y-auto border-b border-l p-1.5">
-                    {cellJobs.map((assignment) => (
-                      <KanbanCard key={assignment.id} assignment={assignment} />
+                    {cellJobs.map((job) => (
+                      <KanbanCard key={job.id} job={job} />
                     ))}
                     ))}
                   </div>
                   </div>
                 )
                 )

+ 174 - 0
src/features/workboard/Components/PresenterTimeline.tsx

@@ -0,0 +1,174 @@
+"use client";
+
+import { useMemo, useEffect, useState } from "react";
+import { cn } from "@/lib/utils";
+import type { WorkBoardJob } from "../Actions/boardActions";
+import type { Technician } from "../store/workboardStore";
+import { Wrench, ClipboardCheck } from "lucide-react";
+import { jobOverlapsDate, getJobDateRange, getDurationMinutes } from "../utils/datetime";
+import { formatDuration } from "./DurationSlider";
+import { useTranslations } from "next-intl";
+
+function timeToMinutes(time: string): number {
+  const [h, m] = time.split(":").map(Number);
+  return h * 60 + m;
+}
+
+function minutesToTime(mins: number): string {
+  const clamped = Math.max(0, Math.min(1439, mins));
+  const h = Math.floor(clamped / 60);
+  const m = clamped % 60;
+  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
+}
+
+function getHourSlots(startTime: string, endTime: string): string[] {
+  const startMins = timeToMinutes(startTime);
+  const endMins = timeToMinutes(endTime);
+  const slots: string[] = [];
+  for (let m = startMins; m < endMins; m += 60) slots.push(minutesToTime(m));
+  return slots;
+}
+
+const BAR_COLORS = [
+  "bg-sky-400/80 text-sky-950 dark:bg-sky-600/80 dark:text-sky-50",
+  "bg-emerald-400/80 text-emerald-950 dark:bg-emerald-600/80 dark:text-emerald-50",
+  "bg-amber-400/80 text-amber-950 dark:bg-amber-600/80 dark:text-amber-50",
+  "bg-violet-400/80 text-violet-950 dark:bg-violet-600/80 dark:text-violet-50",
+  "bg-rose-400/80 text-rose-950 dark:bg-rose-600/80 dark:text-rose-50",
+  "bg-cyan-400/80 text-cyan-950 dark:bg-cyan-600/80 dark:text-cyan-50",
+  "bg-lime-400/80 text-lime-950 dark:bg-lime-600/80 dark:text-lime-50",
+  "bg-orange-400/80 text-orange-950 dark:bg-orange-600/80 dark:text-orange-50",
+];
+
+export function PresenterTimeline({
+  date,
+  technicians,
+  assignments,
+  workDayStart,
+  workDayEnd,
+}: {
+  date: string;
+  technicians: Technician[];
+  assignments: WorkBoardJob[];
+  workDayStart: string;
+  workDayEnd: string;
+}) {
+  const t = useTranslations("workBoard.presenter");
+  const dayStartMins = timeToMinutes(workDayStart);
+  const baseEndMins = timeToMinutes(workDayEnd);
+
+  const dayAssignments = useMemo(
+    () => assignments.filter((a) => jobOverlapsDate(a, date)),
+    [assignments, date],
+  );
+
+  const dayEndMins = useMemo(() => {
+    let max = baseEndMins;
+    for (const a of dayAssignments) {
+      const endStr = a.endDateTime;
+      if (endStr) {
+        const d = new Date(endStr);
+        const endMins = d.getHours() * 60 + d.getMinutes();
+        if (endMins > max) max = endMins;
+      }
+    }
+    return max > baseEndMins ? Math.ceil(max / 60) * 60 : baseEndMins;
+  }, [dayAssignments, baseEndMins]);
+
+  const totalMinutes = dayEndMins - dayStartMins;
+  const hourSlots = useMemo(() => getHourSlots(workDayStart, minutesToTime(dayEndMins)), [workDayStart, dayEndMins]);
+
+  if (technicians.length === 0) {
+    return <div className="flex flex-1 items-center justify-center"><p className="text-lg text-muted-foreground">{t("noTechnicians")}</p></div>;
+  }
+
+  return (
+    <div className="flex flex-1 flex-col overflow-auto">
+      <div className="min-w-[900px]">
+        <div className="flex border-b">
+          <div className="w-[180px] shrink-0 border-r p-2" />
+          <div className="relative flex-1">
+            <div className="flex">
+              {hourSlots.map((slot) => (
+                <div key={slot} className="flex-1 border-r px-1 py-2 text-center text-xs font-semibold text-muted-foreground">{slot}</div>
+              ))}
+            </div>
+          </div>
+        </div>
+
+        {technicians.map((tech) => {
+          const techJobs = dayAssignments.filter((a) => a.technicianId === tech.id).sort((a, b) => a.sortOrder - b.sortOrder);
+          const bookedMinutes = techJobs.reduce((sum, a) => {
+            const { start, end } = getJobDateRange(a);
+            return start && end ? sum + getDurationMinutes(start, end) : sum;
+          }, 0);
+
+          return (
+            <div key={tech.id} className="flex border-b last:border-b-0">
+              <div className="flex w-[180px] shrink-0 flex-col justify-center gap-1 border-r p-3">
+                <div className="flex items-center gap-2">
+                  <div className="h-3.5 w-3.5 shrink-0 rounded-full" style={{ backgroundColor: tech.color }} />
+                  <span className="text-sm font-semibold">{tech.name}</span>
+                </div>
+                <span className="text-[11px] text-muted-foreground">
+                  {techJobs.length === 1 ? t("job", { count: techJobs.length }) : t("jobs", { count: techJobs.length })}
+                  {" · "}
+                  {formatDuration(bookedMinutes)}
+                </span>
+              </div>
+
+              <div className="relative flex-1" style={{ minHeight: "64px" }}>
+                <div className="absolute inset-0 flex pointer-events-none">
+                  {hourSlots.map((slot) => (<div key={slot} className="flex-1 border-r border-dashed border-border/50" />))}
+                </div>
+                <PresenterCurrentTime dayStartMins={dayStartMins} totalMinutes={totalMinutes} date={date} />
+
+                {techJobs.map((job, jobIdx) => {
+                  const startStr = job.startDateTime;
+                  const endStr = job.endDateTime;
+                  let startMins = dayStartMins, endMins = dayStartMins + 60;
+                  if (startStr) { const d = new Date(startStr); startMins = d.getHours() * 60 + d.getMinutes(); }
+                  if (endStr) { const d = new Date(endStr); endMins = d.getHours() * 60 + d.getMinutes(); }
+                  const isUnscheduled = !startStr || !endStr;
+                  const leftPct = ((startMins - dayStartMins) / totalMinutes) * 100;
+                  const widthPct = ((endMins - startMins) / totalMinutes) * 100;
+                  const isServiceRecord = job.type === "serviceRecord";
+
+                  return (
+                    <div
+                      key={job.id}
+                      className={cn("absolute top-1 bottom-1 z-[1] flex items-center overflow-hidden rounded shadow-sm select-none", BAR_COLORS[jobIdx % BAR_COLORS.length], isUnscheduled && "border border-dashed border-current/30 opacity-70")}
+                      style={{ left: `${Math.max(leftPct, 0)}%`, width: `${Math.max(widthPct, 1)}%` }}
+                      title={`${minutesToTime(startMins)}–${minutesToTime(endMins)} · ${job.title}${job.vehicle ? ` - ${job.vehicle.make} ${job.vehicle.model}` : ""}`}
+                    >
+                      <div className="flex items-center gap-1.5 px-3 min-w-0 text-xs font-semibold">
+                        {isServiceRecord ? <Wrench className="h-3.5 w-3.5 shrink-0" /> : <ClipboardCheck className="h-3.5 w-3.5 shrink-0" />}
+                        <span className="truncate">{job.title}{job.vehicle ? ` - ${job.vehicle.licensePlate || `${job.vehicle.make} ${job.vehicle.model}`}` : ""}</span>
+                        <span className="ml-auto shrink-0 text-[10px] opacity-75">{minutesToTime(startMins)}–{minutesToTime(endMins)}</span>
+                      </div>
+                    </div>
+                  );
+                })}
+              </div>
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
+
+function PresenterCurrentTime({ dayStartMins, totalMinutes, date }: { dayStartMins: number; totalMinutes: number; date: string }) {
+  const [now, setNow] = useState(() => new Date());
+  useEffect(() => { const timer = setInterval(() => setNow(new Date()), 60000); return () => clearInterval(timer); }, []);
+  const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
+  if (date !== today) return null;
+  const nowMins = now.getHours() * 60 + now.getMinutes();
+  const leftPct = ((nowMins - dayStartMins) / totalMinutes) * 100;
+  if (leftPct < 0 || leftPct > 100) return null;
+  return (
+    <div className="absolute top-0 bottom-0 z-10 w-0.5 bg-red-500 pointer-events-none" style={{ left: `${leftPct}%` }}>
+      <div className="absolute -top-1 -left-1 h-2.5 w-2.5 rounded-full bg-red-500" />
+    </div>
+  );
+}

+ 49 - 15
src/features/workboard/Components/TechnicianRow.tsx

@@ -1,8 +1,11 @@
 "use client";
 "use client";
 
 
-import type { BoardAssignmentWithJob } from "../Actions/boardActions";
+import { cn } from "@/lib/utils";
+import type { WorkBoardJob } from "../Actions/boardActions";
 import type { Technician } from "../store/workboardStore";
 import type { Technician } from "../store/workboardStore";
 import { BoardDayCell } from "./BoardDayCell";
 import { BoardDayCell } from "./BoardDayCell";
+import { formatDuration } from "./DurationSlider";
+import { jobOverlapsDate, getJobDateRange, getDurationMinutes } from "../utils/datetime";
 
 
 export function TechnicianRow({
 export function TechnicianRow({
   technician,
   technician,
@@ -13,31 +16,61 @@ export function TechnicianRow({
 }: {
 }: {
   technician: Technician;
   technician: Technician;
   days: string[];
   days: string[];
-  assignments: BoardAssignmentWithJob[];
-  onCardClick: (assignment: BoardAssignmentWithJob) => void;
+  assignments: WorkBoardJob[];
+  onCardClick: (job: WorkBoardJob) => void;
   onTechClick?: (technician: Technician) => void;
   onTechClick?: (technician: Technician) => void;
 }) {
 }) {
+  const techAssignments = assignments.filter(
+    (a) => a.technicianId === technician.id,
+  );
+
+  const weekBooked = techAssignments.reduce((sum, a) => {
+    const { start, end } = getJobDateRange(a);
+    if (start && end) return sum + getDurationMinutes(start, end);
+    return sum;
+  }, 0);
+  const weekCapacity = technician.dailyCapacity * days.length;
+  const weekPct = weekCapacity > 0 ? Math.round((weekBooked / weekCapacity) * 100) : 0;
+
   return (
   return (
     <div className="contents">
     <div className="contents">
-      {/* Tech name column */}
       <button
       <button
         type="button"
         type="button"
-        className="flex items-start gap-2 rounded-md bg-muted/50 p-2 text-left transition-colors hover:bg-muted"
+        className="flex flex-col gap-1.5 rounded-md bg-muted/50 p-2 text-left transition-colors hover:bg-muted"
         onClick={() => onTechClick?.(technician)}
         onClick={() => onTechClick?.(technician)}
       >
       >
-        <div
-          className="mt-0.5 h-3 w-3 shrink-0 rounded-full"
-          style={{ backgroundColor: technician.color }}
-        />
-        <span className="text-sm font-medium leading-tight">
-          {technician.name}
-        </span>
+        <div className="flex items-center gap-2">
+          <div
+            className="h-3 w-3 shrink-0 rounded-full"
+            style={{ backgroundColor: technician.color }}
+          />
+          <span className="text-sm font-medium leading-tight">
+            {technician.name}
+          </span>
+        </div>
+        <div className="w-full space-y-0.5">
+          <div className="h-1 w-full overflow-hidden rounded-full bg-background">
+            <div
+              className={cn(
+                "h-full rounded-full transition-all",
+                weekPct > 100
+                  ? "bg-red-500"
+                  : weekPct >= 75
+                    ? "bg-amber-500"
+                    : "bg-emerald-500",
+              )}
+              style={{ width: `${Math.min(weekPct, 100)}%` }}
+            />
+          </div>
+          <p className="text-[10px] text-muted-foreground">
+            {formatDuration(weekBooked)} / {formatDuration(weekCapacity)}
+          </p>
+        </div>
       </button>
       </button>
 
 
-      {/* Day cells */}
       {days.map((day) => {
       {days.map((day) => {
-        const cellAssignments = assignments.filter(
-          (a) => a.technicianId === technician.id && a.date === day,
+        const cellAssignments = techAssignments.filter(
+          (a) => jobOverlapsDate(a, day),
         );
         );
         return (
         return (
           <BoardDayCell
           <BoardDayCell
@@ -45,6 +78,7 @@ export function TechnicianRow({
             technicianId={technician.id}
             technicianId={technician.id}
             date={day}
             date={day}
             assignments={cellAssignments}
             assignments={cellAssignments}
+            dailyCapacity={technician.dailyCapacity}
             onCardClick={onCardClick}
             onCardClick={onCardClick}
           />
           />
         );
         );

+ 125 - 290
src/features/workboard/Components/WorkBoardClient.tsx

@@ -1,19 +1,22 @@
 "use client";
 "use client";
 
 
 import { useState, useEffect, useCallback } from "react";
 import { useState, useEffect, useCallback } from "react";
+import { useSearchParams, useRouter, usePathname } from "next/navigation";
 import { DndContext, DragOverlay, type DragEndEvent, type DragStartEvent, PointerSensor, TouchSensor, KeyboardSensor, useSensor, useSensors } from "@dnd-kit/core";
 import { DndContext, DragOverlay, type DragEndEvent, type DragStartEvent, PointerSensor, TouchSensor, KeyboardSensor, useSensor, useSensors } from "@dnd-kit/core";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { useWorkBoardStore, type Technician } from "../store/workboardStore";
 import { useWorkBoardStore, type Technician } from "../store/workboardStore";
 import { useWorkBoardWebSocket } from "../hooks/useWorkBoardWebSocket";
 import { useWorkBoardWebSocket } from "../hooks/useWorkBoardWebSocket";
-import type { BoardAssignmentWithJob } from "../Actions/boardActions";
+import type { WorkBoardJob } from "../Actions/boardActions";
 import {
 import {
-  getBoardAssignments,
+  getBoardJobs,
   getUnassignedJobs,
   getUnassignedJobs,
-  createBoardAssignment,
-  moveAssignment,
-  removeAssignment,
+  assignTechnician,
+  moveJob,
+  unassignJob,
 } from "../Actions/boardActions";
 } from "../Actions/boardActions";
-import { WorkBoardToolbar } from "./WorkBoardToolbar";
+import { WorkBoardToolbar, type BoardView } from "./WorkBoardToolbar";
+import { DayTimeline } from "./DayTimeline";
+import type { WorkBoardSettings } from "../Actions/boardActions";
 import { TechnicianRow } from "./TechnicianRow";
 import { TechnicianRow } from "./TechnicianRow";
 import { UnassignedJobsPanel } from "./UnassignedJobsPanel";
 import { UnassignedJobsPanel } from "./UnassignedJobsPanel";
 import { TechnicianDialog } from "./TechnicianDialog";
 import { TechnicianDialog } from "./TechnicianDialog";
@@ -22,19 +25,18 @@ import { BoardJobCard } from "./BoardJobCard";
 import { ScrollArea } from "@/components/ui/scroll-area";
 import { ScrollArea } from "@/components/ui/scroll-area";
 import { Users, Wrench, ClipboardCheck } from "lucide-react";
 import { Users, Wrench, ClipboardCheck } from "lucide-react";
 import { useTranslations, useLocale } from "next-intl";
 import { useTranslations, useLocale } from "next-intl";
+import { VehiclePickerDialog } from "@/components/vehicle-picker-dialog";
+import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
+import { getCustomersList } from "@/features/customers/Actions/customerActions";
 
 
 function toLocalDateString(d: Date): string {
 function toLocalDateString(d: Date): string {
-  const y = d.getFullYear();
-  const m = String(d.getMonth() + 1).padStart(2, "0");
-  const day = String(d.getDate()).padStart(2, "0");
-  return `${y}-${m}-${day}`;
+  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
 }
 }
 
 
-function getMonday(date: Date): string {
+function getWeekStartDate(date: Date, weekStartDay: number): string {
   const d = new Date(date);
   const d = new Date(date);
-  const day = d.getDay();
-  const diff = d.getDate() - day + (day === 0 ? -6 : 1);
-  d.setDate(diff);
+  const diff = (d.getDay() - weekStartDay + 7) % 7;
+  d.setDate(d.getDate() - diff);
   return toLocalDateString(d);
   return toLocalDateString(d);
 }
 }
 
 
@@ -48,38 +50,20 @@ function getWeekDays(weekStart: string): string[] {
   return days;
   return days;
 }
 }
 
 
-const DAY_KEYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
+const ALL_DAY_KEYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const;
 
 
-function UnassignedJobOverlayCard({
-  job,
-  type,
-}: {
-  job: Record<string, unknown>;
-  type: "serviceRecord" | "inspection";
-}) {
+function UnassignedJobOverlayCard({ job, type }: { job: Record<string, unknown>; type: "serviceRecord" | "inspection" }) {
   const isServiceRecord = type === "serviceRecord";
   const isServiceRecord = type === "serviceRecord";
   const vehicle = job.vehicle as { year: number; make: string; model: string; licensePlate: string | null } | undefined;
   const vehicle = job.vehicle as { year: number; make: string; model: string; licensePlate: string | null } | undefined;
-  const title = isServiceRecord
-    ? (job.title as string)
-    : ((job.template as { name: string })?.name ?? "");
-
+  const title = isServiceRecord ? (job.title as string) : ((job.template as { name: string })?.name ?? "");
   return (
   return (
     <div className="flex items-start gap-1 rounded-md border bg-card p-1.5 text-xs shadow-md">
     <div className="flex items-start gap-1 rounded-md border bg-card p-1.5 text-xs shadow-md">
       <div className="min-w-0 flex-1">
       <div className="min-w-0 flex-1">
         <div className="flex items-center gap-1">
         <div className="flex items-center gap-1">
-          {isServiceRecord ? (
-            <Wrench className="h-3 w-3 shrink-0 text-blue-500" />
-          ) : (
-            <ClipboardCheck className="h-3 w-3 shrink-0 text-green-500" />
-          )}
+          {isServiceRecord ? <Wrench className="h-3 w-3 shrink-0 text-blue-500" /> : <ClipboardCheck className="h-3 w-3 shrink-0 text-green-500" />}
           <span className="truncate font-medium">{title}</span>
           <span className="truncate font-medium">{title}</span>
         </div>
         </div>
-        {vehicle && (
-          <p className="truncate text-muted-foreground">
-            {vehicle.year} {vehicle.make} {vehicle.model}
-            {vehicle.licensePlate ? ` · ${vehicle.licensePlate}` : ""}
-          </p>
-        )}
+        {vehicle && <p className="truncate text-muted-foreground">{vehicle.year} {vehicle.make} {vehicle.model}{vehicle.licensePlate ? ` · ${vehicle.licensePlate}` : ""}</p>}
       </div>
       </div>
     </div>
     </div>
   );
   );
@@ -90,329 +74,180 @@ export function WorkBoardClient({
   initialAssignments,
   initialAssignments,
   initialUnassigned,
   initialUnassigned,
   initialWeekStart,
   initialWeekStart,
+  boardSettings,
 }: {
 }: {
   initialTechnicians: Technician[];
   initialTechnicians: Technician[];
-  initialAssignments: BoardAssignmentWithJob[];
+  initialAssignments: WorkBoardJob[];
   initialUnassigned: { serviceRecords: unknown[]; inspections: unknown[] };
   initialUnassigned: { serviceRecords: unknown[]; inspections: unknown[] };
   initialWeekStart: string;
   initialWeekStart: string;
+  boardSettings: WorkBoardSettings;
 }) {
 }) {
   const store = useWorkBoardStore();
   const store = useWorkBoardStore();
   const t = useTranslations("workBoard.board");
   const t = useTranslations("workBoard.board");
   const locale = useLocale();
   const locale = useLocale();
+  const searchParams = useSearchParams();
+  const router = useRouter();
+  const pathname = usePathname();
+  const urlView = searchParams.get("view") as BoardView | null;
+  const urlDate = searchParams.get("date");
+  const urlWeek = searchParams.get("week");
 
 
-  // Initialize store from server data
   useEffect(() => {
   useEffect(() => {
     store.setTechnicians(initialTechnicians);
     store.setTechnicians(initialTechnicians);
-    store.setAssignments(initialAssignments);
-    store.setUnassigned(
-      initialUnassigned.serviceRecords as Parameters<typeof store.setUnassigned>[0],
-      initialUnassigned.inspections as Parameters<typeof store.setUnassigned>[1],
-    );
-    store.setWeekStart(initialWeekStart);
+    store.setJobs(initialAssignments);
+    store.setUnassigned(initialUnassigned.serviceRecords as Parameters<typeof store.setUnassigned>[0], initialUnassigned.inspections as Parameters<typeof store.setUnassigned>[1]);
+    store.setWeekStart(urlWeek || initialWeekStart);
+    if (urlWeek && urlWeek !== initialWeekStart) {
+      getBoardJobs(urlWeek).then((res) => { if (res.success && res.data) store.setJobs(res.data as WorkBoardJob[]); });
+    }
     // eslint-disable-next-line react-hooks/exhaustive-deps
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
   }, []);
 
 
   useWorkBoardWebSocket();
   useWorkBoardWebSocket();
 
 
+  const [view, setViewState] = useState<BoardView>(urlView === "day" || urlView === "week" ? urlView : "week");
+  const [selectedDate, setSelectedDateState] = useState(urlDate || toLocalDateString(new Date()));
   const [techDialogOpen, setTechDialogOpen] = useState(false);
   const [techDialogOpen, setTechDialogOpen] = useState(false);
   const [editingTech, setEditingTech] = useState<Technician | null>(null);
   const [editingTech, setEditingTech] = useState<Technician | null>(null);
-  const [selectedAssignment, setSelectedAssignment] = useState<BoardAssignmentWithJob | null>(null);
+  const [selectedJob, setSelectedJob] = useState<WorkBoardJob | null>(null);
   const [popoverOpen, setPopoverOpen] = useState(false);
   const [popoverOpen, setPopoverOpen] = useState(false);
-  const [activeDrag, setActiveDrag] = useState<{
-    id: string;
-    assignment?: BoardAssignmentWithJob;
-    unassignedJob?: { job: Record<string, unknown>; type: "serviceRecord" | "inspection" };
-  } | null>(null);
+  const [activeDrag, setActiveDrag] = useState<{ id: string; job?: WorkBoardJob; unassignedJob?: { job: Record<string, unknown>; type: "serviceRecord" | "inspection" } } | null>(null);
+  const [vehiclePickerOpen, setVehiclePickerOpen] = useState(false);
+  const [vehiclePickerVehicles, setVehiclePickerVehicles] = useState<{ id: string; make: string; model: string; year: number; licensePlate: string | null; customer: { id: string; name: string; company: string | null } | null }[]>([]);
+  const [vehiclePickerCustomers, setVehiclePickerCustomers] = useState<{ id: string; name: string; company: string | null }[]>([]);
+  const [boardContext, setBoardContext] = useState<Record<string, string>>({});
 
 
   const weekStart = store.weekStart || initialWeekStart;
   const weekStart = store.weekStart || initialWeekStart;
   const days = getWeekDays(weekStart);
   const days = getWeekDays(weekStart);
+  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } }), useSensor(KeyboardSensor));
 
 
-  const sensors = useSensors(
-    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
-    useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } }),
-    useSensor(KeyboardSensor),
-  );
+  const updateUrl = useCallback((newView: BoardView, newDate: string, newWeekStart: string) => {
+    const params = new URLSearchParams(); params.set("view", newView); params.set("date", newDate); params.set("week", newWeekStart);
+    router.replace(`${pathname}?${params.toString()}`, { scroll: false });
+  }, [router, pathname]);
 
 
-  const loadWeekData = useCallback(async (ws: string) => {
-    store.setWeekStart(ws);
-    const [assignRes, unassignedRes] = await Promise.all([
-      getBoardAssignments(ws),
-      getUnassignedJobs(),
-    ]);
-    if (assignRes.success && assignRes.data) {
-      store.setAssignments(assignRes.data as BoardAssignmentWithJob[]);
-    }
-    if (unassignedRes.success && unassignedRes.data) {
-      store.setUnassigned(
-        unassignedRes.data.serviceRecords as Parameters<typeof store.setUnassigned>[0],
-        unassignedRes.data.inspections as Parameters<typeof store.setUnassigned>[1],
-      );
-    }
-  }, [store]);
+  const setView = useCallback((v: BoardView) => { setViewState(v); updateUrl(v, selectedDate, weekStart); }, [selectedDate, weekStart, updateUrl]);
+  const setSelectedDate = useCallback((d: string) => { setSelectedDateState(d); updateUrl(view, d, weekStart); }, [view, weekStart, updateUrl]);
 
 
-  const handlePrevWeek = () => {
-    const d = new Date(weekStart + "T12:00:00");
-    d.setDate(d.getDate() - 7);
-    loadWeekData(toLocalDateString(d));
-  };
-
-  const handleNextWeek = () => {
-    const d = new Date(weekStart + "T12:00:00");
-    d.setDate(d.getDate() + 7);
-    loadWeekData(toLocalDateString(d));
-  };
-
-  const handleToday = () => {
-    loadWeekData(getMonday(new Date()));
+  const loadWeekData = useCallback(async (ws: string) => {
+    store.setWeekStart(ws); updateUrl(view, selectedDate, ws);
+    const [assignRes, unassignedRes] = await Promise.all([getBoardJobs(ws), getUnassignedJobs()]);
+    if (assignRes.success && assignRes.data) store.setJobs(assignRes.data as WorkBoardJob[]);
+    if (unassignedRes.success && unassignedRes.data) store.setUnassigned(unassignedRes.data.serviceRecords as Parameters<typeof store.setUnassigned>[0], unassignedRes.data.inspections as Parameters<typeof store.setUnassigned>[1]);
+  }, [store, view, selectedDate, updateUrl]);
+
+  const handlePrevWeek = () => { const d = new Date(weekStart + "T12:00:00"); d.setDate(d.getDate() - 7); loadWeekData(toLocalDateString(d)); };
+  const handleNextWeek = () => { const d = new Date(weekStart + "T12:00:00"); d.setDate(d.getDate() + 7); loadWeekData(toLocalDateString(d)); };
+  const handleToday = () => { setSelectedDate(toLocalDateString(new Date())); loadWeekData(getWeekStartDate(new Date(), boardSettings.weekStartDay)); };
+  const handlePrevDay = () => { const d = new Date(selectedDate + "T12:00:00"); d.setDate(d.getDate() - 1); const nd = toLocalDateString(d); setSelectedDate(nd); const nw = getWeekStartDate(d, boardSettings.weekStartDay); if (nw !== weekStart) loadWeekData(nw); };
+  const handleNextDay = () => { const d = new Date(selectedDate + "T12:00:00"); d.setDate(d.getDate() + 1); const nd = toLocalDateString(d); setSelectedDate(nd); const nw = getWeekStartDate(d, boardSettings.weekStartDay); if (nw !== weekStart) loadWeekData(nw); };
+
+  const handleCreateWorkOrder = async (techId: string, startTime: string, endTime: string) => {
+    setBoardContext({ boardTech: techId, boardDate: selectedDate, boardStart: startTime, boardEnd: endTime });
+    const [vehiclesResult, customersResult] = await Promise.all([getVehicles(), getCustomersList()]);
+    setVehiclePickerVehicles(vehiclesResult.success && vehiclesResult.data ? vehiclesResult.data.map((v) => ({ id: v.id, make: v.make, model: v.model, year: v.year, licensePlate: v.licensePlate, customer: v.customer })) : []);
+    setVehiclePickerCustomers(customersResult.success && customersResult.data ? customersResult.data : []);
+    setVehiclePickerOpen(true);
   };
   };
 
 
   const handleDragStart = (event: DragStartEvent) => {
   const handleDragStart = (event: DragStartEvent) => {
-    const { active } = event;
-    const data = active.data.current;
-    if (data?.assignment) {
-      setActiveDrag({ id: active.id as string, assignment: data.assignment });
-    } else if (data?.job && data?.type) {
-      setActiveDrag({
-        id: active.id as string,
-        unassignedJob: { job: data.job, type: data.type as "serviceRecord" | "inspection" },
-      });
-    } else {
-      setActiveDrag({ id: active.id as string });
-    }
+    const data = event.active.data.current;
+    if (data?.job && data.job.type) setActiveDrag({ id: event.active.id as string, job: data.job });
+    else if (data?.job && data?.type) setActiveDrag({ id: event.active.id as string, unassignedJob: { job: data.job, type: data.type as "serviceRecord" | "inspection" } });
+    else setActiveDrag({ id: event.active.id as string });
   };
   };
 
 
   const handleDragEnd = async (event: DragEndEvent) => {
   const handleDragEnd = async (event: DragEndEvent) => {
     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 || !overData?.date) return;
-
+    const overData = over.data.current as { technicianId: string; date: string } | undefined;
+    if (!overData?.technicianId) return;
     const activeData = active.data.current;
     const activeData = active.data.current;
 
 
-    // Existing assignment being moved
-    if (activeData?.assignment) {
-      const assignment = activeData.assignment as BoardAssignmentWithJob;
-      // Same position — skip
-      if (
-        assignment.technicianId === overData.technicianId &&
-        assignment.date === overData.date
-      ) {
-        return;
-      }
-
-      // Optimistic update
-      store.optimisticMove(assignment.id, overData.technicianId, overData.date);
-
-      const res = await moveAssignment({
-        id: assignment.id,
-        technicianId: overData.technicianId,
-        date: overData.date,
-        sortOrder: 0,
-      });
-
-      if (!res.success) {
-        toast.error(t("failedMove"), { description: res.error });
-        // Revert
-        store.optimisticMove(
-          assignment.id,
-          assignment.technicianId,
-          assignment.date,
-        );
-      } else if (res.data) {
-        store.updateAssignment(res.data as BoardAssignmentWithJob);
-      }
+    if (activeData?.job && activeData.job.type) {
+      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);
       return;
       return;
     }
     }
 
 
-    // Unassigned job being dropped on a cell
     if (activeData?.job && activeData?.type) {
     if (activeData?.job && activeData?.type) {
       const job = activeData.job;
       const job = activeData.job;
       const jobType = activeData.type as "serviceRecord" | "inspection";
       const jobType = activeData.type as "serviceRecord" | "inspection";
-
-      const input = {
-        date: overData.date,
-        technicianId: overData.technicianId,
-        ...(jobType === "serviceRecord"
-          ? { serviceRecordId: job.id }
-          : { inspectionId: job.id }),
-      };
-
-      // Optimistic: remove from unassigned
       store.removeFromUnassigned(job.id, jobType);
       store.removeFromUnassigned(job.id, jobType);
-
-      const res = await createBoardAssignment(input);
-      if (!res.success) {
-        toast.error(t("failedAssign"), { description: res.error });
-        // Revert
-        store.addToUnassigned(job, jobType);
-      } else if (res.data) {
-        store.addAssignment(res.data as BoardAssignmentWithJob);
-      }
+      const res = await assignTechnician({ id: job.id, technicianId: overData.technicianId, type: jobType });
+      if (!res.success) { toast.error(t("failedAssign"), { description: res.error }); store.addToUnassigned(job, jobType); }
+      else if (res.data) store.addJob(res.data as WorkBoardJob);
     }
     }
   };
   };
 
 
-  const handleRemoveAssignment = async (assignment: BoardAssignmentWithJob) => {
-    setPopoverOpen(false);
-    setSelectedAssignment(null);
-
-    // Optimistic removal
-    store.removeAssignment(assignment.id);
-
-    const res = await removeAssignment({ id: assignment.id });
-    if (!res.success) {
-      toast.error(t("failedRemove"), { description: res.error });
-      // Re-add on failure
-      store.addAssignment(assignment);
-    } else {
-      // Refresh unassigned list
-      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],
-        );
-      }
-    }
+  const handleRemoveJob = async (job: WorkBoardJob) => {
+    setPopoverOpen(false); setSelectedJob(null);
+    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]); }
   };
   };
 
 
-  const handleCardClick = (assignment: BoardAssignmentWithJob) => {
-    setSelectedAssignment(assignment);
-    setPopoverOpen(true);
-  };
+  const handleCardClick = (a: WorkBoardJob) => { setSelectedJob(a); setPopoverOpen(true); };
 
 
   if (store.technicians.length === 0) {
   if (store.technicians.length === 0) {
     return (
     return (
       <div className="flex flex-1 flex-col">
       <div className="flex flex-1 flex-col">
-        <WorkBoardToolbar
-          weekStart={weekStart}
-          onPrevWeek={handlePrevWeek}
-          onNextWeek={handleNextWeek}
-          onToday={handleToday}
-          onAddTech={() => setTechDialogOpen(true)}
-        />
+        <WorkBoardToolbar weekStart={weekStart} selectedDate={selectedDate} view={view} onPrevWeek={handlePrevWeek} onNextWeek={handleNextWeek} onPrevDay={handlePrevDay} onNextDay={handleNextDay} onToday={handleToday} onAddTech={() => setTechDialogOpen(true)} onViewChange={setView} />
         <div className="flex flex-1 flex-col items-center justify-center gap-4 py-20">
         <div className="flex flex-1 flex-col items-center justify-center gap-4 py-20">
           <Users className="h-12 w-12 text-muted-foreground/40" />
           <Users className="h-12 w-12 text-muted-foreground/40" />
-          <div className="text-center">
-            <h3 className="text-lg font-medium">{t("noTechnicians")}</h3>
-            <p className="text-sm text-muted-foreground">
-              {t("noTechniciansDescription")}
-            </p>
-          </div>
+          <div className="text-center"><h3 className="text-lg font-medium">{t("noTechnicians")}</h3><p className="text-sm text-muted-foreground">{t("noTechniciansDescription")}</p></div>
         </div>
         </div>
-        <TechnicianDialog
-          open={techDialogOpen}
-          onOpenChange={setTechDialogOpen}
-        />
+        <TechnicianDialog open={techDialogOpen} onOpenChange={setTechDialogOpen} />
       </div>
       </div>
     );
     );
   }
   }
 
 
+  const dragOverlay = activeDrag?.job ? (
+    <div className="w-48 opacity-90"><BoardJobCard job={activeDrag.job} /></div>
+  ) : activeDrag?.unassignedJob ? (
+    <div className="w-48 opacity-90"><UnassignedJobOverlayCard job={activeDrag.unassignedJob.job} type={activeDrag.unassignedJob.type} /></div>
+  ) : null;
+
   return (
   return (
     <div className="flex flex-1 flex-col gap-4">
     <div className="flex flex-1 flex-col gap-4">
-      <WorkBoardToolbar
-        weekStart={weekStart}
-        onPrevWeek={handlePrevWeek}
-        onNextWeek={handleNextWeek}
-        onToday={handleToday}
-        onAddTech={() => {
-          setEditingTech(null);
-          setTechDialogOpen(true);
-        }}
-      />
+      <WorkBoardToolbar weekStart={weekStart} selectedDate={selectedDate} view={view} onPrevWeek={handlePrevWeek} onNextWeek={handleNextWeek} onPrevDay={handlePrevDay} onNextDay={handleNextDay} onToday={handleToday} onAddTech={() => { setEditingTech(null); setTechDialogOpen(true); }} onViewChange={setView} />
 
 
-      <DndContext
-        sensors={sensors}
-        onDragStart={handleDragStart}
-        onDragEnd={handleDragEnd}
-      >
+      <DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
         <div className="flex flex-1 gap-4 overflow-hidden">
         <div className="flex flex-1 gap-4 overflow-hidden">
-          {/* Main grid */}
           <ScrollArea className="flex-1">
           <ScrollArea className="flex-1">
-            <div
-              className="min-w-[900px] grid gap-1"
-              style={{
-                gridTemplateColumns: `140px ${days.map((d) => (d === toLocalDateString(new Date()) ? "minmax(0,2fr)" : "minmax(0,1fr)")).join(" ")}`,
-              }}
-            >
-              {/* Day headers */}
-              <div />
-              {days.map((day, i) => {
-                const isToday = day === toLocalDateString(new Date());
-                const d = new Date(day + "T12:00:00");
-                const dayLabel = `${t(`days.${DAY_KEYS[i]}`)} ${d.getDate()}`;
-                return (
-                  <div
-                    key={day}
-                    className={`rounded-md px-2 py-1 text-center text-xs font-medium ${
-                      isToday
-                        ? "bg-primary text-primary-foreground"
-                        : "text-muted-foreground"
-                    }`}
-                  >
-                    {dayLabel}
-                  </div>
-                );
-              })}
-
-              {/* Technician rows */}
-              {store.technicians.map((tech) => (
-                <TechnicianRow
-                  key={tech.id}
-                  technician={tech}
-                  days={days}
-                  assignments={store.assignments}
-                  onCardClick={handleCardClick}
-                  onTechClick={(t) => {
-                    setEditingTech(t);
-                    setTechDialogOpen(true);
-                  }}
-                />
-              ))}
-            </div>
+            {view === "day" ? (
+              <DayTimeline date={selectedDate} technicians={store.technicians} assignments={store.jobs} workDayStart={boardSettings.workDayStart} workDayEnd={boardSettings.workDayEnd} onCardClick={handleCardClick} onTechClick={(t) => { setEditingTech(t); setTechDialogOpen(true); }} onCreateWorkOrder={handleCreateWorkOrder} />
+            ) : (
+              <div className="min-w-[900px] grid gap-1" style={{ gridTemplateColumns: `140px ${days.map((d) => (d === toLocalDateString(new Date()) ? "minmax(0,2fr)" : "minmax(0,1fr)")).join(" ")}` }}>
+                <div />
+                {days.map((day) => {
+                  const isToday = day === toLocalDateString(new Date());
+                  const d = new Date(day + "T12:00:00");
+                  const dayKey = ALL_DAY_KEYS[d.getDay()];
+                  return <div key={day} className={`rounded-md px-2 py-1 text-center text-xs font-medium ${isToday ? "bg-primary text-primary-foreground" : "text-muted-foreground"}`}>{t(`days.${dayKey}`)} {d.getDate()}</div>;
+                })}
+                {store.technicians.map((tech) => (
+                  <TechnicianRow key={tech.id} technician={tech} days={days} assignments={store.jobs} onCardClick={handleCardClick} onTechClick={(t) => { setEditingTech(t); setTechDialogOpen(true); }} />
+                ))}
+              </div>
+            )}
           </ScrollArea>
           </ScrollArea>
-
-          {/* Unassigned panel */}
           <UnassignedJobsPanel />
           <UnassignedJobsPanel />
         </div>
         </div>
-
-        <DragOverlay dropAnimation={null}>
-          {activeDrag?.assignment ? (
-            <div className="w-48 opacity-90">
-              <BoardJobCard assignment={activeDrag.assignment} />
-            </div>
-          ) : activeDrag?.unassignedJob ? (
-            <div className="w-48 opacity-90">
-              <UnassignedJobOverlayCard
-                job={activeDrag.unassignedJob.job}
-                type={activeDrag.unassignedJob.type}
-              />
-            </div>
-          ) : null}
-        </DragOverlay>
+        <DragOverlay dropAnimation={null}>{dragOverlay}</DragOverlay>
       </DndContext>
       </DndContext>
 
 
-      {/* Detail dialog for selected card */}
-      {selectedAssignment && (
-        <JobDetailPopover
-          assignment={selectedAssignment}
-          open={popoverOpen}
-          onOpenChange={(o) => {
-            setPopoverOpen(o);
-            if (!o) setSelectedAssignment(null);
-          }}
-          onRemove={() => handleRemoveAssignment(selectedAssignment)}
-        />
+      {selectedJob && (
+        <JobDetailPopover job={selectedJob} open={popoverOpen} onOpenChange={(o) => { setPopoverOpen(o); if (!o) setSelectedJob(null); }} onRemove={() => handleRemoveJob(selectedJob)} />
       )}
       )}
-
-      <TechnicianDialog
-        open={techDialogOpen}
-        onOpenChange={setTechDialogOpen}
-        technician={editingTech}
-      />
+      <TechnicianDialog open={techDialogOpen} onOpenChange={setTechDialogOpen} technician={editingTech} />
+      <VehiclePickerDialog open={vehiclePickerOpen} onOpenChange={(open) => { setVehiclePickerOpen(open); if (!open) setBoardContext({}); }} vehicles={vehiclePickerVehicles} customers={vehiclePickerCustomers} redirectQuery={boardContext} />
     </div>
     </div>
   );
   );
 }
 }

+ 69 - 292
src/features/workboard/Components/WorkBoardPresenter.tsx

@@ -1,399 +1,176 @@
 'use client'
 'use client'
 
 
 import { useState, useEffect, useCallback } from 'react'
 import { useState, useEffect, useCallback } from 'react'
+import { useSearchParams, useRouter, usePathname } from 'next/navigation'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { ChevronLeft, ChevronRight, Wrench, ClipboardCheck, Wifi, WifiOff } from 'lucide-react'
 import { ChevronLeft, ChevronRight, Wrench, ClipboardCheck, Wifi, WifiOff } from 'lucide-react'
 import { useWorkBoardStore, type Technician } from '../store/workboardStore'
 import { useWorkBoardStore, type Technician } from '../store/workboardStore'
 import { useWorkBoardWebSocket } from '../hooks/useWorkBoardWebSocket'
 import { useWorkBoardWebSocket } from '../hooks/useWorkBoardWebSocket'
-import type { BoardAssignmentWithJob } from '../Actions/boardActions'
-import { getBoardAssignments } from '../Actions/boardActions'
+import type { WorkBoardJob } from '../Actions/boardActions'
+import { getBoardJobs } from '../Actions/boardActions'
 import { getTechnicians } from '../Actions/technicianActions'
 import { getTechnicians } from '../Actions/technicianActions'
 import { PresenterDayView } from './PresenterDayView'
 import { PresenterDayView } from './PresenterDayView'
 import { PresenterKanbanView } from './PresenterKanbanView'
 import { PresenterKanbanView } from './PresenterKanbanView'
+import { PresenterTimeline } from './PresenterTimeline'
+import { jobOverlapsDate } from '../utils/datetime'
 import { useTranslations, useLocale } from 'next-intl'
 import { useTranslations, useLocale } from 'next-intl'
 
 
-type ViewMode = 'week' | 'day' | 'status'
+type ViewMode = 'week' | 'day' | 'status' | 'timeline'
 
 
 function toLocalDateString(d: Date): string {
 function toLocalDateString(d: Date): string {
-  const y = d.getFullYear()
-  const m = String(d.getMonth() + 1).padStart(2, '0')
-  const day = String(d.getDate()).padStart(2, '0')
-  return `${y}-${m}-${day}`
+  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
 }
 }
 
 
 function getMonday(date: Date): string {
 function getMonday(date: Date): string {
-  const d = new Date(date)
-  const day = d.getDay()
-  const diff = d.getDate() - day + (day === 0 ? -6 : 1)
-  d.setDate(diff)
+  const d = new Date(date); const day = d.getDay(); d.setDate(d.getDate() - day + (day === 0 ? -6 : 1));
   return toLocalDateString(d)
   return toLocalDateString(d)
 }
 }
 
 
 function getWeekDays(weekStart: string): string[] {
 function getWeekDays(weekStart: string): string[] {
   const days: string[] = []
   const days: string[] = []
-  for (let i = 0; i < 7; i++) {
-    const d = new Date(weekStart + 'T12:00:00')
-    d.setDate(d.getDate() + i)
-    days.push(toLocalDateString(d))
-  }
+  for (let i = 0; i < 7; i++) { const d = new Date(weekStart + 'T12:00:00'); d.setDate(d.getDate() + i); days.push(toLocalDateString(d)); }
   return days
   return days
 }
 }
 
 
 const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const
 const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const
 
 
 function formatWeekRange(weekStart: string, locale?: string): string {
 function formatWeekRange(weekStart: string, locale?: string): string {
-  const start = new Date(weekStart + 'T12:00:00')
-  const end = new Date(start)
-  end.setDate(end.getDate() + 6)
+  const start = new Date(weekStart + 'T12:00:00'); const end = new Date(start); end.setDate(end.getDate() + 6);
   const opts: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' }
   const opts: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' }
-  return `${start.toLocaleDateString(locale, opts)} – ${end.toLocaleDateString(locale, opts)}`
+  return `${start.toLocaleDateString(locale, opts)} – ${end.toLocaleDateString(locale, { ...opts, year: 'numeric' })}`
 }
 }
 
 
 function formatDayDate(dateStr: string, locale?: string): string {
 function formatDayDate(dateStr: string, locale?: string): string {
-  const d = new Date(dateStr + 'T12:00:00')
-  return d.toLocaleDateString(locale, {
-    weekday: 'long',
-    month: 'long',
-    day: 'numeric',
-  })
+  return new Date(dateStr + 'T12:00:00').toLocaleDateString(locale, { weekday: 'long', month: 'long', day: 'numeric' })
 }
 }
 
 
 function LiveClock() {
 function LiveClock() {
   const [time, setTime] = useState<string | null>(null)
   const [time, setTime] = useState<string | null>(null)
-  useEffect(() => {
-    function update() {
-      setTime(
-        new Date().toLocaleTimeString(undefined, {
-          hour: '2-digit',
-          minute: '2-digit',
-          second: '2-digit',
-        })
-      )
-    }
-    update()
-    const timer = setInterval(update, 1000)
-    return () => clearInterval(timer)
-  }, [])
+  useEffect(() => { function update() { setTime(new Date().toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })) } update(); const timer = setInterval(update, 1000); return () => clearInterval(timer) }, [])
   if (!time) return <span className="tabular-nums">&nbsp;</span>
   if (!time) return <span className="tabular-nums">&nbsp;</span>
   return <span className="tabular-nums">{time}</span>
   return <span className="tabular-nums">{time}</span>
 }
 }
 
 
-function PresenterJobCard({ assignment }: { assignment: BoardAssignmentWithJob }) {
-  const isServiceRecord = !!assignment.serviceRecordId
-  const vehicle = isServiceRecord
-    ? assignment.serviceRecord?.vehicle
-    : assignment.inspection?.vehicle
-  const title = isServiceRecord
-    ? assignment.serviceRecord?.title
-    : assignment.inspection?.template?.name
-  const status = isServiceRecord ? assignment.serviceRecord?.status : assignment.inspection?.status
-
+function PresenterJobCard({ job }: { job: WorkBoardJob }) {
+  const isServiceRecord = job.type === 'serviceRecord'
   return (
   return (
     <div className="rounded-md border bg-card p-2 text-sm shadow-sm">
     <div className="rounded-md border bg-card p-2 text-sm shadow-sm">
       <div className="flex items-center gap-1.5">
       <div className="flex items-center gap-1.5">
-        {isServiceRecord ? (
-          <Wrench className="h-4 w-4 shrink-0 text-blue-500" />
-        ) : (
-          <ClipboardCheck className="h-4 w-4 shrink-0 text-green-500" />
-        )}
-        <span className="truncate font-semibold">{title}</span>
+        {isServiceRecord ? <Wrench className="h-4 w-4 shrink-0 text-blue-500" /> : <ClipboardCheck className="h-4 w-4 shrink-0 text-green-500" />}
+        <span className="truncate font-semibold">{job.title}</span>
       </div>
       </div>
-      {vehicle && (
-        <p className="mt-0.5 truncate text-muted-foreground">
-          {vehicle.year} {vehicle.make} {vehicle.model}
-          {vehicle.licensePlate ? ` · ${vehicle.licensePlate}` : ''}
-        </p>
-      )}
-      {status && (
-        <span className="mt-1 inline-block rounded bg-muted px-1.5 py-0.5 text-xs capitalize">
-          {status.replace(/_/g, ' ')}
-        </span>
-      )}
+      {job.vehicle && <p className="mt-0.5 truncate text-muted-foreground">{job.vehicle.year} {job.vehicle.make} {job.vehicle.model}{job.vehicle.licensePlate ? ` · ${job.vehicle.licensePlate}` : ''}</p>}
+      {job.status && <span className="mt-1 inline-block rounded bg-muted px-1.5 py-0.5 text-xs capitalize">{job.status.replace(/_/g, ' ')}</span>}
     </div>
     </div>
   )
   )
 }
 }
 
 
-export function WorkBoardPresenter({
-  initialTechnicians,
-  initialAssignments,
-  initialWeekStart,
-}: {
-  initialTechnicians: Technician[]
-  initialAssignments: BoardAssignmentWithJob[]
-  initialWeekStart: string
+export function WorkBoardPresenter({ initialTechnicians, initialAssignments, initialWeekStart, workDayStart = '07:00', workDayEnd = '15:00' }: {
+  initialTechnicians: Technician[]; initialAssignments: WorkBoardJob[]; initialWeekStart: string; workDayStart?: string; workDayEnd?: string
 }) {
 }) {
   const store = useWorkBoardStore()
   const store = useWorkBoardStore()
   const t = useTranslations('workBoard.presenter')
   const t = useTranslations('workBoard.presenter')
   const tb = useTranslations('workBoard.board')
   const tb = useTranslations('workBoard.board')
   const locale = useLocale()
   const locale = useLocale()
-  const [viewMode, setViewMode] = useState<ViewMode>(() => {
-    if (typeof window !== 'undefined') {
-      const saved = localStorage.getItem('presenter-view-mode')
-      if (saved === 'day' || saved === 'week' || saved === 'status') return saved
-    }
-    return 'week'
-  })
-  const [selectedDate, setSelectedDate] = useState(() => toLocalDateString(new Date()))
-
-  const handleSetViewMode = (mode: ViewMode) => {
-    setViewMode(mode)
-    localStorage.setItem('presenter-view-mode', mode)
-  }
-
-  // Initialize store from server data
-  useEffect(() => {
-    store.setTechnicians(initialTechnicians)
-    store.setAssignments(initialAssignments)
-    store.setWeekStart(initialWeekStart)
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [])
-
+  const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname()
+  const VALID_VIEWS: ViewMode[] = ['week', 'day', 'status', 'timeline']
+  const urlView = searchParams.get('view') as ViewMode | null
+  const urlDate = searchParams.get('date')
+  const [viewMode, setViewMode] = useState<ViewMode>(urlView && VALID_VIEWS.includes(urlView) ? urlView : 'week')
+  const [selectedDate, setSelectedDateState] = useState(urlDate || toLocalDateString(new Date()))
+  const updateUrl = useCallback((v: ViewMode, d: string) => { const p = new URLSearchParams(); p.set('view', v); p.set('date', d); router.replace(`${pathname}?${p.toString()}`, { scroll: false }) }, [router, pathname])
+  const setSelectedDate = useCallback((d: string) => { setSelectedDateState(d); updateUrl(viewMode, d) }, [viewMode, updateUrl])
+  const handleSetViewMode = (m: ViewMode) => { setViewMode(m); updateUrl(m, selectedDate) }
+
+  useEffect(() => { store.setTechnicians(initialTechnicians); store.setJobs(initialAssignments); store.setWeekStart(initialWeekStart); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [])
   useWorkBoardWebSocket()
   useWorkBoardWebSocket()
 
 
   const weekStart = store.weekStart || initialWeekStart
   const weekStart = store.weekStart || initialWeekStart
   const days = getWeekDays(weekStart)
   const days = getWeekDays(weekStart)
   const today = toLocalDateString(new Date())
   const today = toLocalDateString(new Date())
 
 
-  const loadWeekData = useCallback(
-    async (ws: string) => {
-      store.setWeekStart(ws)
-      const [assignRes, techRes] = await Promise.all([getBoardAssignments(ws), getTechnicians()])
-      if (assignRes.success && assignRes.data) {
-        store.setAssignments(assignRes.data as BoardAssignmentWithJob[])
-      }
-      if (techRes.success && techRes.data) {
-        store.setTechnicians(techRes.data as Technician[])
-      }
-    },
-    [store]
-  )
-
-  // Ensure the selected day's week data is loaded
-  const ensureWeekLoaded = useCallback(
-    (dateStr: string) => {
-      const monday = getMonday(new Date(dateStr + 'T12:00:00'))
-      if (monday !== weekStart) {
-        loadWeekData(monday)
-      }
-    },
-    [weekStart, loadWeekData]
-  )
-
-  const handlePrev = () => {
-    if (viewMode === 'week') {
-      const d = new Date(weekStart + 'T12:00:00')
-      d.setDate(d.getDate() - 7)
-      loadWeekData(toLocalDateString(d))
-    } else {
-      const d = new Date(selectedDate + 'T12:00:00')
-      d.setDate(d.getDate() - 1)
-      const newDate = toLocalDateString(d)
-      setSelectedDate(newDate)
-      ensureWeekLoaded(newDate)
-    }
-  }
+  const loadWeekData = useCallback(async (ws: string) => {
+    store.setWeekStart(ws)
+    const [assignRes, techRes] = await Promise.all([getBoardJobs(ws), getTechnicians()])
+    if (assignRes.success && assignRes.data) store.setJobs(assignRes.data as WorkBoardJob[])
+    if (techRes.success && techRes.data) store.setTechnicians(techRes.data as Technician[])
+  }, [store])
 
 
-  const handleNext = () => {
-    if (viewMode === 'week') {
-      const d = new Date(weekStart + 'T12:00:00')
-      d.setDate(d.getDate() + 7)
-      loadWeekData(toLocalDateString(d))
-    } else {
-      const d = new Date(selectedDate + 'T12:00:00')
-      d.setDate(d.getDate() + 1)
-      const newDate = toLocalDateString(d)
-      setSelectedDate(newDate)
-      ensureWeekLoaded(newDate)
-    }
-  }
+  const ensureWeekLoaded = useCallback((dateStr: string) => { const m = getMonday(new Date(dateStr + 'T12:00:00')); if (m !== weekStart) loadWeekData(m) }, [weekStart, loadWeekData])
+  const handlePrev = () => { if (viewMode === 'week') { const d = new Date(weekStart + 'T12:00:00'); d.setDate(d.getDate() - 7); loadWeekData(toLocalDateString(d)) } else { const d = new Date(selectedDate + 'T12:00:00'); d.setDate(d.getDate() - 1); const nd = toLocalDateString(d); setSelectedDate(nd); ensureWeekLoaded(nd) } }
+  const handleNext = () => { if (viewMode === 'week') { const d = new Date(weekStart + 'T12:00:00'); d.setDate(d.getDate() + 7); loadWeekData(toLocalDateString(d)) } else { const d = new Date(selectedDate + 'T12:00:00'); d.setDate(d.getDate() + 1); const nd = toLocalDateString(d); setSelectedDate(nd); ensureWeekLoaded(nd) } }
+  const handleToday = () => { setSelectedDate(toLocalDateString(new Date())); loadWeekData(getMonday(new Date())) }
 
 
-  const handleToday = () => {
-    const todayStr = toLocalDateString(new Date())
-    setSelectedDate(todayStr)
-    loadWeekData(getMonday(new Date()))
-  }
-
-  // Auto-advance at midnight
   useEffect(() => {
   useEffect(() => {
-    function msUntilMidnight() {
-      const now = new Date()
-      const midnight = new Date(now)
-      midnight.setHours(24, 0, 0, 0)
-      return midnight.getTime() - now.getTime()
-    }
-
+    function msUntilMidnight() { const now = new Date(); const m = new Date(now); m.setHours(24, 0, 0, 0); return m.getTime() - now.getTime() }
     let timeout: ReturnType<typeof setTimeout>
     let timeout: ReturnType<typeof setTimeout>
+    function schedule() { timeout = setTimeout(() => { const now = new Date(); setSelectedDate(toLocalDateString(now)); loadWeekData(getMonday(now)); schedule() }, msUntilMidnight() + 500) }
+    schedule(); return () => clearTimeout(timeout)
+  }, [loadWeekData, setSelectedDate])
 
 
-    function scheduleAdvance() {
-      timeout = setTimeout(() => {
-        const now = new Date()
-        const newMonday = getMonday(now)
-        setSelectedDate(toLocalDateString(now))
-        loadWeekData(newMonday)
-        scheduleAdvance()
-      }, msUntilMidnight() + 500)
-    }
-
-    scheduleAdvance()
-    return () => clearTimeout(timeout)
-  }, [loadWeekData])
-
-  const dateLabel =
-    viewMode === 'week' ? formatWeekRange(weekStart, locale) : formatDayDate(selectedDate, locale)
+  const dateLabel = viewMode === 'week' ? formatWeekRange(weekStart, locale) : formatDayDate(selectedDate, locale)
 
 
   return (
   return (
     <div className="flex h-screen flex-col bg-background">
     <div className="flex h-screen flex-col bg-background">
-      {/* Header bar */}
       <header className="flex shrink-0 items-center justify-between border-b px-4 py-2">
       <header className="flex shrink-0 items-center justify-between border-b px-4 py-2">
         <div className="flex items-center gap-3">
         <div className="flex items-center gap-3">
           <h1 className="text-lg font-bold">{t('title')}</h1>
           <h1 className="text-lg font-bold">{t('title')}</h1>
           <span className="text-sm text-muted-foreground">{dateLabel}</span>
           <span className="text-sm text-muted-foreground">{dateLabel}</span>
         </div>
         </div>
         <div className="flex items-center gap-3">
         <div className="flex items-center gap-3">
-          {/* View mode toggle */}
           <div className="flex rounded-md border">
           <div className="flex rounded-md border">
-            <Button
-              variant={viewMode === 'day' ? 'default' : 'ghost'}
-              size="sm"
-              className="rounded-none rounded-l-md"
-              onClick={() => handleSetViewMode('day')}
-            >
-              {t('day')}
-            </Button>
-            <Button
-              variant={viewMode === 'status' ? 'default' : 'ghost'}
-              size="sm"
-              className="rounded-none border-x"
-              onClick={() => handleSetViewMode('status')}
-            >
-              {t('status')}
-            </Button>
-            <Button
-              variant={viewMode === 'week' ? 'default' : 'ghost'}
-              size="sm"
-              className="rounded-none rounded-r-md"
-              onClick={() => handleSetViewMode('week')}
-            >
-              {t('week')}
-            </Button>
+            <Button variant={viewMode === 'timeline' ? 'default' : 'ghost'} size="sm" className="rounded-none rounded-l-md" onClick={() => handleSetViewMode('timeline')}>{t('timeline')}</Button>
+            <Button variant={viewMode === 'day' ? 'default' : 'ghost'} size="sm" className="rounded-none border-l" onClick={() => handleSetViewMode('day')}>{t('day')}</Button>
+            <Button variant={viewMode === 'status' ? 'default' : 'ghost'} size="sm" className="rounded-none border-x" onClick={() => handleSetViewMode('status')}>{t('status')}</Button>
+            <Button variant={viewMode === 'week' ? 'default' : 'ghost'} size="sm" className="rounded-none rounded-r-md" onClick={() => handleSetViewMode('week')}>{t('week')}</Button>
           </div>
           </div>
-
-          {/* Navigation */}
           <div className="flex items-center gap-1">
           <div className="flex items-center gap-1">
-            <Button variant="ghost" size="icon" onClick={handlePrev}>
-              <ChevronLeft className="h-5 w-5" />
-            </Button>
-            <Button variant="outline" size="sm" onClick={handleToday}>
-              {t('today')}
-            </Button>
-            <Button variant="ghost" size="icon" onClick={handleNext}>
-              <ChevronRight className="h-5 w-5" />
-            </Button>
+            <Button variant="ghost" size="icon" onClick={handlePrev}><ChevronLeft className="h-5 w-5" /></Button>
+            <Button variant="outline" size="sm" onClick={handleToday}>{t('today')}</Button>
+            <Button variant="ghost" size="icon" onClick={handleNext}><ChevronRight className="h-5 w-5" /></Button>
           </div>
           </div>
-
-          {/* Status */}
           <div className="flex items-center gap-2 text-sm text-muted-foreground">
           <div className="flex items-center gap-2 text-sm text-muted-foreground">
-            {store.isConnected ? (
-              <span
-                className="flex items-center gap-1.5 text-green-600"
-                title={t('live')}
-              >
-                <Wifi className="h-4 w-4" />
-                <span className="text-xs">{t('live')}</span>
-              </span>
-            ) : (
-              <span
-                className="flex items-center gap-1.5 animate-pulse text-red-500"
-                title={t('disconnected')}
-              >
-                <WifiOff className="h-4 w-4" />
-                <span className="text-xs">{t('disconnected')}</span>
-              </span>
-            )}
+            {store.isConnected ? <span className="flex items-center gap-1.5 text-green-600" title={t('live')}><Wifi className="h-4 w-4" /><span className="text-xs">{t('live')}</span></span> : <span className="flex items-center gap-1.5 animate-pulse text-red-500" title={t('disconnected')}><WifiOff className="h-4 w-4" /><span className="text-xs">{t('disconnected')}</span></span>}
             <LiveClock />
             <LiveClock />
           </div>
           </div>
         </div>
         </div>
       </header>
       </header>
 
 
-      {/* Content */}
-      {viewMode === 'day' ? (
-        <PresenterDayView
-          date={selectedDate}
-          technicians={store.technicians}
-          assignments={store.assignments}
-        />
+      {viewMode === 'timeline' ? (
+        <PresenterTimeline date={selectedDate} technicians={store.technicians} assignments={store.jobs} workDayStart={workDayStart} workDayEnd={workDayEnd} />
+      ) : viewMode === 'day' ? (
+        <PresenterDayView date={selectedDate} technicians={store.technicians} assignments={store.jobs} />
       ) : viewMode === 'status' ? (
       ) : viewMode === 'status' ? (
-        <PresenterKanbanView
-          date={selectedDate}
-          technicians={store.technicians}
-          assignments={store.assignments}
-        />
+        <PresenterKanbanView date={selectedDate} technicians={store.technicians} assignments={store.jobs} />
       ) : (
       ) : (
         <div className="flex-1 overflow-auto">
         <div className="flex-1 overflow-auto">
-          <div
-            className="grid h-full min-w-225"
-            style={{
-              gridTemplateColumns: '160px repeat(7, 1fr)',
-              gridTemplateRows: `auto ${store.technicians.length > 0 ? `repeat(${store.technicians.length}, 1fr)` : '1fr'}`,
-            }}
-          >
-            {/* Day header row */}
+          <div className="grid h-full min-w-225" style={{ gridTemplateColumns: '160px repeat(7, 1fr)', gridTemplateRows: `auto ${store.technicians.length > 0 ? `repeat(${store.technicians.length}, 1fr)` : '1fr'}` }}>
             <div className="border-b p-2" />
             <div className="border-b p-2" />
             {days.map((day, i) => {
             {days.map((day, i) => {
-              const isToday = day === today
-              const d = new Date(day + 'T12:00:00')
-              const dayLabel = `${tb(`days.${DAY_KEYS[i]}`)} ${d.getDate()}/${d.getMonth() + 1}`
-              return (
-                <div
-                  key={day}
-                  className={`border-b border-l p-2 text-center text-sm font-semibold ${
-                    isToday ? 'bg-primary text-primary-foreground' : 'text-muted-foreground'
-                  }`}
-                >
-                  {dayLabel}
-                </div>
-              )
+              const isToday = day === today; const d = new Date(day + 'T12:00:00');
+              return <div key={day} className={`border-b border-l p-2 text-center text-sm font-semibold ${isToday ? 'bg-primary text-primary-foreground' : 'text-muted-foreground'}`}>{tb(`days.${DAY_KEYS[i]}`)} {d.getDate()}/{d.getMonth() + 1}</div>
             })}
             })}
-
-            {/* Technician rows */}
             {store.technicians.map((tech) => {
             {store.technicians.map((tech) => {
-              const techAssignments = store.assignments.filter((a) => a.technicianId === tech.id)
+              const techJobs = store.jobs.filter((a) => a.technicianId === tech.id)
               return (
               return (
                 <div key={tech.id} className="contents">
                 <div key={tech.id} className="contents">
                   <div className="flex items-center gap-2 border-b p-2">
                   <div className="flex items-center gap-2 border-b p-2">
-                    <div
-                      className="h-3 w-3 shrink-0 rounded-full"
-                      style={{ backgroundColor: tech.color }}
-                    />
+                    <div className="h-3 w-3 shrink-0 rounded-full" style={{ backgroundColor: tech.color }} />
                     <span className="truncate text-sm font-semibold">{tech.name}</span>
                     <span className="truncate text-sm font-semibold">{tech.name}</span>
                   </div>
                   </div>
                   {days.map((day) => {
                   {days.map((day) => {
-                    const cellAssignments = techAssignments
-                      .filter((a) => a.date === day)
-                      .sort((a, b) => a.sortOrder - b.sortOrder)
+                    const cellJobs = techJobs.filter((a) => jobOverlapsDate(a, day)).sort((a, b) => a.sortOrder - b.sortOrder)
                     const isToday = day === today
                     const isToday = day === today
                     return (
                     return (
-                      <div
-                        key={`${tech.id}-${day}`}
-                        className={`space-y-1 overflow-y-auto border-b border-l p-1.5 ${
-                          isToday ? 'bg-primary/5' : ''
-                        }`}
-                      >
-                        {cellAssignments.map((assignment) => (
-                          <PresenterJobCard key={assignment.id} assignment={assignment} />
-                        ))}
+                      <div key={`${tech.id}-${day}`} className={`space-y-1 overflow-y-auto border-b border-l p-1.5 ${isToday ? 'bg-primary/5' : ''}`}>
+                        {cellJobs.map((a) => <PresenterJobCard key={a.id} job={a} />)}
                       </div>
                       </div>
                     )
                     )
                   })}
                   })}
                 </div>
                 </div>
               )
               )
             })}
             })}
-
-            {store.technicians.length === 0 && (
-              <div className="col-span-8 flex items-center justify-center p-10">
-                <p className="text-lg text-muted-foreground">{t('noTechnicians')}</p>
-              </div>
-            )}
+            {store.technicians.length === 0 && <div className="col-span-8 flex items-center justify-center p-10"><p className="text-lg text-muted-foreground">{t('noTechnicians')}</p></div>}
           </div>
           </div>
         </div>
         </div>
       )}
       )}

+ 68 - 4
src/features/workboard/Components/WorkBoardToolbar.tsx

@@ -2,8 +2,11 @@
 
 
 import Link from "next/link";
 import Link from "next/link";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
-import { ChevronLeft, ChevronRight, Plus, Calendar, Monitor } from "lucide-react";
+import { ChevronLeft, ChevronRight, Plus, Calendar, Monitor, CalendarDays, Clock } from "lucide-react";
 import { useTranslations, useLocale } from "next-intl";
 import { useTranslations, useLocale } from "next-intl";
+import { cn } from "@/lib/utils";
+
+export type BoardView = "week" | "day";
 
 
 function formatWeekRange(weekStart: string, locale?: string) {
 function formatWeekRange(weekStart: string, locale?: string) {
   const start = new Date(weekStart + "T12:00:00");
   const start = new Date(weekStart + "T12:00:00");
@@ -19,18 +22,38 @@ function formatWeekRange(weekStart: string, locale?: string) {
   return `${startStr} – ${endStr}`;
   return `${startStr} – ${endStr}`;
 }
 }
 
 
+function formatDayDate(date: string, locale?: string) {
+  const d = new Date(date + "T12:00:00");
+  return d.toLocaleDateString(locale, {
+    weekday: "long",
+    month: "long",
+    day: "numeric",
+    year: "numeric",
+  });
+}
+
 export function WorkBoardToolbar({
 export function WorkBoardToolbar({
   weekStart,
   weekStart,
+  selectedDate,
+  view,
   onPrevWeek,
   onPrevWeek,
   onNextWeek,
   onNextWeek,
+  onPrevDay,
+  onNextDay,
   onToday,
   onToday,
   onAddTech,
   onAddTech,
+  onViewChange,
 }: {
 }: {
   weekStart: string;
   weekStart: string;
+  selectedDate: string;
+  view: BoardView;
   onPrevWeek: () => void;
   onPrevWeek: () => void;
   onNextWeek: () => void;
   onNextWeek: () => void;
+  onPrevDay: () => void;
+  onNextDay: () => void;
   onToday: () => void;
   onToday: () => void;
   onAddTech: () => void;
   onAddTech: () => void;
+  onViewChange: (view: BoardView) => void;
 }) {
 }) {
   const t = useTranslations("workBoard.toolbar");
   const t = useTranslations("workBoard.toolbar");
   const locale = useLocale();
   const locale = useLocale();
@@ -43,18 +66,59 @@ export function WorkBoardToolbar({
           {t("today")}
           {t("today")}
         </Button>
         </Button>
         <div className="flex items-center gap-1">
         <div className="flex items-center gap-1">
-          <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onPrevWeek}>
+          <Button
+            variant="ghost"
+            size="icon"
+            className="h-8 w-8"
+            onClick={view === "week" ? onPrevWeek : onPrevDay}
+          >
             <ChevronLeft className="h-4 w-4" />
             <ChevronLeft className="h-4 w-4" />
           </Button>
           </Button>
           <span className="min-w-[180px] text-center text-sm font-medium">
           <span className="min-w-[180px] text-center text-sm font-medium">
-            {formatWeekRange(weekStart, locale)}
+            {view === "week"
+              ? formatWeekRange(weekStart, locale)
+              : formatDayDate(selectedDate, locale)}
           </span>
           </span>
-          <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onNextWeek}>
+          <Button
+            variant="ghost"
+            size="icon"
+            className="h-8 w-8"
+            onClick={view === "week" ? onNextWeek : onNextDay}
+          >
             <ChevronRight className="h-4 w-4" />
             <ChevronRight className="h-4 w-4" />
           </Button>
           </Button>
         </div>
         </div>
       </div>
       </div>
       <div className="flex items-center gap-2">
       <div className="flex items-center gap-2">
+        {/* View toggle */}
+        <div className="flex rounded-md border">
+          <button
+            type="button"
+            onClick={() => onViewChange("day")}
+            className={cn(
+              "flex items-center gap-1 rounded-l-md px-2.5 py-1.5 text-xs font-medium transition-colors",
+              view === "day"
+                ? "bg-primary text-primary-foreground"
+                : "text-muted-foreground hover:bg-muted",
+            )}
+          >
+            <Clock className="h-3.5 w-3.5" />
+            {t("day")}
+          </button>
+          <button
+            type="button"
+            onClick={() => onViewChange("week")}
+            className={cn(
+              "flex items-center gap-1 rounded-r-md px-2.5 py-1.5 text-xs font-medium transition-colors",
+              view === "week"
+                ? "bg-primary text-primary-foreground"
+                : "text-muted-foreground hover:bg-muted",
+            )}
+          >
+            <CalendarDays className="h-3.5 w-3.5" />
+            {t("week")}
+          </button>
+        </div>
         <Button variant="outline" size="sm" asChild>
         <Button variant="outline" size="sm" asChild>
           <Link href="/work-board/presenter" target="_blank">
           <Link href="/work-board/presenter" target="_blank">
             <Monitor className="mr-1.5 h-3.5 w-3.5" />
             <Monitor className="mr-1.5 h-3.5 w-3.5" />

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff