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

First-run onboarding: sample data, default presets, getting-started checklist (#255)

* add first-run onboarding: sample data, default presets, getting-started checklist

- translate the onboarding flow and add a load-sample-data toggle (default on)
- on org creation install a default inspection template (locale-aware country
  variant alongside) and six common labor presets
- seed a small prefixed sample dataset and record its ids in AppSetting for
  exact removal from the dashboard card or settings > data
- new dismissible getting-started dashboard card with live step detection
  that excludes sample data; hidden for established orgs
- new onboarding i18n namespace across all 12 locales

* fix nested button hydration error in inspections progress tooltip
Bernt Christian Egeland 1 месяц назад
Родитель
Сommit
3df65f420e
57 измененных файлов с 2794 добавлено и 287 удалено
  1. 2 1
      messages/de/audit.json
  2. 2 1
      messages/de/dashboard.json
  3. 91 0
      messages/de/onboarding.json
  4. 2 1
      messages/en/audit.json
  5. 2 1
      messages/en/dashboard.json
  6. 91 0
      messages/en/onboarding.json
  7. 2 1
      messages/es/audit.json
  8. 2 1
      messages/es/dashboard.json
  9. 91 0
      messages/es/onboarding.json
  10. 2 1
      messages/fr/audit.json
  11. 2 1
      messages/fr/dashboard.json
  12. 91 0
      messages/fr/onboarding.json
  13. 2 1
      messages/it/audit.json
  14. 2 1
      messages/it/dashboard.json
  15. 91 0
      messages/it/onboarding.json
  16. 2 1
      messages/lt/audit.json
  17. 2 1
      messages/lt/dashboard.json
  18. 91 0
      messages/lt/onboarding.json
  19. 2 1
      messages/nb/audit.json
  20. 2 1
      messages/nb/dashboard.json
  21. 91 0
      messages/nb/onboarding.json
  22. 2 1
      messages/nl/audit.json
  23. 2 1
      messages/nl/dashboard.json
  24. 91 0
      messages/nl/onboarding.json
  25. 2 1
      messages/pl/audit.json
  26. 2 1
      messages/pl/dashboard.json
  27. 91 0
      messages/pl/onboarding.json
  28. 2 1
      messages/pt-BR/audit.json
  29. 2 1
      messages/pt-BR/dashboard.json
  30. 91 0
      messages/pt-BR/onboarding.json
  31. 2 1
      messages/ru/audit.json
  32. 2 1
      messages/ru/dashboard.json
  33. 91 0
      messages/ru/onboarding.json
  34. 2 1
      messages/tr/audit.json
  35. 2 1
      messages/tr/dashboard.json
  36. 91 0
      messages/tr/onboarding.json
  37. 229 0
      src/__tests__/features/onboarding/sample-data.test.ts
  38. 14 1
      src/app/(authenticated)/dashboard-client.tsx
  39. 236 188
      src/app/(authenticated)/inspections/inspections-client.tsx
  40. 6 1
      src/app/(authenticated)/page.tsx
  41. 6 0
      src/app/(authenticated)/settings/data/data-settings.tsx
  42. 22 6
      src/app/(authenticated)/settings/data/page.tsx
  43. 5 0
      src/app/api/protected/services/[id]/pdf/route.ts
  44. 15 11
      src/features/dashboard/dashboard-grid-config.ts
  45. 4 46
      src/features/inspections/Actions/templateActions.ts
  46. 50 0
      src/features/inspections/Lib/templatePresets.ts
  47. 160 0
      src/features/onboarding/Actions/checklistActions.ts
  48. 58 0
      src/features/onboarding/Actions/createOnboardingOrg.ts
  49. 180 0
      src/features/onboarding/Components/GettingStartedCard.tsx
  50. 30 10
      src/features/onboarding/Components/OnboardingForm.tsx
  51. 56 0
      src/features/onboarding/Components/SampleDataCard.tsx
  52. 53 0
      src/features/onboarding/Lib/markInvoiceIssued.ts
  53. 111 0
      src/features/onboarding/Lib/onboardingDefaults.ts
  54. 67 0
      src/features/onboarding/Lib/onboardingKeys.ts
  55. 349 0
      src/features/onboarding/Lib/sampleData.ts
  56. 1 0
      src/features/onboarding/Schema/onboardingSchema.ts
  57. 2 0
      src/i18n/request.ts

+ 2 - 1
messages/de/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Geplante Nachricht gesendet",
     "settings_applyTaxRate": "Steuersatz angewendet",
     "settings_convertToExclusive": "Preise auf netto umgestellt",
-    "settings_convertToInclusive": "Preise auf brutto umgestellt"
+    "settings_convertToInclusive": "Preise auf brutto umgestellt",
+    "organization_sampleDataRemove": "Beispieldaten entfernt"
   }
 }

+ 2 - 1
messages/de/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Kürzlich abgeschlossen",
     "activeJobs": "Aktive Aufträge",
     "recentObservations": "Aktuelle Beobachtungen",
-    "recentActivity": "Letzte Aktivitäten"
+    "recentActivity": "Letzte Aktivitäten",
+    "gettingStarted": "Erste Schritte"
   },
   "maintenance": {
     "title": "Voraussichtlich fällige Wartung",

+ 91 - 0
messages/de/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Richten Sie Ihre Werkstatt ein",
+    "subtitle": "Geben Sie den Namen Ihrer Werkstatt ein, um loszulegen",
+    "workshopName": "Name der Werkstatt",
+    "workshopNamePlaceholder": "Meine Autowerkstatt",
+    "submit": "Loslegen",
+    "sampleDataLabel": "Beispieldaten laden",
+    "sampleDataDescription": "Fügt einige Beispielkunden, Fahrzeuge und Aufträge hinzu, damit Sie sich umsehen können. Sie können sie mit einem Klick wieder entfernen.",
+    "setupFailed": "Einrichtung fehlgeschlagen",
+    "couldNotCreate": "Werkstatt konnte nicht erstellt werden",
+    "unexpectedError": "Ein unerwarteter Fehler ist aufgetreten"
+  },
+  "sample": {
+    "prefix": "Beispiel:",
+    "oilService": "Öl- und Filterwechsel",
+    "brakeJob": "Bremsbeläge vorne erneuern",
+    "annualService": "Jahresinspektion",
+    "quoteTitle": "Fahrwerk vorne überholen",
+    "inspectionNote": "Reifen vorne links ist nahe an der Verschleißgrenze",
+    "parts": {
+      "oilFilter": "Ölfilter",
+      "engineOil": "Motoröl 5W-30",
+      "brakePads": "Bremsbelagsatz vorne",
+      "shockAbsorber": "Stoßdämpfer"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Ölwechsel",
+      "description": "Motoröl und Filter wechseln"
+    },
+    "frontBrakePads": {
+      "name": "Bremsbeläge vorne",
+      "description": "Bremsbeläge vorne erneuern"
+    },
+    "tireSwap": {
+      "name": "Radwechsel",
+      "description": "Saisonaler Radwechsel mit Drehmomentkontrolle"
+    },
+    "batteryReplacement": {
+      "name": "Batteriewechsel",
+      "description": "Starterbatterie ersetzen und anlernen"
+    },
+    "diagnostics": {
+      "name": "Diagnose",
+      "description": "Fehlerspeicher auslesen und Erstdiagnose"
+    },
+    "annualService": {
+      "name": "Jahresinspektion",
+      "description": "Komplette Jahresinspektion nach Plan"
+    }
+  },
+  "checklist": {
+    "title": "Erste Schritte",
+    "description": "Vier Schritte, um Ihre Werkstatt zum Laufen zu bringen",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Kunden anlegen",
+        "description": "Legen Sie Ihren ersten Kunden an"
+      },
+      "vehicle": {
+        "title": "Fahrzeug anlegen",
+        "description": "Erfassen Sie ein Fahrzeug für einen Kunden"
+      },
+      "workOrder": {
+        "title": "Auftrag erstellen",
+        "description": "Eröffnen Sie Ihren ersten Auftrag"
+      },
+      "invoice": {
+        "title": "Rechnung senden oder herunterladen",
+        "description": "Teilen Sie eine Rechnung mit einem Kunden oder laden Sie sie als PDF herunter"
+      }
+    },
+    "allDoneTitle": "Alles erledigt",
+    "allDoneDescription": "Alle Schritte sind abgeschlossen und Ihre Werkstatt ist startklar.",
+    "dismiss": "Karte ausblenden",
+    "dismissAriaLabel": "Diese Checkliste ausblenden"
+  },
+  "sampleData": {
+    "title": "Beispieldaten",
+    "description": "Ihre Werkstatt enthält noch die Beispielkunden, Fahrzeuge und Aufträge aus der Einrichtung. Beim Entfernen werden genau diese Datensätze gelöscht und sonst nichts.",
+    "footerNote": "Beispieldaten sind geladen",
+    "remove": "Beispieldaten entfernen",
+    "removeConfirmTitle": "Beispieldaten entfernen?",
+    "removeConfirmDescription": "Damit werden die bei der Einrichtung erstellten Beispielkunden, Fahrzeuge, Aufträge, das Angebot und die Inspektion gelöscht. Ihre eigenen Daten bleiben unberührt.",
+    "removed": "Beispieldaten entfernt",
+    "removeFailed": "Beispieldaten konnten nicht entfernt werden"
+  }
+}

+ 2 - 1
messages/en/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Sent Scheduled Message",
     "settings_applyTaxRate": "Applied Tax Rate",
     "settings_convertToExclusive": "Converted Prices to Tax Exclusive",
-    "settings_convertToInclusive": "Converted Prices to Tax Inclusive"
+    "settings_convertToInclusive": "Converted Prices to Tax Inclusive",
+    "organization_sampleDataRemove": "Removed Sample Data"
   }
 }

+ 2 - 1
messages/en/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Recent Completed",
     "activeJobs": "Active Jobs",
     "recentObservations": "Recent Observations",
-    "recentActivity": "Recent Activity"
+    "recentActivity": "Recent Activity",
+    "gettingStarted": "Getting Started"
   },
   "maintenance": {
     "title": "Predicted Maintenance Due",

+ 91 - 0
messages/en/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Set up your workshop",
+    "subtitle": "Enter your workshop name to get started",
+    "workshopName": "Workshop name",
+    "workshopNamePlaceholder": "My Auto Workshop",
+    "submit": "Get started",
+    "sampleDataLabel": "Load sample data",
+    "sampleDataDescription": "Adds a few sample customers, vehicles and jobs so you can look around. You can remove them again with one click.",
+    "setupFailed": "Setup failed",
+    "couldNotCreate": "Could not create workshop",
+    "unexpectedError": "An unexpected error occurred"
+  },
+  "sample": {
+    "prefix": "Sample:",
+    "oilService": "Oil and filter change",
+    "brakeJob": "Front brake pads replacement",
+    "annualService": "Annual service",
+    "quoteTitle": "Front suspension refresh",
+    "inspectionNote": "Front left tyre is close to the wear limit",
+    "parts": {
+      "oilFilter": "Oil filter",
+      "engineOil": "Engine oil 5W-30",
+      "brakePads": "Front brake pad set",
+      "shockAbsorber": "Shock absorber"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Oil change",
+      "description": "Engine oil and filter change"
+    },
+    "frontBrakePads": {
+      "name": "Front brake pads",
+      "description": "Replace front brake pads"
+    },
+    "tireSwap": {
+      "name": "Tire swap",
+      "description": "Seasonal wheel change with torque check"
+    },
+    "batteryReplacement": {
+      "name": "Battery replacement",
+      "description": "Replace and register the starter battery"
+    },
+    "diagnostics": {
+      "name": "Diagnostics",
+      "description": "Fault code readout and initial diagnosis"
+    },
+    "annualService": {
+      "name": "Annual service",
+      "description": "Full annual service according to schedule"
+    }
+  },
+  "checklist": {
+    "title": "Getting started",
+    "description": "Four steps to get your workshop rolling",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Add a customer",
+        "description": "Create your first customer record"
+      },
+      "vehicle": {
+        "title": "Add a vehicle",
+        "description": "Register a vehicle for a customer"
+      },
+      "workOrder": {
+        "title": "Create a work order",
+        "description": "Open your first job"
+      },
+      "invoice": {
+        "title": "Send or download an invoice",
+        "description": "Share an invoice with a customer or download it as a PDF"
+      }
+    },
+    "allDoneTitle": "You are all set",
+    "allDoneDescription": "All steps are done and your workshop is up and running.",
+    "dismiss": "Hide card",
+    "dismissAriaLabel": "Hide this checklist"
+  },
+  "sampleData": {
+    "title": "Sample data",
+    "description": "Your workshop still contains the sample customers, vehicles and jobs from setup. Removing them deletes exactly those records and nothing else.",
+    "footerNote": "Sample data is loaded",
+    "remove": "Remove sample data",
+    "removeConfirmTitle": "Remove sample data?",
+    "removeConfirmDescription": "This deletes the sample customers, vehicles, work orders, quote and inspection created during setup. Your own data is not affected.",
+    "removed": "Sample data removed",
+    "removeFailed": "Could not remove sample data"
+  }
+}

+ 2 - 1
messages/es/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Mensaje programado enviado",
     "settings_applyTaxRate": "Tipo impositivo aplicado",
     "settings_convertToExclusive": "Precios convertidos a sin impuestos",
-    "settings_convertToInclusive": "Precios convertidos a con impuestos"
+    "settings_convertToInclusive": "Precios convertidos a con impuestos",
+    "organization_sampleDataRemove": "Datos de ejemplo eliminados"
   }
 }

+ 2 - 1
messages/es/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Completados recientemente",
     "activeJobs": "Trabajos activos",
     "recentObservations": "Observaciones recientes",
-    "recentActivity": "Actividad reciente"
+    "recentActivity": "Actividad reciente",
+    "gettingStarted": "Primeros pasos"
   },
   "maintenance": {
     "title": "Mantenimiento previsto pendiente",

+ 91 - 0
messages/es/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Configure su taller",
+    "subtitle": "Ingrese el nombre de su taller para comenzar",
+    "workshopName": "Nombre del taller",
+    "workshopNamePlaceholder": "Mi Taller Mecánico",
+    "submit": "Comenzar",
+    "sampleDataLabel": "Cargar datos de ejemplo",
+    "sampleDataDescription": "Añade algunos clientes, vehículos y trabajos de ejemplo para que pueda explorar. Puede eliminarlos de nuevo con un clic.",
+    "setupFailed": "Error en la configuración",
+    "couldNotCreate": "No se pudo crear el taller",
+    "unexpectedError": "Ocurrió un error inesperado"
+  },
+  "sample": {
+    "prefix": "Ejemplo:",
+    "oilService": "Cambio de aceite y filtro",
+    "brakeJob": "Sustitución de pastillas de freno delanteras",
+    "annualService": "Revisión anual",
+    "quoteTitle": "Renovación de la suspensión delantera",
+    "inspectionNote": "El neumático delantero izquierdo está cerca del límite de desgaste",
+    "parts": {
+      "oilFilter": "Filtro de aceite",
+      "engineOil": "Aceite de motor 5W-30",
+      "brakePads": "Juego de pastillas de freno delanteras",
+      "shockAbsorber": "Amortiguador"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Cambio de aceite",
+      "description": "Cambio de aceite de motor y filtro"
+    },
+    "frontBrakePads": {
+      "name": "Pastillas de freno delanteras",
+      "description": "Sustituir las pastillas de freno delanteras"
+    },
+    "tireSwap": {
+      "name": "Cambio de ruedas",
+      "description": "Cambio de ruedas de temporada con control de apriete"
+    },
+    "batteryReplacement": {
+      "name": "Sustitución de batería",
+      "description": "Sustituir y registrar la batería de arranque"
+    },
+    "diagnostics": {
+      "name": "Diagnóstico",
+      "description": "Lectura de códigos de avería y diagnóstico inicial"
+    },
+    "annualService": {
+      "name": "Revisión anual",
+      "description": "Revisión anual completa según el programa"
+    }
+  },
+  "checklist": {
+    "title": "Primeros pasos",
+    "description": "Cuatro pasos para poner su taller en marcha",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Añadir un cliente",
+        "description": "Cree su primera ficha de cliente"
+      },
+      "vehicle": {
+        "title": "Añadir un vehículo",
+        "description": "Registre un vehículo para un cliente"
+      },
+      "workOrder": {
+        "title": "Crear una orden de trabajo",
+        "description": "Abra su primer trabajo"
+      },
+      "invoice": {
+        "title": "Enviar o descargar una factura",
+        "description": "Comparta una factura con un cliente o descárguela en PDF"
+      }
+    },
+    "allDoneTitle": "Todo listo",
+    "allDoneDescription": "Todos los pasos están completados y su taller ya está en marcha.",
+    "dismiss": "Ocultar tarjeta",
+    "dismissAriaLabel": "Ocultar esta lista"
+  },
+  "sampleData": {
+    "title": "Datos de ejemplo",
+    "description": "Su taller todavía contiene los clientes, vehículos y trabajos de ejemplo de la configuración. Al eliminarlos se borran exactamente esos registros y nada más.",
+    "footerNote": "Hay datos de ejemplo cargados",
+    "remove": "Eliminar datos de ejemplo",
+    "removeConfirmTitle": "¿Eliminar los datos de ejemplo?",
+    "removeConfirmDescription": "Esto elimina los clientes, vehículos, órdenes de trabajo, el presupuesto y la inspección de ejemplo creados durante la configuración. Sus propios datos no se ven afectados.",
+    "removed": "Datos de ejemplo eliminados",
+    "removeFailed": "No se pudieron eliminar los datos de ejemplo"
+  }
+}

+ 2 - 1
messages/fr/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Message planifié envoyé",
     "settings_applyTaxRate": "Taux de taxe appliqué",
     "settings_convertToExclusive": "Prix convertis hors taxes",
-    "settings_convertToInclusive": "Prix convertis toutes taxes comprises"
+    "settings_convertToInclusive": "Prix convertis toutes taxes comprises",
+    "organization_sampleDataRemove": "Données d'exemple supprimées"
   }
 }

+ 2 - 1
messages/fr/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Terminés récemment",
     "activeJobs": "Travaux en cours",
     "recentObservations": "Observations récentes",
-    "recentActivity": "Activité récente"
+    "recentActivity": "Activité récente",
+    "gettingStarted": "Premiers pas"
   },
   "maintenance": {
     "title": "Maintenance prévue à effectuer",

+ 91 - 0
messages/fr/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Configurez votre atelier",
+    "subtitle": "Saisissez le nom de votre atelier pour commencer",
+    "workshopName": "Nom de l'atelier",
+    "workshopNamePlaceholder": "Mon Garage Auto",
+    "submit": "Commencer",
+    "sampleDataLabel": "Charger des données d'exemple",
+    "sampleDataDescription": "Ajoute quelques clients, véhicules et interventions d'exemple pour découvrir l'application. Vous pourrez les supprimer en un clic.",
+    "setupFailed": "Échec de la configuration",
+    "couldNotCreate": "Impossible de créer l'atelier",
+    "unexpectedError": "Une erreur inattendue est survenue"
+  },
+  "sample": {
+    "prefix": "Exemple :",
+    "oilService": "Vidange et remplacement du filtre",
+    "brakeJob": "Remplacement des plaquettes de frein avant",
+    "annualService": "Révision annuelle",
+    "quoteTitle": "Remise en état de la suspension avant",
+    "inspectionNote": "Le pneu avant gauche approche de la limite d'usure",
+    "parts": {
+      "oilFilter": "Filtre à huile",
+      "engineOil": "Huile moteur 5W-30",
+      "brakePads": "Jeu de plaquettes de frein avant",
+      "shockAbsorber": "Amortisseur"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Vidange",
+      "description": "Vidange de l'huile moteur et remplacement du filtre"
+    },
+    "frontBrakePads": {
+      "name": "Plaquettes de frein avant",
+      "description": "Remplacer les plaquettes de frein avant"
+    },
+    "tireSwap": {
+      "name": "Permutation des pneus",
+      "description": "Changement de roues saisonnier avec contrôle du serrage"
+    },
+    "batteryReplacement": {
+      "name": "Remplacement de la batterie",
+      "description": "Remplacer et enregistrer la batterie de démarrage"
+    },
+    "diagnostics": {
+      "name": "Diagnostic",
+      "description": "Lecture des codes défauts et premier diagnostic"
+    },
+    "annualService": {
+      "name": "Révision annuelle",
+      "description": "Révision annuelle complète selon le carnet d'entretien"
+    }
+  },
+  "checklist": {
+    "title": "Premiers pas",
+    "description": "Quatre étapes pour lancer votre atelier",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Ajouter un client",
+        "description": "Créez votre première fiche client"
+      },
+      "vehicle": {
+        "title": "Ajouter un véhicule",
+        "description": "Enregistrez un véhicule pour un client"
+      },
+      "workOrder": {
+        "title": "Créer un ordre de réparation",
+        "description": "Ouvrez votre première intervention"
+      },
+      "invoice": {
+        "title": "Envoyer ou télécharger une facture",
+        "description": "Partagez une facture avec un client ou téléchargez-la en PDF"
+      }
+    },
+    "allDoneTitle": "Tout est prêt",
+    "allDoneDescription": "Toutes les étapes sont terminées et votre atelier est opérationnel.",
+    "dismiss": "Masquer la carte",
+    "dismissAriaLabel": "Masquer cette liste"
+  },
+  "sampleData": {
+    "title": "Données d'exemple",
+    "description": "Votre atelier contient encore les clients, véhicules et interventions d'exemple de la configuration. Leur suppression efface exactement ces enregistrements et rien d'autre.",
+    "footerNote": "Des données d'exemple sont chargées",
+    "remove": "Supprimer les données d'exemple",
+    "removeConfirmTitle": "Supprimer les données d'exemple ?",
+    "removeConfirmDescription": "Cette action supprime les clients, véhicules, ordres de réparation, le devis et l'inspection d'exemple créés lors de la configuration. Vos propres données ne sont pas concernées.",
+    "removed": "Données d'exemple supprimées",
+    "removeFailed": "Impossible de supprimer les données d'exemple"
+  }
+}

+ 2 - 1
messages/it/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Messaggio pianificato inviato",
     "settings_applyTaxRate": "Aliquota applicata",
     "settings_convertToExclusive": "Prezzi convertiti in imponibile",
-    "settings_convertToInclusive": "Prezzi convertiti in ivato"
+    "settings_convertToInclusive": "Prezzi convertiti in ivato",
+    "organization_sampleDataRemove": "Dati di esempio rimossi"
   }
 }

+ 2 - 1
messages/it/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Completati di recente",
     "activeJobs": "Lavori attivi",
     "recentObservations": "Osservazioni recenti",
-    "recentActivity": "Attività recente"
+    "recentActivity": "Attività recente",
+    "gettingStarted": "Per iniziare"
   },
   "maintenance": {
     "title": "Manutenzione prevista in scadenza",

+ 91 - 0
messages/it/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Configuri la Sua officina",
+    "subtitle": "Inserisca il nome della Sua officina per iniziare",
+    "workshopName": "Nome dell'officina",
+    "workshopNamePlaceholder": "La Mia Officina",
+    "submit": "Inizia",
+    "sampleDataLabel": "Carica dati di esempio",
+    "sampleDataDescription": "Aggiunge alcuni clienti, veicoli e lavori di esempio per farsi un'idea. Potrà rimuoverli di nuovo con un clic.",
+    "setupFailed": "Configurazione non riuscita",
+    "couldNotCreate": "Impossibile creare l'officina",
+    "unexpectedError": "Si è verificato un errore imprevisto"
+  },
+  "sample": {
+    "prefix": "Esempio:",
+    "oilService": "Cambio olio e filtro",
+    "brakeJob": "Sostituzione pastiglie freni anteriori",
+    "annualService": "Tagliando annuale",
+    "quoteTitle": "Revisione sospensione anteriore",
+    "inspectionNote": "Lo pneumatico anteriore sinistro è vicino al limite di usura",
+    "parts": {
+      "oilFilter": "Filtro olio",
+      "engineOil": "Olio motore 5W-30",
+      "brakePads": "Kit pastiglie freni anteriori",
+      "shockAbsorber": "Ammortizzatore"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Cambio olio",
+      "description": "Sostituzione olio motore e filtro"
+    },
+    "frontBrakePads": {
+      "name": "Pastiglie freni anteriori",
+      "description": "Sostituire le pastiglie dei freni anteriori"
+    },
+    "tireSwap": {
+      "name": "Cambio gomme",
+      "description": "Cambio stagionale delle ruote con controllo della coppia"
+    },
+    "batteryReplacement": {
+      "name": "Sostituzione batteria",
+      "description": "Sostituire e registrare la batteria di avviamento"
+    },
+    "diagnostics": {
+      "name": "Diagnosi",
+      "description": "Lettura dei codici di errore e prima diagnosi"
+    },
+    "annualService": {
+      "name": "Tagliando annuale",
+      "description": "Tagliando annuale completo secondo il programma"
+    }
+  },
+  "checklist": {
+    "title": "Per iniziare",
+    "description": "Quattro passaggi per avviare la Sua officina",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Aggiungere un cliente",
+        "description": "Crei la Sua prima scheda cliente"
+      },
+      "vehicle": {
+        "title": "Aggiungere un veicolo",
+        "description": "Registri un veicolo per un cliente"
+      },
+      "workOrder": {
+        "title": "Creare un ordine di lavoro",
+        "description": "Apra il Suo primo lavoro"
+      },
+      "invoice": {
+        "title": "Inviare o scaricare una fattura",
+        "description": "Condivida una fattura con un cliente o la scarichi in PDF"
+      }
+    },
+    "allDoneTitle": "Tutto pronto",
+    "allDoneDescription": "Tutti i passaggi sono completati e la Sua officina è operativa.",
+    "dismiss": "Nascondi scheda",
+    "dismissAriaLabel": "Nascondi questa lista"
+  },
+  "sampleData": {
+    "title": "Dati di esempio",
+    "description": "La Sua officina contiene ancora i clienti, i veicoli e i lavori di esempio della configurazione. La rimozione elimina esattamente quei record e nient'altro.",
+    "footerNote": "Sono caricati dati di esempio",
+    "remove": "Rimuovi dati di esempio",
+    "removeConfirmTitle": "Rimuovere i dati di esempio?",
+    "removeConfirmDescription": "Verranno eliminati i clienti, i veicoli, gli ordini di lavoro, il preventivo e l'ispezione di esempio creati durante la configurazione. I Suoi dati non vengono toccati.",
+    "removed": "Dati di esempio rimossi",
+    "removeFailed": "Impossibile rimuovere i dati di esempio"
+  }
+}

+ 2 - 1
messages/lt/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Išsiųsta suplanuota žinutė",
     "settings_applyTaxRate": "Pritaikytas mokesčio tarifas",
     "settings_convertToExclusive": "Kainos konvertuotos be mokesčio",
-    "settings_convertToInclusive": "Kainos konvertuotos su mokesčiu"
+    "settings_convertToInclusive": "Kainos konvertuotos su mokesčiu",
+    "organization_sampleDataRemove": "Pašalinti pavyzdiniai duomenys"
   }
 }

+ 2 - 1
messages/lt/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Neseniai užbaigti",
     "activeJobs": "Aktyvūs darbai",
     "recentObservations": "Naujausi pastebėjimai",
-    "recentActivity": "Naujausia veikla"
+    "recentActivity": "Naujausia veikla",
+    "gettingStarted": "Pradžia"
   },
   "maintenance": {
     "title": "Numatoma priežiūra",

+ 91 - 0
messages/lt/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Sukurkite savo dirbtuves",
+    "subtitle": "Įveskite dirbtuvių pavadinimą, kad pradėtumėte",
+    "workshopName": "Dirbtuvių pavadinimas",
+    "workshopNamePlaceholder": "Mano autoservisas",
+    "submit": "Pradėti",
+    "sampleDataLabel": "Įkelti pavyzdinius duomenis",
+    "sampleDataDescription": "Prideda kelis pavyzdinius klientus, automobilius ir darbus, kad galėtumėte apsižvalgyti. Juos galėsite pašalinti vienu paspaudimu.",
+    "setupFailed": "Nustatymas nepavyko",
+    "couldNotCreate": "Nepavyko sukurti dirbtuvių",
+    "unexpectedError": "Įvyko netikėta klaida"
+  },
+  "sample": {
+    "prefix": "Pavyzdys:",
+    "oilService": "Alyvos ir filtro keitimas",
+    "brakeJob": "Priekinių stabdžių kaladėlių keitimas",
+    "annualService": "Metinė techninė priežiūra",
+    "quoteTitle": "Priekinės pakabos atnaujinimas",
+    "inspectionNote": "Kairė priekinė padanga arti nusidėvėjimo ribos",
+    "parts": {
+      "oilFilter": "Alyvos filtras",
+      "engineOil": "Variklio alyva 5W-30",
+      "brakePads": "Priekinių stabdžių kaladėlių komplektas",
+      "shockAbsorber": "Amortizatorius"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Alyvos keitimas",
+      "description": "Variklio alyvos ir filtro keitimas"
+    },
+    "frontBrakePads": {
+      "name": "Priekinės stabdžių kaladėlės",
+      "description": "Pakeisti priekines stabdžių kaladėles"
+    },
+    "tireSwap": {
+      "name": "Ratų keitimas",
+      "description": "Sezoninis ratų keitimas su užveržimo momento patikra"
+    },
+    "batteryReplacement": {
+      "name": "Akumuliatoriaus keitimas",
+      "description": "Pakeisti ir užregistruoti paleidimo akumuliatorių"
+    },
+    "diagnostics": {
+      "name": "Diagnostika",
+      "description": "Klaidų kodų nuskaitymas ir pirminė diagnozė"
+    },
+    "annualService": {
+      "name": "Metinė techninė priežiūra",
+      "description": "Pilna metinė priežiūra pagal grafiką"
+    }
+  },
+  "checklist": {
+    "title": "Pradžia",
+    "description": "Keturi žingsniai, kad dirbtuvės pradėtų veikti",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Pridėkite klientą",
+        "description": "Sukurkite pirmą kliento kortelę"
+      },
+      "vehicle": {
+        "title": "Pridėkite automobilį",
+        "description": "Užregistruokite kliento automobilį"
+      },
+      "workOrder": {
+        "title": "Sukurkite darbo užsakymą",
+        "description": "Atidarykite pirmą savo darbą"
+      },
+      "invoice": {
+        "title": "Išsiųskite arba atsisiųskite sąskaitą",
+        "description": "Pasidalinkite sąskaita su klientu arba atsisiųskite ją PDF formatu"
+      }
+    },
+    "allDoneTitle": "Viskas paruošta",
+    "allDoneDescription": "Visi žingsniai atlikti ir jūsų dirbtuvės veikia.",
+    "dismiss": "Slėpti kortelę",
+    "dismissAriaLabel": "Slėpti šį sąrašą"
+  },
+  "sampleData": {
+    "title": "Pavyzdiniai duomenys",
+    "description": "Jūsų dirbtuvėse vis dar yra pavyzdiniai klientai, automobiliai ir darbai iš pradinio nustatymo. Pašalinus ištrinami būtent šie įrašai ir nieko daugiau.",
+    "footerNote": "Įkelti pavyzdiniai duomenys",
+    "remove": "Pašalinti pavyzdinius duomenis",
+    "removeConfirmTitle": "Pašalinti pavyzdinius duomenis?",
+    "removeConfirmDescription": "Bus ištrinti pavyzdiniai klientai, automobiliai, darbo užsakymai, pasiūlymas ir apžiūra, sukurti per pradinį nustatymą. Jūsų pačių duomenys nebus paliesti.",
+    "removed": "Pavyzdiniai duomenys pašalinti",
+    "removeFailed": "Nepavyko pašalinti pavyzdinių duomenų"
+  }
+}

+ 2 - 1
messages/nb/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Sendte planlagt melding",
     "settings_applyTaxRate": "Brukte avgiftssats",
     "settings_convertToExclusive": "Konverterte priser til eks. avgift",
-    "settings_convertToInclusive": "Konverterte priser til inkl. avgift"
+    "settings_convertToInclusive": "Konverterte priser til inkl. avgift",
+    "organization_sampleDataRemove": "Fjernet eksempeldata"
   }
 }

+ 2 - 1
messages/nb/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Nylig fullført",
     "activeJobs": "Aktive jobber",
     "recentObservations": "Nylige observasjoner",
-    "recentActivity": "Nylig aktivitet"
+    "recentActivity": "Nylig aktivitet",
+    "gettingStarted": "Kom i gang"
   },
   "maintenance": {
     "title": "Forventet vedlikehold",

+ 91 - 0
messages/nb/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Sett opp verkstedet ditt",
+    "subtitle": "Skriv inn navnet på verkstedet for å komme i gang",
+    "workshopName": "Verkstedets navn",
+    "workshopNamePlaceholder": "Mitt Bilverksted",
+    "submit": "Kom i gang",
+    "sampleDataLabel": "Last inn eksempeldata",
+    "sampleDataDescription": "Legger til noen eksempelkunder, kjøretøy og jobber slik at du kan se deg rundt. Du kan fjerne dem igjen med ett klikk.",
+    "setupFailed": "Oppsettet mislyktes",
+    "couldNotCreate": "Kunne ikke opprette verkstedet",
+    "unexpectedError": "Det oppstod en uventet feil"
+  },
+  "sample": {
+    "prefix": "Eksempel:",
+    "oilService": "Olje- og filterskift",
+    "brakeJob": "Skifte av bremseklosser foran",
+    "annualService": "Årsservice",
+    "quoteTitle": "Overhaling av forstilling",
+    "inspectionNote": "Venstre framdekk er nær slitasjegrensen",
+    "parts": {
+      "oilFilter": "Oljefilter",
+      "engineOil": "Motorolje 5W-30",
+      "brakePads": "Bremseklossett foran",
+      "shockAbsorber": "Støtdemper"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Oljeskift",
+      "description": "Skift av motorolje og filter"
+    },
+    "frontBrakePads": {
+      "name": "Bremseklosser foran",
+      "description": "Skifte bremseklosser foran"
+    },
+    "tireSwap": {
+      "name": "Hjulskift",
+      "description": "Sesonghjulskift med momentkontroll"
+    },
+    "batteryReplacement": {
+      "name": "Batteribytte",
+      "description": "Bytte og registrere startbatteri"
+    },
+    "diagnostics": {
+      "name": "Diagnose",
+      "description": "Avlesing av feilkoder og førstediagnose"
+    },
+    "annualService": {
+      "name": "Årsservice",
+      "description": "Full årsservice etter serviceplan"
+    }
+  },
+  "checklist": {
+    "title": "Kom i gang",
+    "description": "Fire steg for å få verkstedet i gang",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Legg til en kunde",
+        "description": "Opprett din første kunde"
+      },
+      "vehicle": {
+        "title": "Legg til et kjøretøy",
+        "description": "Registrer et kjøretøy på en kunde"
+      },
+      "workOrder": {
+        "title": "Opprett en arbeidsordre",
+        "description": "Åpne din første jobb"
+      },
+      "invoice": {
+        "title": "Send eller last ned en faktura",
+        "description": "Del en faktura med en kunde eller last den ned som PDF"
+      }
+    },
+    "allDoneTitle": "Alt er klart",
+    "allDoneDescription": "Alle stegene er fullført og verkstedet ditt er i gang.",
+    "dismiss": "Skjul kortet",
+    "dismissAriaLabel": "Skjul denne sjekklisten"
+  },
+  "sampleData": {
+    "title": "Eksempeldata",
+    "description": "Verkstedet ditt inneholder fortsatt eksempelkundene, kjøretøyene og jobbene fra oppsettet. Fjerning sletter nøyaktig disse oppføringene og ingenting annet.",
+    "footerNote": "Eksempeldata er lastet inn",
+    "remove": "Fjern eksempeldata",
+    "removeConfirmTitle": "Fjerne eksempeldata?",
+    "removeConfirmDescription": "Dette sletter eksempelkundene, kjøretøyene, arbeidsordrene, tilbudet og kontrollen som ble opprettet under oppsettet. Dine egne data berøres ikke.",
+    "removed": "Eksempeldata fjernet",
+    "removeFailed": "Kunne ikke fjerne eksempeldata"
+  }
+}

+ 2 - 1
messages/nl/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Gepland bericht verzonden",
     "settings_applyTaxRate": "Btw-tarief toegepast",
     "settings_convertToExclusive": "Prijzen omgezet naar exclusief btw",
-    "settings_convertToInclusive": "Prijzen omgezet naar inclusief btw"
+    "settings_convertToInclusive": "Prijzen omgezet naar inclusief btw",
+    "organization_sampleDataRemove": "Voorbeeldgegevens verwijderd"
   }
 }

+ 2 - 1
messages/nl/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Recent afgerond",
     "activeJobs": "Actieve opdrachten",
     "recentObservations": "Recente waarnemingen",
-    "recentActivity": "Recente activiteit"
+    "recentActivity": "Recente activiteit",
+    "gettingStarted": "Aan de slag"
   },
   "maintenance": {
     "title": "Verwacht onderhoud gepland",

+ 91 - 0
messages/nl/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Stel uw werkplaats in",
+    "subtitle": "Voer de naam van uw werkplaats in om te beginnen",
+    "workshopName": "Naam van de werkplaats",
+    "workshopNamePlaceholder": "Mijn Autogarage",
+    "submit": "Aan de slag",
+    "sampleDataLabel": "Voorbeeldgegevens laden",
+    "sampleDataDescription": "Voegt enkele voorbeeldklanten, voertuigen en klussen toe zodat u kunt rondkijken. U kunt ze met één klik weer verwijderen.",
+    "setupFailed": "Instellen mislukt",
+    "couldNotCreate": "Kon de werkplaats niet aanmaken",
+    "unexpectedError": "Er is een onverwachte fout opgetreden"
+  },
+  "sample": {
+    "prefix": "Voorbeeld:",
+    "oilService": "Olie en filter vervangen",
+    "brakeJob": "Remblokken voor vervangen",
+    "annualService": "Jaarlijkse onderhoudsbeurt",
+    "quoteTitle": "Voorwielophanging reviseren",
+    "inspectionNote": "De linkervoorband zit dicht bij de slijtagegrens",
+    "parts": {
+      "oilFilter": "Oliefilter",
+      "engineOil": "Motorolie 5W-30",
+      "brakePads": "Set remblokken voor",
+      "shockAbsorber": "Schokdemper"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Olie verversen",
+      "description": "Motorolie en filter vervangen"
+    },
+    "frontBrakePads": {
+      "name": "Remblokken voor",
+      "description": "Remblokken voor vervangen"
+    },
+    "tireSwap": {
+      "name": "Wielen wisselen",
+      "description": "Seizoenswissel van de wielen met momentcontrole"
+    },
+    "batteryReplacement": {
+      "name": "Accu vervangen",
+      "description": "Startaccu vervangen en registreren"
+    },
+    "diagnostics": {
+      "name": "Diagnose",
+      "description": "Foutcodes uitlezen en eerste diagnose"
+    },
+    "annualService": {
+      "name": "Jaarlijkse onderhoudsbeurt",
+      "description": "Volledige jaarlijkse beurt volgens schema"
+    }
+  },
+  "checklist": {
+    "title": "Aan de slag",
+    "description": "Vier stappen om uw werkplaats op gang te brengen",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Voeg een klant toe",
+        "description": "Maak uw eerste klantkaart aan"
+      },
+      "vehicle": {
+        "title": "Voeg een voertuig toe",
+        "description": "Registreer een voertuig voor een klant"
+      },
+      "workOrder": {
+        "title": "Maak een werkorder aan",
+        "description": "Open uw eerste klus"
+      },
+      "invoice": {
+        "title": "Verstuur of download een factuur",
+        "description": "Deel een factuur met een klant of download deze als PDF"
+      }
+    },
+    "allDoneTitle": "Alles staat klaar",
+    "allDoneDescription": "Alle stappen zijn afgerond en uw werkplaats draait.",
+    "dismiss": "Kaart verbergen",
+    "dismissAriaLabel": "Deze checklist verbergen"
+  },
+  "sampleData": {
+    "title": "Voorbeeldgegevens",
+    "description": "Uw werkplaats bevat nog de voorbeeldklanten, voertuigen en klussen uit de installatie. Bij verwijderen worden precies die records gewist en niets anders.",
+    "footerNote": "Er zijn voorbeeldgegevens geladen",
+    "remove": "Voorbeeldgegevens verwijderen",
+    "removeConfirmTitle": "Voorbeeldgegevens verwijderen?",
+    "removeConfirmDescription": "Hiermee verwijdert u de voorbeeldklanten, voertuigen, werkorders, de offerte en de inspectie die bij de installatie zijn aangemaakt. Uw eigen gegevens blijven onaangetast.",
+    "removed": "Voorbeeldgegevens verwijderd",
+    "removeFailed": "Kon de voorbeeldgegevens niet verwijderen"
+  }
+}

+ 2 - 1
messages/pl/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Wysłano zaplanowaną wiadomość",
     "settings_applyTaxRate": "Zastosowano stawkę podatku",
     "settings_convertToExclusive": "Przeliczono ceny na netto",
-    "settings_convertToInclusive": "Przeliczono ceny na brutto"
+    "settings_convertToInclusive": "Przeliczono ceny na brutto",
+    "organization_sampleDataRemove": "Usunięto dane przykładowe"
   }
 }

+ 2 - 1
messages/pl/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Ostatnio ukończone",
     "activeJobs": "Aktywne zlecenia",
     "recentObservations": "Ostatnie obserwacje",
-    "recentActivity": "Ostatnia aktywność"
+    "recentActivity": "Ostatnia aktywność",
+    "gettingStarted": "Pierwsze kroki"
   },
   "maintenance": {
     "title": "Przewidywany termin konserwacji",

+ 91 - 0
messages/pl/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Skonfiguruj swój warsztat",
+    "subtitle": "Wpisz nazwę warsztatu, aby rozpocząć",
+    "workshopName": "Nazwa warsztatu",
+    "workshopNamePlaceholder": "Mój Warsztat Samochodowy",
+    "submit": "Rozpocznij",
+    "sampleDataLabel": "Wczytaj dane przykładowe",
+    "sampleDataDescription": "Dodaje kilku przykładowych klientów, pojazdów i zleceń, żebyś mógł się rozejrzeć. Możesz je usunąć jednym kliknięciem.",
+    "setupFailed": "Konfiguracja nie powiodła się",
+    "couldNotCreate": "Nie udało się utworzyć warsztatu",
+    "unexpectedError": "Wystąpił nieoczekiwany błąd"
+  },
+  "sample": {
+    "prefix": "Przykład:",
+    "oilService": "Wymiana oleju i filtra",
+    "brakeJob": "Wymiana przednich klocków hamulcowych",
+    "annualService": "Przegląd roczny",
+    "quoteTitle": "Regeneracja przedniego zawieszenia",
+    "inspectionNote": "Lewa przednia opona jest blisko granicy zużycia",
+    "parts": {
+      "oilFilter": "Filtr oleju",
+      "engineOil": "Olej silnikowy 5W-30",
+      "brakePads": "Komplet przednich klocków hamulcowych",
+      "shockAbsorber": "Amortyzator"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Wymiana oleju",
+      "description": "Wymiana oleju silnikowego i filtra"
+    },
+    "frontBrakePads": {
+      "name": "Przednie klocki hamulcowe",
+      "description": "Wymiana przednich klocków hamulcowych"
+    },
+    "tireSwap": {
+      "name": "Wymiana kół",
+      "description": "Sezonowa wymiana kół z kontrolą momentu dokręcenia"
+    },
+    "batteryReplacement": {
+      "name": "Wymiana akumulatora",
+      "description": "Wymiana i rejestracja akumulatora rozruchowego"
+    },
+    "diagnostics": {
+      "name": "Diagnostyka",
+      "description": "Odczyt kodów błędów i wstępna diagnoza"
+    },
+    "annualService": {
+      "name": "Przegląd roczny",
+      "description": "Pełny przegląd roczny zgodnie z harmonogramem"
+    }
+  },
+  "checklist": {
+    "title": "Pierwsze kroki",
+    "description": "Cztery kroki, aby uruchomić swój warsztat",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Dodaj klienta",
+        "description": "Utwórz pierwszą kartotekę klienta"
+      },
+      "vehicle": {
+        "title": "Dodaj pojazd",
+        "description": "Zarejestruj pojazd dla klienta"
+      },
+      "workOrder": {
+        "title": "Utwórz zlecenie",
+        "description": "Otwórz swoje pierwsze zlecenie"
+      },
+      "invoice": {
+        "title": "Wyślij lub pobierz fakturę",
+        "description": "Udostępnij fakturę klientowi lub pobierz ją jako PDF"
+      }
+    },
+    "allDoneTitle": "Wszystko gotowe",
+    "allDoneDescription": "Wszystkie kroki zostały wykonane i Twój warsztat działa.",
+    "dismiss": "Ukryj kartę",
+    "dismissAriaLabel": "Ukryj tę listę"
+  },
+  "sampleData": {
+    "title": "Dane przykładowe",
+    "description": "Twój warsztat nadal zawiera przykładowych klientów, pojazdy i zlecenia z konfiguracji. Usunięcie kasuje dokładnie te rekordy i nic więcej.",
+    "footerNote": "Wczytane są dane przykładowe",
+    "remove": "Usuń dane przykładowe",
+    "removeConfirmTitle": "Usunąć dane przykładowe?",
+    "removeConfirmDescription": "Spowoduje to usunięcie przykładowych klientów, pojazdów, zleceń, wyceny i przeglądu utworzonych podczas konfiguracji. Twoje własne dane pozostaną nietknięte.",
+    "removed": "Dane przykładowe usunięte",
+    "removeFailed": "Nie udało się usunąć danych przykładowych"
+  }
+}

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

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Mensagem agendada enviada",
     "settings_applyTaxRate": "Alíquota aplicada",
     "settings_convertToExclusive": "Preços convertidos para sem imposto",
-    "settings_convertToInclusive": "Preços convertidos para com imposto"
+    "settings_convertToInclusive": "Preços convertidos para com imposto",
+    "organization_sampleDataRemove": "Dados de exemplo removidos"
   }
 }

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

@@ -53,7 +53,8 @@
     "recentCompleted": "Concluídos Recentemente",
     "activeJobs": "Trabalhos Ativos",
     "recentObservations": "Observações recentes",
-    "recentActivity": "Atividade recente"
+    "recentActivity": "Atividade recente",
+    "gettingStarted": "Primeiros passos"
   },
   "maintenance": {
     "title": "Manutenção Prevista",

+ 91 - 0
messages/pt-BR/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Configure sua oficina",
+    "subtitle": "Digite o nome da sua oficina para começar",
+    "workshopName": "Nome da oficina",
+    "workshopNamePlaceholder": "Minha Oficina Mecânica",
+    "submit": "Começar",
+    "sampleDataLabel": "Carregar dados de exemplo",
+    "sampleDataDescription": "Adiciona alguns clientes, veículos e serviços de exemplo para você explorar. Você pode removê-los novamente com um clique.",
+    "setupFailed": "Falha na configuração",
+    "couldNotCreate": "Não foi possível criar a oficina",
+    "unexpectedError": "Ocorreu um erro inesperado"
+  },
+  "sample": {
+    "prefix": "Exemplo:",
+    "oilService": "Troca de óleo e filtro",
+    "brakeJob": "Substituição das pastilhas de freio dianteiras",
+    "annualService": "Revisão anual",
+    "quoteTitle": "Reforma da suspensão dianteira",
+    "inspectionNote": "O pneu dianteiro esquerdo está perto do limite de desgaste",
+    "parts": {
+      "oilFilter": "Filtro de óleo",
+      "engineOil": "Óleo de motor 5W-30",
+      "brakePads": "Jogo de pastilhas de freio dianteiras",
+      "shockAbsorber": "Amortecedor"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Troca de óleo",
+      "description": "Troca de óleo do motor e filtro"
+    },
+    "frontBrakePads": {
+      "name": "Pastilhas de freio dianteiras",
+      "description": "Substituir as pastilhas de freio dianteiras"
+    },
+    "tireSwap": {
+      "name": "Rodízio de pneus",
+      "description": "Troca sazonal de rodas com controle de torque"
+    },
+    "batteryReplacement": {
+      "name": "Substituição da bateria",
+      "description": "Substituir e registrar a bateria de partida"
+    },
+    "diagnostics": {
+      "name": "Diagnóstico",
+      "description": "Leitura de códigos de falha e diagnóstico inicial"
+    },
+    "annualService": {
+      "name": "Revisão anual",
+      "description": "Revisão anual completa conforme o plano"
+    }
+  },
+  "checklist": {
+    "title": "Primeiros passos",
+    "description": "Quatro passos para colocar sua oficina em funcionamento",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Adicionar um cliente",
+        "description": "Crie seu primeiro cadastro de cliente"
+      },
+      "vehicle": {
+        "title": "Adicionar um veículo",
+        "description": "Registre um veículo para um cliente"
+      },
+      "workOrder": {
+        "title": "Criar uma ordem de serviço",
+        "description": "Abra seu primeiro serviço"
+      },
+      "invoice": {
+        "title": "Enviar ou baixar uma fatura",
+        "description": "Compartilhe uma fatura com um cliente ou baixe em PDF"
+      }
+    },
+    "allDoneTitle": "Tudo pronto",
+    "allDoneDescription": "Todos os passos foram concluídos e sua oficina está em funcionamento.",
+    "dismiss": "Ocultar cartão",
+    "dismissAriaLabel": "Ocultar esta lista"
+  },
+  "sampleData": {
+    "title": "Dados de exemplo",
+    "description": "Sua oficina ainda contém os clientes, veículos e serviços de exemplo da configuração. A remoção exclui exatamente esses registros e nada mais.",
+    "footerNote": "Dados de exemplo carregados",
+    "remove": "Remover dados de exemplo",
+    "removeConfirmTitle": "Remover os dados de exemplo?",
+    "removeConfirmDescription": "Isso exclui os clientes, veículos, ordens de serviço, o orçamento e a inspeção de exemplo criados durante a configuração. Seus próprios dados não são afetados.",
+    "removed": "Dados de exemplo removidos",
+    "removeFailed": "Não foi possível remover os dados de exemplo"
+  }
+}

+ 2 - 1
messages/ru/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Отправлено запланированное сообщение",
     "settings_applyTaxRate": "Применена налоговая ставка",
     "settings_convertToExclusive": "Цены переведены в без налога",
-    "settings_convertToInclusive": "Цены переведены во с налогом"
+    "settings_convertToInclusive": "Цены переведены во с налогом",
+    "organization_sampleDataRemove": "Удалены демонстрационные данные"
   }
 }

+ 2 - 1
messages/ru/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Недавно завершённые",
     "activeJobs": "Активные работы",
     "recentObservations": "Недавние наблюдения",
-    "recentActivity": "Последняя активность"
+    "recentActivity": "Последняя активность",
+    "gettingStarted": "Первые шаги"
   },
   "maintenance": {
     "title": "Прогнозируемое техобслуживание",

+ 91 - 0
messages/ru/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Настройте свою мастерскую",
+    "subtitle": "Введите название мастерской, чтобы начать",
+    "workshopName": "Название мастерской",
+    "workshopNamePlaceholder": "Мой автосервис",
+    "submit": "Начать",
+    "sampleDataLabel": "Загрузить демонстрационные данные",
+    "sampleDataDescription": "Добавляет несколько примеров клиентов, автомобилей и заказов, чтобы вы могли осмотреться. Их можно удалить одним кликом.",
+    "setupFailed": "Не удалось выполнить настройку",
+    "couldNotCreate": "Не удалось создать мастерскую",
+    "unexpectedError": "Произошла непредвиденная ошибка"
+  },
+  "sample": {
+    "prefix": "Пример:",
+    "oilService": "Замена масла и фильтра",
+    "brakeJob": "Замена передних тормозных колодок",
+    "annualService": "Годовое техобслуживание",
+    "quoteTitle": "Ремонт передней подвески",
+    "inspectionNote": "Левая передняя шина близка к пределу износа",
+    "parts": {
+      "oilFilter": "Масляный фильтр",
+      "engineOil": "Моторное масло 5W-30",
+      "brakePads": "Комплект передних тормозных колодок",
+      "shockAbsorber": "Амортизатор"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Замена масла",
+      "description": "Замена моторного масла и фильтра"
+    },
+    "frontBrakePads": {
+      "name": "Передние тормозные колодки",
+      "description": "Замена передних тормозных колодок"
+    },
+    "tireSwap": {
+      "name": "Сезонная замена колес",
+      "description": "Сезонная замена колес с контролем момента затяжки"
+    },
+    "batteryReplacement": {
+      "name": "Замена аккумулятора",
+      "description": "Замена и регистрация стартерного аккумулятора"
+    },
+    "diagnostics": {
+      "name": "Диагностика",
+      "description": "Считывание кодов неисправностей и первичная диагностика"
+    },
+    "annualService": {
+      "name": "Годовое техобслуживание",
+      "description": "Полное годовое техобслуживание по регламенту"
+    }
+  },
+  "checklist": {
+    "title": "Первые шаги",
+    "description": "Четыре шага, чтобы запустить вашу мастерскую",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Добавьте клиента",
+        "description": "Создайте первую карточку клиента"
+      },
+      "vehicle": {
+        "title": "Добавьте автомобиль",
+        "description": "Зарегистрируйте автомобиль клиента"
+      },
+      "workOrder": {
+        "title": "Создайте заказ-наряд",
+        "description": "Откройте свой первый заказ"
+      },
+      "invoice": {
+        "title": "Отправьте или скачайте счет",
+        "description": "Поделитесь счетом с клиентом или скачайте его в PDF"
+      }
+    },
+    "allDoneTitle": "Все готово",
+    "allDoneDescription": "Все шаги выполнены, ваша мастерская работает.",
+    "dismiss": "Скрыть карточку",
+    "dismissAriaLabel": "Скрыть этот список"
+  },
+  "sampleData": {
+    "title": "Демонстрационные данные",
+    "description": "В вашей мастерской еще остались примеры клиентов, автомобилей и заказов из настройки. При удалении будут стерты именно эти записи и ничего больше.",
+    "footerNote": "Загружены демонстрационные данные",
+    "remove": "Удалить демонстрационные данные",
+    "removeConfirmTitle": "Удалить демонстрационные данные?",
+    "removeConfirmDescription": "Будут удалены примеры клиентов, автомобилей, заказ-нарядов, смета и осмотр, созданные при настройке. Ваши собственные данные не пострадают.",
+    "removed": "Демонстрационные данные удалены",
+    "removeFailed": "Не удалось удалить демонстрационные данные"
+  }
+}

+ 2 - 1
messages/tr/audit.json

@@ -108,6 +108,7 @@
     "scheduled_message_send_now": "Zamanlanmış mesaj gönderildi",
     "settings_applyTaxRate": "Vergi oranı uygulandı",
     "settings_convertToExclusive": "Fiyatlar vergi hariç yapıldı",
-    "settings_convertToInclusive": "Fiyatlar vergi dahil yapıldı"
+    "settings_convertToInclusive": "Fiyatlar vergi dahil yapıldı",
+    "organization_sampleDataRemove": "Örnek veriler kaldırıldı"
   }
 }

+ 2 - 1
messages/tr/dashboard.json

@@ -53,7 +53,8 @@
     "recentCompleted": "Son Tamamlananlar",
     "activeJobs": "Aktif İşler",
     "recentObservations": "Son Gözlemler",
-    "recentActivity": "Son Etkinlik"
+    "recentActivity": "Son Etkinlik",
+    "gettingStarted": "Başlarken"
   },
   "maintenance": {
     "title": "Tahmini Bakım Zamanı",

+ 91 - 0
messages/tr/onboarding.json

@@ -0,0 +1,91 @@
+{
+  "form": {
+    "title": "Atölyenizi kurun",
+    "subtitle": "Başlamak için atölyenizin adını girin",
+    "workshopName": "Atölye adı",
+    "workshopNamePlaceholder": "Benim Oto Servisim",
+    "submit": "Başla",
+    "sampleDataLabel": "Örnek verileri yükle",
+    "sampleDataDescription": "Etrafı keşfedebilmeniz için birkaç örnek müşteri, araç ve iş ekler. Tek tıkla tekrar kaldırabilirsiniz.",
+    "setupFailed": "Kurulum başarısız",
+    "couldNotCreate": "Atölye oluşturulamadı",
+    "unexpectedError": "Beklenmeyen bir hata oluştu"
+  },
+  "sample": {
+    "prefix": "Örnek:",
+    "oilService": "Yağ ve filtre değişimi",
+    "brakeJob": "Ön fren balatalarının değişimi",
+    "annualService": "Yıllık bakım",
+    "quoteTitle": "Ön süspansiyon yenileme",
+    "inspectionNote": "Sol ön lastik aşınma sınırına yakın",
+    "parts": {
+      "oilFilter": "Yağ filtresi",
+      "engineOil": "Motor yağı 5W-30",
+      "brakePads": "Ön fren balata takımı",
+      "shockAbsorber": "Amortisör"
+    }
+  },
+  "presets": {
+    "oilChange": {
+      "name": "Yağ değişimi",
+      "description": "Motor yağı ve filtre değişimi"
+    },
+    "frontBrakePads": {
+      "name": "Ön fren balataları",
+      "description": "Ön fren balatalarını değiştirme"
+    },
+    "tireSwap": {
+      "name": "Lastik değişimi",
+      "description": "Tork kontrolüyle mevsimlik lastik değişimi"
+    },
+    "batteryReplacement": {
+      "name": "Akü değişimi",
+      "description": "Marş aküsünü değiştirme ve tanıtma"
+    },
+    "diagnostics": {
+      "name": "Arıza tespiti",
+      "description": "Arıza kodlarının okunması ve ilk teşhis"
+    },
+    "annualService": {
+      "name": "Yıllık bakım",
+      "description": "Programa göre eksiksiz yıllık bakım"
+    }
+  },
+  "checklist": {
+    "title": "Başlarken",
+    "description": "Atölyenizi çalışır hale getirmek için dört adım",
+    "progress": "{done}/{total}",
+    "steps": {
+      "customer": {
+        "title": "Müşteri ekleyin",
+        "description": "İlk müşteri kaydınızı oluşturun"
+      },
+      "vehicle": {
+        "title": "Araç ekleyin",
+        "description": "Bir müşteri için araç kaydedin"
+      },
+      "workOrder": {
+        "title": "İş emri oluşturun",
+        "description": "İlk işinizi açın"
+      },
+      "invoice": {
+        "title": "Fatura gönderin veya indirin",
+        "description": "Bir faturayı müşteriyle paylaşın veya PDF olarak indirin"
+      }
+    },
+    "allDoneTitle": "Her şey hazır",
+    "allDoneDescription": "Tüm adımlar tamamlandı ve atölyeniz çalışıyor.",
+    "dismiss": "Kartı gizle",
+    "dismissAriaLabel": "Bu listeyi gizle"
+  },
+  "sampleData": {
+    "title": "Örnek veriler",
+    "description": "Atölyenizde kurulumdan kalan örnek müşteriler, araçlar ve işler hâlâ duruyor. Kaldırma işlemi yalnızca bu kayıtları siler, başka hiçbir şeyi silmez.",
+    "footerNote": "Örnek veriler yüklü",
+    "remove": "Örnek verileri kaldır",
+    "removeConfirmTitle": "Örnek veriler kaldırılsın mı?",
+    "removeConfirmDescription": "Bu işlem kurulum sırasında oluşturulan örnek müşterileri, araçları, iş emirlerini, teklifi ve muayeneyi siler. Kendi verileriniz etkilenmez.",
+    "removed": "Örnek veriler kaldırıldı",
+    "removeFailed": "Örnek veriler kaldırılamadı"
+  }
+}

+ 229 - 0
src/__tests__/features/onboarding/sample-data.test.ts

@@ -0,0 +1,229 @@
+/**
+ * Tests for the first-run sample data lifecycle.
+ *
+ * The seed records every id it creates in one AppSetting row; removal must
+ * delete exactly those ids (scoped to the organization), in an order that
+ * respects the FK graph, and then clear the row. The checklist must exclude
+ * the sample ids from its counts, so the steps only complete on real data.
+ */
+
+import { describe, it, expect, vi, beforeEach } from "vitest";
+
+vi.mock("@/lib/cached-session", () => ({
+  getCachedSession: vi.fn(),
+  getCachedMembership: vi.fn(),
+}));
+
+vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
+
+vi.mock("@/lib/db", () => ({
+  db: {
+    user: { findUnique: vi.fn() },
+    appSetting: {
+      findFirst: vi.fn(),
+      findMany: vi.fn(),
+      delete: vi.fn(),
+      upsert: vi.fn(),
+    },
+    customer: { count: vi.fn(), deleteMany: vi.fn() },
+    vehicle: { count: vi.fn(), deleteMany: vi.fn() },
+    serviceRecord: { count: vi.fn(), deleteMany: vi.fn() },
+    quote: { deleteMany: vi.fn() },
+    inspection: { deleteMany: vi.fn() },
+    $transaction: vi.fn(),
+  },
+}));
+
+import { getCachedSession, getCachedMembership } from "@/lib/cached-session";
+import { db } from "@/lib/db";
+import {
+  getOnboardingChecklist,
+  removeSampleData,
+} from "@/features/onboarding/Actions/checklistActions";
+import {
+  parseSampleDataIds,
+  hasAnySampleIds,
+  EMPTY_SAMPLE_IDS,
+} from "@/features/onboarding/Lib/onboardingKeys";
+
+const ORG = "org-1";
+const USER_ID = "user-1";
+
+const SAMPLE_IDS = {
+  customers: ["c1", "c2", "c3"],
+  vehicles: ["v1", "v2", "v3", "v4"],
+  serviceRecords: ["s1", "s2", "s3"],
+  quotes: ["q1"],
+  inspections: ["i1"],
+};
+
+function setupAuth() {
+  vi.mocked(getCachedSession).mockResolvedValue({
+    user: { id: USER_ID, email: "user@example.com" },
+  } as never);
+  vi.mocked(getCachedMembership).mockResolvedValue({
+    organizationId: ORG,
+    role: "owner",
+    roleId: null,
+    customRole: null,
+  } as never);
+  vi.mocked(db.user.findUnique).mockResolvedValue({
+    isSuperAdmin: false,
+  } as never);
+}
+
+beforeEach(() => {
+  vi.clearAllMocks();
+  setupAuth();
+});
+
+describe("parseSampleDataIds", () => {
+  it("parses a recorded id set", () => {
+    const ids = parseSampleDataIds(JSON.stringify(SAMPLE_IDS));
+    expect(ids).toEqual(SAMPLE_IDS);
+    expect(hasAnySampleIds(ids)).toBe(true);
+  });
+
+  it("survives missing, malformed and partial values", () => {
+    expect(parseSampleDataIds(null)).toEqual(EMPTY_SAMPLE_IDS);
+    expect(parseSampleDataIds("not json")).toEqual(EMPTY_SAMPLE_IDS);
+    expect(parseSampleDataIds('{"customers": ["a", 5]}')).toEqual({
+      ...EMPTY_SAMPLE_IDS,
+      customers: ["a"],
+    });
+    expect(hasAnySampleIds(parseSampleDataIds(null))).toBe(false);
+  });
+});
+
+describe("removeSampleData", () => {
+  it("deletes exactly the recorded ids, org-scoped, and clears the marker", async () => {
+    vi.mocked(db.appSetting.findFirst).mockResolvedValue({
+      id: "setting-1",
+      value: JSON.stringify(SAMPLE_IDS),
+    } as never);
+    vi.mocked(db.$transaction).mockResolvedValue([] as never);
+
+    const result = await removeSampleData();
+    expect(result.success).toBe(true);
+    expect(result.data).toEqual({ removed: true });
+
+    expect(db.serviceRecord.deleteMany).toHaveBeenCalledWith({
+      where: { organizationId: ORG, id: { in: SAMPLE_IDS.serviceRecords } },
+    });
+    expect(db.quote.deleteMany).toHaveBeenCalledWith({
+      where: { organizationId: ORG, id: { in: SAMPLE_IDS.quotes } },
+    });
+    expect(db.inspection.deleteMany).toHaveBeenCalledWith({
+      where: { organizationId: ORG, id: { in: SAMPLE_IDS.inspections } },
+    });
+    expect(db.vehicle.deleteMany).toHaveBeenCalledWith({
+      where: { organizationId: ORG, id: { in: SAMPLE_IDS.vehicles } },
+    });
+    expect(db.customer.deleteMany).toHaveBeenCalledWith({
+      where: { organizationId: ORG, id: { in: SAMPLE_IDS.customers } },
+    });
+    expect(db.appSetting.delete).toHaveBeenCalledWith({
+      where: { id: "setting-1" },
+    });
+    // Everything runs inside one transaction.
+    expect(db.$transaction).toHaveBeenCalledTimes(1);
+  });
+
+  it("is a no-op when nothing was recorded", async () => {
+    vi.mocked(db.appSetting.findFirst).mockResolvedValue(null as never);
+
+    const result = await removeSampleData();
+    expect(result.success).toBe(true);
+    expect(result.data).toEqual({ removed: false });
+    expect(db.$transaction).not.toHaveBeenCalled();
+  });
+});
+
+describe("getOnboardingChecklist", () => {
+  it("excludes sample ids from step detection", async () => {
+    vi.mocked(db.appSetting.findMany).mockResolvedValue([
+      { key: "onboarding.checklistDismissed", value: "false" },
+      { key: "onboarding.sampleDataIds", value: JSON.stringify(SAMPLE_IDS) },
+    ] as never);
+    // Only the sample rows exist, so every real count is zero.
+    vi.mocked(db.customer.count).mockResolvedValue(0 as never);
+    vi.mocked(db.vehicle.count).mockResolvedValue(0 as never);
+    vi.mocked(db.serviceRecord.count).mockResolvedValue(0 as never);
+
+    const result = await getOnboardingChecklist();
+    expect(result.success).toBe(true);
+    expect(result.data).toEqual({
+      steps: { customer: false, vehicle: false, workOrder: false, invoice: false },
+      allDone: false,
+      hasSampleData: true,
+    });
+
+    expect(db.customer.count).toHaveBeenCalledWith({
+      where: { organizationId: ORG, id: { notIn: SAMPLE_IDS.customers } },
+    });
+    expect(db.serviceRecord.count).toHaveBeenCalledWith({
+      where: { organizationId: ORG, id: { notIn: SAMPLE_IDS.serviceRecords } },
+    });
+  });
+
+  it("hides the card once dismissed", async () => {
+    vi.mocked(db.appSetting.findMany).mockResolvedValue([
+      { key: "onboarding.checklistDismissed", value: "true" },
+    ] as never);
+
+    const result = await getOnboardingChecklist();
+    expect(result.success).toBe(true);
+    expect(result.data).toBeNull();
+    expect(db.customer.count).not.toHaveBeenCalled();
+  });
+
+  it("hides the card for pre-existing orgs that already do everything", async () => {
+    // No onboarding rows at all: an org that predates the checklist.
+    vi.mocked(db.appSetting.findMany).mockResolvedValue([] as never);
+    vi.mocked(db.customer.count).mockResolvedValue(12 as never);
+    vi.mocked(db.vehicle.count).mockResolvedValue(30 as never);
+    vi.mocked(db.serviceRecord.count).mockResolvedValue(80 as never);
+
+    const result = await getOnboardingChecklist();
+    expect(result.success).toBe(true);
+    expect(result.data).toBeNull();
+  });
+
+  it("shows open steps for pre-existing orgs with partial data", async () => {
+    vi.mocked(db.appSetting.findMany).mockResolvedValue([] as never);
+    vi.mocked(db.customer.count).mockResolvedValue(3 as never);
+    vi.mocked(db.vehicle.count).mockResolvedValue(2 as never);
+    // First serviceRecord.count call: any work order; second: shared invoices.
+    vi.mocked(db.serviceRecord.count)
+      .mockResolvedValueOnce(1 as never)
+      .mockResolvedValueOnce(0 as never);
+
+    const result = await getOnboardingChecklist();
+    expect(result.success).toBe(true);
+    expect(result.data).toEqual({
+      steps: { customer: true, vehicle: true, workOrder: true, invoice: false },
+      allDone: false,
+      hasSampleData: false,
+    });
+  });
+
+  it("completes the invoice step from the download marker", async () => {
+    vi.mocked(db.appSetting.findMany).mockResolvedValue([
+      { key: "onboarding.checklistDismissed", value: "false" },
+      { key: "onboarding.invoiceIssued", value: "true" },
+    ] as never);
+    vi.mocked(db.customer.count).mockResolvedValue(1 as never);
+    vi.mocked(db.vehicle.count).mockResolvedValue(1 as never);
+    vi.mocked(db.serviceRecord.count)
+      .mockResolvedValueOnce(1 as never)
+      .mockResolvedValueOnce(0 as never);
+
+    const result = await getOnboardingChecklist();
+    expect(result.success).toBe(true);
+    expect(result.data).toEqual({
+      steps: { customer: true, vehicle: true, workOrder: true, invoice: true },
+      allDone: true,
+      hasSampleData: false,
+    });
+  });
+});

+ 14 - 1
src/app/(authenticated)/dashboard-client.tsx

@@ -79,6 +79,8 @@ import { CustomCardDialog } from "@/features/dashboard/Components/CustomCardDial
 import { deleteDashboardWidget } from "@/features/dashboard/Actions/customCardActions";
 import { customCardId, type CustomWidget } from "@/features/dashboard/custom-cards/registry";
 import { useConfirm } from "@/components/confirm-dialog";
+import { GettingStartedCard } from "@/features/onboarding/Components/GettingStartedCard";
+import type { OnboardingChecklistData } from "@/features/onboarding/Actions/checklistActions";
 
 interface ServiceItem {
   id: string;
@@ -263,6 +265,7 @@ export function DashboardClient({
   recentObservations = [],
   initialLayout = null,
   customWidgets = [],
+  onboardingChecklist = null,
 }: {
   stats: DashboardStats;
   currencyCode?: string;
@@ -291,6 +294,7 @@ export function DashboardClient({
   recentObservations?: DashboardObservation[];
   initialLayout?: unknown;
   customWidgets?: CustomWidget[];
+  onboardingChecklist?: OnboardingChecklistData | null;
 }) {
   const formatCurrency = useFormatCurrency();
   const t = useTranslations("dashboard");
@@ -318,7 +322,12 @@ export function DashboardClient({
   const [cardDialogOpen, setCardDialogOpen] = useState(false);
   const [editingWidget, setEditingWidget] = useState<CustomWidget | null>(null);
   const excludedCard: DashboardCardId = smsEnabled ? "notifications" : "sms";
-  const availableIds = DASHBOARD_CARD_IDS.filter((id) => id !== excludedCard);
+  // The getting-started card is offered only while the server says it should
+  // show (new org, steps open or done state not yet dismissed).
+  const excludedIds = new Set<DashboardCardId>(
+    onboardingChecklist ? [excludedCard] : [excludedCard, "gettingStarted"]
+  );
+  const availableIds = DASHBOARD_CARD_IDS.filter((id) => !excludedIds.has(id));
   const hiddenSet = new Set<string>(layout.hidden);
   const visibleIds = [
     ...availableIds,
@@ -545,6 +554,10 @@ export function DashboardClient({
           user's saved arrangement */}
       {(() => {
         const cardNodes: Partial<Record<string, ReactNode>> = {
+        // First-run checklist
+        ...(onboardingChecklist
+          ? { gettingStarted: <GettingStartedCard data={onboardingChecklist} /> }
+          : {}),
         // Vehicles Due for Service
         maintenance: (
           <AppCard

+ 236 - 188
src/app/(authenticated)/inspections/inspections-client.tsx

@@ -1,76 +1,90 @@
-"use client";
+'use client'
 
-import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
-import { interactiveRow } from '@/lib/interactive-row';
-import { useDebouncedSearch } from '@/hooks/use-debounced-search';
+import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
+import { interactiveRow } from '@/lib/interactive-row'
+import { useDebouncedSearch } from '@/hooks/use-debounced-search'
 
-import { useState, useCallback, useTransition } from "react";
-import { useRouter, usePathname, useSearchParams } from "next/navigation";
-import { useFormatDate } from "@/lib/use-format-date";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
+import { useState, useCallback, useTransition } from 'react'
+import { useRouter, usePathname, useSearchParams } from 'next/navigation'
+import { useFormatDate } from '@/lib/use-format-date'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
 import {
-  Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
-} from "@/components/ui/table";
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/components/ui/table'
 import {
   ContextMenu,
   ContextMenuContent,
   ContextMenuItem,
   ContextMenuTrigger,
-} from "@/components/ui/context-menu";
-import { DataTablePagination } from "@/components/data-table-pagination";
-import { TableContextMenuHint } from "@/components/table-context-menu-hint";
-import { TableCellLink } from "@/components/table-cell-link";
-import { ArrowDown, ArrowUp, ArrowUpDown, Car, ExternalLink, Loader2, Plus, Search } from "lucide-react";
-import { useTranslations } from "next-intl";
-import { NewInspectionDialog } from "@/features/inspections/Components/NewInspectionDialog";
+} from '@/components/ui/context-menu'
+import { DataTablePagination } from '@/components/data-table-pagination'
+import { TableContextMenuHint } from '@/components/table-context-menu-hint'
+import { TableCellLink } from '@/components/table-cell-link'
+import {
+  ArrowDown,
+  ArrowUp,
+  ArrowUpDown,
+  Car,
+  ExternalLink,
+  Loader2,
+  Plus,
+  Search,
+} from 'lucide-react'
+import { useTranslations } from 'next-intl'
+import { NewInspectionDialog } from '@/features/inspections/Components/NewInspectionDialog'
 import {
   CONDITION_TOKENS,
   countConditions,
   type Condition,
   type SeverityScale,
-} from "@/features/inspections/Lib/conditions";
-import { useConditionLabels } from "@/features/inspections/Lib/useConditionLabels";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+} from '@/features/inspections/Lib/conditions'
+import { useConditionLabels } from '@/features/inspections/Lib/useConditionLabels'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
 
 interface InspectionRecord {
-  id: string;
-  status: string;
-  mileage: number | null;
-  createdAt: Date;
-  completedAt: Date | null;
-  severityScale: string | null;
-  vehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null };
-  template: { id: string; name: string; severityScale: string | null };
-  items: { id: string; condition: string }[];
+  id: string
+  status: string
+  mileage: number | null
+  createdAt: Date
+  completedAt: Date | null
+  severityScale: string | null
+  vehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null }
+  template: { id: string; name: string; severityScale: string | null }
+  items: { id: string; condition: string }[]
 }
 
 interface PaginatedData {
-  records: InspectionRecord[];
-  total: number;
-  page: number;
-  pageSize: number;
-  totalPages: number;
-  statusCounts: Record<string, number>;
+  records: InspectionRecord[]
+  total: number
+  page: number
+  pageSize: number
+  totalPages: number
+  statusCounts: Record<string, number>
 }
 
 interface TemplateOption {
-  id: string;
-  name: string;
-  isDefault: boolean;
+  id: string
+  name: string
+  isDefault: boolean
 }
 
 const statusTabs = [
-  { key: "all", labelKey: "tabAll" },
-  { key: "in_progress", labelKey: "tabInProgress" },
-  { key: "completed", labelKey: "tabCompleted" },
-] as const;
+  { key: 'all', labelKey: 'tabAll' },
+  { key: 'in_progress', labelKey: 'tabInProgress' },
+  { key: 'completed', labelKey: 'tabCompleted' },
+] as const
 
 const statusColors: Record<string, string> = {
-  in_progress: "bg-blue-500/10 text-blue-500 border-blue-500/20",
-  completed: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20",
-};
+  in_progress: 'bg-blue-500/10 text-blue-500 border-blue-500/20',
+  completed: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20',
+}
 
 /**
  * The rail is a breakdown of the grades given, not a completion meter: a full
@@ -82,40 +96,47 @@ function InspectionProgress({
   items,
   scale,
 }: {
-  items: { condition: string }[];
-  scale: SeverityScale;
+  items: { condition: string }[]
+  scale: SeverityScale
 }) {
-  const t = useTranslations("inspections.list");
-  const { label: gradeLabel } = useConditionLabels(scale);
-  const counts = countConditions(items);
-  if (items.length === 0) return null;
+  const t = useTranslations('inspections.list')
+  const { label: gradeLabel } = useConditionLabels(scale)
+  const counts = countConditions(items)
+  if (items.length === 0) return null
 
-  const summary = t("progressTooltip", { graded: counts.inspected, total: counts.total });
-  const legend = ([
-    ["pass", counts.pass],
-    ["attention", counts.attention],
-    ["fail", counts.fail],
-    ["dangerous", counts.dangerous],
-    ["not_inspected", counts.notInspected],
-  ] as const).filter(([, value]) => value > 0);
+  const summary = t('progressTooltip', { graded: counts.inspected, total: counts.total })
+  const legend = (
+    [
+      ['pass', counts.pass],
+      ['attention', counts.attention],
+      ['fail', counts.fail],
+      ['dangerous', counts.dangerous],
+      ['not_inspected', counts.notInspected],
+    ] as const
+  ).filter(([, value]) => value > 0)
 
   return (
     <Tooltip>
-      <TooltipTrigger
-        className="group flex cursor-help items-center gap-1.5 rounded-sm focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
-        aria-label={summary}
-      >
-        <div className="ring-offset-background group-hover:ring-foreground/25 flex h-2 w-20 overflow-hidden rounded-full bg-gray-200 ring-offset-1 transition-shadow group-hover:ring-2 dark:bg-gray-700">
-          {(["pass", "attention", "fail", "dangerous"] as const).map((c) => {
-            const pct = (counts[c] / items.length) * 100;
-            if (pct === 0) return null;
-            return (
-              <div key={c} className={CONDITION_TOKENS[c].bar} style={{ width: `${pct}%` }} />
-            );
-          })}
-        </div>
-        <span className="text-muted-foreground group-hover:text-foreground text-xs">
-          {counts.inspected}/{counts.total}
+      {/* asChild + span: the mobile list renders this inside a card <button>,
+          and a nested <button> is invalid HTML that breaks hydration. */}
+      <TooltipTrigger asChild>
+        <span
+          tabIndex={0}
+          aria-label={summary}
+          className="group flex cursor-help items-center gap-1.5 rounded-sm focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
+        >
+          <div className="ring-offset-background group-hover:ring-foreground/25 flex h-2 w-20 overflow-hidden rounded-full bg-gray-200 ring-offset-1 transition-shadow group-hover:ring-2 dark:bg-gray-700">
+            {(['pass', 'attention', 'fail', 'dangerous'] as const).map((c) => {
+              const pct = (counts[c] / items.length) * 100
+              if (pct === 0) return null
+              return (
+                <div key={c} className={CONDITION_TOKENS[c].bar} style={{ width: `${pct}%` }} />
+              )
+            })}
+          </div>
+          <span className="text-muted-foreground group-hover:text-foreground text-xs">
+            {counts.inspected}/{counts.total}
+          </span>
         </span>
       </TooltipTrigger>
       <TooltipContent side="top" className="px-3 py-2">
@@ -125,7 +146,9 @@ function InspectionProgress({
             <li key={condition} className="flex items-center gap-2">
               <span
                 className={`h-2 w-2 shrink-0 rounded-full ${
-                  condition === "not_inspected" ? "bg-background/40" : CONDITION_TOKENS[condition].bar
+                  condition === 'not_inspected'
+                    ? 'bg-background/40'
+                    : CONDITION_TOKENS[condition].bar
                 }`}
                 aria-hidden="true"
               />
@@ -136,7 +159,7 @@ function InspectionProgress({
         </ul>
       </TooltipContent>
     </Tooltip>
-  );
+  )
 }
 
 export function InspectionsClient({
@@ -144,43 +167,43 @@ export function InspectionsClient({
   templates,
   search,
   statusFilter,
-  sortBy = "",
-  sortOrder = "desc",
+  sortBy = '',
+  sortOrder = 'desc',
 }: {
-  data: PaginatedData;
-  templates: TemplateOption[];
-  search: string;
-  statusFilter: string;
-  sortBy?: string;
-  sortOrder?: "asc" | "desc";
+  data: PaginatedData
+  templates: TemplateOption[]
+  search: string
+  statusFilter: string
+  sortBy?: string
+  sortOrder?: 'asc' | 'desc'
 }) {
-  const router = useRouter();
-  const { formatDate } = useFormatDate();
-  const pathname = usePathname();
-  const searchParams = useSearchParams();
-  const [isPending, startTransition] = useTransition();
-  const tableNav = useTableKeyboardNav();
-  const [showNewDialog, setShowNewDialog] = useState(false);
-  const tcm = useTranslations("common.contextMenu");
-  const t = useTranslations("inspections.list");
+  const router = useRouter()
+  const { formatDate } = useFormatDate()
+  const pathname = usePathname()
+  const searchParams = useSearchParams()
+  const [isPending, startTransition] = useTransition()
+  const tableNav = useTableKeyboardNav()
+  const [showNewDialog, setShowNewDialog] = useState(false)
+  const tcm = useTranslations('common.contextMenu')
+  const t = useTranslations('inspections.list')
 
   const navigate = useCallback(
     (params: Record<string, string | number | undefined>) => {
-      const newParams = new URLSearchParams(searchParams.toString());
+      const newParams = new URLSearchParams(searchParams.toString())
       for (const [key, value] of Object.entries(params)) {
-        if (value === undefined || value === "") {
-          newParams.delete(key);
+        if (value === undefined || value === '') {
+          newParams.delete(key)
         } else {
-          newParams.set(key, String(value));
+          newParams.set(key, String(value))
         }
       }
-      if (!("page" in params)) newParams.delete("page");
+      if (!('page' in params)) newParams.delete('page')
       startTransition(() => {
-        router.push(`${pathname}?${newParams.toString()}`);
-      });
+        router.push(`${pathname}?${newParams.toString()}`)
+      })
     },
     [router, pathname, searchParams]
-  );
+  )
 
   // Live search: filters as you type, no Enter required. Submitting the
   // form (Enter) commits immediately, bypassing the debounce.
@@ -188,37 +211,39 @@ export function InspectionsClient({
     value: searchInput,
     setValue: setSearchInput,
     commitNow: handleSearch,
-  } = useDebouncedSearch(search, (term) => navigate({ search: term }));
+  } = useDebouncedSearch(search, (term) => navigate({ search: term }))
 
   const handleSort = useCallback(
     (column: string) => {
-      const newOrder = sortBy === column && sortOrder === "asc" ? "desc" : "asc";
-      navigate({ sortBy: column, sortOrder: newOrder });
+      const newOrder = sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc'
+      navigate({ sortBy: column, sortOrder: newOrder })
     },
     [navigate, sortBy, sortOrder]
-  );
+  )
 
   const SortIcon = ({ column }: { column: string }) => {
-    if (sortBy !== column) return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />;
-    return sortOrder === "asc"
-      ? <ArrowUp className="ml-1 h-3 w-3" />
-      : <ArrowDown className="ml-1 h-3 w-3" />;
-  };
+    if (sortBy !== column) return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />
+    return sortOrder === 'asc' ? (
+      <ArrowUp className="ml-1 h-3 w-3" />
+    ) : (
+      <ArrowDown className="ml-1 h-3 w-3" />
+    )
+  }
 
   return (
     <div className="space-y-4">
       {/* Status filters: one scrollable row on phones, wrapped above sm. */}
       <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
         {statusTabs.map((tab) => {
-          const isActive = statusFilter === tab.key;
-          const count = tab.key === "all" ? undefined : data.statusCounts[tab.key] || 0;
+          const isActive = statusFilter === tab.key
+          const count = tab.key === 'all' ? undefined : data.statusCounts[tab.key] || 0
           return (
             <Button
               key={tab.key}
-              variant={isActive ? "default" : "outline"}
+              variant={isActive ? 'default' : 'outline'}
               size="sm"
               className="h-9 shrink-0 sm:h-8"
-              onClick={() => navigate({ status: tab.key === "all" ? undefined : tab.key })}
+              onClick={() => navigate({ status: tab.key === 'all' ? undefined : tab.key })}
             >
               {t(tab.labelKey)}
               {count !== undefined && (
@@ -227,7 +252,7 @@ export function InspectionsClient({
                 </Badge>
               )}
             </Button>
-          );
+          )
         })}
       </div>
 
@@ -236,7 +261,7 @@ export function InspectionsClient({
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
             <Input
-              placeholder={t("search")}
+              placeholder={t('search')}
               value={searchInput}
               onChange={(e) => setSearchInput(e.target.value)}
               className="h-9 pl-9"
@@ -248,12 +273,12 @@ export function InspectionsClient({
         <Button
           size="sm"
           onClick={() => setShowNewDialog(true)}
-          aria-label={t("new")}
-          title={t("new")}
+          aria-label={t('new')}
+          title={t('new')}
           className="h-9 w-9 shrink-0 p-0 md:h-8 md:w-auto md:px-3"
         >
           <Plus className="h-4 w-4 md:mr-1 md:h-3.5 md:w-3.5" />
-          <span className="hidden md:inline">{t("new")}</span>
+          <span className="hidden md:inline">{t('new')}</span>
         </Button>
       </div>
 
@@ -261,7 +286,7 @@ export function InspectionsClient({
       <div className="space-y-2 md:hidden">
         {data.records.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
-            {t("empty")}
+            {t('empty')}
           </div>
         ) : (
           data.records.map((insp) => (
@@ -277,9 +302,9 @@ export function InspectionsClient({
                 </span>
                 <Badge
                   variant="outline"
-                  className={`shrink-0 text-xs ${statusColors[insp.status] || ""}`}
+                  className={`shrink-0 text-xs ${statusColors[insp.status] || ''}`}
                 >
-                  {insp.status === "in_progress" ? t("statusInProgress") : t("statusCompleted")}
+                  {insp.status === 'in_progress' ? t('statusInProgress') : t('statusCompleted')}
                 </Badge>
               </div>
               <p className="truncate text-xs text-muted-foreground">
@@ -292,9 +317,7 @@ export function InspectionsClient({
                 <InspectionProgress
                   items={insp.items}
                   scale={
-                    (insp.severityScale ?? insp.template.severityScale) === "basic"
-                      ? "basic"
-                      : "eu"
+                    (insp.severityScale ?? insp.template.severityScale) === 'basic' ? 'basic' : 'eu'
                   }
                 />
                 <span className="font-mono text-xs text-muted-foreground">
@@ -313,24 +336,44 @@ export function InspectionsClient({
           <TableHeader>
             <TableRow>
               <TableHead>
-                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("vehicle")}>
-                  {t("vehicle")}<SortIcon column="vehicle" />
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('vehicle')}
+                >
+                  {t('vehicle')}
+                  <SortIcon column="vehicle" />
                 </button>
               </TableHead>
               <TableHead className="hidden w-[24%] md:table-cell">
-                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("template")}>
-                  {t("template")}<SortIcon column="template" />
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('template')}
+                >
+                  {t('template')}
+                  <SortIcon column="template" />
                 </button>
               </TableHead>
-              <TableHead className="w-32">{t("progress")}</TableHead>
+              <TableHead className="w-32">{t('progress')}</TableHead>
               <TableHead className="w-28">
-                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("status")}>
-                  {t("status")}<SortIcon column="status" />
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('status')}
+                >
+                  {t('status')}
+                  <SortIcon column="status" />
                 </button>
               </TableHead>
               <TableHead className="w-24">
-                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("createdAt")}>
-                  {t("date")}<SortIcon column="createdAt" />
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('createdAt')}
+                >
+                  {t('date')}
+                  <SortIcon column="createdAt" />
                 </button>
               </TableHead>
             </TableRow>
@@ -339,62 +382,67 @@ export function InspectionsClient({
             {data.records.length === 0 ? (
               <TableRow>
                 <TableCell colSpan={5} className="h-32 text-center text-muted-foreground">
-                  {t("empty")}
+                  {t('empty')}
                 </TableCell>
               </TableRow>
             ) : (
               data.records.map((insp) => (
                 <ContextMenu key={insp.id} modal={false}>
-                <ContextMenuTrigger asChild>
-                <TableRow
-                  className="cursor-pointer"
-                  {...interactiveRow(() => router.push(`/inspections/${insp.id}`))}
-                >
-                  <TableCell>
-                    <TableCellLink href={`/vehicles/${insp.vehicle.id}`} block>
-                      <p className="truncate font-medium">
-                        {insp.vehicle.year} {insp.vehicle.make} {insp.vehicle.model}
-                      </p>
-                      {insp.vehicle.licensePlate && (
-                        <p className="text-xs text-muted-foreground font-mono">
-                          {insp.vehicle.licensePlate}
-                        </p>
-                      )}
-                    </TableCellLink>
-                  </TableCell>
-                  <TableCell className="hidden truncate md:table-cell text-muted-foreground">
-                    {insp.template.name}
-                  </TableCell>
-                  <TableCell>
-                    <InspectionProgress
-                      items={insp.items}
-                      scale={
-                        (insp.severityScale ?? insp.template.severityScale) === "basic"
-                          ? "basic"
-                          : "eu"
-                      }
-                    />
-                  </TableCell>
-                  <TableCell>
-                    <Badge variant="outline" className={`text-xs ${statusColors[insp.status] || ""}`}>
-                      {insp.status === "in_progress" ? t("statusInProgress") : t("statusCompleted")}
-                    </Badge>
-                  </TableCell>
-                  <TableCell className="font-mono text-xs">
-                    {formatDate(new Date(insp.createdAt))}
-                  </TableCell>
-                </TableRow>
-                </ContextMenuTrigger>
-                <ContextMenuContent className="min-w-52">
-                  <ContextMenuItem onClick={() => router.push(`/inspections/${insp.id}`)}>
-                    <ExternalLink className="mr-2 h-4 w-4" />
-                    {tcm("open")}
-                  </ContextMenuItem>
-                  <ContextMenuItem onClick={() => router.push(`/vehicles/${insp.vehicle.id}`)}>
-                    <Car className="mr-2 h-4 w-4" />
-                    {tcm("openVehicle")}
-                  </ContextMenuItem>
-                </ContextMenuContent>
+                  <ContextMenuTrigger asChild>
+                    <TableRow
+                      className="cursor-pointer"
+                      {...interactiveRow(() => router.push(`/inspections/${insp.id}`))}
+                    >
+                      <TableCell>
+                        <TableCellLink href={`/vehicles/${insp.vehicle.id}`} block>
+                          <p className="truncate font-medium">
+                            {insp.vehicle.year} {insp.vehicle.make} {insp.vehicle.model}
+                          </p>
+                          {insp.vehicle.licensePlate && (
+                            <p className="text-xs text-muted-foreground font-mono">
+                              {insp.vehicle.licensePlate}
+                            </p>
+                          )}
+                        </TableCellLink>
+                      </TableCell>
+                      <TableCell className="hidden truncate md:table-cell text-muted-foreground">
+                        {insp.template.name}
+                      </TableCell>
+                      <TableCell>
+                        <InspectionProgress
+                          items={insp.items}
+                          scale={
+                            (insp.severityScale ?? insp.template.severityScale) === 'basic'
+                              ? 'basic'
+                              : 'eu'
+                          }
+                        />
+                      </TableCell>
+                      <TableCell>
+                        <Badge
+                          variant="outline"
+                          className={`text-xs ${statusColors[insp.status] || ''}`}
+                        >
+                          {insp.status === 'in_progress'
+                            ? t('statusInProgress')
+                            : t('statusCompleted')}
+                        </Badge>
+                      </TableCell>
+                      <TableCell className="font-mono text-xs">
+                        {formatDate(new Date(insp.createdAt))}
+                      </TableCell>
+                    </TableRow>
+                  </ContextMenuTrigger>
+                  <ContextMenuContent className="min-w-52">
+                    <ContextMenuItem onClick={() => router.push(`/inspections/${insp.id}`)}>
+                      <ExternalLink className="mr-2 h-4 w-4" />
+                      {tcm('open')}
+                    </ContextMenuItem>
+                    <ContextMenuItem onClick={() => router.push(`/vehicles/${insp.vehicle.id}`)}>
+                      <Car className="mr-2 h-4 w-4" />
+                      {tcm('openVehicle')}
+                    </ContextMenuItem>
+                  </ContextMenuContent>
                 </ContextMenu>
               ))
             )}
@@ -416,5 +464,5 @@ export function InspectionsClient({
         templates={templates}
       />
     </div>
-  );
+  )
 }

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

@@ -13,6 +13,7 @@ import { getNotifications } from "@/features/notifications/Actions/notificationA
 import { getRecentAuditLogs } from "@/features/audit/Actions/auditActions";
 import { getRecentObservations } from "@/features/vehicles/Actions/findingActions";
 import { getMyActiveJobs } from "@/features/vehicles/Actions/getMyActiveJobs";
+import { getOnboardingChecklist } from "@/features/onboarding/Actions/checklistActions";
 import { DashboardClient } from "./dashboard-client";
 import { db } from "@/lib/db";
 import { sanitizeConfig, type CustomWidget } from "@/features/dashboard/custom-cards/registry";
@@ -24,7 +25,7 @@ export default async function DashboardPage() {
   const features = auth ? await getFeatures(auth.organizationId) : null;
   const smsEnabled = features?.sms ?? false;
 
-  const [result, settingsResult, remindersResult, maintenanceResult, dismissedMaintenanceResult, inProgressResult, completedResult, quoteRequestsResult, quoteResponsesResult, smsResult, notificationsResult, auditLogsResult, recentObservationsResult, myJobsResult] = await Promise.all([
+  const [result, settingsResult, remindersResult, maintenanceResult, dismissedMaintenanceResult, inProgressResult, completedResult, quoteRequestsResult, quoteResponsesResult, smsResult, notificationsResult, auditLogsResult, recentObservationsResult, myJobsResult, checklistResult] = await Promise.all([
     getDashboardStats(),
     getSettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.UNIT_SYSTEM]),
     getUpcomingReminders(),
@@ -39,6 +40,7 @@ export default async function DashboardPage() {
     getRecentAuditLogs(10),
     getRecentObservations(),
     getMyActiveJobs(),
+    getOnboardingChecklist(),
   ]);
 
   const [layoutUser, widgetRows] = auth
@@ -83,6 +85,8 @@ export default async function DashboardPage() {
   const recentObservations = recentObservationsResult.success && recentObservationsResult.data ? recentObservationsResult.data : [];
 
   const myJobs = myJobsResult.success && myJobsResult.data ? myJobsResult.data : [];
+  const onboardingChecklist =
+    checklistResult.success && checklistResult.data ? checklistResult.data : null;
 
   return (
     <>
@@ -107,6 +111,7 @@ export default async function DashboardPage() {
           recentObservations={recentObservations}
           initialLayout={layoutUser?.dashboardLayout ?? null}
           customWidgets={customWidgets}
+          onboardingChecklist={onboardingChecklist}
         />
       </div>
     </>

+ 6 - 0
src/app/(authenticated)/settings/data/data-settings.tsx

@@ -17,6 +17,7 @@ import {
   isSupportBubbleHidden,
   setSupportBubbleHidden,
 } from '@/features/support/Lib/supportVisibility'
+import { SampleDataCard } from '@/features/onboarding/Components/SampleDataCard'
 import { deleteContent } from '@/features/settings/Actions/deleteContent'
 import { deleteWorkshop } from '@/features/team/Actions/deleteWorkshop'
 import { deleteAccount } from '@/features/settings/Actions/deleteAccount'
@@ -91,11 +92,13 @@ export function DataSettings({
   lastBackupAt = null,
   workshopName = '',
   isOwner = false,
+  hasSampleData = false,
 }: {
   contentCounts: ContentCounts
   lastBackupAt?: string | null
   workshopName?: string
   isOwner?: boolean
+  hasSampleData?: boolean
 }) {
   const t = useTranslations('settings')
   const format = useFormatter()
@@ -565,6 +568,9 @@ export function DataSettings({
               </button>
             </div>
           </AppCard>
+
+        {/* Onboarding sample data removal */}
+        {hasSampleData && <SampleDataCard />}
       </ReadOnlyWrapper>
 
       {/* LubeLog Import Dialog */}

+ 22 - 6
src/app/(authenticated)/settings/data/page.tsx

@@ -3,6 +3,11 @@ import { getContentCounts } from "@/features/settings/Actions/deleteContent";
 import { getBackupHeartbeat } from "@/lib/backup-heartbeat";
 import { getCachedSession, getCachedMembership } from "@/lib/cached-session";
 import { db } from "@/lib/db";
+import {
+  SAMPLE_DATA_IDS_KEY,
+  hasAnySampleIds,
+  parseSampleDataIds,
+} from "@/features/onboarding/Lib/onboardingKeys";
 
 export default async function DataSettingsPage() {
   const heartbeat = await getBackupHeartbeat();
@@ -12,12 +17,22 @@ export default async function DataSettingsPage() {
   const session = await getCachedSession();
   const membership = session?.user?.id ? await getCachedMembership(session.user.id) : null;
   const isOwner = membership?.role === "owner";
-  const org = membership
-    ? await db.organization.findUnique({
-        where: { id: membership.organizationId },
-        select: { name: true },
-      })
-    : null;
+  const [org, sampleRow] = membership
+    ? await Promise.all([
+        db.organization.findUnique({
+          where: { id: membership.organizationId },
+          select: { name: true },
+        }),
+        db.appSetting.findFirst({
+          where: {
+            organizationId: membership.organizationId,
+            key: SAMPLE_DATA_IDS_KEY,
+          },
+          select: { value: true },
+        }),
+      ])
+    : [null, null];
+  const hasSampleData = hasAnySampleIds(parseSampleDataIds(sampleRow?.value));
   const contentCounts = countsResult.success && countsResult.data
     ? countsResult.data
     : {
@@ -32,6 +47,7 @@ export default async function DataSettingsPage() {
       lastBackupAt={heartbeat?.at ?? null}
       workshopName={org?.name ?? ""}
       isOwner={isOwner}
+      hasSampleData={hasSampleData}
     />
   );
 }

+ 5 - 0
src/app/api/protected/services/[id]/pdf/route.ts

@@ -13,6 +13,7 @@ import { getFeatures } from "@/lib/features";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { formatDateForPdf } from "@/lib/format";
 import { mergeWithDefaults } from "@/features/settings/Schema/invoiceLayoutSchema";
+import { markInvoiceIssued } from "@/features/onboarding/Lib/markInvoiceIssued";
 
 export async function GET(
   _request: Request,
@@ -99,6 +100,10 @@ export async function GET(
       return NextResponse.json({ error: "Record not found" }, { status: 404 });
     }
 
+    // Getting-started checklist: a downloaded invoice leaves no other trace
+    // in the data, so record it here. Best-effort, never blocks the PDF.
+    void markInvoiceIssued(ctx.organizationId, ctx.userId, record.id);
+
     // Fetch findings for this service record (open ones to show on invoice)
     const findings = await db.vehicleFinding.findMany({
       where: { serviceRecordId: record.id, status: { not: "resolved" } },

+ 15 - 11
src/features/dashboard/dashboard-grid-config.ts

@@ -5,6 +5,7 @@
  */
 
 export const DASHBOARD_CARD_IDS = [
+  "gettingStarted",
   "maintenance",
   "reminders",
   "sms",
@@ -50,17 +51,20 @@ export const DEFAULT_LAYOUT: DashboardLayout = {
   version: 1,
   hidden: [],
   cards: {
-    maintenance: half(0, 0),
-    reminders: half(6, 0),
-    sms: half(0, 5),
-    notifications: half(6, 5),
-    inspections: half(0, 10),
-    quoteRequests: half(6, 10),
-    quoteResponses: half(0, 15),
-    recentCompleted: full(20),
-    activeJobs: full(25),
-    recentActivity: half(0, 30),
-    recentObservations: half(6, 30),
+    // First-run checklist leads the grid; rows compact upward once it is
+    // gone (dismissed or not offered), so established users see no hole.
+    gettingStarted: full(0, 4),
+    maintenance: half(0, 4),
+    reminders: half(6, 4),
+    sms: half(0, 9),
+    notifications: half(6, 9),
+    inspections: half(0, 14),
+    quoteRequests: half(6, 14),
+    quoteResponses: half(0, 19),
+    recentCompleted: full(24),
+    activeJobs: full(29),
+    recentActivity: half(0, 34),
+    recentObservations: half(6, 34),
   },
 };
 

+ 4 - 46
src/features/inspections/Actions/templateActions.ts

@@ -7,10 +7,9 @@ import { revalidatePath } from "next/cache";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import type { TemplateSectionInput } from "../Schema/templateSchema";
 import {
-  PRESET_VERSION,
   TEMPLATE_PRESETS,
   presetPackageId,
-  type TemplatePreset,
+  presetToTemplateCreate,
 } from "../Lib/templatePresets";
 
 /**
@@ -227,7 +226,7 @@ export async function createTemplateFromPreset(presetId: string) {
     const isFirst = (await db.inspectionTemplate.count({ where: { organizationId } })) === 0;
 
     const template = await db.inspectionTemplate.create({
-      data: presetToCreate(preset, organizationId, isFirst),
+      data: presetToTemplateCreate(preset, organizationId, isFirst),
       include: { sections: { include: { items: true } } },
     });
 
@@ -314,47 +313,6 @@ export async function duplicateTemplate(id: string) {
   });
 }
 
-/** Turns a preset into the nested create Prisma wants. */
-function presetToCreate(preset: TemplatePreset, organizationId: string, isDefault: boolean) {
-  return {
-    name: preset.name,
-    description: preset.description,
-    isDefault,
-    country: preset.country,
-    standard: preset.standard,
-    severityScale: preset.severityScale,
-    packageId: presetPackageId(preset),
-    packageVersion: PRESET_VERSION,
-    packageSource: "builtin",
-    organizationId,
-    sections: {
-      create: preset.sections.map((section, sIdx) => ({
-        name: section.name,
-        description: section.description || null,
-        code: section.code || null,
-        sortOrder: sIdx,
-        items: {
-          create: section.items.map((item, iIdx) => ({
-            name: item.name,
-            description: item.description || null,
-            code: item.code || null,
-            sortOrder: iIdx,
-            inputType: item.inputType ?? "condition",
-            unit: item.unit || null,
-            minValue: item.minValue ?? null,
-            maxValue: item.maxValue ?? null,
-            choices: item.choices ?? [],
-            required: item.required ?? false,
-            photoRequired: item.photoRequired ?? false,
-            defaultSeverity: item.defaultSeverity ?? null,
-            defectSuggestions: [],
-          })),
-        },
-      })),
-    },
-  };
-}
-
 /** Every preset a workshop would actually run. "blank" is a starting point for
  *  building one, not a checklist, so it stays out of the library. */
 const LIBRARY_PRESETS = TEMPLATE_PRESETS.filter((p) => p.id !== "blank");
@@ -417,7 +375,7 @@ async function syncPresetLibrary(organizationId: string, userId: string) {
     await db.$transaction(
       toCreate.map((preset) =>
         db.inspectionTemplate.create({
-          data: presetToCreate(
+          data: presetToTemplateCreate(
             preset,
             organizationId,
             !hasDefault && preset.id === "standard-multipoint"
@@ -465,7 +423,7 @@ export async function restoreMissingPresets() {
     await db.$transaction(
       missing.map((preset) =>
         db.inspectionTemplate.create({
-          data: presetToCreate(
+          data: presetToTemplateCreate(
             preset,
             organizationId,
             !hasDefault && preset.id === "standard-multipoint"

+ 50 - 0
src/features/inspections/Lib/templatePresets.ts

@@ -744,6 +744,56 @@ export function presetPackageId(preset: TemplatePreset | string): string {
 /** Bumped when a preset's contents change enough to offer as an update. */
 export const PRESET_VERSION = '1.0.0'
 
+/**
+ * Turns a preset into the nested Prisma create for an InspectionTemplate.
+ * Shared by the template library sync and the onboarding installer so both
+ * record the same provenance (packageId/version/source) and the library can
+ * recognise an onboarding-installed template as its own.
+ */
+export function presetToTemplateCreate(
+  preset: TemplatePreset,
+  organizationId: string,
+  isDefault: boolean
+) {
+  return {
+    name: preset.name,
+    description: preset.description,
+    isDefault,
+    country: preset.country,
+    standard: preset.standard,
+    severityScale: preset.severityScale,
+    packageId: presetPackageId(preset),
+    packageVersion: PRESET_VERSION,
+    packageSource: 'builtin',
+    organizationId,
+    sections: {
+      create: preset.sections.map((section, sIdx) => ({
+        name: section.name,
+        description: section.description || null,
+        code: section.code || null,
+        sortOrder: sIdx,
+        items: {
+          create: section.items.map((item, iIdx) => ({
+            name: item.name,
+            description: item.description || null,
+            code: item.code || null,
+            sortOrder: iIdx,
+            inputType: item.inputType ?? 'condition',
+            unit: item.unit || null,
+            minValue: item.minValue ?? null,
+            maxValue: item.maxValue ?? null,
+            choices: item.choices ?? [],
+            required: item.required ?? false,
+            photoRequired: item.photoRequired ?? false,
+            defaultSeverity: item.defaultSeverity ?? null,
+            defectSuggestions: [],
+          })),
+        },
+      })),
+    },
+  }
+}
+
 export function countPresetItems(preset: TemplatePreset): number {
   return preset.sections.reduce((sum, s) => sum + s.items.length, 0)
 }

+ 160 - 0
src/features/onboarding/Actions/checklistActions.ts

@@ -0,0 +1,160 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { revalidatePath } from "next/cache";
+import {
+  CHECKLIST_DISMISSED_KEY,
+  INVOICE_ISSUED_KEY,
+  SAMPLE_DATA_IDS_KEY,
+  hasAnySampleIds,
+  parseSampleDataIds,
+} from "../Lib/onboardingKeys";
+
+export interface OnboardingChecklistData {
+  steps: {
+    customer: boolean;
+    vehicle: boolean;
+    workOrder: boolean;
+    invoice: boolean;
+  };
+  allDone: boolean;
+  hasSampleData: boolean;
+}
+
+/**
+ * The dashboard's getting-started card state.
+ *
+ * Completion is detected from live data counts, with the seeded sample rows
+ * excluded so each step means the user really did the thing. Returns null
+ * when the card should not render: the user dismissed it, or the org
+ * predates the checklist (no dismissed-marker row) and already does all of
+ * this anyway.
+ */
+export async function getOnboardingChecklist() {
+  return withAuth(async ({ organizationId }): Promise<OnboardingChecklistData | null> => {
+    const settings = await db.appSetting.findMany({
+      where: {
+        organizationId,
+        key: {
+          in: [SAMPLE_DATA_IDS_KEY, CHECKLIST_DISMISSED_KEY, INVOICE_ISSUED_KEY],
+        },
+      },
+      select: { key: true, value: true },
+    });
+    const byKey = new Map(settings.map((s) => [s.key, s.value]));
+
+    if (byKey.get(CHECKLIST_DISMISSED_KEY) === "true") return null;
+
+    const sampleIds = parseSampleDataIds(byKey.get(SAMPLE_DATA_IDS_KEY));
+
+    const [customers, vehicles, workOrders, sharedInvoices] = await Promise.all([
+      db.customer.count({
+        where: { organizationId, id: { notIn: sampleIds.customers } },
+      }),
+      db.vehicle.count({
+        where: { organizationId, id: { notIn: sampleIds.vehicles } },
+      }),
+      db.serviceRecord.count({
+        where: { organizationId, id: { notIn: sampleIds.serviceRecords } },
+      }),
+      db.serviceRecord.count({
+        where: {
+          organizationId,
+          id: { notIn: sampleIds.serviceRecords },
+          sharedAt: { not: null },
+        },
+      }),
+    ]);
+
+    const steps = {
+      customer: customers > 0,
+      vehicle: vehicles > 0,
+      workOrder: workOrders > 0,
+      invoice: sharedInvoices > 0 || byKey.get(INVOICE_ISSUED_KEY) === "true",
+    };
+    const allDone = Object.values(steps).every(Boolean);
+
+    // Orgs created before the checklist existed have no dismissed-marker row.
+    // If they already do everything, there is nothing to onboard.
+    if (allDone && !byKey.has(CHECKLIST_DISMISSED_KEY)) return null;
+
+    return {
+      steps,
+      allDone,
+      hasSampleData: hasAnySampleIds(sampleIds),
+    };
+  });
+}
+
+export async function dismissOnboardingChecklist() {
+  return withAuth(async ({ organizationId, userId }) => {
+    await db.appSetting.upsert({
+      where: {
+        organizationId_key: { organizationId, key: CHECKLIST_DISMISSED_KEY },
+      },
+      create: {
+        organizationId,
+        key: CHECKLIST_DISMISSED_KEY,
+        value: "true",
+        userId,
+      },
+      update: { value: "true" },
+    });
+    revalidatePath("/");
+    return { dismissed: true };
+  });
+}
+
+/**
+ * Deletes exactly the entities the onboarding seed created, then clears the
+ * marker. Order respects the FK graph: jobs and quotes first (they reference
+ * inspections and vehicles), then inspections, vehicles and customers. All
+ * deletes are scoped to the organization on top of the recorded ids.
+ */
+export async function removeSampleData() {
+  return withAuth(async ({ organizationId }) => {
+    const row = await db.appSetting.findFirst({
+      where: { organizationId, key: SAMPLE_DATA_IDS_KEY },
+      select: { id: true, value: true },
+    });
+    const ids = parseSampleDataIds(row?.value);
+    if (!row || !hasAnySampleIds(ids)) {
+      if (row) await db.appSetting.delete({ where: { id: row.id } });
+      return { removed: false };
+    }
+
+    await db.$transaction([
+      db.serviceRecord.deleteMany({
+        where: { organizationId, id: { in: ids.serviceRecords } },
+      }),
+      db.quote.deleteMany({
+        where: { organizationId, id: { in: ids.quotes } },
+      }),
+      db.inspection.deleteMany({
+        where: { organizationId, id: { in: ids.inspections } },
+      }),
+      db.vehicle.deleteMany({
+        where: { organizationId, id: { in: ids.vehicles } },
+      }),
+      db.customer.deleteMany({
+        where: { organizationId, id: { in: ids.customers } },
+      }),
+      db.appSetting.delete({ where: { id: row.id } }),
+    ]);
+
+    revalidatePath("/");
+    revalidatePath("/customers");
+    revalidatePath("/vehicles");
+    revalidatePath("/work-orders");
+    revalidatePath("/quotes");
+    revalidatePath("/inspections");
+    return { removed: true };
+  }, {
+    audit: () => ({
+      action: "organization.sampleDataRemove",
+      entity: "Organization",
+      message: "Removed onboarding sample data",
+    }),
+  });
+}

+ 58 - 0
src/features/onboarding/Actions/createOnboardingOrg.ts

@@ -1,10 +1,20 @@
 "use server";
 
 import { headers, cookies } from "next/headers";
+import { getLocale, getTranslations } from "next-intl/server";
 import { auth } from "@/lib/auth";
 import { db } from "@/lib/db";
 import { logAudit } from "@/lib/audit";
 import { onboardingSchema } from "../Schema/onboardingSchema";
+import {
+  installDefaultInspectionTemplates,
+  installDefaultLaborPresets,
+} from "../Lib/onboardingDefaults";
+import { seedSampleData } from "../Lib/sampleData";
+import {
+  CHECKLIST_DISMISSED_KEY,
+  SAMPLE_DATA_IDS_KEY,
+} from "../Lib/onboardingKeys";
 import type { ActionResult } from "@/lib/with-auth";
 
 export async function createOnboardingOrg(
@@ -45,6 +55,54 @@ export async function createOnboardingOrg(
       sameSite: "lax",
     });
 
+    // First-run setup: a default inspection template, common labor presets
+    // and (optionally) removable sample data. All best-effort: the org and
+    // membership above are what onboarding must not lose, so a hiccup here
+    // never fails the action — the library sync tops templates up later
+    // anyway.
+    try {
+      const [locale, t] = await Promise.all([
+        getLocale(),
+        getTranslations("onboarding"),
+      ]);
+
+      const template = await installDefaultInspectionTemplates(org.id, locale);
+      await installDefaultLaborPresets(org.id, session.user.id, t);
+
+      if (data.loadSampleData) {
+        const sampleIds = await seedSampleData(
+          org.id,
+          session.user.id,
+          t,
+          template
+        );
+        await db.appSetting.create({
+          data: {
+            organizationId: org.id,
+            key: SAMPLE_DATA_IDS_KEY,
+            value: JSON.stringify(sampleIds),
+            userId: session.user.id,
+          },
+        });
+      }
+
+      // Written for every new org: its presence is what tells the dashboard
+      // this org should see the getting-started checklist.
+      await db.appSetting.create({
+        data: {
+          organizationId: org.id,
+          key: CHECKLIST_DISMISSED_KEY,
+          value: "false",
+          userId: session.user.id,
+        },
+      });
+    } catch (setupError) {
+      console.error(
+        "[createOnboardingOrg] First-run setup failed:",
+        setupError instanceof Error ? setupError.message : setupError
+      );
+    }
+
     // Audit: log registration + org creation (first org = new user onboarding)
     const h = await headers();
     const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim() || h.get("x-real-ip") || null;

+ 180 - 0
src/features/onboarding/Components/GettingStartedCard.tsx

@@ -0,0 +1,180 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { useTranslations } from "next-intl";
+import { toast } from "sonner";
+import { AppCard } from "@/components/app-card";
+import { Button } from "@/components/ui/button";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { useConfirm } from "@/components/confirm-dialog";
+import {
+  ArrowRight,
+  Car,
+  Check,
+  ClipboardList,
+  FileText,
+  Loader2,
+  PartyPopper,
+  Rocket,
+  Trash2,
+  UserPlus,
+  X,
+} from "lucide-react";
+import {
+  dismissOnboardingChecklist,
+  removeSampleData,
+  type OnboardingChecklistData,
+} from "../Actions/checklistActions";
+
+const STEPS = [
+  { key: "customer", href: "/customers", icon: UserPlus },
+  { key: "vehicle", href: "/vehicles", icon: Car },
+  { key: "workOrder", href: "/work-orders", icon: ClipboardList },
+  { key: "invoice", href: "/work-orders", icon: FileText },
+] as const;
+
+export function GettingStartedCard({ data }: { data: OnboardingChecklistData }) {
+  const t = useTranslations("onboarding.checklist");
+  const tSample = useTranslations("onboarding.sampleData");
+  const router = useRouter();
+  const confirm = useConfirm();
+  const [, startTransition] = useTransition();
+  const [removing, setRemoving] = useState(false);
+
+  const doneCount = STEPS.filter((s) => data.steps[s.key]).length;
+
+  const handleDismiss = () => {
+    startTransition(async () => {
+      await dismissOnboardingChecklist();
+      router.refresh();
+    });
+  };
+
+  const handleRemoveSampleData = async () => {
+    const ok = await confirm({
+      title: tSample("removeConfirmTitle"),
+      description: tSample("removeConfirmDescription"),
+      confirmLabel: tSample("remove"),
+      destructive: true,
+    });
+    if (!ok) return;
+    setRemoving(true);
+    const result = await removeSampleData();
+    setRemoving(false);
+    if (result.success) {
+      toast.success(tSample("removed"));
+      router.refresh();
+    } else {
+      toast.error(result.error || tSample("removeFailed"));
+    }
+  };
+
+  return (
+    <AppCard
+      icon={Rocket}
+      title={t("title")}
+      description={t("description")}
+      badge={t("progress", { done: doneCount, total: STEPS.length })}
+      action={
+        <Tooltip>
+          <TooltipTrigger asChild>
+            <Button
+              variant="ghost"
+              size="icon"
+              className="h-7 w-7 text-muted-foreground hover:text-foreground"
+              onClick={handleDismiss}
+              aria-label={t("dismissAriaLabel")}
+            >
+              <X className="h-3.5 w-3.5" />
+            </Button>
+          </TooltipTrigger>
+          <TooltipContent>{t("dismissAriaLabel")}</TooltipContent>
+        </Tooltip>
+      }
+      contentClassName="p-0"
+      footer={
+        data.hasSampleData ? (
+          <div className="flex w-full items-center justify-between gap-3">
+            <span className="min-w-0 truncate">{tSample("footerNote")}</span>
+            <Button
+              variant="ghost"
+              size="sm"
+              className="h-7 shrink-0 text-xs text-muted-foreground hover:text-destructive"
+              onClick={handleRemoveSampleData}
+              disabled={removing}
+            >
+              {removing ? (
+                <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
+              ) : (
+                <Trash2 className="mr-1 h-3.5 w-3.5" />
+              )}
+              {tSample("remove")}
+            </Button>
+          </div>
+        ) : undefined
+      }
+    >
+      {data.allDone ? (
+        <div className="flex flex-col items-center gap-3 px-5 py-6 text-center sm:flex-row sm:text-left">
+          <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-emerald-500/10">
+            <PartyPopper className="h-5 w-5 text-emerald-500" />
+          </div>
+          <div className="min-w-0 flex-1">
+            <p className="text-sm font-medium">{t("allDoneTitle")}</p>
+            <p className="text-xs text-muted-foreground">
+              {t("allDoneDescription")}
+            </p>
+          </div>
+          <Button size="sm" className="h-8 shrink-0 text-xs" onClick={handleDismiss}>
+            <Check className="mr-1 h-3.5 w-3.5" />
+            {t("dismiss")}
+          </Button>
+        </div>
+      ) : (
+        <div className="divide-y">
+          {STEPS.map(({ key, href, icon: Icon }) => {
+            const done = data.steps[key];
+            return (
+              <Link
+                key={key}
+                href={href}
+                className="flex items-center justify-between gap-3 px-5 py-3 transition-colors hover:bg-muted/50"
+              >
+                <div className="flex min-w-0 items-center gap-3">
+                  <div
+                    className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${
+                      done ? "bg-emerald-500/10" : "bg-primary/10"
+                    }`}
+                  >
+                    {done ? (
+                      <Check className="h-4 w-4 text-emerald-500" />
+                    ) : (
+                      <Icon className="h-4 w-4 text-primary" />
+                    )}
+                  </div>
+                  <div className="min-w-0">
+                    <p
+                      className={`truncate text-sm font-medium ${
+                        done ? "text-muted-foreground line-through" : ""
+                      }`}
+                    >
+                      {t(`steps.${key}.title`)}
+                    </p>
+                    <p className="truncate text-xs text-muted-foreground">
+                      {t(`steps.${key}.description`)}
+                    </p>
+                  </div>
+                </div>
+                {!done && (
+                  <ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground" />
+                )}
+              </Link>
+            );
+          })}
+        </div>
+      )}
+    </AppCard>
+  );
+}

+ 30 - 10
src/features/onboarding/Components/OnboardingForm.tsx

@@ -2,15 +2,19 @@
 
 import { useState } from "react";
 import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
 import { useGlassModal } from "@/components/glass-modal";
 import { Gauge, Loader2 } from "lucide-react";
 import { createOnboardingOrg } from "../Actions/createOnboardingOrg";
 
 export function OnboardingForm({ redirectTo }: { redirectTo?: string }) {
+  const t = useTranslations("onboarding.form");
   const [workshopName, setWorkshopName] = useState("");
+  const [loadSampleData, setLoadSampleData] = useState(true);
   const [loading, setLoading] = useState(false);
   const router = useRouter();
   const modal = useGlassModal();
@@ -20,15 +24,15 @@ export function OnboardingForm({ redirectTo }: { redirectTo?: string }) {
     setLoading(true);
 
     try {
-      const result = await createOnboardingOrg({ workshopName });
+      const result = await createOnboardingOrg({ workshopName, loadSampleData });
       if (!result.success) {
-        modal.open("error", "Setup Failed", result.error || "Could not create workshop");
+        modal.open("error", t("setupFailed"), result.error || t("couldNotCreate"));
       } else {
         router.push(redirectTo || "/");
         router.refresh();
       }
     } catch {
-      modal.open("error", "Setup Failed", "An unexpected error occurred");
+      modal.open("error", t("setupFailed"), t("unexpectedError"));
     } finally {
       setLoading(false);
     }
@@ -43,19 +47,17 @@ export function OnboardingForm({ redirectTo }: { redirectTo?: string }) {
             Torqvoice
           </span>
         </div>
-        <h1 className="text-2xl font-bold tracking-tight">Set up your workshop</h1>
-        <p className="mt-1 text-sm text-muted-foreground">
-          Enter your workshop name to get started
-        </p>
+        <h1 className="text-2xl font-bold tracking-tight">{t("title")}</h1>
+        <p className="mt-1 text-sm text-muted-foreground">{t("subtitle")}</p>
       </div>
 
       <form onSubmit={handleSubmit} className="space-y-4">
         <div className="space-y-2">
-          <Label htmlFor="workshopName">Workshop Name</Label>
+          <Label htmlFor="workshopName">{t("workshopName")}</Label>
           <Input
             id="workshopName"
             type="text"
-            placeholder="My Auto Workshop"
+            placeholder={t("workshopNamePlaceholder")}
             value={workshopName}
             onChange={(e) => setWorkshopName(e.target.value)}
             required
@@ -65,9 +67,27 @@ export function OnboardingForm({ redirectTo }: { redirectTo?: string }) {
           />
         </div>
 
+        <label
+          htmlFor="loadSampleData"
+          className="flex cursor-pointer items-start justify-between gap-3 rounded-lg border bg-background/50 p-3"
+        >
+          <div className="space-y-0.5">
+            <span className="text-sm font-medium">{t("sampleDataLabel")}</span>
+            <p className="text-xs text-muted-foreground">
+              {t("sampleDataDescription")}
+            </p>
+          </div>
+          <Switch
+            id="loadSampleData"
+            checked={loadSampleData}
+            onCheckedChange={setLoadSampleData}
+            className="mt-0.5"
+          />
+        </label>
+
         <Button type="submit" className="h-11 w-full" disabled={loading}>
           {loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
-          Get Started
+          {t("submit")}
         </Button>
       </form>
     </div>

+ 56 - 0
src/features/onboarding/Components/SampleDataCard.tsx

@@ -0,0 +1,56 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { toast } from "sonner";
+import { AppCard } from "@/components/app-card";
+import { Button } from "@/components/ui/button";
+import { useConfirm } from "@/components/confirm-dialog";
+import { Loader2, Sparkles, Trash2 } from "lucide-react";
+import { removeSampleData } from "../Actions/checklistActions";
+
+/**
+ * Settings → Data card offering exact removal of the onboarding sample
+ * dataset. Only rendered while the recorded sample ids still exist, so the
+ * checklist card's dismissal can never orphan the sample rows.
+ */
+export function SampleDataCard() {
+  const t = useTranslations("onboarding.sampleData");
+  const router = useRouter();
+  const confirm = useConfirm();
+  const [removing, setRemoving] = useState(false);
+
+  const handleRemove = async () => {
+    const ok = await confirm({
+      title: t("removeConfirmTitle"),
+      description: t("removeConfirmDescription"),
+      confirmLabel: t("remove"),
+      destructive: true,
+    });
+    if (!ok) return;
+    setRemoving(true);
+    const result = await removeSampleData();
+    setRemoving(false);
+    if (result.success) {
+      toast.success(t("removed"));
+      router.refresh();
+    } else {
+      toast.error(result.error || t("removeFailed"));
+    }
+  };
+
+  return (
+    <AppCard icon={Sparkles} title={t("title")} contentClassName="space-y-4">
+      <p className="text-sm text-muted-foreground">{t("description")}</p>
+      <Button variant="outline" onClick={handleRemove} disabled={removing}>
+        {removing ? (
+          <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+        ) : (
+          <Trash2 className="mr-2 h-4 w-4" />
+        )}
+        {t("remove")}
+      </Button>
+    </AppCard>
+  );
+}

+ 53 - 0
src/features/onboarding/Lib/markInvoiceIssued.ts

@@ -0,0 +1,53 @@
+import { db } from "@/lib/db";
+import {
+  INVOICE_ISSUED_KEY,
+  SAMPLE_DATA_IDS_KEY,
+  parseSampleDataIds,
+} from "./onboardingKeys";
+
+/**
+ * Records that the organization has produced a real invoice PDF.
+ *
+ * Downloading an invoice leaves no trace in the data, so the getting-started
+ * checklist cannot detect it from counts; the invoice PDF route calls this
+ * instead. Best-effort and cheap: once the marker exists the first read
+ * short-circuits, and sample records never set it.
+ */
+export async function markInvoiceIssued(
+  organizationId: string,
+  userId: string,
+  serviceRecordId: string
+): Promise<void> {
+  try {
+    const rows = await db.appSetting.findMany({
+      where: {
+        organizationId,
+        key: { in: [INVOICE_ISSUED_KEY, SAMPLE_DATA_IDS_KEY] },
+      },
+      select: { key: true, value: true },
+    });
+    const byKey = new Map(rows.map((r) => [r.key, r.value]));
+    if (byKey.get(INVOICE_ISSUED_KEY) === "true") return;
+
+    const sampleIds = parseSampleDataIds(byKey.get(SAMPLE_DATA_IDS_KEY));
+    if (sampleIds.serviceRecords.includes(serviceRecordId)) return;
+
+    await db.appSetting.upsert({
+      where: {
+        organizationId_key: { organizationId, key: INVOICE_ISSUED_KEY },
+      },
+      create: {
+        organizationId,
+        key: INVOICE_ISSUED_KEY,
+        value: "true",
+        userId,
+      },
+      update: { value: "true" },
+    });
+  } catch (error) {
+    console.error(
+      "[markInvoiceIssued] Failed:",
+      error instanceof Error ? error.message : error
+    );
+  }
+}

+ 111 - 0
src/features/onboarding/Lib/onboardingDefaults.ts

@@ -0,0 +1,111 @@
+import { db } from "@/lib/db";
+import {
+  getPreset,
+  presetToTemplateCreate,
+} from "@/features/inspections/Lib/templatePresets";
+
+/** Minimal shape of a next-intl translator; keeps this lib decoupled from
+ *  next-intl's generics. */
+export type Translator = (
+  key: string,
+  values?: Record<string, string | number | Date>
+) => string;
+
+/**
+ * The inspection template a brand-new workshop starts with.
+ *
+ * Locales that map onto a country with its own statutory checklist get that
+ * checklist installed alongside; the general multi-point check is always the
+ * default, matching the library's own convention that a new shop should land
+ * on the general checklist rather than a national test it may not be
+ * approved to run.
+ */
+export function pickCountryPresetId(locale: string): string | null {
+  switch (locale) {
+    case "de":
+      return "de-hauptuntersuchung";
+    case "nl":
+      return "nl-apk";
+    case "nb":
+      return "no-eu-kontroll";
+    default:
+      return null;
+  }
+}
+
+export const DEFAULT_TEMPLATE_PRESET_ID = "standard-multipoint";
+
+/**
+ * Installs the default inspection template(s) for a fresh organization and
+ * returns the default one including sections and items, so the sample seed
+ * can build an inspection from it. Records preset provenance, so the
+ * template library recognises these as already installed.
+ */
+export async function installDefaultInspectionTemplates(
+  organizationId: string,
+  locale: string
+) {
+  const generic = getPreset(DEFAULT_TEMPLATE_PRESET_ID);
+  if (!generic) throw new Error("Default template preset not found");
+
+  const template = await db.inspectionTemplate.create({
+    data: presetToTemplateCreate(generic, organizationId, true),
+    include: {
+      sections: {
+        include: { items: { orderBy: { sortOrder: "asc" } } },
+        orderBy: { sortOrder: "asc" },
+      },
+    },
+  });
+
+  const countryPresetId = pickCountryPresetId(locale);
+  const countryPreset = countryPresetId ? getPreset(countryPresetId) : null;
+  if (countryPreset) {
+    await db.inspectionTemplate.create({
+      data: presetToTemplateCreate(countryPreset, organizationId, false),
+    });
+  }
+
+  return template;
+}
+
+/** Common jobs every workshop quotes daily. Hours only; rates stay at zero so
+ *  the workshop's own hourly rate applies when the preset is used. */
+const DEFAULT_LABOR_PRESETS: { key: string; hours: number }[] = [
+  { key: "oilChange", hours: 0.8 },
+  { key: "frontBrakePads", hours: 1.5 },
+  { key: "tireSwap", hours: 0.7 },
+  { key: "batteryReplacement", hours: 0.5 },
+  { key: "diagnostics", hours: 1 },
+  { key: "annualService", hours: 2.5 },
+];
+
+export async function installDefaultLaborPresets(
+  organizationId: string,
+  userId: string,
+  t: Translator
+) {
+  await db.$transaction(
+    DEFAULT_LABOR_PRESETS.map(({ key, hours }) =>
+      db.laborPreset.create({
+        data: {
+          name: t(`presets.${key}.name`),
+          description: t(`presets.${key}.description`),
+          userId,
+          organizationId,
+          items: {
+            create: [
+              {
+                description: t(`presets.${key}.name`),
+                hours,
+                rate: 0,
+                pricingType: "hourly",
+                sortOrder: 0,
+              },
+            ],
+          },
+        },
+      })
+    )
+  );
+}

+ 67 - 0
src/features/onboarding/Lib/onboardingKeys.ts

@@ -0,0 +1,67 @@
+/**
+ * AppSetting keys that carry the first-run state of an organization.
+ *
+ * All onboarding state lives in per-org AppSetting rows rather than schema
+ * columns, so it can ship without a migration and disappears with the org.
+ */
+
+/** JSON object with the ids of every entity the sample-data seed created,
+ *  keyed by model ({ customers, vehicles, serviceRecords, quotes,
+ *  inspections }). Removal deletes exactly these ids and nothing else. */
+export const SAMPLE_DATA_IDS_KEY = "onboarding.sampleDataIds";
+
+/**
+ * "false" while the getting-started checklist should show, "true" once the
+ * user hid it. The row is written at org creation, so its mere presence marks
+ * an org that went through the new onboarding: orgs without it (created
+ * before this feature) only see the checklist while steps remain open.
+ */
+export const CHECKLIST_DISMISSED_KEY = "onboarding.checklistDismissed";
+
+/** "true" once a non-sample invoice PDF was downloaded. Downloading leaves no
+ *  other trace in the data, so the route records this one-time marker. */
+export const INVOICE_ISSUED_KEY = "onboarding.invoiceIssued";
+
+export interface SampleDataIds {
+  customers: string[];
+  vehicles: string[];
+  serviceRecords: string[];
+  quotes: string[];
+  inspections: string[];
+}
+
+export const EMPTY_SAMPLE_IDS: SampleDataIds = {
+  customers: [],
+  vehicles: [],
+  serviceRecords: [],
+  quotes: [],
+  inspections: [],
+};
+
+export function parseSampleDataIds(value: string | null | undefined): SampleDataIds {
+  if (!value) return { ...EMPTY_SAMPLE_IDS };
+  try {
+    const parsed = JSON.parse(value) as Partial<Record<keyof SampleDataIds, unknown>>;
+    const list = (v: unknown): string[] =>
+      Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
+    return {
+      customers: list(parsed.customers),
+      vehicles: list(parsed.vehicles),
+      serviceRecords: list(parsed.serviceRecords),
+      quotes: list(parsed.quotes),
+      inspections: list(parsed.inspections),
+    };
+  } catch {
+    return { ...EMPTY_SAMPLE_IDS };
+  }
+}
+
+export function hasAnySampleIds(ids: SampleDataIds): boolean {
+  return (
+    ids.customers.length > 0 ||
+    ids.vehicles.length > 0 ||
+    ids.serviceRecords.length > 0 ||
+    ids.quotes.length > 0 ||
+    ids.inspections.length > 0
+  );
+}

+ 349 - 0
src/features/onboarding/Lib/sampleData.ts

@@ -0,0 +1,349 @@
+import { db } from "@/lib/db";
+import { resolveInvoicePrefix } from "@/lib/invoice-utils";
+import type { SampleDataIds } from "./onboardingKeys";
+import type { Translator } from "./onboardingDefaults";
+
+/** The slice of an inspection template the seed copies items from. */
+interface TemplateForSeed {
+  id: string;
+  severityScale: string;
+  country: string | null;
+  sections: {
+    name: string;
+    code: string | null;
+    items: {
+      name: string;
+      description: string | null;
+      code: string | null;
+      sortOrder: number;
+      inputType: string;
+      unit: string | null;
+      minValue: number | null;
+      maxValue: number | null;
+      choices: string[];
+      required: boolean;
+      photoRequired: boolean;
+      defaultSeverity: string | null;
+      defectSuggestions: string[];
+    }[];
+  }[];
+}
+
+const daysAgo = (n: number) => {
+  const d = new Date();
+  d.setDate(d.getDate() - n);
+  return d;
+};
+
+const daysAhead = (n: number) => daysAgo(-n);
+
+/**
+ * Seeds a small, clearly-labelled demo dataset for a brand-new organization:
+ * three customers, four vehicles, three work orders in different states (one
+ * completed with parts and labor), one sent quote and one inspection in
+ * progress. Every name carries the translated sample prefix, and every
+ * created id is returned so removal can delete exactly this set.
+ *
+ * Invoice and quote numbers follow the same default numbering the real
+ * actions use, so real records created afterwards simply continue the
+ * sequence.
+ */
+export async function seedSampleData(
+  organizationId: string,
+  userId: string,
+  t: Translator,
+  template: TemplateForSeed
+): Promise<SampleDataIds> {
+  const prefix = t("sample.prefix");
+  const label = (name: string) => `${prefix} ${name}`;
+  const invoicePrefix = resolveInvoicePrefix("{year}-");
+  const hourlyRate = 95;
+
+  return db.$transaction(async (tx) => {
+    // ── Customers ────────────────────────────────────────────────────────
+    const anna = await tx.customer.create({
+      data: {
+        name: label("Anna Bergström"),
+        customerNumber: "1",
+        email: "anna.sample@example.com",
+        phone: "+1 555 0101",
+        userId,
+        organizationId,
+      },
+    });
+    const marco = await tx.customer.create({
+      data: {
+        name: label("Marco Rossi"),
+        customerNumber: "2",
+        email: "marco.sample@example.com",
+        phone: "+1 555 0102",
+        userId,
+        organizationId,
+      },
+    });
+    const nordvik = await tx.customer.create({
+      data: {
+        name: label("Nordvik Logistics"),
+        customerNumber: "3",
+        company: label("Nordvik Logistics"),
+        email: "fleet.sample@example.com",
+        phone: "+1 555 0103",
+        userId,
+        organizationId,
+      },
+    });
+
+    // ── Vehicles ─────────────────────────────────────────────────────────
+    const golf = await tx.vehicle.create({
+      data: {
+        make: "Volkswagen",
+        model: "Golf",
+        year: 2018,
+        licensePlate: "SAMPLE-1",
+        mileage: 86500,
+        fuelType: "gasoline",
+        customerId: anna.id,
+        userId,
+        organizationId,
+      },
+    });
+    const rav4 = await tx.vehicle.create({
+      data: {
+        make: "Toyota",
+        model: "RAV4",
+        year: 2020,
+        licensePlate: "SAMPLE-2",
+        mileage: 45200,
+        fuelType: "hybrid",
+        customerId: marco.id,
+        userId,
+        organizationId,
+      },
+    });
+    const transit = await tx.vehicle.create({
+      data: {
+        make: "Ford",
+        model: "Transit",
+        year: 2016,
+        licensePlate: "SAMPLE-3",
+        mileage: 158000,
+        fuelType: "diesel",
+        customerId: nordvik.id,
+        userId,
+        organizationId,
+      },
+    });
+    const bmw = await tx.vehicle.create({
+      data: {
+        make: "BMW",
+        model: "320d",
+        year: 2019,
+        licensePlate: "SAMPLE-4",
+        mileage: 98400,
+        fuelType: "diesel",
+        customerId: anna.id,
+        userId,
+        organizationId,
+      },
+    });
+
+    // ── Work orders ──────────────────────────────────────────────────────
+    // Completed job with parts + labor, invoiced and paid.
+    const oilParts = [
+      { name: t("sample.parts.oilFilter"), quantity: 1, unitPrice: 14.9, total: 14.9 },
+      { name: t("sample.parts.engineOil"), quantity: 4, unitPrice: 11.5, total: 46 },
+    ];
+    const oilLaborTotal = 0.8 * hourlyRate;
+    const oilSubtotal =
+      oilParts.reduce((sum, p) => sum + p.total, 0) + oilLaborTotal;
+    const oilJob = await tx.serviceRecord.create({
+      data: {
+        title: label(t("sample.oilService")),
+        type: "maintenance",
+        status: "completed",
+        mileage: 86200,
+        serviceDate: daysAgo(12),
+        invoiceDate: daysAgo(12),
+        invoiceNumber: `${invoicePrefix}1001`,
+        subtotal: oilSubtotal,
+        totalAmount: oilSubtotal,
+        cost: oilSubtotal,
+        manuallyPaid: true,
+        vehicleId: golf.id,
+        organizationId,
+        partItems: { create: oilParts },
+        laborItems: {
+          create: [
+            {
+              description: t("sample.oilService"),
+              hours: 0.8,
+              rate: hourlyRate,
+              total: oilLaborTotal,
+              pricingType: "hourly",
+            },
+          ],
+        },
+      },
+    });
+
+    // Job on the lift right now.
+    const brakeLaborTotal = 1.5 * hourlyRate;
+    const brakeSubtotal = 62 + brakeLaborTotal;
+    const brakeJob = await tx.serviceRecord.create({
+      data: {
+        title: label(t("sample.brakeJob")),
+        type: "repair",
+        status: "in-progress",
+        mileage: 45200,
+        serviceDate: new Date(),
+        startDateTime: new Date(),
+        invoiceNumber: `${invoicePrefix}1002`,
+        subtotal: brakeSubtotal,
+        totalAmount: brakeSubtotal,
+        cost: brakeSubtotal,
+        vehicleId: rav4.id,
+        organizationId,
+        partItems: {
+          create: [
+            {
+              name: t("sample.parts.brakePads"),
+              quantity: 1,
+              unitPrice: 62,
+              total: 62,
+            },
+          ],
+        },
+        laborItems: {
+          create: [
+            {
+              description: t("sample.brakeJob"),
+              hours: 1.5,
+              rate: hourlyRate,
+              total: brakeLaborTotal,
+              pricingType: "hourly",
+            },
+          ],
+        },
+      },
+    });
+
+    // Booked but not started.
+    const annualJob = await tx.serviceRecord.create({
+      data: {
+        title: label(t("sample.annualService")),
+        type: "maintenance",
+        status: "pending",
+        serviceDate: daysAhead(3),
+        startDateTime: daysAhead(3),
+        invoiceNumber: `${invoicePrefix}1003`,
+        vehicleId: transit.id,
+        organizationId,
+      },
+    });
+
+    // ── Quote ────────────────────────────────────────────────────────────
+    const quoteLaborTotal = 2 * hourlyRate;
+    const quoteSubtotal = 178 + quoteLaborTotal;
+    const quote = await tx.quote.create({
+      data: {
+        quoteNumber: "QT-1001",
+        title: label(t("sample.quoteTitle")),
+        status: "sent",
+        validUntil: daysAhead(30),
+        subtotal: quoteSubtotal,
+        totalAmount: quoteSubtotal,
+        customerId: anna.id,
+        vehicleId: bmw.id,
+        userId,
+        organizationId,
+        partItems: {
+          create: [
+            {
+              name: t("sample.parts.shockAbsorber"),
+              quantity: 2,
+              unitPrice: 89,
+              total: 178,
+            },
+          ],
+        },
+        laborItems: {
+          create: [
+            {
+              description: t("sample.quoteTitle"),
+              hours: 2,
+              rate: hourlyRate,
+              total: quoteLaborTotal,
+              pricingType: "hourly",
+            },
+          ],
+        },
+      },
+    });
+
+    // ── Inspection ───────────────────────────────────────────────────────
+    // Items are copied from the installed default template exactly like
+    // createInspection does; the first few carry results so the inspection
+    // screen shows what a partially-done checklist looks like.
+    const inspection = await tx.inspection.create({
+      data: {
+        status: "in_progress",
+        mileage: 86500,
+        severityScale: template.severityScale,
+        country: template.country,
+        vehicleId: golf.id,
+        templateId: template.id,
+        organizationId,
+      },
+    });
+
+    let inspected = 0;
+    const items = template.sections.flatMap((section, sIdx) =>
+      section.items.map((item) => {
+        // Grade the first handful of plain condition checks, leave the rest
+        // untouched; one gets flagged so the attention state is visible too.
+        let condition = "not_inspected";
+        let notes: string | null = null;
+        if (item.inputType === "condition" && inspected < 5) {
+          inspected += 1;
+          if (inspected === 4) {
+            condition = "attention";
+            notes = t("sample.inspectionNote");
+          } else {
+            condition = "pass";
+          }
+        }
+        return {
+          name: item.name,
+          section: section.name,
+          sectionCode: section.code,
+          description: item.description,
+          code: item.code,
+          sortOrder: sIdx * 1000 + item.sortOrder,
+          inputType: item.inputType,
+          unit: item.unit,
+          minValue: item.minValue,
+          maxValue: item.maxValue,
+          choices: item.choices,
+          required: item.required,
+          photoRequired: item.photoRequired,
+          defaultSeverity: item.defaultSeverity,
+          defectSuggestions: item.defectSuggestions,
+          condition,
+          notes,
+          inspectionId: inspection.id,
+        };
+      })
+    );
+    if (items.length > 0) {
+      await tx.inspectionItem.createMany({ data: items });
+    }
+
+    return {
+      customers: [anna.id, marco.id, nordvik.id],
+      vehicles: [golf.id, rav4.id, transit.id, bmw.id],
+      serviceRecords: [oilJob.id, brakeJob.id, annualJob.id],
+      quotes: [quote.id],
+      inspections: [inspection.id],
+    };
+  });
+}

+ 1 - 0
src/features/onboarding/Schema/onboardingSchema.ts

@@ -6,6 +6,7 @@ export const onboardingSchema = z.object({
     .min(2, "Workshop name must be at least 2 characters")
     .max(100, "Workshop name must be at most 100 characters")
     .trim(),
+  loadSampleData: z.boolean().default(true),
 });
 
 export type OnboardingInput = z.infer<typeof onboardingSchema>;

+ 2 - 0
src/i18n/request.ts

@@ -61,6 +61,7 @@ export default getRequestConfig(async () => {
   const statusReport = (await import(`../../messages/${locale}/statusReport.json`)).default
   const support = (await import(`../../messages/${locale}/support.json`)).default
   const inspections = (await import(`../../messages/${locale}/inspections.json`)).default
+  const onboarding = (await import(`../../messages/${locale}/onboarding.json`)).default
 
   return {
     locale,
@@ -96,6 +97,7 @@ export default getRequestConfig(async () => {
       statusReport,
       support,
       inspections,
+      onboarding,
     },
   }
 })