Bläddra i källkod

Add a vehicle condition map for dents, scratches and chips (#427)

A line drawing of the vehicle in five views, as an inspection check type
and on the work order's drop-off tab. Marks belong to the vehicle: earlier
visits' marks come back in grey and can be cleared as repaired. The map
prints on certificates and work orders with a numbered legend. Eight body
drawings, a Vehicle check-in checklist, and strings in all 13 locales.
Bernt Christian Egeland 5 dagar sedan
förälder
incheckning
43f8650cd5
100 ändrade filer med 4661 tillägg och 190 borttagningar
  1. 6 2
      messages/de/audit.json
  2. 146 0
      messages/de/conditionMap.json
  3. 26 4
      messages/de/inspectionLibrary.json
  4. 4 2
      messages/de/inspections.json
  5. 7 1
      messages/de/pdf.json
  6. 23 3
      messages/de/settings.json
  7. 6 2
      messages/en/audit.json
  8. 146 0
      messages/en/conditionMap.json
  9. 26 4
      messages/en/inspectionLibrary.json
  10. 4 2
      messages/en/inspections.json
  11. 7 1
      messages/en/pdf.json
  12. 23 3
      messages/en/settings.json
  13. 6 2
      messages/es/audit.json
  14. 146 0
      messages/es/conditionMap.json
  15. 26 4
      messages/es/inspectionLibrary.json
  16. 4 2
      messages/es/inspections.json
  17. 7 1
      messages/es/pdf.json
  18. 23 3
      messages/es/settings.json
  19. 6 2
      messages/fr/audit.json
  20. 146 0
      messages/fr/conditionMap.json
  21. 26 4
      messages/fr/inspectionLibrary.json
  22. 4 2
      messages/fr/inspections.json
  23. 7 1
      messages/fr/pdf.json
  24. 23 3
      messages/fr/settings.json
  25. 6 2
      messages/it/audit.json
  26. 146 0
      messages/it/conditionMap.json
  27. 26 4
      messages/it/inspectionLibrary.json
  28. 4 2
      messages/it/inspections.json
  29. 7 1
      messages/it/pdf.json
  30. 23 3
      messages/it/settings.json
  31. 6 2
      messages/lt/audit.json
  32. 146 0
      messages/lt/conditionMap.json
  33. 26 4
      messages/lt/inspectionLibrary.json
  34. 4 2
      messages/lt/inspections.json
  35. 7 1
      messages/lt/pdf.json
  36. 23 3
      messages/lt/settings.json
  37. 6 2
      messages/nb/audit.json
  38. 146 0
      messages/nb/conditionMap.json
  39. 26 4
      messages/nb/inspectionLibrary.json
  40. 4 2
      messages/nb/inspections.json
  41. 7 1
      messages/nb/pdf.json
  42. 23 3
      messages/nb/settings.json
  43. 6 2
      messages/nl/audit.json
  44. 146 0
      messages/nl/conditionMap.json
  45. 26 4
      messages/nl/inspectionLibrary.json
  46. 4 2
      messages/nl/inspections.json
  47. 7 1
      messages/nl/pdf.json
  48. 23 3
      messages/nl/settings.json
  49. 6 2
      messages/pl/audit.json
  50. 146 0
      messages/pl/conditionMap.json
  51. 26 4
      messages/pl/inspectionLibrary.json
  52. 4 2
      messages/pl/inspections.json
  53. 7 1
      messages/pl/pdf.json
  54. 23 3
      messages/pl/settings.json
  55. 6 2
      messages/pt-BR/audit.json
  56. 146 0
      messages/pt-BR/conditionMap.json
  57. 26 4
      messages/pt-BR/inspectionLibrary.json
  58. 4 2
      messages/pt-BR/inspections.json
  59. 7 1
      messages/pt-BR/pdf.json
  60. 23 3
      messages/pt-BR/settings.json
  61. 6 2
      messages/pt-PT/audit.json
  62. 146 0
      messages/pt-PT/conditionMap.json
  63. 26 4
      messages/pt-PT/inspectionLibrary.json
  64. 4 2
      messages/pt-PT/inspections.json
  65. 7 1
      messages/pt-PT/pdf.json
  66. 23 3
      messages/pt-PT/settings.json
  67. 6 2
      messages/ru/audit.json
  68. 146 0
      messages/ru/conditionMap.json
  69. 26 4
      messages/ru/inspectionLibrary.json
  70. 4 2
      messages/ru/inspections.json
  71. 7 1
      messages/ru/pdf.json
  72. 23 3
      messages/ru/settings.json
  73. 6 2
      messages/tr/audit.json
  74. 146 0
      messages/tr/conditionMap.json
  75. 26 4
      messages/tr/inspectionLibrary.json
  76. 4 2
      messages/tr/inspections.json
  77. 7 1
      messages/tr/pdf.json
  78. 23 3
      messages/tr/settings.json
  79. 65 0
      prisma/migrations/20260925180000_condition_marks/migration.sql
  80. 10 8
      prisma/schema/auth.prisma
  81. 61 0
      prisma/schema/condition-marks.prisma
  82. 21 18
      prisma/schema/inspections.prisma
  83. 1 0
      prisma/schema/organization.prisma
  84. 1 0
      prisma/schema/service-records.prisma
  85. 5 0
      prisma/schema/vehicles.prisma
  86. 94 0
      src/__tests__/features/condition-map/marks.test.ts
  87. 43 0
      src/__tests__/features/condition-map/path-transform.test.ts
  88. 268 0
      src/__tests__/features/condition-map/print.test.ts
  89. 2 1
      src/__tests__/features/inspections/library-localization.test.ts
  90. 1 0
      src/__tests__/features/invoice-designer/certificate-blocks.test.tsx
  91. 1 0
      src/__tests__/lib/backup-file-urls.test.ts
  92. 2 0
      src/__tests__/lib/files/collect.test.ts
  93. 14 7
      src/app/(authenticated)/inspections/[id]/page.tsx
  94. 7 0
      src/app/(public)/share/inspection/[orgId]/[token]/page.tsx
  95. 3 0
      src/app/api/protected/backup/export/route.ts
  96. 62 0
      src/app/api/protected/backup/import/route.ts
  97. 1 0
      src/features/ai/tools/workshop-columns.ts
  98. 326 0
      src/features/condition-map/Actions/conditionMarkActions.ts
  99. 392 0
      src/features/condition-map/Components/ConditionMap.tsx
  100. 525 0
      src/features/condition-map/Components/ConditionMapCard.tsx

+ 6 - 2
messages/de/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Prüfungsfoto hinzugefügt",
     "inspection_file_added": "Prüfungsdatei hinzugefügt",
     "signature_save": "Unterschrift gespeichert",
-    "signature_remove": "Unterschrift entfernt"
+    "signature_remove": "Unterschrift entfernt",
+    "conditionMark_add": "Zustandsmarkierung hinzugefügt",
+    "conditionMark_remove": "Zustandsmarkierung entfernt"
   },
   "summary": {
     "customField_create": "Benutzerdefiniertes Feld „{name}“ erstellt",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Zeiteintrag für {technician} auf \"{job}\" auf {minutes} Minuten korrigiert",
     "timeEntry_stop": "Uhr für {technician} auf \"{job}\" bei {minutes} Minuten gestoppt",
     "timeEntry_delete": "Zeiteintrag {id} gelöscht",
-    "inspection_phoneUpload": "Datei vom Handy zur Prüfung {ref} hinzugefügt"
+    "inspection_phoneUpload": "Datei vom Handy zur Prüfung {ref} hinzugefügt",
+    "condition_mark_add": "Markierung zur Zustandskarte hinzugefügt",
+    "condition_mark_remove": "Markierung von der Zustandskarte entfernt"
   }
 }

+ 146 - 0
messages/de/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Fahrzeugzustand",
+  "description": "Tippen Sie auf die Zeichnung, wo der Schaden ist. Jede Markierung bekommt Art, Schwere, Notiz und Foto.",
+  "addHint": "Tippen Sie auf ein Teil, um eine Markierung zu setzen",
+  "bodyType": "Karosserie",
+  "legend": "Markierungen",
+  "noMarks": "Noch keine Markierungen. Tippen Sie auf die Zeichnung, wo der Schaden ist.",
+  "previousHeading": "Bei einem früheren Besuch erfasst",
+  "previousHint": "Noch da seit dem letzten Mal, grau gezeichnet. Löschen Sie, was nicht mehr da ist.",
+  "newSinceLastVisit": "Neu seit dem letzten Besuch",
+  "confirmed": "Noch da",
+  "clearRepaired": "Als repariert löschen",
+  "restore": "Wiederherstellen",
+  "edit": "Markierung bearbeiten",
+  "remove": "Markierung entfernen",
+  "removeTitle": "Diese Markierung entfernen?",
+  "removeBody": "Markierung {n} an {area} und ihre Notiz werden entfernt.",
+  "kind": "Art",
+  "severity": "Schwere",
+  "note": "Notiz",
+  "notePlaceholder": "Wo genau, wie groß, was der Kunde gesagt hat",
+  "photo": "Foto",
+  "addPhoto": "Foto hinzufügen",
+  "takePhoto": "Foto aufnehmen",
+  "removePhoto": "Foto entfernen",
+  "save": "Markierung speichern",
+  "done": "Fertig",
+  "cancel": "Abbrechen",
+  "moveHint": "Ziehen Sie eine Markierung, um sie zu verschieben",
+  "enlarge": "{view} vergrößern",
+  "allViews": "Alle Ansichten",
+  "markLabel": "Markierung {n}: {kind} an {area}",
+  "markOn": "{kind} an {area}",
+  "photosOf": "Fotos von Markierung {n}",
+  "saved": "Gespeichert",
+  "saveFailed": "Markierung konnte nicht gespeichert werden",
+  "uploadFailed": "Foto konnte nicht hochgeladen werden",
+  "readOnly": "Zum Ändern der Zustandskarte wieder öffnen.",
+  "count": "{count, plural, =0 {Keine Markierungen} one {# Markierung} other {# Markierungen}}",
+  "previousCount": "{count, plural, one {# von einem früheren Besuch} other {# von früheren Besuchen}}",
+  "elsewhere": "{count, plural, one {# Markierung wurde auf einer anderen Karosserie gesetzt und erscheint nur im Ausdruck} other {# Markierungen wurden auf einer anderen Karosserie gesetzt und erscheinen nur im Ausdruck}}",
+  "photoCount": "{count, plural, one {# Foto} other {# Fotos}}",
+  "clearBody": "Markierung {n} an {area} wird als repariert vermerkt. Sie bleibt in der Historie und wird nicht mehr gedruckt.",
+  "views": {
+    "top": "Von oben",
+    "left": "Linke Seite",
+    "right": "Rechte Seite",
+    "front": "Vorn",
+    "rear": "Hinten"
+  },
+  "bodies": {
+    "sedan": "Limousine",
+    "hatchback": "Schrägheck",
+    "estate": "Kombi",
+    "suv": "SUV",
+    "van": "Transporter",
+    "pickup": "Pickup",
+    "motorcycle": "Motorrad",
+    "boat": "Boot"
+  },
+  "kinds": {
+    "dent": "Delle",
+    "scratch": "Kratzer",
+    "chip": "Steinschlag",
+    "crack": "Riss",
+    "rust": "Rost",
+    "paint": "Lackschaden",
+    "previous_repair": "Frühere Reparatur",
+    "missing": "Fehlendes Teil"
+  },
+  "severities": {
+    "minor": "Leicht",
+    "major": "Schwer"
+  },
+  "panels": {
+    "hood": "Motorhaube",
+    "roof": "Dach",
+    "trunk": "Kofferraumdeckel",
+    "tailgate": "Heckklappe",
+    "front_bumper": "Stoßstange vorn",
+    "rear_bumper": "Stoßstange hinten",
+    "grille": "Kühlergrill",
+    "windshield": "Windschutzscheibe",
+    "rear_window": "Heckscheibe",
+    "left_headlight": "Scheinwerfer links",
+    "right_headlight": "Scheinwerfer rechts",
+    "left_taillight": "Rücklicht links",
+    "right_taillight": "Rücklicht rechts",
+    "left_front_fender": "Kotflügel vorn links",
+    "right_front_fender": "Kotflügel vorn rechts",
+    "left_front_door": "Tür vorn links",
+    "right_front_door": "Tür vorn rechts",
+    "left_rear_door": "Tür hinten links",
+    "right_rear_door": "Tür hinten rechts",
+    "left_rear_quarter": "Seitenteil hinten links",
+    "right_rear_quarter": "Seitenteil hinten rechts",
+    "left_sill": "Schweller links",
+    "right_sill": "Schweller rechts",
+    "left_mirror": "Spiegel links",
+    "right_mirror": "Spiegel rechts",
+    "left_front_wheel": "Rad vorn links",
+    "right_front_wheel": "Rad vorn rechts",
+    "left_rear_wheel": "Rad hinten links",
+    "right_rear_wheel": "Rad hinten rechts",
+    "left_a_pillar": "A-Säule links",
+    "right_a_pillar": "A-Säule rechts",
+    "left_windows": "Seitenscheiben links",
+    "right_windows": "Seitenscheiben rechts",
+    "left_sliding_door": "Schiebetür links",
+    "right_sliding_door": "Schiebetür rechts",
+    "left_side_panel": "Seitenwand links",
+    "right_side_panel": "Seitenwand rechts",
+    "rear_doors": "Hecktüren",
+    "bed": "Ladefläche",
+    "bed_side_left": "Ladeflächenwand links",
+    "bed_side_right": "Ladeflächenwand rechts",
+    "tank": "Tank",
+    "seat": "Sitz",
+    "front_fairing": "Frontverkleidung",
+    "left_fairing": "Verkleidung links",
+    "right_fairing": "Verkleidung rechts",
+    "front_fender": "Schutzblech vorn",
+    "rear_fender": "Schutzblech hinten",
+    "front_wheel": "Vorderrad",
+    "rear_wheel": "Hinterrad",
+    "exhaust": "Auspuff",
+    "handlebars": "Lenker",
+    "headlamp": "Scheinwerfer",
+    "tail": "Heck",
+    "bow": "Bug",
+    "stern": "Heck",
+    "transom": "Spiegel (Heck)",
+    "port_hull": "Rumpf Backbord",
+    "starboard_hull": "Rumpf Steuerbord",
+    "deck": "Deck",
+    "cabin": "Kabine",
+    "windscreen": "Windschutzscheibe",
+    "gunwale_port": "Dollbord Backbord",
+    "gunwale_starboard": "Dollbord Steuerbord",
+    "keel": "Kiel",
+    "outboard": "Außenborder"
+  },
+  "print": {
+    "previous": "früher erfasst"
+  }
+}

+ 26 - 4
messages/de/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Leere Vorlage",
       "description": "Beginnen Sie bei null und erstellen Sie die Checkliste, die Ihre Werkstatt tatsächlich verwendet.",
       "standardLabel": "Leer"
+    },
+    "vehicle-intake": {
+      "name": "Fahrzeugannahme",
+      "description": "Der Rundgang bei der Abgabe, mit dem Kunden daneben: wie das Fahrzeug ankam, was schon an der Karosserie war, was innen liegen blieb. Wird auf dem Arbeitsauftrag gedruckt, den der Kunde unterschreibt.",
+      "standardLabel": "Werkstatt"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Fahrgestell und daran befestigte Teile",
     "no_s7": "Sonstige Ausrüstung",
     "no_s8": "Umweltbelastung",
-    "no_s10": "Vorführung bei einer trafikkstasjon (Verkehrsstation)"
+    "no_s10": "Vorführung bei einer trafikkstasjon (Verkehrsstation)",
+    "arrival": "Ankunft",
+    "exteriorWalkround": "Rundgang außen"
   },
   "checks": {
     "registrationPlates": "Kennzeichen",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diesel: Abgastrübung, älter als Euro 4, mit Turbolader",
     "no_8_2_3_1": "Abgas allgemein und Motorlauf",
     "no_8_3_1": "Flüssigkeitsverluste",
-    "no_10_1": "Vorführung bei einer trafikkstasjon"
+    "no_10_1": "Vorführung bei einer trafikkstasjon",
+    "fuelLevel": "Tankfüllstand",
+    "keysHandedOver": "Übergebene Schlüssel",
+    "warningLightsOnArrival": "Warnleuchten bei Ankunft",
+    "personalBelongings": "Persönliche Gegenstände im Fahrzeug",
+    "glassCondition": "Zustand der Scheiben",
+    "wheelsAndTyresVisual": "Räder und Reifen, Sichtprüfung",
+    "seatsAndTrim": "Sitze und Verkleidung",
+    "floorAndMats": "Boden und Matten",
+    "infotainmentControls": "Infotainment und Bedienelemente"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Anhang I, Nummer 0. Bestätigt, dass das vorgeführte Fahrzeug mit seinen Papieren übereinstimmt.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Max. 3,0 m⁻¹. Der Grenzwert des Herstellers hat Vorrang.",
     "no_8_2_3_1": "Bei laufendem Motor: prüfen, ob die OBD-Fehlfunktionsanzeige einen Fehler anzeigt und ob die Eigendiagnose richtig funktioniert; jede Abweichung ergibt e). Bei der Probefahrt und bei einem Gasstoß aus dem Leerlauf auf sichtbaren blauen, dunkelgrauen oder schwarzen Rauch achten und den Motorlauf prüfen.",
     "no_8_3_1": "Bewertung 2 bei jedem großen Flüssigkeitsverlust (außer Wasser), der die Umwelt schädigen kann oder ein Sicherheitsrisiko für andere Verkehrsteilnehmer darstellt, sofern während der Prüfung Flüssigkeit auf den Boden tropft.",
-    "no_10_1": "Wird vergeben, wenn Umstände erfordern, dass das Fahrzeug zur Beurteilung, Neugenehmigung oder Eintragung im vognkort zu einer trafikkstasjon muss, z. B. unzulässig geänderter Motor oder unzulässig geändertes Steuergerät, tiefer-/höhergelegt, unzulässiger Frontschutz (Frontschutzbügel), mehr Sitzplätze als im vognkort, Umbau auf einen anderen Kraftstoff oder eine genehmigungspflichtige Änderung. Wird zusätzlich zum zutreffenden Mangel in derselben Sache vergeben; bei der Nachprüfung kann nur die Statens vegvesen ihn aufheben."
+    "no_10_1": "Wird vergeben, wenn Umstände erfordern, dass das Fahrzeug zur Beurteilung, Neugenehmigung oder Eintragung im vognkort zu einer trafikkstasjon muss, z. B. unzulässig geänderter Motor oder unzulässig geändertes Steuergerät, tiefer-/höhergelegt, unzulässiger Frontschutz (Frontschutzbügel), mehr Sitzplätze als im vognkort, Umbau auf einen anderen Kraftstoff oder eine genehmigungspflichtige Änderung. Wird zusätzlich zum zutreffenden Mangel in derselben Sache vergeben; bei der Nachprüfung kann nur die Statens vegvesen ihn aufheben.",
+    "bodyConditionMap": "Tippen Sie auf die Zeichnung, wo jede Delle, jeder Kratzer oder Steinschlag ist, und ergänzen Sie Notiz und Foto. Markierungen früherer Besuche erscheinen grau."
   },
   "choices": {
     "full": "Vollständig",
     "partial": "Teilweise",
-    "none": "Keine"
+    "none": "Keine",
+    "fuelEmpty": "Leer",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Voll"
   },
   "defects": {
     "registrationPlateMissing": "Kennzeichen fehlt",

+ 4 - 2
messages/de/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Mängelbewertung",
       "measurement": "Messwert",
       "text": "Freitext",
-      "choice": "Auswahl"
+      "choice": "Auswahl",
+      "condition_map": "Zustandskarte"
     },
     "inputHint": {
       "condition": "Der Mechaniker bewertet den Prüfpunkt nach der Mängelskala.",
       "measurement": "Erfasst eine Zahl mit Einheit, automatisch gegen einen Bereich bewertet.",
       "text": "Erfasst eine schriftliche Beobachtung, etwa eine Reifenmarke oder einen Fehlercode.",
-      "choice": "Der Mechaniker wählt eine Antwort aus einer festen Liste."
+      "choice": "Der Mechaniker wählt eine Antwort aus einer festen Liste.",
+      "condition_map": "Eine Zeichnung des Fahrzeugs, auf der Dellen, Kratzer und Steinschläge markiert werden, jeweils mit Notiz und Foto."
     },
     "cancel": "Abbrechen",
     "create": "Checkliste erstellen",

+ 7 - 1
messages/de/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Bereitgestellt von",
     "signature": "Unterschrift",
     "signedBy": "Unterzeichnet von",
-    "signatureDate": "Datum"
+    "signatureDate": "Datum",
+    "conditionMapTitle": "Fahrzeugzustand",
+    "conditionMapNo": "Nr.",
+    "conditionMapArea": "Bereich",
+    "conditionMapKind": "Art",
+    "conditionMapSeverity": "Schwere",
+    "conditionMapNote": "Notiz"
   },
   "workOrder": {
     "title": "ARBEITSAUFTRAG",

+ 23 - 3
messages/de/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Auf dem Handy öffnen",
       "concerns": "Kundenanliegen",
       "job_description": "Gewünschte Arbeiten",
-      "work_checklist": "Arbeitscheckliste"
+      "work_checklist": "Arbeitscheckliste",
+      "condition_map": "Fahrzeugzustand"
     },
     "fields": {
       "customer_name": "Kundenname",
@@ -770,7 +771,9 @@
       "scheduled": "Geplant",
       "promised": "Zugesagt",
       "work_bay": "Arbeitsplatz",
-      "customer_line": "Unterschriftszeile Kunde"
+      "customer_line": "Unterschriftszeile Kunde",
+      "legend": "Legende",
+      "previous_marks": "Frühere Markierungen"
     },
     "presetsTitle": "Mit einer Anordnung beginnen",
     "presetsHint": "Ersetzt die Anordnung unten. Nichts wird gespeichert, bis Sie speichern.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Bremsbeläge und -scheiben vorne verschlissen; ersetzt",
       "concernWarningLight": "Warnleuchte im Armaturenbrett seit letzter Woche",
       "workRequested": "Bremsbeläge und -scheiben vorne ersetzen, Serviceanzeige zurücksetzen und Probefahrt.",
-      "checklistTestDrive": "Probefahrt"
+      "checklistTestDrive": "Probefahrt",
+      "markScratch": "Kratzer entlang der Tür, 20 cm",
+      "markDent": "Delle unter dem Griff, Lack intakt",
+      "markChip": "Steinschläge im unteren Stoßfänger",
+      "viewTop": "Von oben",
+      "viewLeft": "Linke Seite",
+      "viewRight": "Rechte Seite",
+      "viewFront": "Vorn",
+      "viewRear": "Hinten",
+      "panelLeftRearDoor": "Tür hinten links",
+      "panelLeftFrontDoor": "Tür vorn links",
+      "panelFrontBumper": "Stoßstange vorn",
+      "kindScratch": "Kratzer",
+      "kindDent": "Delle",
+      "kindChip": "Steinschlag",
+      "severityMinor": "Leicht",
+      "severityMajor": "Schwer",
+      "markPrevious": "früher erfasst"
     },
     "logoUpload": "Logo hochladen",
     "logoReplace": "Logo ersetzen",

+ 6 - 2
messages/en/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Added Inspection Photo",
     "inspection_file_added": "Added Inspection File",
     "signature_save": "Saved Signature",
-    "signature_remove": "Removed Signature"
+    "signature_remove": "Removed Signature",
+    "conditionMark_add": "Added a condition mark",
+    "conditionMark_remove": "Removed a condition mark"
   },
   "summary": {
     "customField_create": "Created custom field \"{name}\"",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Corrected time entry for {technician} on \"{job}\" to {minutes} minutes",
     "timeEntry_stop": "Stopped the clock for {technician} on \"{job}\" at {minutes} minutes",
     "timeEntry_delete": "Deleted time entry {id}",
-    "inspection_phoneUpload": "Added a file from a phone to inspection {ref}"
+    "inspection_phoneUpload": "Added a file from a phone to inspection {ref}",
+    "condition_mark_add": "Added a mark to the condition map",
+    "condition_mark_remove": "Removed a mark from the condition map"
   }
 }

+ 146 - 0
messages/en/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Vehicle condition",
+  "description": "Tap the drawing where the damage is. Each mark gets a type, a severity, a note and a photo.",
+  "addHint": "Tap a panel to add a mark",
+  "bodyType": "Body type",
+  "legend": "Marks",
+  "noMarks": "No marks yet. Tap the drawing where the damage is.",
+  "previousHeading": "Recorded on an earlier visit",
+  "previousHint": "Still there from last time, drawn in grey. Clear the ones that are gone.",
+  "newSinceLastVisit": "New since the last visit",
+  "confirmed": "Still there",
+  "clearRepaired": "Clear as repaired",
+  "restore": "Restore",
+  "edit": "Edit mark",
+  "remove": "Remove mark",
+  "removeTitle": "Remove this mark?",
+  "removeBody": "Mark {n} on the {area} and its note will be removed.",
+  "kind": "Type",
+  "severity": "Severity",
+  "note": "Note",
+  "notePlaceholder": "Where exactly, how big, anything the customer said",
+  "photo": "Photo",
+  "addPhoto": "Add photo",
+  "takePhoto": "Take photo",
+  "removePhoto": "Remove photo",
+  "save": "Save mark",
+  "done": "Done",
+  "cancel": "Cancel",
+  "moveHint": "Drag a mark to move it",
+  "enlarge": "Enlarge {view}",
+  "allViews": "All views",
+  "markLabel": "Mark {n}: {kind} on the {area}",
+  "markOn": "{kind} on the {area}",
+  "photosOf": "Photos of mark {n}",
+  "saved": "Saved",
+  "saveFailed": "Could not save the mark",
+  "uploadFailed": "Could not upload the photo",
+  "readOnly": "Reopen to change the condition map.",
+  "count": "{count, plural, =0 {No marks} one {# mark} other {# marks}}",
+  "previousCount": "{count, plural, one {# from an earlier visit} other {# from earlier visits}}",
+  "views": {
+    "top": "Top",
+    "left": "Left side",
+    "right": "Right side",
+    "front": "Front",
+    "rear": "Rear"
+  },
+  "bodies": {
+    "sedan": "Sedan",
+    "hatchback": "Hatchback",
+    "estate": "Estate",
+    "suv": "SUV",
+    "van": "Van",
+    "pickup": "Pickup",
+    "motorcycle": "Motorcycle",
+    "boat": "Boat"
+  },
+  "kinds": {
+    "dent": "Dent",
+    "scratch": "Scratch",
+    "chip": "Stone chip",
+    "crack": "Crack",
+    "rust": "Rust",
+    "paint": "Paint damage",
+    "previous_repair": "Previous repair",
+    "missing": "Missing part"
+  },
+  "severities": {
+    "minor": "Minor",
+    "major": "Major"
+  },
+  "panels": {
+    "hood": "Bonnet",
+    "roof": "Roof",
+    "trunk": "Boot lid",
+    "tailgate": "Tailgate",
+    "front_bumper": "Front bumper",
+    "rear_bumper": "Rear bumper",
+    "grille": "Grille",
+    "windshield": "Windscreen",
+    "rear_window": "Rear window",
+    "left_headlight": "Left headlight",
+    "right_headlight": "Right headlight",
+    "left_taillight": "Left tail light",
+    "right_taillight": "Right tail light",
+    "left_front_fender": "Left front wing",
+    "right_front_fender": "Right front wing",
+    "left_front_door": "Left front door",
+    "right_front_door": "Right front door",
+    "left_rear_door": "Left rear door",
+    "right_rear_door": "Right rear door",
+    "left_rear_quarter": "Left rear quarter",
+    "right_rear_quarter": "Right rear quarter",
+    "left_sill": "Left sill",
+    "right_sill": "Right sill",
+    "left_mirror": "Left mirror",
+    "right_mirror": "Right mirror",
+    "left_front_wheel": "Left front wheel",
+    "right_front_wheel": "Right front wheel",
+    "left_rear_wheel": "Left rear wheel",
+    "right_rear_wheel": "Right rear wheel",
+    "left_a_pillar": "Left A-pillar",
+    "right_a_pillar": "Right A-pillar",
+    "left_windows": "Left side windows",
+    "right_windows": "Right side windows",
+    "left_sliding_door": "Left sliding door",
+    "right_sliding_door": "Right sliding door",
+    "left_side_panel": "Left side panel",
+    "right_side_panel": "Right side panel",
+    "rear_doors": "Rear doors",
+    "bed": "Load bed",
+    "bed_side_left": "Left bed side",
+    "bed_side_right": "Right bed side",
+    "tank": "Tank",
+    "seat": "Seat",
+    "front_fairing": "Front fairing",
+    "left_fairing": "Left fairing",
+    "right_fairing": "Right fairing",
+    "front_fender": "Front mudguard",
+    "rear_fender": "Rear mudguard",
+    "front_wheel": "Front wheel",
+    "rear_wheel": "Rear wheel",
+    "exhaust": "Exhaust",
+    "handlebars": "Handlebars",
+    "headlamp": "Headlamp",
+    "tail": "Tail",
+    "bow": "Bow",
+    "stern": "Stern",
+    "transom": "Transom",
+    "port_hull": "Port hull",
+    "starboard_hull": "Starboard hull",
+    "deck": "Deck",
+    "cabin": "Cabin",
+    "windscreen": "Windscreen",
+    "gunwale_port": "Port gunwale",
+    "gunwale_starboard": "Starboard gunwale",
+    "keel": "Keel",
+    "outboard": "Outboard"
+  },
+  "print": {
+    "previous": "recorded earlier"
+  },
+  "elsewhere": "{count, plural, one {# mark was drawn on another body type and is listed on the print only} other {# marks were drawn on another body type and are listed on the print only}}",
+  "photoCount": "{count, plural, one {# photo} other {# photos}}",
+  "clearBody": "Mark {n} on the {area} will be recorded as repaired. It stays in the history and stops printing."
+}

+ 26 - 4
messages/en/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Blank template",
       "description": "Start from nothing and build the checklist your workshop actually uses.",
       "standardLabel": "Empty"
+    },
+    "vehicle-intake": {
+      "name": "Vehicle check-in",
+      "description": "The walk-round at drop-off, done with the customer standing there: how the car arrived, what was already on the body, what was left inside. Prints on the work order the customer signs.",
+      "standardLabel": "Workshop"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Chassis and chassis attachments",
     "no_s7": "Other equipment",
     "no_s8": "Nuisance",
-    "no_s10": "Presentation at a traffic station"
+    "no_s10": "Presentation at a traffic station",
+    "arrival": "Arrival",
+    "exteriorWalkround": "Exterior walk-round"
   },
   "checks": {
     "registrationPlates": "Registration plates",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diesel: smoke opacity, older than Euro 4, turbocharged",
     "no_8_2_3_1": "Exhaust in general and engine running",
     "no_8_3_1": "Fluid leaks",
-    "no_10_1": "Presentation at a traffic station"
+    "no_10_1": "Presentation at a traffic station",
+    "fuelLevel": "Fuel level",
+    "keysHandedOver": "Keys handed over",
+    "warningLightsOnArrival": "Warning lights on arrival",
+    "personalBelongings": "Personal belongings left in the vehicle",
+    "glassCondition": "Glass condition",
+    "wheelsAndTyresVisual": "Wheels and tyres, visual",
+    "seatsAndTrim": "Seats and trim",
+    "floorAndMats": "Floor and mats",
+    "infotainmentControls": "Infotainment and controls"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Annex I, item 0. Confirms the vehicle presented matches its papers.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Max 3.0 m⁻¹. The manufacturer's limit takes precedence.",
     "no_8_2_3_1": "With the engine running: check whether the OBD malfunction indicator shows a fault and whether self-diagnosis works correctly; any deviation gives e). Look for visible blue, dark grey or black smoke on the road test and on a throttle blip from idle, and check how the engine runs.",
     "no_8_3_1": "Grade 2 for any large fluid leak (other than water) likely to harm the environment or be a safety risk for other road users, provided it drips on the ground during the inspection.",
-    "no_10_1": "Given when something requires the vehicle to go to a traffic station for assessment, new approval or an amendment to the registration certificate, e.g. illegal engine or ECU modification, lowered/raised, non-compliant front protection (bull bar), more seats than registered, converted to another fuel or a modification requiring approval. Given in addition to the relevant defect on the same matter; at re-inspection only Statens vegvesen can clear it."
+    "no_10_1": "Given when something requires the vehicle to go to a traffic station for assessment, new approval or an amendment to the registration certificate, e.g. illegal engine or ECU modification, lowered/raised, non-compliant front protection (bull bar), more seats than registered, converted to another fuel or a modification requiring approval. Given in addition to the relevant defect on the same matter; at re-inspection only Statens vegvesen can clear it.",
+    "bodyConditionMap": "Tap the drawing where each dent, scratch or chip is, and add a note and a photo. Marks from earlier visits show in grey."
   },
   "choices": {
     "full": "Full",
     "partial": "Partial",
-    "none": "None"
+    "none": "None",
+    "fuelEmpty": "Empty",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Full"
   },
   "defects": {
     "registrationPlateMissing": "Registration plate missing",

+ 4 - 2
messages/en/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Defect grading",
       "measurement": "Measurement",
       "text": "Free text",
-      "choice": "Choice"
+      "choice": "Choice",
+      "condition_map": "Condition map"
     },
     "inputHint": {
       "condition": "Technician grades the check against the defect scale.",
       "measurement": "Records a number in a unit, graded automatically against a range.",
       "text": "Records a written observation, e.g. a tyre brand or a fault code.",
-      "choice": "Technician picks one of a fixed list of answers."
+      "choice": "Technician picks one of a fixed list of answers.",
+      "condition_map": "A drawing of the vehicle to mark dents, scratches and chips on, each with a note and a photo."
     },
     "cancel": "Cancel",
     "create": "Create checklist",

+ 7 - 1
messages/en/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Powered by",
     "signature": "Signature",
     "signedBy": "Signed by",
-    "signatureDate": "Date"
+    "signatureDate": "Date",
+    "conditionMapTitle": "Vehicle condition",
+    "conditionMapNo": "#",
+    "conditionMapArea": "Area",
+    "conditionMapKind": "Type",
+    "conditionMapSeverity": "Severity",
+    "conditionMapNote": "Note"
   },
   "workOrder": {
     "title": "WORK ORDER",

+ 23 - 3
messages/en/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Open on Phone",
       "concerns": "Customer Concerns",
       "job_description": "Work Requested",
-      "work_checklist": "Work Checklist"
+      "work_checklist": "Work Checklist",
+      "condition_map": "Vehicle Condition"
     },
     "fields": {
       "customer_name": "Customer Name",
@@ -770,7 +771,9 @@
       "scheduled": "Scheduled",
       "promised": "Promised",
       "work_bay": "Work Bay",
-      "customer_line": "Customer signature line"
+      "customer_line": "Customer signature line",
+      "legend": "Legend",
+      "previous_marks": "Earlier marks"
     },
     "presetsTitle": "Start from an arrangement",
     "presetsHint": "Replaces the arrangement below. Nothing is saved until you save.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Front pads and discs worn; replaced",
       "concernWarningLight": "Warning light on the dashboard since last week",
       "workRequested": "Replace front brake pads and discs, reset the service indicator and road test.",
-      "checklistTestDrive": "Road test"
+      "checklistTestDrive": "Road test",
+      "markScratch": "Key scratch along the door, 20 cm",
+      "markDent": "Dent below the handle, paint intact",
+      "markChip": "Stone chips in the lower bumper",
+      "viewTop": "Top",
+      "viewLeft": "Left side",
+      "viewRight": "Right side",
+      "viewFront": "Front",
+      "viewRear": "Rear",
+      "panelLeftRearDoor": "Left rear door",
+      "panelLeftFrontDoor": "Left front door",
+      "panelFrontBumper": "Front bumper",
+      "kindScratch": "Scratch",
+      "kindDent": "Dent",
+      "kindChip": "Stone chip",
+      "severityMinor": "Minor",
+      "severityMajor": "Major",
+      "markPrevious": "recorded earlier"
     },
     "logoUpload": "Upload logo",
     "logoReplace": "Replace logo",

+ 6 - 2
messages/es/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Añadió foto de inspección",
     "inspection_file_added": "Añadió archivo de inspección",
     "signature_save": "Guardó la firma",
-    "signature_remove": "Eliminó la firma"
+    "signature_remove": "Eliminó la firma",
+    "conditionMark_add": "Marca de estado añadida",
+    "conditionMark_remove": "Marca de estado eliminada"
   },
   "summary": {
     "customField_create": "Campo personalizado «{name}» creado",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Entrada de tiempo de {technician} en \"{job}\" corregida a {minutes} minutos",
     "timeEntry_stop": "Reloj de {technician} en \"{job}\" detenido en {minutes} minutos",
     "timeEntry_delete": "Entrada de tiempo {id} eliminada",
-    "inspection_phoneUpload": "Añadió un archivo desde un teléfono a la inspección {ref}"
+    "inspection_phoneUpload": "Añadió un archivo desde un teléfono a la inspección {ref}",
+    "condition_mark_add": "Añadió una marca al mapa de estado",
+    "condition_mark_remove": "Eliminó una marca del mapa de estado"
   }
 }

+ 146 - 0
messages/es/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Estado del vehículo",
+  "description": "Toque el dibujo donde está el daño. Cada marca lleva un tipo, una gravedad, una nota y una foto.",
+  "addHint": "Toque un panel para añadir una marca",
+  "bodyType": "Carrocería",
+  "legend": "Marcas",
+  "noMarks": "Aún no hay marcas. Toque el dibujo donde está el daño.",
+  "previousHeading": "Registrado en una visita anterior",
+  "previousHint": "Sigue ahí desde la última vez, en gris. Borre las que ya no estén.",
+  "newSinceLastVisit": "Nuevo desde la última visita",
+  "confirmed": "Sigue ahí",
+  "clearRepaired": "Borrar como reparado",
+  "restore": "Restaurar",
+  "edit": "Editar marca",
+  "remove": "Quitar marca",
+  "removeTitle": "¿Quitar esta marca?",
+  "removeBody": "La marca {n} en {area} y su nota se quitarán.",
+  "kind": "Tipo",
+  "severity": "Gravedad",
+  "note": "Nota",
+  "notePlaceholder": "Dónde exactamente, tamaño, lo que dijo el cliente",
+  "photo": "Foto",
+  "addPhoto": "Añadir foto",
+  "takePhoto": "Hacer foto",
+  "removePhoto": "Quitar foto",
+  "save": "Guardar marca",
+  "done": "Listo",
+  "cancel": "Cancelar",
+  "moveHint": "Arrastre una marca para moverla",
+  "enlarge": "Ampliar {view}",
+  "allViews": "Todas las vistas",
+  "markLabel": "Marca {n}: {kind} en {area}",
+  "markOn": "{kind} en {area}",
+  "photosOf": "Fotos de la marca {n}",
+  "saved": "Guardado",
+  "saveFailed": "No se pudo guardar la marca",
+  "uploadFailed": "No se pudo subir la foto",
+  "readOnly": "Reabra para cambiar el mapa de estado.",
+  "count": "{count, plural, =0 {Sin marcas} one {# marca} other {# marcas}}",
+  "previousCount": "{count, plural, one {# de una visita anterior} other {# de visitas anteriores}}",
+  "elsewhere": "{count, plural, one {# marca se hizo en otra carrocería y solo aparece en la impresión} other {# marcas se hicieron en otra carrocería y solo aparecen en la impresión}}",
+  "photoCount": "{count, plural, one {# foto} other {# fotos}}",
+  "clearBody": "La marca {n} en {area} quedará registrada como reparada. Se conserva en el historial y deja de imprimirse.",
+  "views": {
+    "top": "Superior",
+    "left": "Lado izquierdo",
+    "right": "Lado derecho",
+    "front": "Frontal",
+    "rear": "Trasera"
+  },
+  "bodies": {
+    "sedan": "Sedán",
+    "hatchback": "Compacto",
+    "estate": "Familiar",
+    "suv": "SUV",
+    "van": "Furgoneta",
+    "pickup": "Pickup",
+    "motorcycle": "Motocicleta",
+    "boat": "Embarcación"
+  },
+  "kinds": {
+    "dent": "Abolladura",
+    "scratch": "Arañazo",
+    "chip": "Picada de piedra",
+    "crack": "Grieta",
+    "rust": "Óxido",
+    "paint": "Daño en la pintura",
+    "previous_repair": "Reparación anterior",
+    "missing": "Pieza faltante"
+  },
+  "severities": {
+    "minor": "Leve",
+    "major": "Grave"
+  },
+  "panels": {
+    "hood": "Capó",
+    "roof": "Techo",
+    "trunk": "Tapa del maletero",
+    "tailgate": "Portón trasero",
+    "front_bumper": "Paragolpes delantero",
+    "rear_bumper": "Paragolpes trasero",
+    "grille": "Parrilla",
+    "windshield": "Parabrisas",
+    "rear_window": "Luneta trasera",
+    "left_headlight": "Faro izquierdo",
+    "right_headlight": "Faro derecho",
+    "left_taillight": "Piloto trasero izquierdo",
+    "right_taillight": "Piloto trasero derecho",
+    "left_front_fender": "Aleta delantera izquierda",
+    "right_front_fender": "Aleta delantera derecha",
+    "left_front_door": "Puerta delantera izquierda",
+    "right_front_door": "Puerta delantera derecha",
+    "left_rear_door": "Puerta trasera izquierda",
+    "right_rear_door": "Puerta trasera derecha",
+    "left_rear_quarter": "Aleta trasera izquierda",
+    "right_rear_quarter": "Aleta trasera derecha",
+    "left_sill": "Estribo izquierdo",
+    "right_sill": "Estribo derecho",
+    "left_mirror": "Retrovisor izquierdo",
+    "right_mirror": "Retrovisor derecho",
+    "left_front_wheel": "Rueda delantera izquierda",
+    "right_front_wheel": "Rueda delantera derecha",
+    "left_rear_wheel": "Rueda trasera izquierda",
+    "right_rear_wheel": "Rueda trasera derecha",
+    "left_a_pillar": "Pilar A izquierdo",
+    "right_a_pillar": "Pilar A derecho",
+    "left_windows": "Ventanillas izquierdas",
+    "right_windows": "Ventanillas derechas",
+    "left_sliding_door": "Puerta corredera izquierda",
+    "right_sliding_door": "Puerta corredera derecha",
+    "left_side_panel": "Panel lateral izquierdo",
+    "right_side_panel": "Panel lateral derecho",
+    "rear_doors": "Puertas traseras",
+    "bed": "Caja de carga",
+    "bed_side_left": "Lateral izquierdo de la caja",
+    "bed_side_right": "Lateral derecho de la caja",
+    "tank": "Depósito",
+    "seat": "Asiento",
+    "front_fairing": "Carenado frontal",
+    "left_fairing": "Carenado izquierdo",
+    "right_fairing": "Carenado derecho",
+    "front_fender": "Guardabarros delantero",
+    "rear_fender": "Guardabarros trasero",
+    "front_wheel": "Rueda delantera",
+    "rear_wheel": "Rueda trasera",
+    "exhaust": "Escape",
+    "handlebars": "Manillar",
+    "headlamp": "Faro",
+    "tail": "Cola",
+    "bow": "Proa",
+    "stern": "Popa",
+    "transom": "Espejo de popa",
+    "port_hull": "Casco babor",
+    "starboard_hull": "Casco estribor",
+    "deck": "Cubierta",
+    "cabin": "Cabina",
+    "windscreen": "Parabrisas",
+    "gunwale_port": "Regala babor",
+    "gunwale_starboard": "Regala estribor",
+    "keel": "Quilla",
+    "outboard": "Fueraborda"
+  },
+  "print": {
+    "previous": "registrado antes"
+  }
+}

+ 26 - 4
messages/es/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Plantilla en blanco",
       "description": "Empieza desde cero y crea la lista de comprobación que realmente usa tu taller.",
       "standardLabel": "Vacía"
+    },
+    "vehicle-intake": {
+      "name": "Recepción del vehículo",
+      "description": "La vuelta al vehículo en la entrega, con el cliente delante: cómo llegó, qué tenía ya la carrocería, qué quedó dentro. Se imprime en la orden de trabajo que firma el cliente.",
+      "standardLabel": "Taller"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Bastidor y elementos fijados al bastidor",
     "no_s7": "Otro equipamiento",
     "no_s8": "Efectos perjudiciales",
-    "no_s10": "Presentación en la trafikkstasjon (estación de inspección de Statens vegvesen)"
+    "no_s10": "Presentación en la trafikkstasjon (estación de inspección de Statens vegvesen)",
+    "arrival": "Llegada",
+    "exteriorWalkround": "Vuelta exterior"
   },
   "checks": {
     "registrationPlates": "Placas de matrícula",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diésel: opacidad de los humos, anterior a EU-4, turboalimentado",
     "no_8_2_3_1": "Escape en general y motor en marcha",
     "no_8_3_1": "Fugas de líquidos",
-    "no_10_1": "Presentación en la trafikkstasjon"
+    "no_10_1": "Presentación en la trafikkstasjon",
+    "fuelLevel": "Nivel de combustible",
+    "keysHandedOver": "Llaves entregadas",
+    "warningLightsOnArrival": "Testigos encendidos a la llegada",
+    "personalBelongings": "Objetos personales dejados en el vehículo",
+    "glassCondition": "Estado de los cristales",
+    "wheelsAndTyresVisual": "Ruedas y neumáticos, visual",
+    "seatsAndTrim": "Asientos y tapicería",
+    "floorAndMats": "Suelo y alfombrillas",
+    "infotainmentControls": "Infoentretenimiento y mandos"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Anexo I, punto 0. Confirma que el vehículo presentado coincide con su documentación.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Máx. 3,0 m⁻¹. Prevalece el límite del fabricante.",
     "no_8_2_3_1": "Con el motor en marcha: comprueba si el testigo de avería OBD indica un fallo y si el autodiagnóstico funciona correctamente; cualquier desviación da e). Observa si hay humo azul, gris oscuro o negro visible en la prueba en carretera y en un golpe de acelerador desde ralentí, y comprueba cómo funciona el motor.",
     "no_8_3_1": "Grado 2 para cualquier fuga importante de líquido (distinto del agua) que pueda dañar el medio ambiente o suponer un riesgo para la seguridad de otros usuarios de la vía, siempre que gotee al suelo durante la inspección.",
-    "no_10_1": "Se anota cuando alguna circunstancia obliga a llevar el vehículo a la trafikkstasjon para su valoración, nueva homologación o anotación en el vognkort, p. ej. motor o centralita modificados ilegalmente, rebajado/elevado, protección frontal no conforme (defensa delantera), más plazas que las registradas, transformado a otro combustible o una modificación sujeta a homologación. Se anota además del defecto correspondiente sobre el mismo asunto; en la reinspección solo Statens vegvesen puede darlo por subsanado."
+    "no_10_1": "Se anota cuando alguna circunstancia obliga a llevar el vehículo a la trafikkstasjon para su valoración, nueva homologación o anotación en el vognkort, p. ej. motor o centralita modificados ilegalmente, rebajado/elevado, protección frontal no conforme (defensa delantera), más plazas que las registradas, transformado a otro combustible o una modificación sujeta a homologación. Se anota además del defecto correspondiente sobre el mismo asunto; en la reinspección solo Statens vegvesen puede darlo por subsanado.",
+    "bodyConditionMap": "Toque el dibujo donde está cada abolladura, arañazo o picada y añada una nota y una foto. Las marcas de visitas anteriores aparecen en gris."
   },
   "choices": {
     "full": "Completo",
     "partial": "Parcial",
-    "none": "Ninguno"
+    "none": "Ninguno",
+    "fuelEmpty": "Vacío",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Lleno"
   },
   "defects": {
     "registrationPlateMissing": "Falta la placa de matrícula",

+ 4 - 2
messages/es/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Calificación de deficiencia",
       "measurement": "Medición",
       "text": "Texto libre",
-      "choice": "Elección"
+      "choice": "Elección",
+      "condition_map": "Mapa de estado"
     },
     "inputHint": {
       "condition": "El técnico califica el punto según la escala de deficiencias.",
       "measurement": "Registra un número con unidad, calificado automáticamente frente a un intervalo.",
       "text": "Registra una observación escrita, por ejemplo una marca de neumático o un código de avería.",
-      "choice": "El técnico elige una respuesta de una lista fija."
+      "choice": "El técnico elige una respuesta de una lista fija.",
+      "condition_map": "Un dibujo del vehículo para marcar abolladuras, arañazos y picadas, cada una con nota y foto."
     },
     "cancel": "Cancelar",
     "create": "Crear lista de comprobación",

+ 7 - 1
messages/es/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Desarrollado por",
     "signature": "Firma",
     "signedBy": "Firmado por",
-    "signatureDate": "Fecha"
+    "signatureDate": "Fecha",
+    "conditionMapTitle": "Estado del vehículo",
+    "conditionMapNo": "N.º",
+    "conditionMapArea": "Zona",
+    "conditionMapKind": "Tipo",
+    "conditionMapSeverity": "Gravedad",
+    "conditionMapNote": "Nota"
   },
   "workOrder": {
     "title": "ORDEN DE TRABAJO",

+ 23 - 3
messages/es/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Abrir en el móvil",
       "concerns": "Inquietudes del cliente",
       "job_description": "Trabajo solicitado",
-      "work_checklist": "Lista de trabajos"
+      "work_checklist": "Lista de trabajos",
+      "condition_map": "Estado del vehículo"
     },
     "fields": {
       "customer_name": "Nombre del cliente",
@@ -770,7 +771,9 @@
       "scheduled": "Programado",
       "promised": "Prometido",
       "work_bay": "Puesto",
-      "customer_line": "Línea de firma del cliente"
+      "customer_line": "Línea de firma del cliente",
+      "legend": "Leyenda",
+      "previous_marks": "Marcas anteriores"
     },
     "presetsTitle": "Empezar con una disposición",
     "presetsHint": "Sustituye la disposición de abajo. No se guarda nada hasta que guardes.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Pastillas y discos delanteros desgastados; sustituidos",
       "concernWarningLight": "Testigo encendido en el cuadro desde la semana pasada",
       "workRequested": "Sustituir pastillas y discos delanteros, reiniciar el indicador de servicio y probar en carretera.",
-      "checklistTestDrive": "Prueba en carretera"
+      "checklistTestDrive": "Prueba en carretera",
+      "markScratch": "Arañazo de llave a lo largo de la puerta, 20 cm",
+      "markDent": "Abolladura bajo la manilla, pintura intacta",
+      "markChip": "Picadas de piedra en la parte baja del paragolpes",
+      "viewTop": "Superior",
+      "viewLeft": "Lado izquierdo",
+      "viewRight": "Lado derecho",
+      "viewFront": "Frontal",
+      "viewRear": "Trasera",
+      "panelLeftRearDoor": "Puerta trasera izquierda",
+      "panelLeftFrontDoor": "Puerta delantera izquierda",
+      "panelFrontBumper": "Paragolpes delantero",
+      "kindScratch": "Arañazo",
+      "kindDent": "Abolladura",
+      "kindChip": "Picada de piedra",
+      "severityMinor": "Leve",
+      "severityMajor": "Grave",
+      "markPrevious": "registrado antes"
     },
     "logoUpload": "Subir logotipo",
     "logoReplace": "Cambiar logotipo",

+ 6 - 2
messages/fr/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Photo d'inspection ajoutée",
     "inspection_file_added": "Fichier d'inspection ajouté",
     "signature_save": "Signature enregistrée",
-    "signature_remove": "Signature supprimée"
+    "signature_remove": "Signature supprimée",
+    "conditionMark_add": "Marque d'état ajoutée",
+    "conditionMark_remove": "Marque d'état supprimée"
   },
   "summary": {
     "customField_create": "Champ personnalisé « {name} » créé",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Entrée de temps de {technician} sur « {job} » corrigée à {minutes} minutes",
     "timeEntry_stop": "Pointage de {technician} sur « {job} » arrêté à {minutes} minutes",
     "timeEntry_delete": "Entrée de temps {id} supprimée",
-    "inspection_phoneUpload": "Fichier ajouté depuis un téléphone à l'inspection {ref}"
+    "inspection_phoneUpload": "Fichier ajouté depuis un téléphone à l'inspection {ref}",
+    "condition_mark_add": "A ajouté une marque à la carte d'état",
+    "condition_mark_remove": "A supprimé une marque de la carte d'état"
   }
 }

+ 146 - 0
messages/fr/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "État du véhicule",
+  "description": "Touchez le dessin là où se trouve le dommage. Chaque marque reçoit un type, une gravité, une note et une photo.",
+  "addHint": "Touchez un élément pour ajouter une marque",
+  "bodyType": "Carrosserie",
+  "legend": "Marques",
+  "noMarks": "Pas encore de marque. Touchez le dessin là où se trouve le dommage.",
+  "previousHeading": "Relevé lors d'une visite précédente",
+  "previousHint": "Toujours là depuis la dernière fois, en gris. Effacez celles qui ont disparu.",
+  "newSinceLastVisit": "Nouveau depuis la dernière visite",
+  "confirmed": "Toujours là",
+  "clearRepaired": "Effacer comme réparé",
+  "restore": "Restaurer",
+  "edit": "Modifier la marque",
+  "remove": "Supprimer la marque",
+  "removeTitle": "Supprimer cette marque ?",
+  "removeBody": "La marque {n} sur {area} et sa note seront supprimées.",
+  "kind": "Type",
+  "severity": "Gravité",
+  "note": "Note",
+  "notePlaceholder": "Où exactement, quelle taille, ce que le client a dit",
+  "photo": "Photo",
+  "addPhoto": "Ajouter une photo",
+  "takePhoto": "Prendre une photo",
+  "removePhoto": "Supprimer la photo",
+  "save": "Enregistrer la marque",
+  "done": "Terminé",
+  "cancel": "Annuler",
+  "moveHint": "Faites glisser une marque pour la déplacer",
+  "enlarge": "Agrandir {view}",
+  "allViews": "Toutes les vues",
+  "markLabel": "Marque {n} : {kind} sur {area}",
+  "markOn": "{kind} sur {area}",
+  "photosOf": "Photos de la marque {n}",
+  "saved": "Enregistré",
+  "saveFailed": "Impossible d'enregistrer la marque",
+  "uploadFailed": "Impossible d'envoyer la photo",
+  "readOnly": "Rouvrez pour modifier la carte d'état.",
+  "count": "{count, plural, =0 {Aucune marque} one {# marque} other {# marques}}",
+  "previousCount": "{count, plural, one {# d'une visite précédente} other {# de visites précédentes}}",
+  "elsewhere": "{count, plural, one {# marque a été posée sur une autre carrosserie et n'apparaît que sur l'impression} other {# marques ont été posées sur une autre carrosserie et n'apparaissent que sur l'impression}}",
+  "photoCount": "{count, plural, one {# photo} other {# photos}}",
+  "clearBody": "La marque {n} sur {area} sera notée comme réparée. Elle reste dans l'historique et ne s'imprime plus.",
+  "views": {
+    "top": "Dessus",
+    "left": "Côté gauche",
+    "right": "Côté droit",
+    "front": "Avant",
+    "rear": "Arrière"
+  },
+  "bodies": {
+    "sedan": "Berline",
+    "hatchback": "Compacte",
+    "estate": "Break",
+    "suv": "SUV",
+    "van": "Utilitaire",
+    "pickup": "Pick-up",
+    "motorcycle": "Moto",
+    "boat": "Bateau"
+  },
+  "kinds": {
+    "dent": "Bosse",
+    "scratch": "Rayure",
+    "chip": "Éclat de gravillon",
+    "crack": "Fissure",
+    "rust": "Rouille",
+    "paint": "Peinture abîmée",
+    "previous_repair": "Réparation antérieure",
+    "missing": "Pièce manquante"
+  },
+  "severities": {
+    "minor": "Légère",
+    "major": "Importante"
+  },
+  "panels": {
+    "hood": "Capot",
+    "roof": "Toit",
+    "trunk": "Couvercle de coffre",
+    "tailgate": "Hayon",
+    "front_bumper": "Pare-chocs avant",
+    "rear_bumper": "Pare-chocs arrière",
+    "grille": "Calandre",
+    "windshield": "Pare-brise",
+    "rear_window": "Lunette arrière",
+    "left_headlight": "Phare gauche",
+    "right_headlight": "Phare droit",
+    "left_taillight": "Feu arrière gauche",
+    "right_taillight": "Feu arrière droit",
+    "left_front_fender": "Aile avant gauche",
+    "right_front_fender": "Aile avant droite",
+    "left_front_door": "Porte avant gauche",
+    "right_front_door": "Porte avant droite",
+    "left_rear_door": "Porte arrière gauche",
+    "right_rear_door": "Porte arrière droite",
+    "left_rear_quarter": "Aile arrière gauche",
+    "right_rear_quarter": "Aile arrière droite",
+    "left_sill": "Bas de caisse gauche",
+    "right_sill": "Bas de caisse droit",
+    "left_mirror": "Rétroviseur gauche",
+    "right_mirror": "Rétroviseur droit",
+    "left_front_wheel": "Roue avant gauche",
+    "right_front_wheel": "Roue avant droite",
+    "left_rear_wheel": "Roue arrière gauche",
+    "right_rear_wheel": "Roue arrière droite",
+    "left_a_pillar": "Montant A gauche",
+    "right_a_pillar": "Montant A droit",
+    "left_windows": "Vitres gauches",
+    "right_windows": "Vitres droites",
+    "left_sliding_door": "Porte coulissante gauche",
+    "right_sliding_door": "Porte coulissante droite",
+    "left_side_panel": "Panneau latéral gauche",
+    "right_side_panel": "Panneau latéral droit",
+    "rear_doors": "Portes arrière",
+    "bed": "Benne",
+    "bed_side_left": "Ridelle gauche",
+    "bed_side_right": "Ridelle droite",
+    "tank": "Réservoir",
+    "seat": "Selle",
+    "front_fairing": "Carénage avant",
+    "left_fairing": "Carénage gauche",
+    "right_fairing": "Carénage droit",
+    "front_fender": "Garde-boue avant",
+    "rear_fender": "Garde-boue arrière",
+    "front_wheel": "Roue avant",
+    "rear_wheel": "Roue arrière",
+    "exhaust": "Échappement",
+    "handlebars": "Guidon",
+    "headlamp": "Phare",
+    "tail": "Arrière",
+    "bow": "Proue",
+    "stern": "Poupe",
+    "transom": "Tableau arrière",
+    "port_hull": "Coque bâbord",
+    "starboard_hull": "Coque tribord",
+    "deck": "Pont",
+    "cabin": "Cabine",
+    "windscreen": "Pare-brise",
+    "gunwale_port": "Plat-bord bâbord",
+    "gunwale_starboard": "Plat-bord tribord",
+    "keel": "Quille",
+    "outboard": "Hors-bord"
+  },
+  "print": {
+    "previous": "relevé auparavant"
+  }
+}

+ 26 - 4
messages/fr/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Modèle vierge",
       "description": "Partez de zéro et construisez la liste de contrôle que votre atelier utilise réellement.",
       "standardLabel": "Vide"
+    },
+    "vehicle-intake": {
+      "name": "Prise en charge du véhicule",
+      "description": "Le tour du véhicule au dépôt, le client à côté : comment il est arrivé, ce qu'il y avait déjà sur la carrosserie, ce qui est resté à l'intérieur. S'imprime sur l'ordre de travail que le client signe.",
+      "standardLabel": "Atelier"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Châssis et accessoires du châssis",
     "no_s7": "Autres équipements",
     "no_s8": "Nuisances",
-    "no_s10": "Présentation à la trafikkstasjon (centre de contrôle de Statens vegvesen)"
+    "no_s10": "Présentation à la trafikkstasjon (centre de contrôle de Statens vegvesen)",
+    "arrival": "Arrivée",
+    "exteriorWalkround": "Tour extérieur"
   },
   "checks": {
     "registrationPlates": "Plaques d'immatriculation",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diesel : opacité des fumées, antérieur à EU-4, turbocompressé",
     "no_8_2_3_1": "Gaz d'échappement en général et fonctionnement du moteur",
     "no_8_3_1": "Fuites de liquides",
-    "no_10_1": "Présentation à la trafikkstasjon"
+    "no_10_1": "Présentation à la trafikkstasjon",
+    "fuelLevel": "Niveau de carburant",
+    "keysHandedOver": "Clés remises",
+    "warningLightsOnArrival": "Voyants allumés à l'arrivée",
+    "personalBelongings": "Effets personnels laissés dans le véhicule",
+    "glassCondition": "État des vitres",
+    "wheelsAndTyresVisual": "Roues et pneus, visuel",
+    "seatsAndTrim": "Sièges et garnitures",
+    "floorAndMats": "Plancher et tapis",
+    "infotainmentControls": "Infodivertissement et commandes"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Annexe I, point 0. Confirme que le véhicule présenté correspond à ses documents.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Max 3,0 m⁻¹. La limite du constructeur prime.",
     "no_8_2_3_1": "Moteur en marche : vérifier si le témoin de défaillance OBD signale un défaut et si l'autodiagnostic fonctionne correctement ; tout écart donne e). Rechercher une fumée bleue, gris foncé ou noire visible lors de l'essai sur route et lors d'un coup d'accélérateur depuis le ralenti, et contrôler le fonctionnement du moteur.",
     "no_8_3_1": "Gravité 2 pour toute fuite de liquide importante (autre que de l'eau) susceptible de nuire à l'environnement ou de constituer un risque pour la sécurité, à condition qu'elle goutte au sol pendant le contrôle.",
-    "no_10_1": "Attribué lorsqu'une situation impose que le véhicule soit présenté à une trafikkstasjon pour évaluation, nouvelle homologation ou mention au vognkort, p. ex. moteur ou calculateur modifié illégalement, véhicule abaissé/rehaussé, protection frontale non conforme (pare-buffle), plus de places assises que dans le vognkort, conversion à un autre carburant ou transformation soumise à homologation. Attribué en plus du défaut correspondant sur le même point ; lors de la contre-visite, seul Statens vegvesen peut le lever."
+    "no_10_1": "Attribué lorsqu'une situation impose que le véhicule soit présenté à une trafikkstasjon pour évaluation, nouvelle homologation ou mention au vognkort, p. ex. moteur ou calculateur modifié illégalement, véhicule abaissé/rehaussé, protection frontale non conforme (pare-buffle), plus de places assises que dans le vognkort, conversion à un autre carburant ou transformation soumise à homologation. Attribué en plus du défaut correspondant sur le même point ; lors de la contre-visite, seul Statens vegvesen peut le lever.",
+    "bodyConditionMap": "Touchez le dessin là où se trouve chaque bosse, rayure ou éclat, puis ajoutez une note et une photo. Les marques des visites précédentes apparaissent en gris."
   },
   "choices": {
     "full": "Complet",
     "partial": "Partiel",
-    "none": "Aucun"
+    "none": "Aucun",
+    "fuelEmpty": "Vide",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Plein"
   },
   "defects": {
     "registrationPlateMissing": "Plaque d'immatriculation manquante",

+ 4 - 2
messages/fr/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Évaluation de défaillance",
       "measurement": "Mesure",
       "text": "Texte libre",
-      "choice": "Choix"
+      "choice": "Choix",
+      "condition_map": "Carte d'état"
     },
     "inputHint": {
       "condition": "Le technicien évalue le point selon l'échelle de défaillances.",
       "measurement": "Enregistre un nombre assorti d'une unité, évalué automatiquement par rapport à une plage.",
       "text": "Enregistre une observation écrite, par exemple une marque de pneumatique ou un code défaut.",
-      "choice": "Le technicien choisit une réponse parmi une liste fixe."
+      "choice": "Le technicien choisit une réponse parmi une liste fixe.",
+      "condition_map": "Un dessin du véhicule pour y marquer bosses, rayures et éclats, chacun avec une note et une photo."
     },
     "cancel": "Annuler",
     "create": "Créer la liste de contrôle",

+ 7 - 1
messages/fr/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Propulsé par",
     "signature": "Signature",
     "signedBy": "Signé par",
-    "signatureDate": "Date"
+    "signatureDate": "Date",
+    "conditionMapTitle": "État du véhicule",
+    "conditionMapNo": "N°",
+    "conditionMapArea": "Zone",
+    "conditionMapKind": "Type",
+    "conditionMapSeverity": "Gravité",
+    "conditionMapNote": "Note"
   },
   "workOrder": {
     "title": "ORDRE DE TRAVAIL",

+ 23 - 3
messages/fr/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Ouvrir sur le téléphone",
       "concerns": "Demandes du client",
       "job_description": "Travaux demandés",
-      "work_checklist": "Liste des travaux"
+      "work_checklist": "Liste des travaux",
+      "condition_map": "État du véhicule"
     },
     "fields": {
       "customer_name": "Nom du client",
@@ -770,7 +771,9 @@
       "scheduled": "Planifié",
       "promised": "Promis",
       "work_bay": "Poste",
-      "customer_line": "Ligne de signature du client"
+      "customer_line": "Ligne de signature du client",
+      "legend": "Légende",
+      "previous_marks": "Marques antérieures"
     },
     "presetsTitle": "Partir d’une disposition",
     "presetsHint": "Remplace la disposition ci-dessous. Rien n’est enregistré avant votre validation.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Plaquettes et disques avant usés ; remplacés",
       "concernWarningLight": "Voyant allumé au tableau de bord depuis la semaine dernière",
       "workRequested": "Remplacer les plaquettes et disques avant, réinitialiser l'indicateur d'entretien et faire un essai routier.",
-      "checklistTestDrive": "Essai routier"
+      "checklistTestDrive": "Essai routier",
+      "markScratch": "Rayure de clé le long de la porte, 20 cm",
+      "markDent": "Bosse sous la poignée, peinture intacte",
+      "markChip": "Éclats de gravillons dans le bas du pare-chocs",
+      "viewTop": "Dessus",
+      "viewLeft": "Côté gauche",
+      "viewRight": "Côté droit",
+      "viewFront": "Avant",
+      "viewRear": "Arrière",
+      "panelLeftRearDoor": "Porte arrière gauche",
+      "panelLeftFrontDoor": "Porte avant gauche",
+      "panelFrontBumper": "Pare-chocs avant",
+      "kindScratch": "Rayure",
+      "kindDent": "Bosse",
+      "kindChip": "Éclat de gravillon",
+      "severityMinor": "Légère",
+      "severityMajor": "Importante",
+      "markPrevious": "relevé auparavant"
     },
     "logoUpload": "Importer un logo",
     "logoReplace": "Remplacer le logo",

+ 6 - 2
messages/it/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Foto di ispezione aggiunta",
     "inspection_file_added": "File di ispezione aggiunto",
     "signature_save": "Firma salvata",
-    "signature_remove": "Firma rimossa"
+    "signature_remove": "Firma rimossa",
+    "conditionMark_add": "Segno di stato aggiunto",
+    "conditionMark_remove": "Segno di stato rimosso"
   },
   "summary": {
     "customField_create": "Campo personalizzato «{name}» creato",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Voce oraria di {technician} su \"{job}\" corretta a {minutes} minuti",
     "timeEntry_stop": "Timer di {technician} su \"{job}\" fermato a {minutes} minuti",
     "timeEntry_delete": "Voce oraria {id} eliminata",
-    "inspection_phoneUpload": "Aggiunto un file da telefono all'ispezione {ref}"
+    "inspection_phoneUpload": "Aggiunto un file da telefono all'ispezione {ref}",
+    "condition_mark_add": "Ha aggiunto un segno alla mappa dello stato",
+    "condition_mark_remove": "Ha rimosso un segno dalla mappa dello stato"
   }
 }

+ 146 - 0
messages/it/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Stato del veicolo",
+  "description": "Tocca il disegno dove si trova il danno. Ogni segno ha un tipo, una gravità, una nota e una foto.",
+  "addHint": "Tocca un pannello per aggiungere un segno",
+  "bodyType": "Carrozzeria",
+  "legend": "Segni",
+  "noMarks": "Nessun segno ancora. Tocca il disegno dove si trova il danno.",
+  "previousHeading": "Registrato in una visita precedente",
+  "previousHint": "Ancora presente dall'ultima volta, in grigio. Cancella quelli che non ci sono più.",
+  "newSinceLastVisit": "Nuovo dall'ultima visita",
+  "confirmed": "Ancora presente",
+  "clearRepaired": "Cancella come riparato",
+  "restore": "Ripristina",
+  "edit": "Modifica segno",
+  "remove": "Rimuovi segno",
+  "removeTitle": "Rimuovere questo segno?",
+  "removeBody": "Il segno {n} su {area} e la sua nota verranno rimossi.",
+  "kind": "Tipo",
+  "severity": "Gravità",
+  "note": "Nota",
+  "notePlaceholder": "Dove esattamente, quanto grande, cosa ha detto il cliente",
+  "photo": "Foto",
+  "addPhoto": "Aggiungi foto",
+  "takePhoto": "Scatta foto",
+  "removePhoto": "Rimuovi foto",
+  "save": "Salva segno",
+  "done": "Fatto",
+  "cancel": "Annulla",
+  "moveHint": "Trascina un segno per spostarlo",
+  "enlarge": "Ingrandisci {view}",
+  "allViews": "Tutte le viste",
+  "markLabel": "Segno {n}: {kind} su {area}",
+  "markOn": "{kind} su {area}",
+  "photosOf": "Foto del segno {n}",
+  "saved": "Salvato",
+  "saveFailed": "Impossibile salvare il segno",
+  "uploadFailed": "Impossibile caricare la foto",
+  "readOnly": "Riapri per modificare la mappa dello stato.",
+  "count": "{count, plural, =0 {Nessun segno} one {# segno} other {# segni}}",
+  "previousCount": "{count, plural, one {# da una visita precedente} other {# da visite precedenti}}",
+  "elsewhere": "{count, plural, one {# segno è stato fatto su un'altra carrozzeria e compare solo nella stampa} other {# segni sono stati fatti su un'altra carrozzeria e compaiono solo nella stampa}}",
+  "photoCount": "{count, plural, one {# foto} other {# foto}}",
+  "clearBody": "Il segno {n} su {area} verrà registrato come riparato. Resta nello storico e non viene più stampato.",
+  "views": {
+    "top": "Dall'alto",
+    "left": "Lato sinistro",
+    "right": "Lato destro",
+    "front": "Anteriore",
+    "rear": "Posteriore"
+  },
+  "bodies": {
+    "sedan": "Berlina",
+    "hatchback": "Due volumi",
+    "estate": "Station wagon",
+    "suv": "SUV",
+    "van": "Furgone",
+    "pickup": "Pick-up",
+    "motorcycle": "Motocicletta",
+    "boat": "Barca"
+  },
+  "kinds": {
+    "dent": "Ammaccatura",
+    "scratch": "Graffio",
+    "chip": "Scheggiatura",
+    "crack": "Crepa",
+    "rust": "Ruggine",
+    "paint": "Danno alla vernice",
+    "previous_repair": "Riparazione precedente",
+    "missing": "Parte mancante"
+  },
+  "severities": {
+    "minor": "Lieve",
+    "major": "Grave"
+  },
+  "panels": {
+    "hood": "Cofano",
+    "roof": "Tetto",
+    "trunk": "Portellone del bagagliaio",
+    "tailgate": "Portellone posteriore",
+    "front_bumper": "Paraurti anteriore",
+    "rear_bumper": "Paraurti posteriore",
+    "grille": "Griglia",
+    "windshield": "Parabrezza",
+    "rear_window": "Lunotto",
+    "left_headlight": "Faro sinistro",
+    "right_headlight": "Faro destro",
+    "left_taillight": "Fanale posteriore sinistro",
+    "right_taillight": "Fanale posteriore destro",
+    "left_front_fender": "Parafango anteriore sinistro",
+    "right_front_fender": "Parafango anteriore destro",
+    "left_front_door": "Porta anteriore sinistra",
+    "right_front_door": "Porta anteriore destra",
+    "left_rear_door": "Porta posteriore sinistra",
+    "right_rear_door": "Porta posteriore destra",
+    "left_rear_quarter": "Fiancata posteriore sinistra",
+    "right_rear_quarter": "Fiancata posteriore destra",
+    "left_sill": "Sottoporta sinistro",
+    "right_sill": "Sottoporta destro",
+    "left_mirror": "Specchietto sinistro",
+    "right_mirror": "Specchietto destro",
+    "left_front_wheel": "Ruota anteriore sinistra",
+    "right_front_wheel": "Ruota anteriore destra",
+    "left_rear_wheel": "Ruota posteriore sinistra",
+    "right_rear_wheel": "Ruota posteriore destra",
+    "left_a_pillar": "Montante A sinistro",
+    "right_a_pillar": "Montante A destro",
+    "left_windows": "Finestrini sinistri",
+    "right_windows": "Finestrini destri",
+    "left_sliding_door": "Porta scorrevole sinistra",
+    "right_sliding_door": "Porta scorrevole destra",
+    "left_side_panel": "Pannello laterale sinistro",
+    "right_side_panel": "Pannello laterale destro",
+    "rear_doors": "Porte posteriori",
+    "bed": "Cassone",
+    "bed_side_left": "Sponda sinistra",
+    "bed_side_right": "Sponda destra",
+    "tank": "Serbatoio",
+    "seat": "Sella",
+    "front_fairing": "Carenatura anteriore",
+    "left_fairing": "Carenatura sinistra",
+    "right_fairing": "Carenatura destra",
+    "front_fender": "Parafango anteriore",
+    "rear_fender": "Parafango posteriore",
+    "front_wheel": "Ruota anteriore",
+    "rear_wheel": "Ruota posteriore",
+    "exhaust": "Scarico",
+    "handlebars": "Manubrio",
+    "headlamp": "Faro",
+    "tail": "Coda",
+    "bow": "Prua",
+    "stern": "Poppa",
+    "transom": "Specchio di poppa",
+    "port_hull": "Scafo sinistra",
+    "starboard_hull": "Scafo dritta",
+    "deck": "Coperta",
+    "cabin": "Cabina",
+    "windscreen": "Parabrezza",
+    "gunwale_port": "Falchetta sinistra",
+    "gunwale_starboard": "Falchetta dritta",
+    "keel": "Chiglia",
+    "outboard": "Fuoribordo"
+  },
+  "print": {
+    "previous": "registrato in precedenza"
+  }
+}

+ 26 - 4
messages/it/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Modello vuoto",
       "description": "Parti da zero e costruisci la lista di controllo che la tua officina usa davvero.",
       "standardLabel": "Vuoto"
+    },
+    "vehicle-intake": {
+      "name": "Accettazione del veicolo",
+      "description": "Il giro intorno al veicolo alla consegna, con il cliente presente: com'è arrivato, cosa c'era già sulla carrozzeria, cosa è rimasto dentro. Si stampa sull'ordine di lavoro che il cliente firma.",
+      "standardLabel": "Officina"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Telaio e accessori fissati al telaio",
     "no_s7": "Altre attrezzature",
     "no_s8": "Effetti nocivi",
-    "no_s10": "Presentazione presso una trafikkstasjon"
+    "no_s10": "Presentazione presso una trafikkstasjon",
+    "arrival": "Arrivo",
+    "exteriorWalkround": "Giro esterno"
   },
   "checks": {
     "registrationPlates": "Targhe d'immatricolazione",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diesel: opacità dei fumi, anteriore a Euro 4, turbocompresso",
     "no_8_2_3_1": "Scarico in generale e motore in funzione",
     "no_8_3_1": "Perdite di liquidi",
-    "no_10_1": "Presentazione presso una trafikkstasjon (stazione di controllo di Statens vegvesen)"
+    "no_10_1": "Presentazione presso una trafikkstasjon (stazione di controllo di Statens vegvesen)",
+    "fuelLevel": "Livello carburante",
+    "keysHandedOver": "Chiavi consegnate",
+    "warningLightsOnArrival": "Spie accese all'arrivo",
+    "personalBelongings": "Effetti personali lasciati nel veicolo",
+    "glassCondition": "Stato dei vetri",
+    "wheelsAndTyresVisual": "Ruote e pneumatici, visivo",
+    "seatsAndTrim": "Sedili e rivestimenti",
+    "floorAndMats": "Pavimento e tappetini",
+    "infotainmentControls": "Infotainment e comandi"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Allegato I, punto 0. Conferma che il veicolo presentato corrisponde ai suoi documenti.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Max 3,0 m⁻¹. Prevale il limite del costruttore.",
     "no_8_2_3_1": "Con il motore in funzione: verificare se la spia di malfunzionamento OBD segnala un guasto e se l'autodiagnosi funziona correttamente; qualsiasi scostamento dà e). Cercare fumo blu, grigio scuro o nero visibile durante la prova su strada e con un colpo di acceleratore dal minimo, e controllare come gira il motore.",
     "no_8_3_1": "Grado 2 per qualsiasi perdita consistente di liquido (diverso dall'acqua) che possa danneggiare l'ambiente o costituire un rischio per la sicurezza degli altri utenti della strada, purché goccioli a terra durante il controllo.",
-    "no_10_1": "Si assegna quando qualcosa richiede che il veicolo vada a una trafikkstasjon per una valutazione, una nuova omologazione o una modifica del vognkort, ad es. modifica illegale del motore o della centralina, veicolo ribassato/rialzato, protezione frontale non conforme (bull bar), più posti di quelli immatricolati, conversione a un altro carburante o una modifica che richiede omologazione. Si assegna in aggiunta alla carenza pertinente sulla stessa questione; al ricontrollo solo Statens vegvesen può annullarla."
+    "no_10_1": "Si assegna quando qualcosa richiede che il veicolo vada a una trafikkstasjon per una valutazione, una nuova omologazione o una modifica del vognkort, ad es. modifica illegale del motore o della centralina, veicolo ribassato/rialzato, protezione frontale non conforme (bull bar), più posti di quelli immatricolati, conversione a un altro carburante o una modifica che richiede omologazione. Si assegna in aggiunta alla carenza pertinente sulla stessa questione; al ricontrollo solo Statens vegvesen può annullarla.",
+    "bodyConditionMap": "Tocca il disegno dove si trova ogni ammaccatura, graffio o scheggiatura e aggiungi una nota e una foto. I segni delle visite precedenti compaiono in grigio."
   },
   "choices": {
     "full": "Completo",
     "partial": "Parziale",
-    "none": "Nessuno"
+    "none": "Nessuno",
+    "fuelEmpty": "Vuoto",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Pieno"
   },
   "defects": {
     "registrationPlateMissing": "Targa d'immatricolazione mancante",

+ 4 - 2
messages/it/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Valutazione della carenza",
       "measurement": "Misura",
       "text": "Testo libero",
-      "choice": "Scelta"
+      "choice": "Scelta",
+      "condition_map": "Mappa dello stato"
     },
     "inputHint": {
       "condition": "Il tecnico valuta il punto secondo la scala delle carenze.",
       "measurement": "Registra un numero con unità, valutato automaticamente rispetto a un intervallo.",
       "text": "Registra un'osservazione scritta, per esempio una marca di pneumatico o un codice guasto.",
-      "choice": "Il tecnico sceglie una risposta da un elenco fisso."
+      "choice": "Il tecnico sceglie una risposta da un elenco fisso.",
+      "condition_map": "Un disegno del veicolo su cui segnare ammaccature, graffi e scheggiature, ciascuno con nota e foto."
     },
     "cancel": "Annulla",
     "create": "Crea checklist",

+ 7 - 1
messages/it/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Realizzato con",
     "signature": "Firma",
     "signedBy": "Firmato da",
-    "signatureDate": "Data"
+    "signatureDate": "Data",
+    "conditionMapTitle": "Stato del veicolo",
+    "conditionMapNo": "N.",
+    "conditionMapArea": "Zona",
+    "conditionMapKind": "Tipo",
+    "conditionMapSeverity": "Gravità",
+    "conditionMapNote": "Nota"
   },
   "workOrder": {
     "title": "ORDINE DI LAVORO",

+ 23 - 3
messages/it/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Apri sul telefono",
       "concerns": "Richieste del cliente",
       "job_description": "Lavoro richiesto",
-      "work_checklist": "Elenco lavori"
+      "work_checklist": "Elenco lavori",
+      "condition_map": "Stato del veicolo"
     },
     "fields": {
       "customer_name": "Nome cliente",
@@ -770,7 +771,9 @@
       "scheduled": "Programmato",
       "promised": "Promesso",
       "work_bay": "Postazione",
-      "customer_line": "Riga della firma del cliente"
+      "customer_line": "Riga della firma del cliente",
+      "legend": "Legenda",
+      "previous_marks": "Segni precedenti"
     },
     "presetsTitle": "Partire da una disposizione",
     "presetsHint": "Sostituisce la disposizione qui sotto. Nulla viene salvato finché non salvi.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Pastiglie e dischi anteriori usurati; sostituiti",
       "concernWarningLight": "Spia accesa sul cruscotto dalla settimana scorsa",
       "workRequested": "Sostituire pastiglie e dischi anteriori, azzerare l'indicatore di manutenzione e provare su strada.",
-      "checklistTestDrive": "Prova su strada"
+      "checklistTestDrive": "Prova su strada",
+      "markScratch": "Graffio da chiave lungo la porta, 20 cm",
+      "markDent": "Ammaccatura sotto la maniglia, vernice intatta",
+      "markChip": "Scheggiature nella parte bassa del paraurti",
+      "viewTop": "Dall'alto",
+      "viewLeft": "Lato sinistro",
+      "viewRight": "Lato destro",
+      "viewFront": "Anteriore",
+      "viewRear": "Posteriore",
+      "panelLeftRearDoor": "Porta posteriore sinistra",
+      "panelLeftFrontDoor": "Porta anteriore sinistra",
+      "panelFrontBumper": "Paraurti anteriore",
+      "kindScratch": "Graffio",
+      "kindDent": "Ammaccatura",
+      "kindChip": "Scheggiatura",
+      "severityMinor": "Lieve",
+      "severityMajor": "Grave",
+      "markPrevious": "registrato in precedenza"
     },
     "logoUpload": "Carica logo",
     "logoReplace": "Sostituisci logo",

+ 6 - 2
messages/lt/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Pridėta apžiūros nuotrauka",
     "inspection_file_added": "Pridėtas apžiūros failas",
     "signature_save": "Išsaugotas parašas",
-    "signature_remove": "Pašalintas parašas"
+    "signature_remove": "Pašalintas parašas",
+    "conditionMark_add": "Pridėta būklės žyma",
+    "conditionMark_remove": "Pašalinta būklės žyma"
   },
   "summary": {
     "customField_create": "Sukurtas pasirinktinis laukas „{name}“",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Techniko {technician} laiko įrašas darbe „{job}“ pataisytas į {minutes} min.",
     "timeEntry_stop": "Techniko {technician} laikmatis darbe „{job}“ sustabdytas ties {minutes} min.",
     "timeEntry_delete": "Ištrintas laiko įrašas {id}",
-    "inspection_phoneUpload": "Pridėtas failas iš telefono prie apžiūros {ref}"
+    "inspection_phoneUpload": "Pridėtas failas iš telefono prie apžiūros {ref}",
+    "condition_mark_add": "Pridėjo žymą būklės žemėlapyje",
+    "condition_mark_remove": "Pašalino žymą iš būklės žemėlapio"
   }
 }

+ 146 - 0
messages/lt/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Transporto priemonės būklė",
+  "description": "Palieskite brėžinį ten, kur yra pažeidimas. Kiekviena žyma gauna tipą, sunkumą, pastabą ir nuotrauką.",
+  "addHint": "Palieskite dalį, kad pridėtumėte žymą",
+  "bodyType": "Kėbulo tipas",
+  "legend": "Žymos",
+  "noMarks": "Žymų dar nėra. Palieskite brėžinį ten, kur yra pažeidimas.",
+  "previousHeading": "Užfiksuota ankstesnio vizito metu",
+  "previousHint": "Vis dar yra nuo praėjusio karto, pilka spalva. Pašalinkite tas, kurių nebėra.",
+  "newSinceLastVisit": "Nauja nuo praėjusio vizito",
+  "confirmed": "Vis dar yra",
+  "clearRepaired": "Pašalinti kaip sutaisytą",
+  "restore": "Atkurti",
+  "edit": "Redaguoti žymą",
+  "remove": "Pašalinti žymą",
+  "removeTitle": "Pašalinti šią žymą?",
+  "removeBody": "Žyma {n} ant {area} ir jos pastaba bus pašalintos.",
+  "kind": "Tipas",
+  "severity": "Sunkumas",
+  "note": "Pastaba",
+  "notePlaceholder": "Kur tiksliai, kokio dydžio, ką sakė klientas",
+  "photo": "Nuotrauka",
+  "addPhoto": "Pridėti nuotrauką",
+  "takePhoto": "Fotografuoti",
+  "removePhoto": "Pašalinti nuotrauką",
+  "save": "Išsaugoti žymą",
+  "done": "Atlikta",
+  "cancel": "Atšaukti",
+  "moveHint": "Vilkite žymą, kad ją perkeltumėte",
+  "enlarge": "Padidinti: {view}",
+  "allViews": "Visi vaizdai",
+  "markLabel": "Žyma {n}: {kind} ant {area}",
+  "markOn": "{kind} ant {area}",
+  "photosOf": "Žymos {n} nuotraukos",
+  "saved": "Išsaugota",
+  "saveFailed": "Nepavyko išsaugoti žymos",
+  "uploadFailed": "Nepavyko įkelti nuotraukos",
+  "readOnly": "Norėdami keisti būklės žemėlapį, atidarykite iš naujo.",
+  "count": "{count, plural, =0 {Žymų nėra} one {# žyma} few {# žymos} other {# žymų}}",
+  "previousCount": "{count, plural, one {# iš ankstesnio vizito} few {# iš ankstesnių vizitų} other {# iš ankstesnių vizitų}}",
+  "elsewhere": "{count, plural, one {# žyma padėta ant kito kėbulo tipo ir rodoma tik spaudinyje} few {# žymos padėtos ant kito kėbulo tipo ir rodomos tik spaudinyje} other {# žymų padėta ant kito kėbulo tipo ir rodomos tik spaudinyje}}",
+  "photoCount": "{count, plural, one {# nuotrauka} few {# nuotraukos} other {# nuotraukų}}",
+  "clearBody": "Žyma {n} ant {area} bus pažymėta kaip sutaisyta. Ji lieka istorijoje ir nebespausdinama.",
+  "views": {
+    "top": "Iš viršaus",
+    "left": "Kairė pusė",
+    "right": "Dešinė pusė",
+    "front": "Priekis",
+    "rear": "Galas"
+  },
+  "bodies": {
+    "sedan": "Sedanas",
+    "hatchback": "Hečbekas",
+    "estate": "Universalas",
+    "suv": "Visureigis",
+    "van": "Furgonas",
+    "pickup": "Pikapas",
+    "motorcycle": "Motociklas",
+    "boat": "Valtis"
+  },
+  "kinds": {
+    "dent": "Įlenkimas",
+    "scratch": "Įbrėžimas",
+    "chip": "Akmens atskėlimas",
+    "crack": "Įtrūkimas",
+    "rust": "Rūdys",
+    "paint": "Dažų pažeidimas",
+    "previous_repair": "Ankstesnis remontas",
+    "missing": "Trūkstama dalis"
+  },
+  "severities": {
+    "minor": "Nedidelis",
+    "major": "Didelis"
+  },
+  "panels": {
+    "hood": "Variklio dangtis",
+    "roof": "Stogas",
+    "trunk": "Bagažinės dangtis",
+    "tailgate": "Galinis dangtis",
+    "front_bumper": "Priekinis buferis",
+    "rear_bumper": "Galinis buferis",
+    "grille": "Grotelės",
+    "windshield": "Priekinis stiklas",
+    "rear_window": "Galinis stiklas",
+    "left_headlight": "Kairysis žibintas",
+    "right_headlight": "Dešinysis žibintas",
+    "left_taillight": "Kairysis galinis žibintas",
+    "right_taillight": "Dešinysis galinis žibintas",
+    "left_front_fender": "Kairysis priekinis sparnas",
+    "right_front_fender": "Dešinysis priekinis sparnas",
+    "left_front_door": "Kairės priekinės durys",
+    "right_front_door": "Dešinės priekinės durys",
+    "left_rear_door": "Kairės galinės durys",
+    "right_rear_door": "Dešinės galinės durys",
+    "left_rear_quarter": "Kairysis galinis sparnas",
+    "right_rear_quarter": "Dešinysis galinis sparnas",
+    "left_sill": "Kairysis slenkstis",
+    "right_sill": "Dešinysis slenkstis",
+    "left_mirror": "Kairysis veidrodėlis",
+    "right_mirror": "Dešinysis veidrodėlis",
+    "left_front_wheel": "Kairysis priekinis ratas",
+    "right_front_wheel": "Dešinysis priekinis ratas",
+    "left_rear_wheel": "Kairysis galinis ratas",
+    "right_rear_wheel": "Dešinysis galinis ratas",
+    "left_a_pillar": "Kairysis A statramstis",
+    "right_a_pillar": "Dešinysis A statramstis",
+    "left_windows": "Kairieji langai",
+    "right_windows": "Dešinieji langai",
+    "left_sliding_door": "Kairės stumdomos durys",
+    "right_sliding_door": "Dešinės stumdomos durys",
+    "left_side_panel": "Kairysis šoninis skydas",
+    "right_side_panel": "Dešinysis šoninis skydas",
+    "rear_doors": "Galinės durys",
+    "bed": "Krovinių platforma",
+    "bed_side_left": "Kairysis platformos bortas",
+    "bed_side_right": "Dešinysis platformos bortas",
+    "tank": "Bakas",
+    "seat": "Sėdynė",
+    "front_fairing": "Priekinis apdailos skydas",
+    "left_fairing": "Kairysis apdailos skydas",
+    "right_fairing": "Dešinysis apdailos skydas",
+    "front_fender": "Priekinis purvasaugis",
+    "rear_fender": "Galinis purvasaugis",
+    "front_wheel": "Priekinis ratas",
+    "rear_wheel": "Galinis ratas",
+    "exhaust": "Išmetimo sistema",
+    "handlebars": "Vairas",
+    "headlamp": "Žibintas",
+    "tail": "Galas",
+    "bow": "Laivo priekis",
+    "stern": "Laivagalis",
+    "transom": "Transomas",
+    "port_hull": "Korpusas, kairysis bortas",
+    "starboard_hull": "Korpusas, dešinysis bortas",
+    "deck": "Denis",
+    "cabin": "Kajutė",
+    "windscreen": "Priekinis stiklas",
+    "gunwale_port": "Kairiojo borto kraštas",
+    "gunwale_starboard": "Dešiniojo borto kraštas",
+    "keel": "Kilis",
+    "outboard": "Pakabinamas variklis"
+  },
+  "print": {
+    "previous": "užfiksuota anksčiau"
+  }
+}

+ 26 - 4
messages/lt/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Tuščias šablonas",
       "description": "Pradėkite nuo nulio ir sukurkite kontrolinį sąrašą, kurį iš tikrųjų naudoja jūsų dirbtuvės.",
       "standardLabel": "Tuščias"
+    },
+    "vehicle-intake": {
+      "name": "Transporto priemonės priėmimas",
+      "description": "Apžiūra aplink automobilį jį paliekant, klientui stovint šalia: kaip atvyko, kas jau buvo ant kėbulo, kas liko viduje. Spausdinama darbo užsakyme, kurį klientas pasirašo.",
+      "standardLabel": "Dirbtuvės"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Važiuoklė ir prie jos pritvirtintos dalys",
     "no_s7": "Kita įranga",
     "no_s8": "Neigiamas poveikis aplinkai",
-    "no_s10": "Pateikimas į trafikkstasjon"
+    "no_s10": "Pateikimas į trafikkstasjon",
+    "arrival": "Atvykimas",
+    "exteriorWalkround": "Išorės apžiūra"
   },
   "checks": {
     "registrationPlates": "Valstybinio numerio ženklai",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Dyzelinas: dūmingumas, senesnis nei EU-4, su turbokompresoriumi",
     "no_8_2_3_1": "Išmetimas bendrai ir variklio darbas",
     "no_8_3_1": "Skysčių nuotėkis",
-    "no_10_1": "Pateikimas į trafikkstasjon"
+    "no_10_1": "Pateikimas į trafikkstasjon",
+    "fuelLevel": "Degalų lygis",
+    "keysHandedOver": "Perduoti raktai",
+    "warningLightsOnArrival": "Įspėjamosios lemputės atvykus",
+    "personalBelongings": "Automobilyje palikti asmeniniai daiktai",
+    "glassCondition": "Stiklų būklė",
+    "wheelsAndTyresVisual": "Ratai ir padangos, apžiūra",
+    "seatsAndTrim": "Sėdynės ir apdaila",
+    "floorAndMats": "Grindys ir kilimėliai",
+    "infotainmentControls": "Multimedija ir valdikliai"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "I priedo 0 punktas. Patvirtina, kad pateikta transporto priemonė atitinka jos dokumentus.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Ne daugiau kaip 3,0 m⁻¹. Gamintojo riba turi pirmenybę.",
     "no_8_2_3_1": "Varikliui veikiant: patikrinkite, ar OBD gedimų indikatorius nerodo gedimo ir ar savidiagnostika veikia tinkamai; bet koks nukrypimas yra e). Bandomojo važiavimo metu ir staigiai padidinus apsukas iš laisvosios eigos ieškokite matomų mėlynų, tamsiai pilkų ar juodų dūmų ir patikrinkite, kaip dirba variklis.",
     "no_8_3_1": "2 lygis bet kokiam dideliam skysčio (išskyrus vandenį) nuotėkiui, galinčiam pakenkti aplinkai arba kelti pavojų kitiems eismo dalyviams, jei apžiūros metu jis laša ant žemės.",
-    "no_10_1": "Rašoma, kai dėl tam tikrų aplinkybių transporto priemonė turi būti pateikta trafikkstasjon (Statens vegvesen eismo stotyje) įvertinti, iš naujo patvirtinti arba pakeisti vognkort įrašą, pvz., neteisėtai pakeistas variklis ar valdymo blokas, nuleista / pakelta, reikalavimų neatitinkanti priekinė apsauga (buferio apsauginis rėmas), daugiau sėdimų vietų nei registruota, pertvarkyta kitai degalų rūšiai arba atliktas patvirtinimo reikalaujantis pertvarkymas. Rašoma kartu su atitinkamu trūkumu dėl to paties dalyko; pakartotinės apžiūros metu jį panaikinti gali tik Statens vegvesen."
+    "no_10_1": "Rašoma, kai dėl tam tikrų aplinkybių transporto priemonė turi būti pateikta trafikkstasjon (Statens vegvesen eismo stotyje) įvertinti, iš naujo patvirtinti arba pakeisti vognkort įrašą, pvz., neteisėtai pakeistas variklis ar valdymo blokas, nuleista / pakelta, reikalavimų neatitinkanti priekinė apsauga (buferio apsauginis rėmas), daugiau sėdimų vietų nei registruota, pertvarkyta kitai degalų rūšiai arba atliktas patvirtinimo reikalaujantis pertvarkymas. Rašoma kartu su atitinkamu trūkumu dėl to paties dalyko; pakartotinės apžiūros metu jį panaikinti gali tik Statens vegvesen.",
+    "bodyConditionMap": "Palieskite brėžinį ten, kur yra kiekvienas įlenkimas, įbrėžimas ar atskėlimas, ir pridėkite pastabą bei nuotrauką. Ankstesnių vizitų žymos rodomos pilkai."
   },
   "choices": {
     "full": "Pilnas",
     "partial": "Dalinis",
-    "none": "Nėra"
+    "none": "Nėra",
+    "fuelEmpty": "Tuščias",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Pilnas"
   },
   "defects": {
     "registrationPlateMissing": "Nėra valstybinio numerio ženklo",

+ 4 - 2
messages/lt/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Trūkumo įvertinimas",
       "measurement": "Matavimas",
       "text": "Laisvas tekstas",
-      "choice": "Pasirinkimas"
+      "choice": "Pasirinkimas",
+      "condition_map": "Būklės žemėlapis"
     },
     "inputHint": {
       "condition": "Mechanikas vertina punktą pagal trūkumų skalę.",
       "measurement": "Fiksuoja skaičių su matavimo vienetu, automatiškai vertinamą pagal intervalą.",
       "text": "Fiksuoja įrašytą pastebėjimą, pavyzdžiui padangos markę ar gedimo kodą.",
-      "choice": "Mechanikas pasirenka vieną atsakymą iš nustatyto sąrašo."
+      "choice": "Mechanikas pasirenka vieną atsakymą iš nustatyto sąrašo.",
+      "condition_map": "Transporto priemonės brėžinys, kuriame pažymimi įlenkimai, įbrėžimai ir atskėlimai, kiekvienas su pastaba ir nuotrauka."
     },
     "cancel": "Atšaukti",
     "create": "Sukurti kontrolinį sąrašą",

+ 7 - 1
messages/lt/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Sukurta su",
     "signature": "Parašas",
     "signedBy": "Pasirašė",
-    "signatureDate": "Data"
+    "signatureDate": "Data",
+    "conditionMapTitle": "Transporto priemonės būklė",
+    "conditionMapNo": "Nr.",
+    "conditionMapArea": "Vieta",
+    "conditionMapKind": "Tipas",
+    "conditionMapSeverity": "Sunkumas",
+    "conditionMapNote": "Pastaba"
   },
   "workOrder": {
     "title": "DARBO UŽSAKYMAS",

+ 23 - 3
messages/lt/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Atidaryti telefone",
       "concerns": "Kliento nusiskundimai",
       "job_description": "Užsakyti darbai",
-      "work_checklist": "Darbų sąrašas"
+      "work_checklist": "Darbų sąrašas",
+      "condition_map": "Transporto priemonės būklė"
     },
     "fields": {
       "customer_name": "Kliento vardas",
@@ -770,7 +771,9 @@
       "scheduled": "Suplanuota",
       "promised": "Pažadėta",
       "work_bay": "Darbo vieta",
-      "customer_line": "Kliento parašo eilutė"
+      "customer_line": "Kliento parašo eilutė",
+      "legend": "Legenda",
+      "previous_marks": "Ankstesnės žymos"
     },
     "presetsTitle": "Pradėti nuo išdėstymo",
     "presetsHint": "Pakeičia žemiau esantį išdėstymą. Niekas neįrašoma, kol neišsaugote.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Priekinės kaladėlės ir diskai nusidėvėję; pakeisti",
       "concernWarningLight": "Įspėjamoji lemputė prietaisų skydelyje nuo praėjusios savaitės",
       "workRequested": "Pakeisti priekines stabdžių kaladėles ir diskus, atstatyti aptarnavimo indikatorių ir išbandyti kelyje.",
-      "checklistTestDrive": "Bandomasis važiavimas"
+      "checklistTestDrive": "Bandomasis važiavimas",
+      "markScratch": "Rakto įbrėžimas išilgai durų, 20 cm",
+      "markDent": "Įlenkimas po rankena, dažai nepažeisti",
+      "markChip": "Akmenų atskėlimai apatinėje buferio dalyje",
+      "viewTop": "Iš viršaus",
+      "viewLeft": "Kairė pusė",
+      "viewRight": "Dešinė pusė",
+      "viewFront": "Priekis",
+      "viewRear": "Galas",
+      "panelLeftRearDoor": "Kairės galinės durys",
+      "panelLeftFrontDoor": "Kairės priekinės durys",
+      "panelFrontBumper": "Priekinis buferis",
+      "kindScratch": "Įbrėžimas",
+      "kindDent": "Įlenkimas",
+      "kindChip": "Akmens atskėlimas",
+      "severityMinor": "Nedidelis",
+      "severityMajor": "Didelis",
+      "markPrevious": "užfiksuota anksčiau"
     },
     "logoUpload": "Įkelti logotipą",
     "logoReplace": "Pakeisti logotipą",

+ 6 - 2
messages/nb/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "La til kontrollbilde",
     "inspection_file_added": "La til kontrollfil",
     "signature_save": "Lagret signatur",
-    "signature_remove": "Fjernet signatur"
+    "signature_remove": "Fjernet signatur",
+    "conditionMark_add": "La til tilstandsmerke",
+    "conditionMark_remove": "Fjernet tilstandsmerke"
   },
   "summary": {
     "customField_create": "Opprettet egendefinert felt «{name}»",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Korrigerte tidsoppføringen til {technician} på «{job}» til {minutes} minutter",
     "timeEntry_stop": "Stoppet klokken til {technician} på «{job}» ved {minutes} minutter",
     "timeEntry_delete": "Slettet tidsoppføring {id}",
-    "inspection_phoneUpload": "La til en fil fra telefon på kontroll {ref}"
+    "inspection_phoneUpload": "La til en fil fra telefon på kontroll {ref}",
+    "condition_mark_add": "La til et merke på tilstandskartet",
+    "condition_mark_remove": "Fjernet et merke fra tilstandskartet"
   }
 }

+ 146 - 0
messages/nb/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Kjøretøyets tilstand",
+  "description": "Trykk på tegningen der skaden er. Hvert merke får en type, en alvorlighet, et notat og et bilde.",
+  "addHint": "Trykk på en del for å sette et merke",
+  "bodyType": "Karosseri",
+  "legend": "Merker",
+  "noMarks": "Ingen merker ennå. Trykk på tegningen der skaden er.",
+  "previousHeading": "Registrert ved et tidligere besøk",
+  "previousHint": "Fortsatt der siden sist, tegnet i grått. Fjern dem som er borte.",
+  "newSinceLastVisit": "Nytt siden forrige besøk",
+  "confirmed": "Fortsatt der",
+  "clearRepaired": "Fjern som reparert",
+  "restore": "Gjenopprett",
+  "edit": "Rediger merke",
+  "remove": "Fjern merke",
+  "removeTitle": "Fjerne dette merket?",
+  "removeBody": "Merke {n} på {area} og notatet fjernes.",
+  "kind": "Type",
+  "severity": "Alvorlighet",
+  "note": "Notat",
+  "notePlaceholder": "Hvor nøyaktig, hvor stort, hva kunden sa",
+  "photo": "Bilde",
+  "addPhoto": "Legg til bilde",
+  "takePhoto": "Ta bilde",
+  "removePhoto": "Fjern bilde",
+  "save": "Lagre merke",
+  "done": "Ferdig",
+  "cancel": "Avbryt",
+  "moveHint": "Dra et merke for å flytte det",
+  "enlarge": "Forstørr {view}",
+  "allViews": "Alle visninger",
+  "markLabel": "Merke {n}: {kind} på {area}",
+  "markOn": "{kind} på {area}",
+  "photosOf": "Bilder av merke {n}",
+  "saved": "Lagret",
+  "saveFailed": "Kunne ikke lagre merket",
+  "uploadFailed": "Kunne ikke laste opp bildet",
+  "readOnly": "Åpne på nytt for å endre tilstandskartet.",
+  "count": "{count, plural, =0 {Ingen merker} one {# merke} other {# merker}}",
+  "previousCount": "{count, plural, one {# fra et tidligere besøk} other {# fra tidligere besøk}}",
+  "elsewhere": "{count, plural, one {# merke ble satt på et annet karosseri og vises bare på utskriften} other {# merker ble satt på et annet karosseri og vises bare på utskriften}}",
+  "photoCount": "{count, plural, one {# bilde} other {# bilder}}",
+  "clearBody": "Merke {n} på {area} registreres som reparert. Det blir i historikken og skrives ikke ut lenger.",
+  "views": {
+    "top": "Ovenfra",
+    "left": "Venstre side",
+    "right": "Høyre side",
+    "front": "Front",
+    "rear": "Bak"
+  },
+  "bodies": {
+    "sedan": "Sedan",
+    "hatchback": "Kombi",
+    "estate": "Stasjonsvogn",
+    "suv": "SUV",
+    "van": "Varebil",
+    "pickup": "Pickup",
+    "motorcycle": "Motorsykkel",
+    "boat": "Båt"
+  },
+  "kinds": {
+    "dent": "Bulk",
+    "scratch": "Ripe",
+    "chip": "Steinsprut",
+    "crack": "Sprekk",
+    "rust": "Rust",
+    "paint": "Lakkskade",
+    "previous_repair": "Tidligere reparasjon",
+    "missing": "Manglende del"
+  },
+  "severities": {
+    "minor": "Liten",
+    "major": "Stor"
+  },
+  "panels": {
+    "hood": "Panser",
+    "roof": "Tak",
+    "trunk": "Bagasjelokk",
+    "tailgate": "Bakluke",
+    "front_bumper": "Støtfanger foran",
+    "rear_bumper": "Støtfanger bak",
+    "grille": "Grill",
+    "windshield": "Frontrute",
+    "rear_window": "Bakrute",
+    "left_headlight": "Frontlykt venstre",
+    "right_headlight": "Frontlykt høyre",
+    "left_taillight": "Baklykt venstre",
+    "right_taillight": "Baklykt høyre",
+    "left_front_fender": "Skjerm foran venstre",
+    "right_front_fender": "Skjerm foran høyre",
+    "left_front_door": "Dør foran venstre",
+    "right_front_door": "Dør foran høyre",
+    "left_rear_door": "Dør bak venstre",
+    "right_rear_door": "Dør bak høyre",
+    "left_rear_quarter": "Bakskjerm venstre",
+    "right_rear_quarter": "Bakskjerm høyre",
+    "left_sill": "Terskel venstre",
+    "right_sill": "Terskel høyre",
+    "left_mirror": "Speil venstre",
+    "right_mirror": "Speil høyre",
+    "left_front_wheel": "Hjul foran venstre",
+    "right_front_wheel": "Hjul foran høyre",
+    "left_rear_wheel": "Hjul bak venstre",
+    "right_rear_wheel": "Hjul bak høyre",
+    "left_a_pillar": "A-stolpe venstre",
+    "right_a_pillar": "A-stolpe høyre",
+    "left_windows": "Sideruter venstre",
+    "right_windows": "Sideruter høyre",
+    "left_sliding_door": "Skyvedør venstre",
+    "right_sliding_door": "Skyvedør høyre",
+    "left_side_panel": "Sidepanel venstre",
+    "right_side_panel": "Sidepanel høyre",
+    "rear_doors": "Bakdører",
+    "bed": "Lasteplan",
+    "bed_side_left": "Lastekarm venstre",
+    "bed_side_right": "Lastekarm høyre",
+    "tank": "Tank",
+    "seat": "Sete",
+    "front_fairing": "Frontkåpe",
+    "left_fairing": "Kåpe venstre",
+    "right_fairing": "Kåpe høyre",
+    "front_fender": "Skjerm foran",
+    "rear_fender": "Skjerm bak",
+    "front_wheel": "Forhjul",
+    "rear_wheel": "Bakhjul",
+    "exhaust": "Eksos",
+    "handlebars": "Styre",
+    "headlamp": "Frontlykt",
+    "tail": "Hekk",
+    "bow": "Baug",
+    "stern": "Akter",
+    "transom": "Akterspeil",
+    "port_hull": "Skrog babord",
+    "starboard_hull": "Skrog styrbord",
+    "deck": "Dekk",
+    "cabin": "Kahytt",
+    "windscreen": "Vindskjerm",
+    "gunwale_port": "Ripe babord",
+    "gunwale_starboard": "Ripe styrbord",
+    "keel": "Kjøl",
+    "outboard": "Påhengsmotor"
+  },
+  "print": {
+    "previous": "registrert tidligere"
+  }
+}

+ 26 - 4
messages/nb/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Tom mal",
       "description": "Start helt fra bunnen og bygg sjekklisten verkstedet ditt faktisk bruker.",
       "standardLabel": "Tom"
+    },
+    "vehicle-intake": {
+      "name": "Innsjekk av kjøretøy",
+      "description": "Runden rundt bilen ved innlevering, med kunden til stede: hvordan den kom, hva som allerede var på karosseriet, hva som lå igjen inni. Skrives ut på arbeidsordren kunden signerer.",
+      "standardLabel": "Verksted"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Understell og understellsutstyr",
     "no_s7": "Annet utstyr",
     "no_s8": "Skadevirkninger",
-    "no_s10": "Forevisning for trafikkstasjon"
+    "no_s10": "Forevisning for trafikkstasjon",
+    "arrival": "Ankomst",
+    "exteriorWalkround": "Runde utvendig"
   },
   "checks": {
     "registrationPlates": "Kjennemerker",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diesel: røyktetthet, eldre enn EU-4, turbo",
     "no_8_2_3_1": "Avgass generelt og motorgange",
     "no_8_3_1": "Væskelekkasjer",
-    "no_10_1": "Forevisning for trafikkstasjon"
+    "no_10_1": "Forevisning for trafikkstasjon",
+    "fuelLevel": "Drivstoffnivå",
+    "keysHandedOver": "Nøkler levert",
+    "warningLightsOnArrival": "Varsellamper ved ankomst",
+    "personalBelongings": "Personlige eiendeler i kjøretøyet",
+    "glassCondition": "Rutenes tilstand",
+    "wheelsAndTyresVisual": "Hjul og dekk, visuelt",
+    "seatsAndTrim": "Seter og interiør",
+    "floorAndMats": "Gulv og matter",
+    "infotainmentControls": "Infotainment og betjening"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Vedlegg I, punkt 0. Bekrefter at kjøretøyet som er fremstilt stemmer med papirene.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Maks 3,0 m⁻¹. Produsentens grenseverdi går foran.",
     "no_8_2_3_1": "Med motoren i gang: kontroller om OBD-feilindikatoren viser feil og om egendiagnosen virker korrekt; avvik gir e). Se etter synlig blå, mørkegrå eller svart røyk ved prøvekjøring og gasstøt fra tomgang, og kontroller motorgangen.",
     "no_8_3_1": "Grad 2 ved enhver stor væskelekkasje (unntatt vann) som trolig vil skade miljøet eller være en sikkerhetsrisiko, forutsatt at det drypper på bakken under kontrollen.",
-    "no_10_1": "Gis når forhold gjør at kjøretøyet må til trafikkstasjon for vurdering, ny godkjenning eller påtegning i vognkort, f.eks. ulovlig endret motor eller styreenhet, senket/hevet, ulovlig frontbeskyttelse (kufanger), flere sitteplasser enn i vognkort, ombygget til annet drivstoff eller godkjenningspliktig ombygging. Gis i tillegg til relevant merknad på samme forhold, og ved etterkontroll kan bare Statens vegvesen godkjenne den."
+    "no_10_1": "Gis når forhold gjør at kjøretøyet må til trafikkstasjon for vurdering, ny godkjenning eller påtegning i vognkort, f.eks. ulovlig endret motor eller styreenhet, senket/hevet, ulovlig frontbeskyttelse (kufanger), flere sitteplasser enn i vognkort, ombygget til annet drivstoff eller godkjenningspliktig ombygging. Gis i tillegg til relevant merknad på samme forhold, og ved etterkontroll kan bare Statens vegvesen godkjenne den.",
+    "bodyConditionMap": "Trykk på tegningen der hver bulk, ripe eller steinsprut er, og legg til notat og bilde. Merker fra tidligere besøk vises i grått."
   },
   "choices": {
     "full": "Full",
     "partial": "Delvis",
-    "none": "Ingen"
+    "none": "Ingen",
+    "fuelEmpty": "Tom",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Full"
   },
   "defects": {
     "registrationPlateMissing": "Kjennemerke mangler",

+ 4 - 2
messages/nb/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Mangelvurdering",
       "measurement": "Måling",
       "text": "Fritekst",
-      "choice": "Valg"
+      "choice": "Valg",
+      "condition_map": "Tilstandskart"
     },
     "inputHint": {
       "condition": "Mekanikeren vurderer punktet etter mangelskalaen.",
       "measurement": "Registrerer et tall med benevning, vurdert automatisk mot et intervall.",
       "text": "Registrerer en skreven observasjon, for eksempel et dekkmerke eller en feilkode.",
-      "choice": "Mekanikeren velger ett av flere faste svar."
+      "choice": "Mekanikeren velger ett av flere faste svar.",
+      "condition_map": "En tegning av kjøretøyet der bulker, riper og steinsprut merkes av, hver med notat og bilde."
     },
     "cancel": "Avbryt",
     "create": "Opprett sjekkliste",

+ 7 - 1
messages/nb/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Drevet av",
     "signature": "Signatur",
     "signedBy": "Signert av",
-    "signatureDate": "Dato"
+    "signatureDate": "Dato",
+    "conditionMapTitle": "Kjøretøyets tilstand",
+    "conditionMapNo": "Nr.",
+    "conditionMapArea": "Område",
+    "conditionMapKind": "Type",
+    "conditionMapSeverity": "Alvorlighet",
+    "conditionMapNote": "Notat"
   },
   "workOrder": {
     "title": "ARBEIDSORDRE",

+ 23 - 3
messages/nb/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Åpne på telefonen",
       "concerns": "Kundens henvendelser",
       "job_description": "Ønsket arbeid",
-      "work_checklist": "Sjekkliste for arbeid"
+      "work_checklist": "Sjekkliste for arbeid",
+      "condition_map": "Kjøretøyets tilstand"
     },
     "fields": {
       "customer_name": "Kundenavn",
@@ -770,7 +771,9 @@
       "scheduled": "Planlagt",
       "promised": "Lovet",
       "work_bay": "Arbeidsplass",
-      "customer_line": "Signaturlinje for kunde"
+      "customer_line": "Signaturlinje for kunde",
+      "legend": "Forklaring",
+      "previous_marks": "Tidligere merker"
     },
     "presetsTitle": "Start fra et oppsett",
     "presetsHint": "Erstatter oppsettet under. Ingenting lagres før du lagrer.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Klosser og skiver foran slitt; byttet",
       "concernWarningLight": "Varsellampe i dashbordet siden forrige uke",
       "workRequested": "Bytt bremseklosser og skiver foran, nullstill serviceindikatoren og prøvekjør.",
-      "checklistTestDrive": "Prøvekjøring"
+      "checklistTestDrive": "Prøvekjøring",
+      "markScratch": "Nøkkelripe langs døra, 20 cm",
+      "markDent": "Bulk under håndtaket, lakken hel",
+      "markChip": "Steinsprut nederst i støtfangeren",
+      "viewTop": "Ovenfra",
+      "viewLeft": "Venstre side",
+      "viewRight": "Høyre side",
+      "viewFront": "Front",
+      "viewRear": "Bak",
+      "panelLeftRearDoor": "Dør bak venstre",
+      "panelLeftFrontDoor": "Dør foran venstre",
+      "panelFrontBumper": "Støtfanger foran",
+      "kindScratch": "Ripe",
+      "kindDent": "Bulk",
+      "kindChip": "Steinsprut",
+      "severityMinor": "Liten",
+      "severityMajor": "Stor",
+      "markPrevious": "registrert tidligere"
     },
     "logoUpload": "Last opp logo",
     "logoReplace": "Bytt logo",

+ 6 - 2
messages/nl/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Keuringsfoto toegevoegd",
     "inspection_file_added": "Keuringsbestand toegevoegd",
     "signature_save": "Handtekening opgeslagen",
-    "signature_remove": "Handtekening verwijderd"
+    "signature_remove": "Handtekening verwijderd",
+    "conditionMark_add": "Staatmarkering toegevoegd",
+    "conditionMark_remove": "Staatmarkering verwijderd"
   },
   "summary": {
     "customField_create": "Aangepast veld \"{name}\" aangemaakt",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Tijdregistratie van {technician} op \"{job}\" gecorrigeerd naar {minutes} minuten",
     "timeEntry_stop": "Klok van {technician} op \"{job}\" gestopt op {minutes} minuten",
     "timeEntry_delete": "Tijdregistratie {id} verwijderd",
-    "inspection_phoneUpload": "Bestand vanaf een telefoon toegevoegd aan keuring {ref}"
+    "inspection_phoneUpload": "Bestand vanaf een telefoon toegevoegd aan keuring {ref}",
+    "condition_mark_add": "Markering aan de staatkaart toegevoegd",
+    "condition_mark_remove": "Markering van de staatkaart verwijderd"
   }
 }

+ 146 - 0
messages/nl/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Staat van het voertuig",
+  "description": "Tik op de tekening waar de schade zit. Elke markering krijgt een type, een ernst, een notitie en een foto.",
+  "addHint": "Tik op een deel om een markering toe te voegen",
+  "bodyType": "Carrosserie",
+  "legend": "Markeringen",
+  "noMarks": "Nog geen markeringen. Tik op de tekening waar de schade zit.",
+  "previousHeading": "Vastgelegd bij een eerder bezoek",
+  "previousHint": "Nog aanwezig sinds de vorige keer, in grijs. Wis wat er niet meer is.",
+  "newSinceLastVisit": "Nieuw sinds het vorige bezoek",
+  "confirmed": "Nog aanwezig",
+  "clearRepaired": "Wissen als hersteld",
+  "restore": "Herstellen",
+  "edit": "Markering bewerken",
+  "remove": "Markering verwijderen",
+  "removeTitle": "Deze markering verwijderen?",
+  "removeBody": "Markering {n} op {area} en de notitie worden verwijderd.",
+  "kind": "Type",
+  "severity": "Ernst",
+  "note": "Notitie",
+  "notePlaceholder": "Waar precies, hoe groot, wat de klant zei",
+  "photo": "Foto",
+  "addPhoto": "Foto toevoegen",
+  "takePhoto": "Foto maken",
+  "removePhoto": "Foto verwijderen",
+  "save": "Markering opslaan",
+  "done": "Klaar",
+  "cancel": "Annuleren",
+  "moveHint": "Sleep een markering om hem te verplaatsen",
+  "enlarge": "{view} vergroten",
+  "allViews": "Alle aanzichten",
+  "markLabel": "Markering {n}: {kind} op {area}",
+  "markOn": "{kind} op {area}",
+  "photosOf": "Foto's van markering {n}",
+  "saved": "Opgeslagen",
+  "saveFailed": "Kon de markering niet opslaan",
+  "uploadFailed": "Kon de foto niet uploaden",
+  "readOnly": "Heropen om de staatkaart te wijzigen.",
+  "count": "{count, plural, =0 {Geen markeringen} one {# markering} other {# markeringen}}",
+  "previousCount": "{count, plural, one {# van een eerder bezoek} other {# van eerdere bezoeken}}",
+  "elsewhere": "{count, plural, one {# markering is op een andere carrosserie gezet en staat alleen op de afdruk} other {# markeringen zijn op een andere carrosserie gezet en staan alleen op de afdruk}}",
+  "photoCount": "{count, plural, one {# foto} other {# foto's}}",
+  "clearBody": "Markering {n} op {area} wordt als hersteld vastgelegd. Hij blijft in de historie en wordt niet meer afgedrukt.",
+  "views": {
+    "top": "Bovenaanzicht",
+    "left": "Linkerkant",
+    "right": "Rechterkant",
+    "front": "Voorkant",
+    "rear": "Achterkant"
+  },
+  "bodies": {
+    "sedan": "Sedan",
+    "hatchback": "Hatchback",
+    "estate": "Stationwagen",
+    "suv": "SUV",
+    "van": "Bestelwagen",
+    "pickup": "Pick-up",
+    "motorcycle": "Motor",
+    "boat": "Boot"
+  },
+  "kinds": {
+    "dent": "Deuk",
+    "scratch": "Kras",
+    "chip": "Steenslag",
+    "crack": "Scheur",
+    "rust": "Roest",
+    "paint": "Lakschade",
+    "previous_repair": "Eerdere reparatie",
+    "missing": "Ontbrekend onderdeel"
+  },
+  "severities": {
+    "minor": "Licht",
+    "major": "Ernstig"
+  },
+  "panels": {
+    "hood": "Motorkap",
+    "roof": "Dak",
+    "trunk": "Kofferklep",
+    "tailgate": "Achterklep",
+    "front_bumper": "Voorbumper",
+    "rear_bumper": "Achterbumper",
+    "grille": "Grille",
+    "windshield": "Voorruit",
+    "rear_window": "Achterruit",
+    "left_headlight": "Koplamp links",
+    "right_headlight": "Koplamp rechts",
+    "left_taillight": "Achterlicht links",
+    "right_taillight": "Achterlicht rechts",
+    "left_front_fender": "Voorscherm links",
+    "right_front_fender": "Voorscherm rechts",
+    "left_front_door": "Voorportier links",
+    "right_front_door": "Voorportier rechts",
+    "left_rear_door": "Achterportier links",
+    "right_rear_door": "Achterportier rechts",
+    "left_rear_quarter": "Achterscherm links",
+    "right_rear_quarter": "Achterscherm rechts",
+    "left_sill": "Dorpel links",
+    "right_sill": "Dorpel rechts",
+    "left_mirror": "Spiegel links",
+    "right_mirror": "Spiegel rechts",
+    "left_front_wheel": "Wiel linksvoor",
+    "right_front_wheel": "Wiel rechtsvoor",
+    "left_rear_wheel": "Wiel linksachter",
+    "right_rear_wheel": "Wiel rechtsachter",
+    "left_a_pillar": "A-stijl links",
+    "right_a_pillar": "A-stijl rechts",
+    "left_windows": "Zijruiten links",
+    "right_windows": "Zijruiten rechts",
+    "left_sliding_door": "Schuifdeur links",
+    "right_sliding_door": "Schuifdeur rechts",
+    "left_side_panel": "Zijpaneel links",
+    "right_side_panel": "Zijpaneel rechts",
+    "rear_doors": "Achterdeuren",
+    "bed": "Laadbak",
+    "bed_side_left": "Laadbakzijde links",
+    "bed_side_right": "Laadbakzijde rechts",
+    "tank": "Tank",
+    "seat": "Zadel",
+    "front_fairing": "Voorkuip",
+    "left_fairing": "Kuip links",
+    "right_fairing": "Kuip rechts",
+    "front_fender": "Voorspatbord",
+    "rear_fender": "Achterspatbord",
+    "front_wheel": "Voorwiel",
+    "rear_wheel": "Achterwiel",
+    "exhaust": "Uitlaat",
+    "handlebars": "Stuur",
+    "headlamp": "Koplamp",
+    "tail": "Achterkant",
+    "bow": "Boeg",
+    "stern": "Achtersteven",
+    "transom": "Spiegel (achter)",
+    "port_hull": "Romp bakboord",
+    "starboard_hull": "Romp stuurboord",
+    "deck": "Dek",
+    "cabin": "Kajuit",
+    "windscreen": "Windscherm",
+    "gunwale_port": "Dolboord bakboord",
+    "gunwale_starboard": "Dolboord stuurboord",
+    "keel": "Kiel",
+    "outboard": "Buitenboordmotor"
+  },
+  "print": {
+    "previous": "eerder vastgelegd"
+  }
+}

+ 26 - 4
messages/nl/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Lege sjabloon",
       "description": "Begin met niets en bouw de checklist die uw werkplaats echt gebruikt.",
       "standardLabel": "Leeg"
+    },
+    "vehicle-intake": {
+      "name": "Inname van het voertuig",
+      "description": "Het rondje om de auto bij het afgeven, met de klant erbij: hoe hij binnenkwam, wat er al op de carrosserie zat, wat er binnenin achterbleef. Wordt afgedrukt op de werkorder die de klant tekent.",
+      "standardLabel": "Werkplaats"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Onderstel en onderstelonderdelen",
     "no_s7": "Overige uitrusting",
     "no_s8": "Hinder en schadelijke effecten",
-    "no_s10": "Aanbieding bij het trafikkstasjon"
+    "no_s10": "Aanbieding bij het trafikkstasjon",
+    "arrival": "Aankomst",
+    "exteriorWalkround": "Rondje buitenkant"
   },
   "checks": {
     "registrationPlates": "Kentekenplaten",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diesel: rookopaciteit, ouder dan EU-4, met turbo",
     "no_8_2_3_1": "Uitlaat in het algemeen en draaiende motor",
     "no_8_3_1": "Vloeistoflekkage",
-    "no_10_1": "Aanbieding bij het trafikkstasjon"
+    "no_10_1": "Aanbieding bij het trafikkstasjon",
+    "fuelLevel": "Brandstofpeil",
+    "keysHandedOver": "Overhandigde sleutels",
+    "warningLightsOnArrival": "Waarschuwingslampjes bij aankomst",
+    "personalBelongings": "Persoonlijke spullen in het voertuig",
+    "glassCondition": "Staat van de ruiten",
+    "wheelsAndTyresVisual": "Wielen en banden, visueel",
+    "seatsAndTrim": "Stoelen en bekleding",
+    "floorAndMats": "Vloer en matten",
+    "infotainmentControls": "Infotainment en bediening"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Bijlage I, punt 0. Bevestigt dat het aangeboden voertuig overeenkomt met de papieren.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Max. 3,0 m⁻¹. De grenswaarde van de fabrikant gaat voor.",
     "no_8_2_3_1": "Met draaiende motor: controleer of het OBD-storingslampje een storing aangeeft en of de zelfdiagnose correct werkt; elke afwijking geeft e). Let op zichtbare blauwe, donkergrijze of zwarte rook tijdens de proefrit en bij een gasstoot vanuit stationair, en controleer hoe de motor loopt.",
     "no_8_3_1": "Ernst 2 bij elke grote vloeistoflekkage (anders dan water) die waarschijnlijk schadelijk is voor het milieu of een veiligheidsrisico vormt voor andere weggebruikers, mits deze tijdens de keuring op de grond druppelt.",
-    "no_10_1": "Wordt gegeven wanneer iets vereist dat het voertuig naar een trafikkstasjon (keuringsstation van Statens vegvesen) moet voor beoordeling, nieuwe goedkeuring of een aantekening in het vognkort, bijv. illegaal gewijzigde motor of regeleenheid, verlaagd/verhoogd, niet-toegestane frontbescherming (bullbar), meer zitplaatsen dan geregistreerd, omgebouwd naar een andere brandstof of een goedkeuringsplichtige ombouw. Wordt gegeven naast het relevante gebrek voor hetzelfde punt; bij de herkeuring kan alleen Statens vegvesen het goedkeuren."
+    "no_10_1": "Wordt gegeven wanneer iets vereist dat het voertuig naar een trafikkstasjon (keuringsstation van Statens vegvesen) moet voor beoordeling, nieuwe goedkeuring of een aantekening in het vognkort, bijv. illegaal gewijzigde motor of regeleenheid, verlaagd/verhoogd, niet-toegestane frontbescherming (bullbar), meer zitplaatsen dan geregistreerd, omgebouwd naar een andere brandstof of een goedkeuringsplichtige ombouw. Wordt gegeven naast het relevante gebrek voor hetzelfde punt; bij de herkeuring kan alleen Statens vegvesen het goedkeuren.",
+    "bodyConditionMap": "Tik op de tekening waar elke deuk, kras of steenslag zit en voeg een notitie en een foto toe. Markeringen van eerdere bezoeken staan in grijs."
   },
   "choices": {
     "full": "Volledig",
     "partial": "Gedeeltelijk",
-    "none": "Geen"
+    "none": "Geen",
+    "fuelEmpty": "Leeg",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Vol"
   },
   "defects": {
     "registrationPlateMissing": "Kentekenplaat ontbreekt",

+ 4 - 2
messages/nl/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Gebrekbeoordeling",
       "measurement": "Meting",
       "text": "Vrije tekst",
-      "choice": "Keuze"
+      "choice": "Keuze",
+      "condition_map": "Staatkaart"
     },
     "inputHint": {
       "condition": "De monteur beoordeelt het punt volgens de gebrekenschaal.",
       "measurement": "Legt een getal met eenheid vast, automatisch beoordeeld tegen een bereik.",
       "text": "Legt een geschreven waarneming vast, bijvoorbeeld een bandenmerk of foutcode.",
-      "choice": "De monteur kiest één antwoord uit een vaste lijst."
+      "choice": "De monteur kiest één antwoord uit een vaste lijst.",
+      "condition_map": "Een tekening van het voertuig om deuken, krassen en steenslag op te markeren, elk met een notitie en een foto."
     },
     "cancel": "Annuleren",
     "create": "Checklist maken",

+ 7 - 1
messages/nl/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Mogelijk gemaakt door",
     "signature": "Handtekening",
     "signedBy": "Ondertekend door",
-    "signatureDate": "Datum"
+    "signatureDate": "Datum",
+    "conditionMapTitle": "Staat van het voertuig",
+    "conditionMapNo": "Nr.",
+    "conditionMapArea": "Plaats",
+    "conditionMapKind": "Type",
+    "conditionMapSeverity": "Ernst",
+    "conditionMapNote": "Notitie"
   },
   "workOrder": {
     "title": "WERKORDER",

+ 23 - 3
messages/nl/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Openen op telefoon",
       "concerns": "Klachten van de klant",
       "job_description": "Gevraagd werk",
-      "work_checklist": "Werkchecklist"
+      "work_checklist": "Werkchecklist",
+      "condition_map": "Staat van het voertuig"
     },
     "fields": {
       "customer_name": "Klantnaam",
@@ -770,7 +771,9 @@
       "scheduled": "Gepland",
       "promised": "Toegezegd",
       "work_bay": "Werkplek",
-      "customer_line": "Handtekeningregel klant"
+      "customer_line": "Handtekeningregel klant",
+      "legend": "Legenda",
+      "previous_marks": "Eerdere markeringen"
     },
     "presetsTitle": "Begin met een indeling",
     "presetsHint": "Vervangt de indeling hieronder. Er wordt niets opgeslagen tot u opslaat.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Remblokken en -schijven voor versleten; vervangen",
       "concernWarningLight": "Waarschuwingslampje op het dashboard sinds vorige week",
       "workRequested": "Remblokken en -schijven voor vervangen, service-indicator resetten en proefrit maken.",
-      "checklistTestDrive": "Proefrit"
+      "checklistTestDrive": "Proefrit",
+      "markScratch": "Sleutelkras langs het portier, 20 cm",
+      "markDent": "Deuk onder de greep, lak intact",
+      "markChip": "Steenslag onderin de bumper",
+      "viewTop": "Bovenaanzicht",
+      "viewLeft": "Linkerkant",
+      "viewRight": "Rechterkant",
+      "viewFront": "Voorkant",
+      "viewRear": "Achterkant",
+      "panelLeftRearDoor": "Achterportier links",
+      "panelLeftFrontDoor": "Voorportier links",
+      "panelFrontBumper": "Voorbumper",
+      "kindScratch": "Kras",
+      "kindDent": "Deuk",
+      "kindChip": "Steenslag",
+      "severityMinor": "Licht",
+      "severityMajor": "Ernstig",
+      "markPrevious": "eerder vastgelegd"
     },
     "logoUpload": "Logo uploaden",
     "logoReplace": "Logo vervangen",

+ 6 - 2
messages/pl/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Dodano zdjęcie przeglądu",
     "inspection_file_added": "Dodano plik przeglądu",
     "signature_save": "Zapisano podpis",
-    "signature_remove": "Usunięto podpis"
+    "signature_remove": "Usunięto podpis",
+    "conditionMark_add": "Dodano oznaczenie stanu",
+    "conditionMark_remove": "Usunięto oznaczenie stanu"
   },
   "summary": {
     "customField_create": "Utworzono pole niestandardowe „{name}”",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Poprawiono wpis czasu {technician} w zleceniu „{job}” na {minutes} min",
     "timeEntry_stop": "Zatrzymano zegar {technician} w zleceniu „{job}” na {minutes} min",
     "timeEntry_delete": "Usunięto wpis czasu {id}",
-    "inspection_phoneUpload": "Dodano plik z telefonu do przeglądu {ref}"
+    "inspection_phoneUpload": "Dodano plik z telefonu do przeglądu {ref}",
+    "condition_mark_add": "Dodano oznaczenie do mapy stanu",
+    "condition_mark_remove": "Usunięto oznaczenie z mapy stanu"
   }
 }

+ 146 - 0
messages/pl/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Stan pojazdu",
+  "description": "Dotknij rysunku tam, gdzie jest uszkodzenie. Każde oznaczenie ma typ, stopień, notatkę i zdjęcie.",
+  "addHint": "Dotknij elementu, aby dodać oznaczenie",
+  "bodyType": "Nadwozie",
+  "legend": "Oznaczenia",
+  "noMarks": "Brak oznaczeń. Dotknij rysunku tam, gdzie jest uszkodzenie.",
+  "previousHeading": "Zarejestrowane podczas wcześniejszej wizyty",
+  "previousHint": "Nadal obecne od ostatniego razu, na szaro. Usuń te, których już nie ma.",
+  "newSinceLastVisit": "Nowe od ostatniej wizyty",
+  "confirmed": "Nadal jest",
+  "clearRepaired": "Usuń jako naprawione",
+  "restore": "Przywróć",
+  "edit": "Edytuj oznaczenie",
+  "remove": "Usuń oznaczenie",
+  "removeTitle": "Usunąć to oznaczenie?",
+  "removeBody": "Oznaczenie {n} na {area} i jego notatka zostaną usunięte.",
+  "kind": "Typ",
+  "severity": "Stopień",
+  "note": "Notatka",
+  "notePlaceholder": "Gdzie dokładnie, jak duże, co powiedział klient",
+  "photo": "Zdjęcie",
+  "addPhoto": "Dodaj zdjęcie",
+  "takePhoto": "Zrób zdjęcie",
+  "removePhoto": "Usuń zdjęcie",
+  "save": "Zapisz oznaczenie",
+  "done": "Gotowe",
+  "cancel": "Anuluj",
+  "moveHint": "Przeciągnij oznaczenie, aby je przesunąć",
+  "enlarge": "Powiększ: {view}",
+  "allViews": "Wszystkie widoki",
+  "markLabel": "Oznaczenie {n}: {kind} na {area}",
+  "markOn": "{kind} na {area}",
+  "photosOf": "Zdjęcia oznaczenia {n}",
+  "saved": "Zapisano",
+  "saveFailed": "Nie udało się zapisać oznaczenia",
+  "uploadFailed": "Nie udało się przesłać zdjęcia",
+  "readOnly": "Otwórz ponownie, aby zmienić mapę stanu.",
+  "count": "{count, plural, =0 {Brak oznaczeń} one {# oznaczenie} few {# oznaczenia} other {# oznaczeń}}",
+  "previousCount": "{count, plural, one {# z wcześniejszej wizyty} few {# z wcześniejszych wizyt} other {# z wcześniejszych wizyt}}",
+  "elsewhere": "{count, plural, one {# oznaczenie naniesiono na inne nadwozie i widać je tylko na wydruku} few {# oznaczenia naniesiono na inne nadwozie i widać je tylko na wydruku} other {# oznaczeń naniesiono na inne nadwozie i widać je tylko na wydruku}}",
+  "photoCount": "{count, plural, one {# zdjęcie} few {# zdjęcia} other {# zdjęć}}",
+  "clearBody": "Oznaczenie {n} na {area} zostanie zapisane jako naprawione. Pozostaje w historii i nie jest już drukowane.",
+  "views": {
+    "top": "Z góry",
+    "left": "Lewa strona",
+    "right": "Prawa strona",
+    "front": "Przód",
+    "rear": "Tył"
+  },
+  "bodies": {
+    "sedan": "Sedan",
+    "hatchback": "Hatchback",
+    "estate": "Kombi",
+    "suv": "SUV",
+    "van": "Van",
+    "pickup": "Pickup",
+    "motorcycle": "Motocykl",
+    "boat": "Łódź"
+  },
+  "kinds": {
+    "dent": "Wgniecenie",
+    "scratch": "Rysa",
+    "chip": "Odprysk",
+    "crack": "Pęknięcie",
+    "rust": "Rdza",
+    "paint": "Uszkodzenie lakieru",
+    "previous_repair": "Wcześniejsza naprawa",
+    "missing": "Brakująca część"
+  },
+  "severities": {
+    "minor": "Drobne",
+    "major": "Poważne"
+  },
+  "panels": {
+    "hood": "Maska",
+    "roof": "Dach",
+    "trunk": "Pokrywa bagażnika",
+    "tailgate": "Klapa tylna",
+    "front_bumper": "Zderzak przedni",
+    "rear_bumper": "Zderzak tylny",
+    "grille": "Grill",
+    "windshield": "Szyba przednia",
+    "rear_window": "Szyba tylna",
+    "left_headlight": "Reflektor lewy",
+    "right_headlight": "Reflektor prawy",
+    "left_taillight": "Lampa tylna lewa",
+    "right_taillight": "Lampa tylna prawa",
+    "left_front_fender": "Błotnik przedni lewy",
+    "right_front_fender": "Błotnik przedni prawy",
+    "left_front_door": "Drzwi przednie lewe",
+    "right_front_door": "Drzwi przednie prawe",
+    "left_rear_door": "Drzwi tylne lewe",
+    "right_rear_door": "Drzwi tylne prawe",
+    "left_rear_quarter": "Błotnik tylny lewy",
+    "right_rear_quarter": "Błotnik tylny prawy",
+    "left_sill": "Próg lewy",
+    "right_sill": "Próg prawy",
+    "left_mirror": "Lusterko lewe",
+    "right_mirror": "Lusterko prawe",
+    "left_front_wheel": "Koło przednie lewe",
+    "right_front_wheel": "Koło przednie prawe",
+    "left_rear_wheel": "Koło tylne lewe",
+    "right_rear_wheel": "Koło tylne prawe",
+    "left_a_pillar": "Słupek A lewy",
+    "right_a_pillar": "Słupek A prawy",
+    "left_windows": "Szyby boczne lewe",
+    "right_windows": "Szyby boczne prawe",
+    "left_sliding_door": "Drzwi przesuwne lewe",
+    "right_sliding_door": "Drzwi przesuwne prawe",
+    "left_side_panel": "Panel boczny lewy",
+    "right_side_panel": "Panel boczny prawy",
+    "rear_doors": "Drzwi tylne",
+    "bed": "Skrzynia ładunkowa",
+    "bed_side_left": "Burta skrzyni lewa",
+    "bed_side_right": "Burta skrzyni prawa",
+    "tank": "Zbiornik",
+    "seat": "Siedzenie",
+    "front_fairing": "Owiewka przednia",
+    "left_fairing": "Owiewka lewa",
+    "right_fairing": "Owiewka prawa",
+    "front_fender": "Błotnik przedni",
+    "rear_fender": "Błotnik tylny",
+    "front_wheel": "Koło przednie",
+    "rear_wheel": "Koło tylne",
+    "exhaust": "Wydech",
+    "handlebars": "Kierownica",
+    "headlamp": "Reflektor",
+    "tail": "Tył",
+    "bow": "Dziób",
+    "stern": "Rufa",
+    "transom": "Pawęż",
+    "port_hull": "Kadłub, lewa burta",
+    "starboard_hull": "Kadłub, prawa burta",
+    "deck": "Pokład",
+    "cabin": "Kabina",
+    "windscreen": "Szyba przednia",
+    "gunwale_port": "Nadburcie lewe",
+    "gunwale_starboard": "Nadburcie prawe",
+    "keel": "Kil",
+    "outboard": "Silnik zaburtowy"
+  },
+  "print": {
+    "previous": "zarejestrowane wcześniej"
+  }
+}

+ 26 - 4
messages/pl/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Pusty szablon",
       "description": "Zacznij od zera i zbuduj listę kontrolną, której naprawdę używa Twój warsztat.",
       "standardLabel": "Pusty"
+    },
+    "vehicle-intake": {
+      "name": "Przyjęcie pojazdu",
+      "description": "Obejście auta przy przekazaniu, z klientem obok: jak przyjechało, co już było na nadwoziu, co zostało w środku. Drukuje się na zleceniu, które podpisuje klient.",
+      "standardLabel": "Warsztat"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Podwozie i elementy przymocowane do podwozia",
     "no_s7": "Inne wyposażenie",
     "no_s8": "Uciążliwość dla otoczenia",
-    "no_s10": "Stawienie pojazdu na trafikkstasjon (stacja kontroli Statens vegvesen)"
+    "no_s10": "Stawienie pojazdu na trafikkstasjon (stacja kontroli Statens vegvesen)",
+    "arrival": "Przyjazd",
+    "exteriorWalkround": "Obejście z zewnątrz"
   },
   "checks": {
     "registrationPlates": "Tablice rejestracyjne",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diesel: zadymienie spalin, starszy niż Euro 4, turbodoładowany",
     "no_8_2_3_1": "Spaliny ogólnie i praca silnika",
     "no_8_3_1": "Wycieki płynów",
-    "no_10_1": "Stawienie pojazdu na trafikkstasjon"
+    "no_10_1": "Stawienie pojazdu na trafikkstasjon",
+    "fuelLevel": "Poziom paliwa",
+    "keysHandedOver": "Przekazane kluczyki",
+    "warningLightsOnArrival": "Kontrolki po przyjeździe",
+    "personalBelongings": "Rzeczy osobiste pozostawione w pojeździe",
+    "glassCondition": "Stan szyb",
+    "wheelsAndTyresVisual": "Koła i opony, wizualnie",
+    "seatsAndTrim": "Fotele i tapicerka",
+    "floorAndMats": "Podłoga i dywaniki",
+    "infotainmentControls": "Multimedia i sterowanie"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Załącznik I, pkt 0. Potwierdza, że przedstawiony pojazd jest zgodny z dokumentami.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Maks. 3,0 m⁻¹. Wartość graniczna producenta ma pierwszeństwo.",
     "no_8_2_3_1": "Przy pracującym silniku: sprawdź, czy wskaźnik usterki OBD sygnalizuje awarię i czy autodiagnostyka działa prawidłowo; każde odstępstwo to usterka e). Podczas jazdy próbnej i przy krótkim dodaniu gazu z biegu jałowego obserwuj, czy widoczny jest niebieski, ciemnoszary lub czarny dym, i sprawdź, jak pracuje silnik.",
     "no_8_3_1": "Stopień 2 przy każdym dużym wycieku płynu (innego niż woda), który może szkodzić środowisku lub stwarzać zagrożenie dla innych uczestników ruchu, pod warunkiem że płyn kapie na podłoże podczas kontroli.",
-    "no_10_1": "Wpisuje się, gdy coś wymaga stawienia pojazdu na trafikkstasjon w celu oceny, nowej homologacji lub zmiany w vognkort, np. niedozwolona przeróbka silnika lub sterownika, obniżenie/podwyższenie, niezgodna osłona przednia (orurowanie, bull bar), więcej miejsc siedzących niż zarejestrowano, przerobienie na inne paliwo lub zmiana wymagająca homologacji. Wpisuje się dodatkowo do właściwej usterki dotyczącej tej samej sprawy; przy kontroli ponownej może ją usunąć tylko Statens vegvesen."
+    "no_10_1": "Wpisuje się, gdy coś wymaga stawienia pojazdu na trafikkstasjon w celu oceny, nowej homologacji lub zmiany w vognkort, np. niedozwolona przeróbka silnika lub sterownika, obniżenie/podwyższenie, niezgodna osłona przednia (orurowanie, bull bar), więcej miejsc siedzących niż zarejestrowano, przerobienie na inne paliwo lub zmiana wymagająca homologacji. Wpisuje się dodatkowo do właściwej usterki dotyczącej tej samej sprawy; przy kontroli ponownej może ją usunąć tylko Statens vegvesen.",
+    "bodyConditionMap": "Dotknij rysunku tam, gdzie jest każde wgniecenie, rysa lub odprysk, i dodaj notatkę oraz zdjęcie. Oznaczenia z wcześniejszych wizyt są szare."
   },
   "choices": {
     "full": "Pełny",
     "partial": "Częściowy",
-    "none": "Brak"
+    "none": "Brak",
+    "fuelEmpty": "Pusty",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Pełny"
   },
   "defects": {
     "registrationPlateMissing": "Brak tablicy rejestracyjnej",

+ 4 - 2
messages/pl/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Ocena usterki",
       "measurement": "Pomiar",
       "text": "Tekst dowolny",
-      "choice": "Wybór"
+      "choice": "Wybór",
+      "condition_map": "Mapa stanu"
     },
     "inputHint": {
       "condition": "Diagnosta ocenia pozycję według skali usterek.",
       "measurement": "Zapisuje liczbę z jednostką, ocenianą automatycznie względem zakresu.",
       "text": "Zapisuje obserwację, na przykład markę opony albo kod usterki.",
-      "choice": "Diagnosta wybiera jedną odpowiedź ze stałej listy."
+      "choice": "Diagnosta wybiera jedną odpowiedź ze stałej listy.",
+      "condition_map": "Rysunek pojazdu, na którym zaznacza się wgniecenia, rysy i odpryski, każde z notatką i zdjęciem."
     },
     "cancel": "Anuluj",
     "create": "Utwórz listę kontrolną",

+ 7 - 1
messages/pl/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Obsługiwane przez",
     "signature": "Podpis",
     "signedBy": "Podpisał(a)",
-    "signatureDate": "Data"
+    "signatureDate": "Data",
+    "conditionMapTitle": "Stan pojazdu",
+    "conditionMapNo": "Nr",
+    "conditionMapArea": "Miejsce",
+    "conditionMapKind": "Typ",
+    "conditionMapSeverity": "Stopień",
+    "conditionMapNote": "Notatka"
   },
   "workOrder": {
     "title": "ZLECENIE",

+ 23 - 3
messages/pl/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Otwórz na telefonie",
       "concerns": "Uwagi klienta",
       "job_description": "Zlecone prace",
-      "work_checklist": "Lista prac"
+      "work_checklist": "Lista prac",
+      "condition_map": "Stan pojazdu"
     },
     "fields": {
       "customer_name": "Nazwa klienta",
@@ -770,7 +771,9 @@
       "scheduled": "Zaplanowano",
       "promised": "Obiecano",
       "work_bay": "Stanowisko",
-      "customer_line": "Linia podpisu klienta"
+      "customer_line": "Linia podpisu klienta",
+      "legend": "Legenda",
+      "previous_marks": "Wcześniejsze oznaczenia"
     },
     "presetsTitle": "Zacznij od układu",
     "presetsHint": "Zastępuje układ poniżej. Nic nie zostanie zapisane, dopóki nie zapiszesz.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Przednie klocki i tarcze zużyte; wymienione",
       "concernWarningLight": "Kontrolka na desce rozdzielczej od zeszłego tygodnia",
       "workRequested": "Wymienić przednie klocki i tarcze hamulcowe, zresetować wskaźnik serwisu i wykonać jazdę próbną.",
-      "checklistTestDrive": "Jazda próbna"
+      "checklistTestDrive": "Jazda próbna",
+      "markScratch": "Rysa od klucza wzdłuż drzwi, 20 cm",
+      "markDent": "Wgniecenie pod klamką, lakier nienaruszony",
+      "markChip": "Odpryski w dolnej części zderzaka",
+      "viewTop": "Z góry",
+      "viewLeft": "Lewa strona",
+      "viewRight": "Prawa strona",
+      "viewFront": "Przód",
+      "viewRear": "Tył",
+      "panelLeftRearDoor": "Drzwi tylne lewe",
+      "panelLeftFrontDoor": "Drzwi przednie lewe",
+      "panelFrontBumper": "Zderzak przedni",
+      "kindScratch": "Rysa",
+      "kindDent": "Wgniecenie",
+      "kindChip": "Odprysk",
+      "severityMinor": "Drobne",
+      "severityMajor": "Poważne",
+      "markPrevious": "zarejestrowane wcześniej"
     },
     "logoUpload": "Prześlij logo",
     "logoReplace": "Zmień logo",

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

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Adicionou foto de inspeção",
     "inspection_file_added": "Adicionou arquivo de inspeção",
     "signature_save": "Salvou a assinatura",
-    "signature_remove": "Removeu a assinatura"
+    "signature_remove": "Removeu a assinatura",
+    "conditionMark_add": "Marca de estado adicionada",
+    "conditionMark_remove": "Marca de estado removida"
   },
   "summary": {
     "customField_create": "Campo personalizado \"{name}\" criado",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Registro de tempo de {technician} em \"{job}\" corrigido para {minutes} minutos",
     "timeEntry_stop": "Relógio de {technician} em \"{job}\" parado em {minutes} minutos",
     "timeEntry_delete": "Registro de tempo {id} excluído",
-    "inspection_phoneUpload": "Adicionou um arquivo pelo celular à inspeção {ref}"
+    "inspection_phoneUpload": "Adicionou um arquivo pelo celular à inspeção {ref}",
+    "condition_mark_add": "Adicionou uma marca ao mapa de estado",
+    "condition_mark_remove": "Removeu uma marca do mapa de estado"
   }
 }

+ 146 - 0
messages/pt-BR/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Estado do veículo",
+  "description": "Toque no desenho onde está o dano. Cada marca recebe um tipo, uma gravidade, uma nota e uma foto.",
+  "addHint": "Toque em uma peça para adicionar uma marca",
+  "bodyType": "Carroceria",
+  "legend": "Marcas",
+  "noMarks": "Nenhuma marca ainda. Toque no desenho onde está o dano.",
+  "previousHeading": "Registrado em uma visita anterior",
+  "previousHint": "Ainda presente desde a última vez, em cinza. Apague as que não existem mais.",
+  "newSinceLastVisit": "Novo desde a última visita",
+  "confirmed": "Ainda presente",
+  "clearRepaired": "Apagar como reparado",
+  "restore": "Restaurar",
+  "edit": "Editar marca",
+  "remove": "Remover marca",
+  "removeTitle": "Remover esta marca?",
+  "removeBody": "A marca {n} em {area} e sua nota serão removidas.",
+  "kind": "Tipo",
+  "severity": "Gravidade",
+  "note": "Nota",
+  "notePlaceholder": "Onde exatamente, tamanho, o que o cliente disse",
+  "photo": "Foto",
+  "addPhoto": "Adicionar foto",
+  "takePhoto": "Tirar foto",
+  "removePhoto": "Remover foto",
+  "save": "Salvar marca",
+  "done": "Concluído",
+  "cancel": "Cancelar",
+  "moveHint": "Arraste uma marca para movê-la",
+  "enlarge": "Ampliar {view}",
+  "allViews": "Todas as vistas",
+  "markLabel": "Marca {n}: {kind} em {area}",
+  "markOn": "{kind} em {area}",
+  "photosOf": "Fotos da marca {n}",
+  "saved": "Salvo",
+  "saveFailed": "Não foi possível salvar a marca",
+  "uploadFailed": "Não foi possível enviar a foto",
+  "readOnly": "Reabra para alterar o mapa de estado.",
+  "count": "{count, plural, =0 {Sem marcas} one {# marca} other {# marcas}}",
+  "previousCount": "{count, plural, one {# de uma visita anterior} other {# de visitas anteriores}}",
+  "elsewhere": "{count, plural, one {# marca foi feita em outra carroceria e aparece só na impressão} other {# marcas foram feitas em outra carroceria e aparecem só na impressão}}",
+  "photoCount": "{count, plural, one {# foto} other {# fotos}}",
+  "clearBody": "A marca {n} em {area} será registrada como reparada. Ela fica no histórico e deixa de ser impressa.",
+  "views": {
+    "top": "Vista de cima",
+    "left": "Lado esquerdo",
+    "right": "Lado direito",
+    "front": "Frente",
+    "rear": "Traseira"
+  },
+  "bodies": {
+    "sedan": "Sedã",
+    "hatchback": "Hatch",
+    "estate": "Perua",
+    "suv": "SUV",
+    "van": "Van",
+    "pickup": "Picape",
+    "motorcycle": "Moto",
+    "boat": "Barco"
+  },
+  "kinds": {
+    "dent": "Amassado",
+    "scratch": "Arranhão",
+    "chip": "Lascado por pedra",
+    "crack": "Trinca",
+    "rust": "Ferrugem",
+    "paint": "Dano na pintura",
+    "previous_repair": "Reparo anterior",
+    "missing": "Peça faltando"
+  },
+  "severities": {
+    "minor": "Leve",
+    "major": "Grave"
+  },
+  "panels": {
+    "hood": "Capô",
+    "roof": "Teto",
+    "trunk": "Tampa do porta-malas",
+    "tailgate": "Tampa traseira",
+    "front_bumper": "Para-choque dianteiro",
+    "rear_bumper": "Para-choque traseiro",
+    "grille": "Grade",
+    "windshield": "Para-brisa",
+    "rear_window": "Vidro traseiro",
+    "left_headlight": "Farol esquerdo",
+    "right_headlight": "Farol direito",
+    "left_taillight": "Lanterna traseira esquerda",
+    "right_taillight": "Lanterna traseira direita",
+    "left_front_fender": "Para-lama dianteiro esquerdo",
+    "right_front_fender": "Para-lama dianteiro direito",
+    "left_front_door": "Porta dianteira esquerda",
+    "right_front_door": "Porta dianteira direita",
+    "left_rear_door": "Porta traseira esquerda",
+    "right_rear_door": "Porta traseira direita",
+    "left_rear_quarter": "Lateral traseira esquerda",
+    "right_rear_quarter": "Lateral traseira direita",
+    "left_sill": "Soleira esquerda",
+    "right_sill": "Soleira direita",
+    "left_mirror": "Retrovisor esquerdo",
+    "right_mirror": "Retrovisor direito",
+    "left_front_wheel": "Roda dianteira esquerda",
+    "right_front_wheel": "Roda dianteira direita",
+    "left_rear_wheel": "Roda traseira esquerda",
+    "right_rear_wheel": "Roda traseira direita",
+    "left_a_pillar": "Coluna A esquerda",
+    "right_a_pillar": "Coluna A direita",
+    "left_windows": "Vidros laterais esquerdos",
+    "right_windows": "Vidros laterais direitos",
+    "left_sliding_door": "Porta deslizante esquerda",
+    "right_sliding_door": "Porta deslizante direita",
+    "left_side_panel": "Painel lateral esquerdo",
+    "right_side_panel": "Painel lateral direito",
+    "rear_doors": "Portas traseiras",
+    "bed": "Caçamba",
+    "bed_side_left": "Lateral esquerda da caçamba",
+    "bed_side_right": "Lateral direita da caçamba",
+    "tank": "Tanque",
+    "seat": "Banco",
+    "front_fairing": "Carenagem frontal",
+    "left_fairing": "Carenagem esquerda",
+    "right_fairing": "Carenagem direita",
+    "front_fender": "Para-lama dianteiro",
+    "rear_fender": "Para-lama traseiro",
+    "front_wheel": "Roda dianteira",
+    "rear_wheel": "Roda traseira",
+    "exhaust": "Escapamento",
+    "handlebars": "Guidão",
+    "headlamp": "Farol",
+    "tail": "Traseira",
+    "bow": "Proa",
+    "stern": "Popa",
+    "transom": "Espelho de popa",
+    "port_hull": "Casco bombordo",
+    "starboard_hull": "Casco boreste",
+    "deck": "Convés",
+    "cabin": "Cabine",
+    "windscreen": "Para-brisa",
+    "gunwale_port": "Borda bombordo",
+    "gunwale_starboard": "Borda boreste",
+    "keel": "Quilha",
+    "outboard": "Motor de popa"
+  },
+  "print": {
+    "previous": "registrado antes"
+  }
+}

+ 26 - 4
messages/pt-BR/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Modelo em branco",
       "description": "Comece do zero e monte o checklist que a sua oficina realmente usa.",
       "standardLabel": "Vazio"
+    },
+    "vehicle-intake": {
+      "name": "Check-in do veículo",
+      "description": "A volta ao redor do carro na entrega, com o cliente do lado: como chegou, o que já havia na carroceria, o que ficou dentro. Imprime na ordem de serviço que o cliente assina.",
+      "standardLabel": "Oficina"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Chassi e componentes do chassi",
     "no_s7": "Outros equipamentos",
     "no_s8": "Efeitos nocivos",
-    "no_s10": "Apresentação na trafikkstasjon"
+    "no_s10": "Apresentação na trafikkstasjon",
+    "arrival": "Chegada",
+    "exteriorWalkround": "Volta externa"
   },
   "checks": {
     "registrationPlates": "Placas",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Diesel: opacidade da fumaça, anterior ao Euro 4, turboalimentado",
     "no_8_2_3_1": "Escapamento em geral e funcionamento do motor",
     "no_8_3_1": "Vazamentos de fluidos",
-    "no_10_1": "Apresentação na trafikkstasjon (posto de inspeção veicular da Statens vegvesen)"
+    "no_10_1": "Apresentação na trafikkstasjon (posto de inspeção veicular da Statens vegvesen)",
+    "fuelLevel": "Nível de combustível",
+    "keysHandedOver": "Chaves entregues",
+    "warningLightsOnArrival": "Luzes de aviso na chegada",
+    "personalBelongings": "Pertences pessoais deixados no veículo",
+    "glassCondition": "Estado dos vidros",
+    "wheelsAndTyresVisual": "Rodas e pneus, visual",
+    "seatsAndTrim": "Bancos e acabamento",
+    "floorAndMats": "Assoalho e tapetes",
+    "infotainmentControls": "Multimídia e controles"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Anexo I, ponto 0. Confirma que o veículo apresentado confere com os documentos.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Máx. 3,0 m⁻¹. O limite do fabricante tem precedência.",
     "no_8_2_3_1": "Com o motor ligado: verifique se o indicador de falha do OBD mostra algum defeito e se o autodiagnóstico funciona corretamente; qualquer desvio dá e). Procure fumaça azul, cinza-escura ou preta visível no teste de rodagem e em uma acelerada rápida a partir da marcha lenta, e verifique como o motor funciona.",
     "no_8_3_1": "Grau 2 para qualquer grande vazamento de fluido (exceto água) que possa prejudicar o meio ambiente ou representar risco à segurança de outros usuários da via, desde que pingue no chão durante a inspeção.",
-    "no_10_1": "Dado quando algo exige que o veículo vá a uma trafikkstasjon para avaliação, nova homologação ou alteração do vognkort, p. ex., modificação ilegal do motor ou da ECU, veículo rebaixado/elevado, proteção dianteira fora das normas (quebra-mato), mais bancos do que o registrado, conversão para outro combustível ou uma modificação que exige homologação. Dado além do defeito correspondente sobre o mesmo assunto; na reinspeção, só a Statens vegvesen pode liberá-lo."
+    "no_10_1": "Dado quando algo exige que o veículo vá a uma trafikkstasjon para avaliação, nova homologação ou alteração do vognkort, p. ex., modificação ilegal do motor ou da ECU, veículo rebaixado/elevado, proteção dianteira fora das normas (quebra-mato), mais bancos do que o registrado, conversão para outro combustível ou uma modificação que exige homologação. Dado além do defeito correspondente sobre o mesmo assunto; na reinspeção, só a Statens vegvesen pode liberá-lo.",
+    "bodyConditionMap": "Toque no desenho onde está cada amassado, arranhão ou lascado e adicione uma nota e uma foto. Marcas de visitas anteriores aparecem em cinza."
   },
   "choices": {
     "full": "Completo",
     "partial": "Parcial",
-    "none": "Nenhum"
+    "none": "Nenhum",
+    "fuelEmpty": "Vazio",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Cheio"
   },
   "defects": {
     "registrationPlateMissing": "Placa ausente",

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

@@ -378,13 +378,15 @@
       "condition": "Classificação de deficiência",
       "measurement": "Medição",
       "text": "Texto livre",
-      "choice": "Escolha"
+      "choice": "Escolha",
+      "condition_map": "Mapa de estado"
     },
     "inputHint": {
       "condition": "O técnico classifica o item pela escala de deficiências.",
       "measurement": "Registra um número com unidade, classificado automaticamente contra uma faixa.",
       "text": "Registra uma observação escrita, por exemplo uma marca de pneu ou um código de falha.",
-      "choice": "O técnico escolhe uma resposta de uma lista fixa."
+      "choice": "O técnico escolhe uma resposta de uma lista fixa.",
+      "condition_map": "Um desenho do veículo para marcar amassados, arranhões e lascados, cada um com nota e foto."
     },
     "cancel": "Cancelar",
     "create": "Criar checklist",

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

@@ -208,7 +208,13 @@
     "poweredBy": "Desenvolvido por",
     "signature": "Assinatura",
     "signedBy": "Assinado por",
-    "signatureDate": "Data"
+    "signatureDate": "Data",
+    "conditionMapTitle": "Estado do veículo",
+    "conditionMapNo": "Nº",
+    "conditionMapArea": "Local",
+    "conditionMapKind": "Tipo",
+    "conditionMapSeverity": "Gravidade",
+    "conditionMapNote": "Nota"
   },
   "workOrder": {
     "title": "ORDEM DE SERVIÇO",

+ 23 - 3
messages/pt-BR/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Abrir no celular",
       "concerns": "Reclamações do cliente",
       "job_description": "Serviço solicitado",
-      "work_checklist": "Lista de serviços"
+      "work_checklist": "Lista de serviços",
+      "condition_map": "Estado do veículo"
     },
     "fields": {
       "customer_name": "Nome do cliente",
@@ -770,7 +771,9 @@
       "scheduled": "Agendado",
       "promised": "Prometido",
       "work_bay": "Box",
-      "customer_line": "Linha de assinatura do cliente"
+      "customer_line": "Linha de assinatura do cliente",
+      "legend": "Legenda",
+      "previous_marks": "Marcas anteriores"
     },
     "presetsTitle": "Começar por uma disposição",
     "presetsHint": "Substitui a disposição abaixo. Nada é salvo até você salvar.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Pastilhas e discos dianteiros gastos; substituídos",
       "concernWarningLight": "Luz de aviso no painel desde a semana passada",
       "workRequested": "Substituir pastilhas e discos dianteiros, zerar o indicador de revisão e fazer teste de rua.",
-      "checklistTestDrive": "Teste de rua"
+      "checklistTestDrive": "Teste de rua",
+      "markScratch": "Arranhão de chave ao longo da porta, 20 cm",
+      "markDent": "Amassado abaixo da maçaneta, pintura intacta",
+      "markChip": "Lascados por pedra na parte de baixo do para-choque",
+      "viewTop": "Vista de cima",
+      "viewLeft": "Lado esquerdo",
+      "viewRight": "Lado direito",
+      "viewFront": "Frente",
+      "viewRear": "Traseira",
+      "panelLeftRearDoor": "Porta traseira esquerda",
+      "panelLeftFrontDoor": "Porta dianteira esquerda",
+      "panelFrontBumper": "Para-choque dianteiro",
+      "kindScratch": "Arranhão",
+      "kindDent": "Amassado",
+      "kindChip": "Lascado por pedra",
+      "severityMinor": "Leve",
+      "severityMajor": "Grave",
+      "markPrevious": "registrado antes"
     },
     "logoUpload": "Enviar logotipo",
     "logoReplace": "Substituir logotipo",

+ 6 - 2
messages/pt-PT/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Adicionou fotografia de inspeção",
     "inspection_file_added": "Adicionou ficheiro de inspeção",
     "signature_save": "Guardou a assinatura",
-    "signature_remove": "Removeu a assinatura"
+    "signature_remove": "Removeu a assinatura",
+    "conditionMark_add": "Marca de estado adicionada",
+    "conditionMark_remove": "Marca de estado removida"
   },
   "summary": {
     "customField_create": "Campo personalizado \"{name}\" criado",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Registo de tempo de {technician} em \"{job}\" corrigido para {minutes} minutos",
     "timeEntry_stop": "Relógio de {technician} em \"{job}\" parado aos {minutes} minutos",
     "timeEntry_delete": "Registo de tempo {id} eliminado",
-    "inspection_phoneUpload": "Adicionou um ficheiro a partir de um telemóvel à inspeção {ref}"
+    "inspection_phoneUpload": "Adicionou um ficheiro a partir de um telemóvel à inspeção {ref}",
+    "condition_mark_add": "Adicionou uma marca ao mapa de estado",
+    "condition_mark_remove": "Removeu uma marca do mapa de estado"
   }
 }

+ 146 - 0
messages/pt-PT/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Estado da viatura",
+  "description": "Toque no desenho onde está o dano. Cada marca recebe um tipo, uma gravidade, uma nota e uma fotografia.",
+  "addHint": "Toque numa peça para adicionar uma marca",
+  "bodyType": "Carroçaria",
+  "legend": "Marcas",
+  "noMarks": "Ainda sem marcas. Toque no desenho onde está o dano.",
+  "previousHeading": "Registado numa visita anterior",
+  "previousHint": "Ainda presente desde a última vez, a cinzento. Apague as que já não existem.",
+  "newSinceLastVisit": "Novo desde a última visita",
+  "confirmed": "Ainda presente",
+  "clearRepaired": "Apagar como reparado",
+  "restore": "Repor",
+  "edit": "Editar marca",
+  "remove": "Remover marca",
+  "removeTitle": "Remover esta marca?",
+  "removeBody": "A marca {n} em {area} e a sua nota serão removidas.",
+  "kind": "Tipo",
+  "severity": "Gravidade",
+  "note": "Nota",
+  "notePlaceholder": "Onde exatamente, tamanho, o que o cliente disse",
+  "photo": "Fotografia",
+  "addPhoto": "Adicionar fotografia",
+  "takePhoto": "Tirar fotografia",
+  "removePhoto": "Remover fotografia",
+  "save": "Guardar marca",
+  "done": "Concluído",
+  "cancel": "Cancelar",
+  "moveHint": "Arraste uma marca para a mover",
+  "enlarge": "Ampliar {view}",
+  "allViews": "Todas as vistas",
+  "markLabel": "Marca {n}: {kind} em {area}",
+  "markOn": "{kind} em {area}",
+  "photosOf": "Fotografias da marca {n}",
+  "saved": "Guardado",
+  "saveFailed": "Não foi possível guardar a marca",
+  "uploadFailed": "Não foi possível carregar a fotografia",
+  "readOnly": "Reabra para alterar o mapa de estado.",
+  "count": "{count, plural, =0 {Sem marcas} one {# marca} other {# marcas}}",
+  "previousCount": "{count, plural, one {# de uma visita anterior} other {# de visitas anteriores}}",
+  "elsewhere": "{count, plural, one {# marca foi feita noutra carroçaria e aparece só na impressão} other {# marcas foram feitas noutra carroçaria e aparecem só na impressão}}",
+  "photoCount": "{count, plural, one {# fotografia} other {# fotografias}}",
+  "clearBody": "A marca {n} em {area} será registada como reparada. Fica no histórico e deixa de ser impressa.",
+  "views": {
+    "top": "Vista de cima",
+    "left": "Lado esquerdo",
+    "right": "Lado direito",
+    "front": "Frente",
+    "rear": "Traseira"
+  },
+  "bodies": {
+    "sedan": "Sedan",
+    "hatchback": "Hatchback",
+    "estate": "Carrinha",
+    "suv": "SUV",
+    "van": "Furgão",
+    "pickup": "Pick-up",
+    "motorcycle": "Motociclo",
+    "boat": "Barco"
+  },
+  "kinds": {
+    "dent": "Amolgadela",
+    "scratch": "Risco",
+    "chip": "Lascado por gravilha",
+    "crack": "Fissura",
+    "rust": "Ferrugem",
+    "paint": "Dano na pintura",
+    "previous_repair": "Reparação anterior",
+    "missing": "Peça em falta"
+  },
+  "severities": {
+    "minor": "Ligeira",
+    "major": "Grave"
+  },
+  "panels": {
+    "hood": "Capô",
+    "roof": "Tejadilho",
+    "trunk": "Tampa da mala",
+    "tailgate": "Porta da bagageira",
+    "front_bumper": "Para-choques dianteiro",
+    "rear_bumper": "Para-choques traseiro",
+    "grille": "Grelha",
+    "windshield": "Para-brisas",
+    "rear_window": "Óculo traseiro",
+    "left_headlight": "Farol esquerdo",
+    "right_headlight": "Farol direito",
+    "left_taillight": "Farolim traseiro esquerdo",
+    "right_taillight": "Farolim traseiro direito",
+    "left_front_fender": "Guarda-lamas dianteiro esquerdo",
+    "right_front_fender": "Guarda-lamas dianteiro direito",
+    "left_front_door": "Porta dianteira esquerda",
+    "right_front_door": "Porta dianteira direita",
+    "left_rear_door": "Porta traseira esquerda",
+    "right_rear_door": "Porta traseira direita",
+    "left_rear_quarter": "Painel traseiro esquerdo",
+    "right_rear_quarter": "Painel traseiro direito",
+    "left_sill": "Embaladeira esquerda",
+    "right_sill": "Embaladeira direita",
+    "left_mirror": "Espelho esquerdo",
+    "right_mirror": "Espelho direito",
+    "left_front_wheel": "Roda dianteira esquerda",
+    "right_front_wheel": "Roda dianteira direita",
+    "left_rear_wheel": "Roda traseira esquerda",
+    "right_rear_wheel": "Roda traseira direita",
+    "left_a_pillar": "Pilar A esquerdo",
+    "right_a_pillar": "Pilar A direito",
+    "left_windows": "Vidros laterais esquerdos",
+    "right_windows": "Vidros laterais direitos",
+    "left_sliding_door": "Porta de correr esquerda",
+    "right_sliding_door": "Porta de correr direita",
+    "left_side_panel": "Painel lateral esquerdo",
+    "right_side_panel": "Painel lateral direito",
+    "rear_doors": "Portas traseiras",
+    "bed": "Caixa de carga",
+    "bed_side_left": "Taipal esquerdo",
+    "bed_side_right": "Taipal direito",
+    "tank": "Depósito",
+    "seat": "Assento",
+    "front_fairing": "Carenagem frontal",
+    "left_fairing": "Carenagem esquerda",
+    "right_fairing": "Carenagem direita",
+    "front_fender": "Guarda-lamas dianteiro",
+    "rear_fender": "Guarda-lamas traseiro",
+    "front_wheel": "Roda dianteira",
+    "rear_wheel": "Roda traseira",
+    "exhaust": "Escape",
+    "handlebars": "Guiador",
+    "headlamp": "Farol",
+    "tail": "Traseira",
+    "bow": "Proa",
+    "stern": "Popa",
+    "transom": "Painel de popa",
+    "port_hull": "Casco a bombordo",
+    "starboard_hull": "Casco a estibordo",
+    "deck": "Convés",
+    "cabin": "Cabina",
+    "windscreen": "Para-brisas",
+    "gunwale_port": "Borda a bombordo",
+    "gunwale_starboard": "Borda a estibordo",
+    "keel": "Quilha",
+    "outboard": "Motor fora de borda"
+  },
+  "print": {
+    "previous": "registado antes"
+  }
+}

+ 26 - 4
messages/pt-PT/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Modelo em branco",
       "description": "Comece do zero e crie a lista de verificação que a sua oficina realmente usa.",
       "standardLabel": "Vazio"
+    },
+    "vehicle-intake": {
+      "name": "Check-in da viatura",
+      "description": "A volta à viatura na entrega, com o cliente ao lado: como chegou, o que já estava na carroçaria, o que ficou lá dentro. Imprime na ordem de trabalho que o cliente assina.",
+      "standardLabel": "Oficina"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Quadro e acessórios do quadro",
     "no_s7": "Outro equipamento",
     "no_s8": "Incómodos e danos ambientais",
-    "no_s10": "Apresentação na trafikkstasjon (estação de inspeção da Statens vegvesen)"
+    "no_s10": "Apresentação na trafikkstasjon (estação de inspeção da Statens vegvesen)",
+    "arrival": "Chegada",
+    "exteriorWalkround": "Volta exterior"
   },
   "checks": {
     "registrationPlates": "Chapas de matrícula",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Gasóleo: opacidade dos fumos, anterior à EU-4, turbo",
     "no_8_2_3_1": "Escape em geral e funcionamento do motor",
     "no_8_3_1": "Fugas de fluidos",
-    "no_10_1": "Apresentação na trafikkstasjon"
+    "no_10_1": "Apresentação na trafikkstasjon",
+    "fuelLevel": "Nível de combustível",
+    "keysHandedOver": "Chaves entregues",
+    "warningLightsOnArrival": "Luzes de aviso à chegada",
+    "personalBelongings": "Objetos pessoais deixados na viatura",
+    "glassCondition": "Estado dos vidros",
+    "wheelsAndTyresVisual": "Rodas e pneus, visual",
+    "seatsAndTrim": "Bancos e estofos",
+    "floorAndMats": "Chão e tapetes",
+    "infotainmentControls": "Infoentretenimento e comandos"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Anexo I, ponto 0. Confirma que o veículo apresentado corresponde aos seus documentos.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Máx. 3,0 m⁻¹. O limite do fabricante prevalece.",
     "no_8_2_3_1": "Com o motor a trabalhar: verificar se o avisador de avaria OBD indica uma avaria e se o autodiagnóstico funciona corretamente; qualquer desvio dá e). Procurar fumo azul, cinzento-escuro ou preto visível no ensaio em estrada e numa aceleração brusca a partir do ralenti, e verificar o funcionamento do motor.",
     "no_8_3_1": "Grau 2 para qualquer fuga importante de fluido (exceto água) suscetível de prejudicar o ambiente ou de constituir risco para a segurança, desde que pingue para o chão durante a inspeção.",
-    "no_10_1": "Atribuída quando algo obriga o veículo a ir à trafikkstasjon para avaliação, nova homologação ou averbamento no vognkort, p. ex. motor ou centralina modificados ilegalmente, rebaixado/elevado, proteção frontal não conforme (quebra-mato), mais lugares sentados do que os registados no vognkort, convertido para outro combustível ou modificação sujeita a homologação. É atribuída em conjunto com a anomalia relevante sobre a mesma questão; na reinspeção só a Statens vegvesen a pode aceitar."
+    "no_10_1": "Atribuída quando algo obriga o veículo a ir à trafikkstasjon para avaliação, nova homologação ou averbamento no vognkort, p. ex. motor ou centralina modificados ilegalmente, rebaixado/elevado, proteção frontal não conforme (quebra-mato), mais lugares sentados do que os registados no vognkort, convertido para outro combustível ou modificação sujeita a homologação. É atribuída em conjunto com a anomalia relevante sobre a mesma questão; na reinspeção só a Statens vegvesen a pode aceitar.",
+    "bodyConditionMap": "Toque no desenho onde está cada amolgadela, risco ou lascado e acrescente uma nota e uma fotografia. As marcas de visitas anteriores aparecem a cinzento."
   },
   "choices": {
     "full": "Completo",
     "partial": "Parcial",
-    "none": "Nenhum"
+    "none": "Nenhum",
+    "fuelEmpty": "Vazio",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Cheio"
   },
   "defects": {
     "registrationPlateMissing": "Chapa de matrícula em falta",

+ 4 - 2
messages/pt-PT/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Classificação de deficiência",
       "measurement": "Medição",
       "text": "Texto livre",
-      "choice": "Escolha"
+      "choice": "Escolha",
+      "condition_map": "Mapa de estado"
     },
     "inputHint": {
       "condition": "O técnico classifica o item segundo a escala de deficiências.",
       "measurement": "Regista um número numa unidade, classificado automaticamente face a um intervalo.",
       "text": "Regista uma observação escrita, por exemplo uma marca de pneu ou um código de avaria.",
-      "choice": "O técnico escolhe uma resposta de uma lista fixa."
+      "choice": "O técnico escolhe uma resposta de uma lista fixa.",
+      "condition_map": "Um desenho da viatura para marcar amolgadelas, riscos e lascados, cada um com nota e fotografia."
     },
     "cancel": "Cancelar",
     "create": "Criar lista de verificação",

+ 7 - 1
messages/pt-PT/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Desenvolvido por",
     "signature": "Assinatura",
     "signedBy": "Assinado por",
-    "signatureDate": "Data"
+    "signatureDate": "Data",
+    "conditionMapTitle": "Estado da viatura",
+    "conditionMapNo": "N.º",
+    "conditionMapArea": "Zona",
+    "conditionMapKind": "Tipo",
+    "conditionMapSeverity": "Gravidade",
+    "conditionMapNote": "Nota"
   },
   "workOrder": {
     "title": "ORDEM DE TRABALHO",

+ 23 - 3
messages/pt-PT/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Abrir no telemóvel",
       "concerns": "Reclamações do cliente",
       "job_description": "Trabalho pedido",
-      "work_checklist": "Lista de trabalhos"
+      "work_checklist": "Lista de trabalhos",
+      "condition_map": "Estado da viatura"
     },
     "fields": {
       "customer_name": "Nome do cliente",
@@ -770,7 +771,9 @@
       "scheduled": "Agendado",
       "promised": "Prometido",
       "work_bay": "Posto",
-      "customer_line": "Linha de assinatura do cliente"
+      "customer_line": "Linha de assinatura do cliente",
+      "legend": "Legenda",
+      "previous_marks": "Marcas anteriores"
     },
     "presetsTitle": "Começar por uma disposição",
     "presetsHint": "Substitui a disposição abaixo. Nada é guardado até clicar em Guardar.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Pastilhas e discos dianteiros gastos; substituídos",
       "concernWarningLight": "Luz de aviso no painel desde a semana passada",
       "workRequested": "Substituir pastilhas e discos dianteiros, repor o indicador de manutenção e fazer teste de estrada.",
-      "checklistTestDrive": "Teste de estrada"
+      "checklistTestDrive": "Teste de estrada",
+      "markScratch": "Risco de chave ao longo da porta, 20 cm",
+      "markDent": "Amolgadela por baixo do puxador, pintura intacta",
+      "markChip": "Lascados por gravilha na parte inferior do para-choques",
+      "viewTop": "Vista de cima",
+      "viewLeft": "Lado esquerdo",
+      "viewRight": "Lado direito",
+      "viewFront": "Frente",
+      "viewRear": "Traseira",
+      "panelLeftRearDoor": "Porta traseira esquerda",
+      "panelLeftFrontDoor": "Porta dianteira esquerda",
+      "panelFrontBumper": "Para-choques dianteiro",
+      "kindScratch": "Risco",
+      "kindDent": "Amolgadela",
+      "kindChip": "Lascado por gravilha",
+      "severityMinor": "Ligeira",
+      "severityMajor": "Grave",
+      "markPrevious": "registado antes"
     },
     "logoUpload": "Carregar logótipo",
     "logoReplace": "Substituir logótipo",

+ 6 - 2
messages/ru/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Добавлено фото осмотра",
     "inspection_file_added": "Добавлен файл осмотра",
     "signature_save": "Сохранена подпись",
-    "signature_remove": "Удалена подпись"
+    "signature_remove": "Удалена подпись",
+    "conditionMark_add": "Добавлена отметка состояния",
+    "conditionMark_remove": "Удалена отметка состояния"
   },
   "summary": {
     "customField_create": "Создано настраиваемое поле «{name}»",
@@ -331,6 +333,8 @@
     "timeEntry_update": "Запись времени {technician} по заказу «{job}» исправлена на {minutes} мин",
     "timeEntry_stop": "Таймер {technician} по заказу «{job}» остановлен на {minutes} мин",
     "timeEntry_delete": "Удалена запись времени {id}",
-    "inspection_phoneUpload": "Добавлен файл с телефона к осмотру {ref}"
+    "inspection_phoneUpload": "Добавлен файл с телефона к осмотру {ref}",
+    "condition_mark_add": "Добавил(а) отметку на карту состояния",
+    "condition_mark_remove": "Удалил(а) отметку с карты состояния"
   }
 }

+ 146 - 0
messages/ru/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Состояние автомобиля",
+  "description": "Нажмите на рисунок там, где повреждение. У каждой отметки есть тип, степень, заметка и фото.",
+  "addHint": "Нажмите на деталь, чтобы добавить отметку",
+  "bodyType": "Тип кузова",
+  "legend": "Отметки",
+  "noMarks": "Отметок пока нет. Нажмите на рисунок там, где повреждение.",
+  "previousHeading": "Зафиксировано при прошлом визите",
+  "previousHint": "Осталось с прошлого раза, показано серым. Уберите то, чего больше нет.",
+  "newSinceLastVisit": "Новое с прошлого визита",
+  "confirmed": "Ещё есть",
+  "clearRepaired": "Убрать как отремонтированное",
+  "restore": "Восстановить",
+  "edit": "Изменить отметку",
+  "remove": "Удалить отметку",
+  "removeTitle": "Удалить эту отметку?",
+  "removeBody": "Отметка {n} на {area} и её заметка будут удалены.",
+  "kind": "Тип",
+  "severity": "Степень",
+  "note": "Заметка",
+  "notePlaceholder": "Где именно, какого размера, что сказал клиент",
+  "photo": "Фото",
+  "addPhoto": "Добавить фото",
+  "takePhoto": "Сделать фото",
+  "removePhoto": "Удалить фото",
+  "save": "Сохранить отметку",
+  "done": "Готово",
+  "cancel": "Отмена",
+  "moveHint": "Перетащите отметку, чтобы переместить",
+  "enlarge": "Увеличить: {view}",
+  "allViews": "Все виды",
+  "markLabel": "Отметка {n}: {kind} на {area}",
+  "markOn": "{kind} на {area}",
+  "photosOf": "Фото отметки {n}",
+  "saved": "Сохранено",
+  "saveFailed": "Не удалось сохранить отметку",
+  "uploadFailed": "Не удалось загрузить фото",
+  "readOnly": "Откройте заново, чтобы изменить карту состояния.",
+  "count": "{count, plural, =0 {Нет отметок} one {# отметка} few {# отметки} other {# отметок}}",
+  "previousCount": "{count, plural, one {# с прошлого визита} few {# с прошлых визитов} other {# с прошлых визитов}}",
+  "elsewhere": "{count, plural, one {# отметка сделана на другом типе кузова и есть только в распечатке} few {# отметки сделаны на другом типе кузова и есть только в распечатке} other {# отметок сделано на другом типе кузова и есть только в распечатке}}",
+  "photoCount": "{count, plural, one {# фото} few {# фото} other {# фото}}",
+  "clearBody": "Отметка {n} на {area} будет записана как отремонтированная. Она остаётся в истории и больше не печатается.",
+  "views": {
+    "top": "Сверху",
+    "left": "Левая сторона",
+    "right": "Правая сторона",
+    "front": "Спереди",
+    "rear": "Сзади"
+  },
+  "bodies": {
+    "sedan": "Седан",
+    "hatchback": "Хэтчбек",
+    "estate": "Универсал",
+    "suv": "Внедорожник",
+    "van": "Фургон",
+    "pickup": "Пикап",
+    "motorcycle": "Мотоцикл",
+    "boat": "Лодка"
+  },
+  "kinds": {
+    "dent": "Вмятина",
+    "scratch": "Царапина",
+    "chip": "Скол",
+    "crack": "Трещина",
+    "rust": "Ржавчина",
+    "paint": "Повреждение краски",
+    "previous_repair": "Прежний ремонт",
+    "missing": "Отсутствует деталь"
+  },
+  "severities": {
+    "minor": "Небольшое",
+    "major": "Серьёзное"
+  },
+  "panels": {
+    "hood": "Капот",
+    "roof": "Крыша",
+    "trunk": "Крышка багажника",
+    "tailgate": "Задняя дверь",
+    "front_bumper": "Передний бампер",
+    "rear_bumper": "Задний бампер",
+    "grille": "Решётка радиатора",
+    "windshield": "Лобовое стекло",
+    "rear_window": "Заднее стекло",
+    "left_headlight": "Левая фара",
+    "right_headlight": "Правая фара",
+    "left_taillight": "Левый задний фонарь",
+    "right_taillight": "Правый задний фонарь",
+    "left_front_fender": "Левое переднее крыло",
+    "right_front_fender": "Правое переднее крыло",
+    "left_front_door": "Левая передняя дверь",
+    "right_front_door": "Правая передняя дверь",
+    "left_rear_door": "Левая задняя дверь",
+    "right_rear_door": "Правая задняя дверь",
+    "left_rear_quarter": "Левое заднее крыло",
+    "right_rear_quarter": "Правое заднее крыло",
+    "left_sill": "Левый порог",
+    "right_sill": "Правый порог",
+    "left_mirror": "Левое зеркало",
+    "right_mirror": "Правое зеркало",
+    "left_front_wheel": "Левое переднее колесо",
+    "right_front_wheel": "Правое переднее колесо",
+    "left_rear_wheel": "Левое заднее колесо",
+    "right_rear_wheel": "Правое заднее колесо",
+    "left_a_pillar": "Левая стойка A",
+    "right_a_pillar": "Правая стойка A",
+    "left_windows": "Левые боковые стёкла",
+    "right_windows": "Правые боковые стёкла",
+    "left_sliding_door": "Левая сдвижная дверь",
+    "right_sliding_door": "Правая сдвижная дверь",
+    "left_side_panel": "Левая боковая панель",
+    "right_side_panel": "Правая боковая панель",
+    "rear_doors": "Задние двери",
+    "bed": "Кузов",
+    "bed_side_left": "Левый борт кузова",
+    "bed_side_right": "Правый борт кузова",
+    "tank": "Бак",
+    "seat": "Сиденье",
+    "front_fairing": "Передний обтекатель",
+    "left_fairing": "Левый обтекатель",
+    "right_fairing": "Правый обтекатель",
+    "front_fender": "Переднее крыло",
+    "rear_fender": "Заднее крыло",
+    "front_wheel": "Переднее колесо",
+    "rear_wheel": "Заднее колесо",
+    "exhaust": "Выхлоп",
+    "handlebars": "Руль",
+    "headlamp": "Фара",
+    "tail": "Хвост",
+    "bow": "Нос",
+    "stern": "Корма",
+    "transom": "Транец",
+    "port_hull": "Корпус, левый борт",
+    "starboard_hull": "Корпус, правый борт",
+    "deck": "Палуба",
+    "cabin": "Каюта",
+    "windscreen": "Ветровое стекло",
+    "gunwale_port": "Планширь левого борта",
+    "gunwale_starboard": "Планширь правого борта",
+    "keel": "Киль",
+    "outboard": "Подвесной мотор"
+  },
+  "print": {
+    "previous": "зафиксировано ранее"
+  }
+}

+ 26 - 4
messages/ru/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Пустой шаблон",
       "description": "Начните с нуля и соберите чек-лист, которым на самом деле пользуется ваш сервис.",
       "standardLabel": "Пустой"
+    },
+    "vehicle-intake": {
+      "name": "Приёмка автомобиля",
+      "description": "Обход машины при сдаче, в присутствии клиента: как она приехала, что уже было на кузове, что осталось внутри. Печатается на заказ-наряде, который подписывает клиент.",
+      "standardLabel": "Мастерская"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Рама и элементы крепления к раме",
     "no_s7": "Прочее оборудование",
     "no_s8": "Вредное воздействие",
-    "no_s10": "Предъявление на trafikkstasjon"
+    "no_s10": "Предъявление на trafikkstasjon",
+    "arrival": "Прибытие",
+    "exteriorWalkround": "Наружный обход"
   },
   "checks": {
     "registrationPlates": "Государственные регистрационные знаки",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Дизель: дымность, старше Euro 4, с турбонаддувом",
     "no_8_2_3_1": "Выхлоп в целом и работа двигателя",
     "no_8_3_1": "Утечки жидкостей",
-    "no_10_1": "Предъявление на trafikkstasjon"
+    "no_10_1": "Предъявление на trafikkstasjon",
+    "fuelLevel": "Уровень топлива",
+    "keysHandedOver": "Переданные ключи",
+    "warningLightsOnArrival": "Индикаторы при прибытии",
+    "personalBelongings": "Личные вещи, оставленные в автомобиле",
+    "glassCondition": "Состояние стёкол",
+    "wheelsAndTyresVisual": "Колёса и шины, визуально",
+    "seatsAndTrim": "Сиденья и обивка",
+    "floorAndMats": "Пол и коврики",
+    "infotainmentControls": "Мультимедиа и органы управления"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Приложение I, пункт 0. Подтверждает, что представленное транспортное средство соответствует документам.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "Не более 3,0 m⁻¹. Предел производителя имеет приоритет.",
     "no_8_2_3_1": "При работающем двигателе: проверьте, показывает ли индикатор неисправности OBD сбой и правильно ли работает самодиагностика; любое отклонение даёт e). Следите за видимым синим, тёмно-серым или чёрным дымом на пробной поездке и при перегазовке с холостого хода, проверьте работу двигателя.",
     "no_8_3_1": "Степень 2 для любой крупной утечки жидкости (кроме воды), которая может нанести вред окружающей среде или создать угрозу безопасности для других участников движения, при условии что она капает на землю во время контроля.",
-    "no_10_1": "Выставляется, если что-либо требует предъявления ТС на trafikkstasjon (станция технического контроля Statens vegvesen) для оценки, нового одобрения или изменения vognkort, например незаконная модификация двигателя или ЭБУ, заниженная/завышенная подвеска, несоответствующая передняя защита (кенгурятник), больше мест, чем зарегистрировано, переоборудование на другое топливо или модификация, требующая одобрения. Выставляется дополнительно к соответствующему замечанию по тому же вопросу; при повторном контроле снять его может только Statens vegvesen."
+    "no_10_1": "Выставляется, если что-либо требует предъявления ТС на trafikkstasjon (станция технического контроля Statens vegvesen) для оценки, нового одобрения или изменения vognkort, например незаконная модификация двигателя или ЭБУ, заниженная/завышенная подвеска, несоответствующая передняя защита (кенгурятник), больше мест, чем зарегистрировано, переоборудование на другое топливо или модификация, требующая одобрения. Выставляется дополнительно к соответствующему замечанию по тому же вопросу; при повторном контроле снять его может только Statens vegvesen.",
+    "bodyConditionMap": "Нажмите на рисунок там, где каждая вмятина, царапина или скол, и добавьте заметку и фото. Отметки прошлых визитов показаны серым."
   },
   "choices": {
     "full": "Полная",
     "partial": "Частичная",
-    "none": "Нет"
+    "none": "Нет",
+    "fuelEmpty": "Пусто",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Полный"
   },
   "defects": {
     "registrationPlateMissing": "Регистрационный знак отсутствует",

+ 4 - 2
messages/ru/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Оценка неисправности",
       "measurement": "Измерение",
       "text": "Произвольный текст",
-      "choice": "Выбор"
+      "choice": "Выбор",
+      "condition_map": "Карта состояния"
     },
     "inputHint": {
       "condition": "Механик оценивает пункт по шкале неисправностей.",
       "measurement": "Фиксирует число с единицей измерения, автоматически оцениваемое по диапазону.",
       "text": "Фиксирует записанное наблюдение, например марку шины или код ошибки.",
-      "choice": "Механик выбирает один ответ из заданного списка."
+      "choice": "Механик выбирает один ответ из заданного списка.",
+      "condition_map": "Рисунок автомобиля, на котором отмечают вмятины, царапины и сколы, каждую с заметкой и фото."
     },
     "cancel": "Отмена",
     "create": "Создать чек-лист",

+ 7 - 1
messages/ru/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Работает на",
     "signature": "Подпись",
     "signedBy": "Подписал(а)",
-    "signatureDate": "Дата"
+    "signatureDate": "Дата",
+    "conditionMapTitle": "Состояние автомобиля",
+    "conditionMapNo": "№",
+    "conditionMapArea": "Место",
+    "conditionMapKind": "Тип",
+    "conditionMapSeverity": "Степень",
+    "conditionMapNote": "Заметка"
   },
   "workOrder": {
     "title": "ЗАКАЗ-НАРЯД",

+ 23 - 3
messages/ru/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Открыть на телефоне",
       "concerns": "Обращения клиента",
       "job_description": "Запрошенные работы",
-      "work_checklist": "Чек-лист работ"
+      "work_checklist": "Чек-лист работ",
+      "condition_map": "Состояние автомобиля"
     },
     "fields": {
       "customer_name": "Имя клиента",
@@ -770,7 +771,9 @@
       "scheduled": "Запланировано",
       "promised": "Обещано",
       "work_bay": "Пост",
-      "customer_line": "Строка подписи клиента"
+      "customer_line": "Строка подписи клиента",
+      "legend": "Легенда",
+      "previous_marks": "Прежние отметки"
     },
     "presetsTitle": "Начать с расположения",
     "presetsHint": "Заменяет расположение ниже. Ничего не сохранится, пока вы не сохраните.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Передние колодки и диски изношены; заменены",
       "concernWarningLight": "Индикатор на панели горит с прошлой недели",
       "workRequested": "Заменить передние колодки и диски, сбросить индикатор ТО и провести тест-драйв.",
-      "checklistTestDrive": "Тест-драйв"
+      "checklistTestDrive": "Тест-драйв",
+      "markScratch": "Царапина от ключа вдоль двери, 20 см",
+      "markDent": "Вмятина под ручкой, краска цела",
+      "markChip": "Сколы в нижней части бампера",
+      "viewTop": "Сверху",
+      "viewLeft": "Левая сторона",
+      "viewRight": "Правая сторона",
+      "viewFront": "Спереди",
+      "viewRear": "Сзади",
+      "panelLeftRearDoor": "Левая задняя дверь",
+      "panelLeftFrontDoor": "Левая передняя дверь",
+      "panelFrontBumper": "Передний бампер",
+      "kindScratch": "Царапина",
+      "kindDent": "Вмятина",
+      "kindChip": "Скол",
+      "severityMinor": "Небольшое",
+      "severityMajor": "Серьёзное",
+      "markPrevious": "зафиксировано ранее"
     },
     "logoUpload": "Загрузить логотип",
     "logoReplace": "Заменить логотип",

+ 6 - 2
messages/tr/audit.json

@@ -184,7 +184,9 @@
     "inspection_photo_added": "Muayene fotoğrafı eklendi",
     "inspection_file_added": "Muayene dosyası eklendi",
     "signature_save": "İmza kaydedildi",
-    "signature_remove": "İmza kaldırıldı"
+    "signature_remove": "İmza kaldırıldı",
+    "conditionMark_add": "Durum işareti eklendi",
+    "conditionMark_remove": "Durum işareti kaldırıldı"
   },
   "summary": {
     "customField_create": "\"{name}\" özel alanı oluşturuldu",
@@ -331,6 +333,8 @@
     "timeEntry_update": "{technician} için \"{job}\" işindeki zaman kaydı {minutes} dakika olarak düzeltildi",
     "timeEntry_stop": "{technician} için \"{job}\" işindeki sayaç {minutes} dakikada durduruldu",
     "timeEntry_delete": "{id} zaman kaydı silindi",
-    "inspection_phoneUpload": "{ref} muayenesine telefondan dosya eklendi"
+    "inspection_phoneUpload": "{ref} muayenesine telefondan dosya eklendi",
+    "condition_mark_add": "Durum haritasına işaret ekledi",
+    "condition_mark_remove": "Durum haritasından işaret kaldırdı"
   }
 }

+ 146 - 0
messages/tr/conditionMap.json

@@ -0,0 +1,146 @@
+{
+  "title": "Araç durumu",
+  "description": "Hasarın olduğu yerde çizime dokunun. Her işaretin bir türü, önemi, notu ve fotoğrafı olur.",
+  "addHint": "İşaret eklemek için bir parçaya dokunun",
+  "bodyType": "Kasa tipi",
+  "legend": "İşaretler",
+  "noMarks": "Henüz işaret yok. Hasarın olduğu yerde çizime dokunun.",
+  "previousHeading": "Önceki ziyarette kaydedildi",
+  "previousHint": "Geçen seferden beri duruyor, gri çizildi. Artık olmayanları silin.",
+  "newSinceLastVisit": "Son ziyaretten beri yeni",
+  "confirmed": "Hâlâ var",
+  "clearRepaired": "Onarıldı olarak sil",
+  "restore": "Geri al",
+  "edit": "İşareti düzenle",
+  "remove": "İşareti kaldır",
+  "removeTitle": "Bu işaret kaldırılsın mı?",
+  "removeBody": "{area} üzerindeki {n} numaralı işaret ve notu kaldırılacak.",
+  "kind": "Tür",
+  "severity": "Önem",
+  "note": "Not",
+  "notePlaceholder": "Tam olarak nerede, ne büyüklükte, müşteri ne dedi",
+  "photo": "Fotoğraf",
+  "addPhoto": "Fotoğraf ekle",
+  "takePhoto": "Fotoğraf çek",
+  "removePhoto": "Fotoğrafı kaldır",
+  "save": "İşareti kaydet",
+  "done": "Bitti",
+  "cancel": "İptal",
+  "moveHint": "Taşımak için işareti sürükleyin",
+  "enlarge": "{view} büyüt",
+  "allViews": "Tüm görünümler",
+  "markLabel": "İşaret {n}: {area} üzerinde {kind}",
+  "markOn": "{area} üzerinde {kind}",
+  "photosOf": "{n} numaralı işaretin fotoğrafları",
+  "saved": "Kaydedildi",
+  "saveFailed": "İşaret kaydedilemedi",
+  "uploadFailed": "Fotoğraf yüklenemedi",
+  "readOnly": "Durum haritasını değiştirmek için yeniden açın.",
+  "count": "{count, plural, =0 {İşaret yok} one {# işaret} other {# işaret}}",
+  "previousCount": "{count, plural, one {# önceki ziyaretten} other {# önceki ziyaretlerden}}",
+  "elsewhere": "{count, plural, one {# işaret başka bir kasa tipine konuldu ve yalnızca çıktıda listelenir} other {# işaret başka bir kasa tipine konuldu ve yalnızca çıktıda listelenir}}",
+  "photoCount": "{count, plural, one {# fotoğraf} other {# fotoğraf}}",
+  "clearBody": "{area} üzerindeki {n} numaralı işaret onarıldı olarak kaydedilecek. Geçmişte kalır ve artık yazdırılmaz.",
+  "views": {
+    "top": "Üstten",
+    "left": "Sol taraf",
+    "right": "Sağ taraf",
+    "front": "Ön",
+    "rear": "Arka"
+  },
+  "bodies": {
+    "sedan": "Sedan",
+    "hatchback": "Hatchback",
+    "estate": "Station wagon",
+    "suv": "SUV",
+    "van": "Panelvan",
+    "pickup": "Pikap",
+    "motorcycle": "Motosiklet",
+    "boat": "Tekne"
+  },
+  "kinds": {
+    "dent": "Göçük",
+    "scratch": "Çizik",
+    "chip": "Taş izi",
+    "crack": "Çatlak",
+    "rust": "Pas",
+    "paint": "Boya hasarı",
+    "previous_repair": "Önceki onarım",
+    "missing": "Eksik parça"
+  },
+  "severities": {
+    "minor": "Hafif",
+    "major": "Ağır"
+  },
+  "panels": {
+    "hood": "Kaput",
+    "roof": "Tavan",
+    "trunk": "Bagaj kapağı",
+    "tailgate": "Arka kapak",
+    "front_bumper": "Ön tampon",
+    "rear_bumper": "Arka tampon",
+    "grille": "Panjur",
+    "windshield": "Ön cam",
+    "rear_window": "Arka cam",
+    "left_headlight": "Sol far",
+    "right_headlight": "Sağ far",
+    "left_taillight": "Sol stop lambası",
+    "right_taillight": "Sağ stop lambası",
+    "left_front_fender": "Sol ön çamurluk",
+    "right_front_fender": "Sağ ön çamurluk",
+    "left_front_door": "Sol ön kapı",
+    "right_front_door": "Sağ ön kapı",
+    "left_rear_door": "Sol arka kapı",
+    "right_rear_door": "Sağ arka kapı",
+    "left_rear_quarter": "Sol arka çamurluk",
+    "right_rear_quarter": "Sağ arka çamurluk",
+    "left_sill": "Sol marşpiyel",
+    "right_sill": "Sağ marşpiyel",
+    "left_mirror": "Sol ayna",
+    "right_mirror": "Sağ ayna",
+    "left_front_wheel": "Sol ön tekerlek",
+    "right_front_wheel": "Sağ ön tekerlek",
+    "left_rear_wheel": "Sol arka tekerlek",
+    "right_rear_wheel": "Sağ arka tekerlek",
+    "left_a_pillar": "Sol A direği",
+    "right_a_pillar": "Sağ A direği",
+    "left_windows": "Sol yan camlar",
+    "right_windows": "Sağ yan camlar",
+    "left_sliding_door": "Sol sürgülü kapı",
+    "right_sliding_door": "Sağ sürgülü kapı",
+    "left_side_panel": "Sol yan panel",
+    "right_side_panel": "Sağ yan panel",
+    "rear_doors": "Arka kapılar",
+    "bed": "Kasa",
+    "bed_side_left": "Sol kasa yanı",
+    "bed_side_right": "Sağ kasa yanı",
+    "tank": "Depo",
+    "seat": "Sele",
+    "front_fairing": "Ön grenaj",
+    "left_fairing": "Sol grenaj",
+    "right_fairing": "Sağ grenaj",
+    "front_fender": "Ön çamurluk",
+    "rear_fender": "Arka çamurluk",
+    "front_wheel": "Ön tekerlek",
+    "rear_wheel": "Arka tekerlek",
+    "exhaust": "Egzoz",
+    "handlebars": "Gidon",
+    "headlamp": "Far",
+    "tail": "Kuyruk",
+    "bow": "Pruva",
+    "stern": "Kıç",
+    "transom": "Ayna (kıç)",
+    "port_hull": "İskele gövde",
+    "starboard_hull": "Sancak gövde",
+    "deck": "Güverte",
+    "cabin": "Kabin",
+    "windscreen": "Ön cam",
+    "gunwale_port": "İskele küpeşte",
+    "gunwale_starboard": "Sancak küpeşte",
+    "keel": "Omurga",
+    "outboard": "Dıştan takma motor"
+  },
+  "print": {
+    "previous": "daha önce kaydedildi"
+  }
+}

+ 26 - 4
messages/tr/inspectionLibrary.json

@@ -49,6 +49,11 @@
       "name": "Boş şablon",
       "description": "Sıfırdan başlayın ve servisinizin gerçekten kullandığı kontrol listesini oluşturun.",
       "standardLabel": "Boş"
+    },
+    "vehicle-intake": {
+      "name": "Araç kabulü",
+      "description": "Teslimde müşteri yanınızdayken aracın etrafında tur: nasıl geldi, kasada zaten ne vardı, içeride ne kaldı. Müşterinin imzaladığı iş emrine yazdırılır.",
+      "standardLabel": "Atölye"
     }
   },
   "sections": {
@@ -89,7 +94,9 @@
     "no_s6": "Şasi ve şasiye bağlı parçalar",
     "no_s7": "Diğer donanımlar",
     "no_s8": "Çevresel rahatsızlık",
-    "no_s10": "Trafikkstasjon'a (trafik istasyonu) sevk"
+    "no_s10": "Trafikkstasjon'a (trafik istasyonu) sevk",
+    "arrival": "Geliş",
+    "exteriorWalkround": "Dış tur"
   },
   "checks": {
     "registrationPlates": "Tescil plakaları",
@@ -426,7 +433,16 @@
     "no_m_opacity_older_turbo": "Dizel: duman opaklığı, Euro 4'ten eski, turbo beslemeli",
     "no_8_2_3_1": "Egzoz genel ve motorun çalışması",
     "no_8_3_1": "Sıvı kaçakları",
-    "no_10_1": "Trafikkstasjon'a (trafik istasyonu) sevk"
+    "no_10_1": "Trafikkstasjon'a (trafik istasyonu) sevk",
+    "fuelLevel": "Yakıt seviyesi",
+    "keysHandedOver": "Teslim edilen anahtarlar",
+    "warningLightsOnArrival": "Gelişte yanan uyarı ışıkları",
+    "personalBelongings": "Araçta bırakılan kişisel eşyalar",
+    "glassCondition": "Camların durumu",
+    "wheelsAndTyresVisual": "Jant ve lastikler, görsel",
+    "seatsAndTrim": "Koltuklar ve döşeme",
+    "floorAndMats": "Taban ve paspaslar",
+    "infotainmentControls": "Multimedya ve kumandalar"
   },
   "descriptions": {
     "annexIItem0ConfirmsVehicle": "Ek I, madde 0. Muayeneye getirilen aracın belgeleriyle uyumlu olduğunu doğrular.",
@@ -555,12 +571,18 @@
     "no_m_opacity_older_turbo": "En fazla 3,0 m⁻¹. Üreticinin sınırı önceliklidir.",
     "no_8_2_3_1": "Motor çalışırken: OBD arıza göstergesinin arıza gösterip göstermediğini ve kendi kendine teşhisin doğru çalışıp çalışmadığını kontrol edin; herhangi bir sapma e) verir. Yol testinde ve rölantiden gaz verildiğinde görünür mavi, koyu gri veya siyah duman olup olmadığına bakın ve motorun nasıl çalıştığını kontrol edin.",
     "no_8_3_1": "Çevreye zarar verme veya diğer yol kullanıcıları için güvenlik riski oluşturma olasılığı bulunan (su dışındaki) her büyük sıvı kaçağı için, muayene sırasında yere damlaması koşuluyla, 2. derece.",
-    "no_10_1": "Aracın değerlendirme, yeni onay veya vognkort değişikliği için trafikkstasjon'a gitmesini gerektiren bir durum olduğunda verilir; örneğin kurallara aykırı motor veya ECU modifikasyonu, alçaltma/yükseltme, uygun olmayan ön koruma (bull bar), tescilli olandan fazla koltuk, başka yakıta dönüştürme veya onay gerektiren bir modifikasyon. Aynı konudaki ilgili kusura ek olarak verilir; tekrar muayenede yalnızca Statens vegvesen kaldırabilir."
+    "no_10_1": "Aracın değerlendirme, yeni onay veya vognkort değişikliği için trafikkstasjon'a gitmesini gerektiren bir durum olduğunda verilir; örneğin kurallara aykırı motor veya ECU modifikasyonu, alçaltma/yükseltme, uygun olmayan ön koruma (bull bar), tescilli olandan fazla koltuk, başka yakıta dönüştürme veya onay gerektiren bir modifikasyon. Aynı konudaki ilgili kusura ek olarak verilir; tekrar muayenede yalnızca Statens vegvesen kaldırabilir.",
+    "bodyConditionMap": "Her göçük, çizik veya taş izinin olduğu yerde çizime dokunun, not ve fotoğraf ekleyin. Önceki ziyaretlerin işaretleri gri görünür."
   },
   "choices": {
     "full": "Tam",
     "partial": "Kısmi",
-    "none": "Yok"
+    "none": "Yok",
+    "fuelEmpty": "Boş",
+    "fuelQuarter": "1/4",
+    "fuelHalf": "1/2",
+    "fuelThreeQuarters": "3/4",
+    "fuelFull": "Dolu"
   },
   "defects": {
     "registrationPlateMissing": "Tescil plakası yok",

+ 4 - 2
messages/tr/inspections.json

@@ -378,13 +378,15 @@
       "condition": "Kusur değerlendirmesi",
       "measurement": "Ölçüm",
       "text": "Serbest metin",
-      "choice": "Seçim"
+      "choice": "Seçim",
+      "condition_map": "Durum haritası"
     },
     "inputHint": {
       "condition": "Teknisyen maddeyi kusur ölçeğine göre değerlendirir.",
       "measurement": "Birimli bir sayı kaydeder, bir aralığa göre otomatik değerlendirilir.",
       "text": "Yazılı bir gözlem kaydeder, örneğin bir lastik markası veya arıza kodu.",
-      "choice": "Teknisyen sabit bir listeden bir yanıt seçer."
+      "choice": "Teknisyen sabit bir listeden bir yanıt seçer.",
+      "condition_map": "Göçük, çizik ve taş izlerinin her biri not ve fotoğrafla işaretlendiği bir araç çizimi."
     },
     "cancel": "İptal",
     "create": "Kontrol listesi oluştur",

+ 7 - 1
messages/tr/pdf.json

@@ -208,7 +208,13 @@
     "poweredBy": "Destekleyen",
     "signature": "İmza",
     "signedBy": "İmzalayan",
-    "signatureDate": "Tarih"
+    "signatureDate": "Tarih",
+    "conditionMapTitle": "Araç durumu",
+    "conditionMapNo": "No",
+    "conditionMapArea": "Bölge",
+    "conditionMapKind": "Tür",
+    "conditionMapSeverity": "Önem",
+    "conditionMapNote": "Not"
   },
   "workOrder": {
     "title": "İŞ EMRİ",

+ 23 - 3
messages/tr/settings.json

@@ -713,7 +713,8 @@
       "job_qr": "Telefonda aç",
       "concerns": "Müşteri şikayetleri",
       "job_description": "İstenen iş",
-      "work_checklist": "İş kontrol listesi"
+      "work_checklist": "İş kontrol listesi",
+      "condition_map": "Araç durumu"
     },
     "fields": {
       "customer_name": "Müşteri adı",
@@ -770,7 +771,9 @@
       "scheduled": "Planlanan",
       "promised": "Söz verilen",
       "work_bay": "Çalışma alanı",
-      "customer_line": "Müşteri imza satırı"
+      "customer_line": "Müşteri imza satırı",
+      "legend": "Açıklama",
+      "previous_marks": "Önceki işaretler"
     },
     "presetsTitle": "Bir düzenle başlayın",
     "presetsHint": "Aşağıdaki düzeni değiştirir. Siz kaydetmeden hiçbir şey kaydedilmez.",
@@ -2191,7 +2194,24 @@
       "concernNoiseDetail": "Ön balatalar ve diskler aşınmış; değiştirildi",
       "concernWarningLight": "Geçen haftadan beri göstergede uyarı ışığı",
       "workRequested": "Ön fren balatalarını ve disklerini değiştir, servis göstergesini sıfırla ve yol testi yap.",
-      "checklistTestDrive": "Yol testi"
+      "checklistTestDrive": "Yol testi",
+      "markScratch": "Kapı boyunca anahtar çiziği, 20 cm",
+      "markDent": "Kolun altında göçük, boya sağlam",
+      "markChip": "Tamponun altında taş izleri",
+      "viewTop": "Üstten",
+      "viewLeft": "Sol taraf",
+      "viewRight": "Sağ taraf",
+      "viewFront": "Ön",
+      "viewRear": "Arka",
+      "panelLeftRearDoor": "Sol arka kapı",
+      "panelLeftFrontDoor": "Sol ön kapı",
+      "panelFrontBumper": "Ön tampon",
+      "kindScratch": "Çizik",
+      "kindDent": "Göçük",
+      "kindChip": "Taş izi",
+      "severityMinor": "Hafif",
+      "severityMajor": "Ağır",
+      "markPrevious": "daha önce kaydedildi"
     },
     "logoUpload": "Logo yükle",
     "logoReplace": "Logoyu değiştir",

+ 65 - 0
prisma/migrations/20260925180000_condition_marks/migration.sql

@@ -0,0 +1,65 @@
+-- AlterTable
+ALTER TABLE "vehicles" ADD COLUMN     "bodyType" TEXT;
+
+-- CreateTable
+CREATE TABLE "condition_marks" (
+    "id" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    "inspectionId" TEXT,
+    "inspectionItemId" TEXT,
+    "serviceRecordId" TEXT,
+    "bodyType" TEXT NOT NULL,
+    "view" TEXT NOT NULL,
+    "panel" TEXT NOT NULL,
+    "x" DOUBLE PRECISION NOT NULL,
+    "y" DOUBLE PRECISION NOT NULL,
+    "kind" TEXT NOT NULL,
+    "severity" TEXT NOT NULL DEFAULT 'minor',
+    "note" TEXT,
+    "imageUrls" TEXT[] DEFAULT ARRAY[]::TEXT[],
+    "recordedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "recordedById" TEXT,
+    "resolvedAt" TIMESTAMP(3),
+    "resolvedById" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+
+    CONSTRAINT "condition_marks_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "condition_marks_vehicleId_idx" ON "condition_marks"("vehicleId");
+
+-- CreateIndex
+CREATE INDEX "condition_marks_inspectionId_idx" ON "condition_marks"("inspectionId");
+
+-- CreateIndex
+CREATE INDEX "condition_marks_inspectionItemId_idx" ON "condition_marks"("inspectionItemId");
+
+-- CreateIndex
+CREATE INDEX "condition_marks_serviceRecordId_idx" ON "condition_marks"("serviceRecordId");
+
+-- CreateIndex
+CREATE INDEX "condition_marks_organizationId_idx" ON "condition_marks"("organizationId");
+
+-- AddForeignKey
+ALTER TABLE "condition_marks" ADD CONSTRAINT "condition_marks_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "condition_marks" ADD CONSTRAINT "condition_marks_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "condition_marks" ADD CONSTRAINT "condition_marks_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "condition_marks" ADD CONSTRAINT "condition_marks_inspectionItemId_fkey" FOREIGN KEY ("inspectionItemId") REFERENCES "inspection_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "condition_marks" ADD CONSTRAINT "condition_marks_serviceRecordId_fkey" FOREIGN KEY ("serviceRecordId") REFERENCES "service_records"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "condition_marks" ADD CONSTRAINT "condition_marks_recordedById_fkey" FOREIGN KEY ("recordedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "condition_marks" ADD CONSTRAINT "condition_marks_resolvedById_fkey" FOREIGN KEY ("resolvedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 10 - 8
prisma/schema/auth.prisma

@@ -17,13 +17,13 @@ model User {
   createdAt     DateTime @default(now())
   updatedAt     DateTime @updatedAt
 
-  twoFactorEnabled Boolean   @default(false)
-  termsAcceptedAt  DateTime?
-  lastLogin        DateTime?
-  lastSeen         DateTime?
+  twoFactorEnabled    Boolean   @default(false)
+  termsAcceptedAt     DateTime?
+  lastLogin           DateTime?
+  lastSeen            DateTime?
   /// App version this user last saw/dismissed the update banner for.
   /// Null = never seeded (seeded silently on first load, no banner).
-  lastSeenVersion  String?
+  lastSeenVersion     String?
   /// The release the update banner is up for, and when it first appeared to
   /// this user on any device. The hour it stays up runs from here, so a
   /// second device does not restart them and a closed tab cannot stop them.
@@ -31,7 +31,7 @@ model User {
   updateBannerShownAt DateTime?
   /// Per-user dashboard grid layout (card positions/sizes/hidden set).
   /// Null = default layout. Shape validated in dashboardLayoutActions.
-  dashboardLayout  Json?
+  dashboardLayout     Json?
 
   sessions               Session[]
   devices                UserDevice[]
@@ -49,8 +49,10 @@ model User {
   organizationMembers    OrganizationMember[]
   dashboardWidgets       DashboardWidget[]
   auditLogs              AuditLog[]
-  openedServiceRecords   ServiceRecord[]        @relation("ServiceRecordCreatedBy")
-  confirmedConcerns      ServiceConcern[]       @relation("ConcernConfirmedBy")
+  openedServiceRecords   ServiceRecord[]         @relation("ServiceRecordCreatedBy")
+  confirmedConcerns      ServiceConcern[]        @relation("ConcernConfirmedBy")
+  recordedConditionMarks ConditionMark[]         @relation("ConditionMarkRecordedBy")
+  resolvedConditionMarks ConditionMark[]         @relation("ConditionMarkResolvedBy")
   technicians            Technician[]
   stockMovements         StockMovement[]
   tireWarehouses         TireWarehouse[]

+ 61 - 0
prisma/schema/condition-marks.prisma

@@ -0,0 +1,61 @@
+/// A mark on the vehicle's condition map: a dent, a scratch, a chip, drawn
+/// on a line drawing of the car by tapping where it is.
+///
+/// Marks belong to the vehicle, not to the sheet they were drawn on. An
+/// inspection or a work order records new ones; the next visit starts with
+/// the ones still there, greyed, and either confirms them or clears them as
+/// repaired. That is what makes "new since last time" a fact rather than an
+/// argument. The sheet a mark was drawn on is kept for the print, and lets
+/// go of the mark rather than taking it with it when the sheet is deleted.
+model ConditionMark {
+  id             String       @id @default(cuid())
+  organizationId String
+  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+
+  vehicleId String
+  vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
+
+  /// Where it was recorded: the inspection check or the work order's drop-off.
+  inspectionId     String?
+  inspection       Inspection?     @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
+  inspectionItemId String?
+  inspectionItem   InspectionItem? @relation(fields: [inspectionItemId], references: [id], onDelete: SetNull)
+  serviceRecordId  String?
+  serviceRecord    ServiceRecord?  @relation(fields: [serviceRecordId], references: [id], onDelete: SetNull)
+
+  /// Which drawing it sits on, so a body type changed later cannot move it.
+  bodyType  String
+  /// top | left | right | front | rear
+  view      String
+  /// The named panel the tap landed on; see condition-map/Lib/drawingTypes.ts.
+  panel     String
+  /// Position as a fraction of the view's box, 0..1 from the top-left.
+  x         Float
+  y         Float
+  /// dent | scratch | chip | crack | rust | paint | previous_repair | missing
+  kind      String
+  /// minor | major
+  severity  String   @default("minor")
+  note      String?
+  imageUrls String[] @default([])
+
+  recordedAt   DateTime @default(now())
+  recordedById String?
+  recordedBy   User?    @relation("ConditionMarkRecordedBy", fields: [recordedById], references: [id], onDelete: SetNull)
+
+  /// Set when a later visit found the damage gone. The mark stays as history
+  /// and stops printing as current.
+  resolvedAt   DateTime?
+  resolvedById String?
+  resolvedBy   User?     @relation("ConditionMarkResolvedBy", fields: [resolvedById], references: [id], onDelete: SetNull)
+
+  createdAt DateTime @default(now())
+  updatedAt DateTime @updatedAt
+
+  @@index([vehicleId])
+  @@index([inspectionId])
+  @@index([inspectionItemId])
+  @@index([serviceRecordId])
+  @@index([organizationId])
+  @@map("condition_marks")
+}

+ 21 - 18
prisma/schema/inspections.prisma

@@ -69,15 +69,15 @@ model InspectionTemplateItem {
   maxValue  Float?
   choices   String[] @default([])
 
-  required          Boolean  @default(false)
-  photoRequired     Boolean  @default(false)
+  required           Boolean  @default(false)
+  photoRequired      Boolean  @default(false)
   /// The check may be graded "not applicable": the vehicle has no such part.
-  allowNotApplicable Boolean @default(true)
+  allowNotApplicable Boolean  @default(true)
   // Defect severity applied when a measurement falls outside min/max.
-  defaultSeverity   String?
+  defaultSeverity    String?
   // Phrases this workshop wants offered when a defect is recorded against the
   // check, on top of the built-in catalogue. See Lib/defectCatalogue.ts.
-  defectSuggestions String[] @default([])
+  defectSuggestions  String[] @default([])
 
   sectionId String
   section   InspectionTemplateSection @relation(fields: [sectionId], references: [id], onDelete: Cascade)
@@ -149,6 +149,7 @@ model Inspection {
   quoteRequests  InspectionQuoteRequest[]
   quotes         Quote[]
   serviceRecords ServiceRecord[]
+  conditionMarks ConditionMark[]
 
   importBatchId String?
   importBatch   ImportBatch? @relation(fields: [importBatchId], references: [id], onDelete: SetNull)
@@ -198,20 +199,20 @@ model InspectionItem {
 
   // Copied from the template at creation so an inspection stays readable even
   // after its template is edited or deleted.
-  description       String?
-  code              String?
-  sectionCode       String?
-  inputType         String   @default("condition")
-  unit              String?
-  minValue          Float?
-  maxValue          Float?
-  choices           String[] @default([])
-  required          Boolean  @default(false)
-  photoRequired     Boolean  @default(false)
+  description        String?
+  code               String?
+  sectionCode        String?
+  inputType          String   @default("condition")
+  unit               String?
+  minValue           Float?
+  maxValue           Float?
+  choices            String[] @default([])
+  required           Boolean  @default(false)
+  photoRequired      Boolean  @default(false)
   /// The check may be graded "not applicable": the vehicle has no such part.
-  allowNotApplicable Boolean @default(true)
-  defaultSeverity   String?
-  defectSuggestions String[] @default([])
+  allowNotApplicable Boolean  @default(true)
+  defaultSeverity    String?
+  defectSuggestions  String[] @default([])
 
   measuredValue Float?
   textValue     String?
@@ -219,6 +220,8 @@ model InspectionItem {
   inspectionId String
   inspection   Inspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
 
+  conditionMarks ConditionMark[]
+
   @@index([inspectionId])
   @@map("inspection_items")
 }

+ 1 - 0
prisma/schema/organization.prisma

@@ -53,6 +53,7 @@ model Organization {
   subscription                Subscription?
   invitations                 TeamInvitation[]
   inspectionTemplates         InspectionTemplate[]
+  conditionMarks              ConditionMark[]
   smsMessages                 SmsMessage[]
   telegramMessages            TelegramMessage[]
   whatsappMessages            WhatsappMessage[]

+ 1 - 0
prisma/schema/service-records.prisma

@@ -163,6 +163,7 @@ model ServiceRecord {
   timeEntries    TimeEntry[]
 
   concerns           ServiceConcern[]
+  conditionMarks     ConditionMark[]
   discoveredFindings VehicleFinding[] @relation("FindingDiscoveredIn")
   resolvedFindings   VehicleFinding[] @relation("FindingResolvedIn")
 

+ 5 - 0
prisma/schema/vehicles.prisma

@@ -14,6 +14,10 @@ model Vehicle {
   purchaseDate           DateTime?
   purchasePrice          Float?
   imageUrl               String?
+  /// sedan | hatchback | estate | suv | van | pickup | motorcycle | boat: which
+  /// line drawing the condition map is marked on. Null until somebody picks
+  /// one, and the map then guesses from the workshop's trade.
+  bodyType               String?
   isArchived             Boolean              @default(false)
   archiveReason          String?
   /// Set when the customer said in the portal that the vehicle is no longer theirs.
@@ -48,6 +52,7 @@ model Vehicle {
 
   inspectionStatus    VehicleInspectionStatus?
   inspectionReminders InspectionReminderSend[]
+  conditionMarks      ConditionMark[]
 
   importBatchId String?
   importBatch   ImportBatch? @relation(fields: [importBatchId], references: [id], onDelete: SetNull)

+ 94 - 0
src/__tests__/features/condition-map/marks.test.ts

@@ -0,0 +1,94 @@
+/**
+ * Whose marks are whose: a sheet draws its own in colour and the vehicle's
+ * earlier ones in grey, never a resolved one, and a body type is guessed
+ * from what the registry or the trade says.
+ */
+import { describe, expect, it } from 'vitest'
+import {
+  bodyTypeFor,
+  guessBodyType,
+  markInputSchema,
+  numberedMarks,
+  splitMarks,
+  type ConditionMarkData,
+} from '@/features/condition-map/Lib/marks'
+import { mirrorPanel } from '@/features/condition-map/Lib/drawingTypes'
+
+const mark = (over: Partial<ConditionMarkData>): ConditionMarkData => ({
+  id: 'm',
+  vehicleId: 'v',
+  inspectionId: null,
+  inspectionItemId: null,
+  serviceRecordId: null,
+  bodyType: 'sedan',
+  view: 'left',
+  panel: 'left_front_door',
+  x: 0.5,
+  y: 0.5,
+  kind: 'dent',
+  severity: 'minor',
+  note: null,
+  imageUrls: [],
+  recordedAt: '2026-09-01T00:00:00Z',
+  resolvedAt: null,
+  ...over,
+})
+
+describe('condition marks', () => {
+  it("splits a sheet's own marks from the vehicle's earlier ones and drops resolved ones", () => {
+    const marks = [
+      mark({ id: 'a', inspectionId: 'i1', inspectionItemId: 'c1' }),
+      mark({ id: 'b', inspectionId: 'i0', inspectionItemId: 'c0' }),
+      mark({ id: 'c', serviceRecordId: 's1', resolvedAt: '2026-09-02T00:00:00Z' }),
+      mark({ id: 'd', serviceRecordId: 's1' }),
+    ]
+    const onInspection = splitMarks(marks, { inspectionId: 'i1', inspectionItemId: 'c1' })
+    expect(onInspection.own.map((m) => m.id)).toEqual(['a'])
+    expect(onInspection.previous.map((m) => m.id)).toEqual(['b', 'd'])
+    const onJob = splitMarks(marks, { serviceRecordId: 's1' })
+    expect(onJob.own.map((m) => m.id)).toEqual(['d'])
+    expect(onJob.previous.map((m) => m.id)).toEqual(['a', 'b'])
+  })
+
+  it('numbers marks by when they were recorded', () => {
+    const ordered = numberedMarks([
+      mark({ id: 'late', recordedAt: '2026-09-03T00:00:00Z' }),
+      mark({ id: 'early', recordedAt: '2026-09-01T00:00:00Z' }),
+    ])
+    expect(ordered.map((m) => m.id)).toEqual(['early', 'late'])
+  })
+
+  it("guesses the drawing from the registry's word for the body, and the trade", () => {
+    expect(guessBodyType('Stasjonsvogn')).toBe('estate')
+    expect(guessBodyType('Pickup truck')).toBe('pickup')
+    expect(guessBodyType('Kombilimousine')).toBe('hatchback')
+    expect(guessBodyType('unknown thing')).toBeNull()
+    expect(bodyTypeFor({ bodyType: 'van' })).toBe('van')
+    expect(bodyTypeFor({ bodyType: null }, { serviceType: 'marine' })).toBe('boat')
+    expect(bodyTypeFor({ bodyType: 'nonsense' }, { registryBody: 'SUV' })).toBe('suv')
+    expect(bodyTypeFor({ bodyType: null })).toBe('sedan')
+  })
+
+  it('mirrors side panels and leaves shared ones alone', () => {
+    expect(mirrorPanel('left_front_door')).toBe('right_front_door')
+    expect(mirrorPanel('right_mirror')).toBe('left_mirror')
+    expect(mirrorPanel('port_hull')).toBe('starboard_hull')
+    expect(mirrorPanel('hood')).toBe('hood')
+  })
+
+  it('refuses a mark off the drawing or of an unknown kind', () => {
+    const base = {
+      vehicleId: 'v',
+      bodyType: 'sedan',
+      view: 'left',
+      panel: 'left_sill',
+      x: 0.2,
+      y: 0.9,
+      kind: 'scratch',
+    }
+    expect(markInputSchema.safeParse(base).success).toBe(true)
+    expect(markInputSchema.safeParse({ ...base, x: 1.2 }).success).toBe(false)
+    expect(markInputSchema.safeParse({ ...base, kind: 'smudge' }).success).toBe(false)
+    expect(markInputSchema.safeParse({ ...base, panel: 'wing_mirror' }).success).toBe(false)
+  })
+})

+ 43 - 0
src/__tests__/features/condition-map/path-transform.test.ts

@@ -0,0 +1,43 @@
+/**
+ * Path data through the map's transforms: a mirror flips arcs the right
+ * way, a quarter turn keeps a circle a circle, and relative commands come
+ * out absolute so the renderer never has to think about them.
+ */
+import { describe, expect, it } from 'vitest'
+import {
+  applyPoint,
+  compose,
+  invert,
+  mirrorX,
+  rotate90,
+  scale,
+  transformPath,
+  translate,
+} from '@/features/condition-map/Lib/pathTransform'
+
+describe('path transforms', () => {
+  it('mirrors points across the axis and flips an arc sweep', () => {
+    const m = mirrorX(500)
+    expect(applyPoint(m, 100, 40)).toEqual([900, 40])
+    expect(transformPath('M100 40A10 10 0 0 1 120 40', m)).toBe('M900 40A10 10 0 0 0 880 40')
+  })
+
+  it('turns a quarter clockwise and scales arc radii with the drawing', () => {
+    const m = compose(scale(2), rotate90())
+    expect(applyPoint(m, 10, 0)).toEqual([0, 20])
+    expect(transformPath('M0 0A5 5 0 0 1 10 0', m)).toBe('M0 0A10 10 90 0 1 0 20')
+  })
+
+  it('makes relative commands absolute and turns H and V into lines', () => {
+    expect(transformPath('m10 10h20v5l-5 5z', translate(1, 1))).toBe('M11 11L31 11L31 16L26 21Z')
+    expect(transformPath('M0 0c1 2 3 4 5 6', translate(0, 0))).toBe('M0 0C1 2 3 4 5 6')
+  })
+
+  it('inverts what it composes', () => {
+    const m = compose(translate(300, -20), compose(scale(0.5), mirrorX(500)))
+    const [x, y] = applyPoint(m, 123, 456)
+    const [bx, by] = applyPoint(invert(m), x, y)
+    expect(bx).toBeCloseTo(123)
+    expect(by).toBeCloseTo(456)
+  })
+})

+ 268 - 0
src/__tests__/features/condition-map/print.test.ts

@@ -0,0 +1,268 @@
+/**
+ * The condition map as the documents draw it: every body has its five
+ * views on the sheet, a mark lands on the panel it was tapped on and comes
+ * back to the same spot, the legend numbers this sheet's marks after the
+ * earlier ones, and a car with no marks draws nothing.
+ */
+import { describe, expect, it } from 'vitest'
+import { BODY_DRAWINGS, getBodyDrawing } from '@/features/condition-map/Drawings'
+import {
+  composeViews,
+  locateOnSheet,
+  markPosition,
+  placedPanels,
+} from '@/features/condition-map/Lib/compose'
+import { BODY_TYPES, PANELS, VIEWS, mirrorPanel } from '@/features/condition-map/Lib/drawingTypes'
+import { conditionMapForPrint } from '@/features/condition-map/Lib/print'
+import type { ConditionMarkData } from '@/features/condition-map/Lib/marks'
+import { buildCertificatePrintSpec } from '@/features/inspections/Pdf/buildCertificatePrint'
+import { buildWorkOrderPrintSpec } from '@/features/invoice-designer/Pdf/buildWorkOrderPrint'
+import { getDefaultLayout } from '@/features/settings/Schema/invoiceLayoutSchema'
+import type { InvoiceData } from '@/features/vehicles/Components/invoice-pdf/types'
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+const PATH = /^M[\d.\s,MLCQAZHV-]+$/i
+
+describe('the body drawings', () => {
+  it.each(BODY_TYPES)('%s has its four drawn views with known, unrepeated panels', (body) => {
+    const drawing = getBodyDrawing(body)
+    expect(BODY_DRAWINGS[body]).toBe(drawing)
+    for (const view of ['top', 'left', 'front', 'rear'] as const) {
+      const drawn = drawing.views[view]
+      expect(drawn.panels.length).toBeGreaterThan(0)
+      const ids = drawn.panels.map((p) => p.id)
+      expect(new Set(ids).size).toBe(ids.length)
+      for (const panel of drawn.panels) {
+        expect(PANELS).toContain(panel.id)
+        expect(panel.d.trim()).toMatch(PATH)
+      }
+      for (const line of drawn.lines) expect(line.trim()).toMatch(PATH)
+    }
+  })
+
+  it('places every view on the sheet, the right side mirrored from the left', () => {
+    const composition = composeViews()
+    expect(composition.views.map((v) => v.view).sort()).toEqual([...VIEWS].sort())
+    const placed = placedPanels(getBodyDrawing('sedan'), composition)
+    const left = placed.filter((p) => p.view === 'left').map((p) => p.panel)
+    const right = placed.filter((p) => p.view === 'right').map((p) => p.panel)
+    expect(right.sort()).toEqual(left.map(mirrorPanel).sort())
+  })
+
+  it('takes a mark to the sheet and back to the same place in its view', () => {
+    const composition = composeViews()
+    for (const view of VIEWS) {
+      const at = markPosition(composition, { view, x: 0.37, y: 0.61 })
+      expect(at).not.toBeNull()
+      const back = locateOnSheet(composition, at![0], at![1])
+      expect(back?.view).toBe(view)
+      expect(back?.x).toBeCloseTo(0.37, 3)
+      expect(back?.y).toBeCloseTo(0.61, 3)
+    }
+  })
+})
+
+const mark = (over: Partial<ConditionMarkData>): ConditionMarkData => ({
+  id: 'm',
+  vehicleId: 'v',
+  inspectionId: null,
+  inspectionItemId: null,
+  serviceRecordId: null,
+  bodyType: 'sedan',
+  view: 'left',
+  panel: 'left_front_door',
+  x: 0.5,
+  y: 0.5,
+  kind: 'dent',
+  severity: 'minor',
+  note: null,
+  imageUrls: [],
+  recordedAt: '2026-09-01T00:00:00Z',
+  resolvedAt: null,
+  ...over,
+})
+
+const labels = {
+  views: { top: 'Top', left: 'Left', right: 'Right', front: 'Front', rear: 'Rear' },
+  panels: { left_front_door: 'Left front door', rear_bumper: 'Rear bumper' },
+  kinds: { dent: 'Dent', scratch: 'Scratch' },
+  severities: { minor: 'Minor', major: 'Major' },
+  previous: 'earlier',
+}
+
+describe('the printed condition map', () => {
+  it('draws nothing for a car with no open marks', () => {
+    expect(
+      conditionMapForPrint({
+        bodyType: 'sedan',
+        marks: [],
+        includePrevious: true,
+        labels,
+        width: 500,
+      })
+    ).toBeNull()
+    const resolved = mark({ resolvedAt: '2026-09-02T00:00:00Z' })
+    expect(
+      conditionMapForPrint({
+        bodyType: 'sedan',
+        marks: [resolved],
+        includePrevious: true,
+        labels,
+        width: 500,
+      })
+    ).toBeNull()
+  })
+
+  it('numbers earlier marks first, greys them, and can leave them out', () => {
+    const marks = [
+      mark({ id: 'new', inspectionItemId: 'c1', recordedAt: '2026-09-05T00:00:00Z' }),
+      mark({
+        id: 'old',
+        serviceRecordId: 's0',
+        panel: 'rear_bumper',
+        view: 'rear',
+        kind: 'scratch',
+        recordedAt: '2026-01-01T00:00:00Z',
+      }),
+    ]
+    const withOld = conditionMapForPrint({
+      bodyType: 'sedan',
+      marks,
+      scope: { inspectionId: 'i', inspectionItemId: 'c1' },
+      includePrevious: true,
+      labels,
+      width: 500,
+    })!
+    expect(withOld.rows.map((r) => [r.n, r.area, r.kind, r.previous])).toEqual([
+      ['1', 'Rear bumper (earlier)', 'Scratch', true],
+      ['2', 'Left front door', 'Dent', false],
+    ])
+    expect(withOld.ownCount).toBe(1)
+    expect(withOld.height).toBeGreaterThan(0)
+    expect(withOld.shapes.some((s) => s.type === 'text' && s.text === '2')).toBe(true)
+    const without = conditionMapForPrint({
+      bodyType: 'sedan',
+      marks,
+      scope: { inspectionId: 'i', inspectionItemId: 'c1' },
+      includePrevious: false,
+      labels,
+      width: 500,
+    })!
+    expect(without.rows.map((r) => r.n)).toEqual(['1'])
+  })
+})
+
+function ids(spec: any): string[] {
+  return spec.blocks.map((b: any) => b.id)
+}
+
+describe('the documents', () => {
+  const certificate = (over: Record<string, unknown> = {}) =>
+    buildCertificatePrintSpec({
+      data: {
+        id: 'insp1',
+        status: 'completed',
+        mileage: 1,
+        notes: null,
+        createdAt: new Date('2026-09-24T09:00:00Z'),
+        completedAt: new Date('2026-09-24T10:00:00Z'),
+        severityScale: 'basic',
+        country: null,
+        vehicleCategory: null,
+        nextTestDue: null,
+        certificateNumber: null,
+        inspectorName: null,
+        testLocation: null,
+        template: { name: 'Intake', severityScale: 'basic', country: null },
+        vehicle: {
+          make: 'Volvo',
+          model: 'V60',
+          year: 2020,
+          vin: null,
+          licensePlate: 'AB 1',
+          mileage: 1,
+          customer: null,
+        },
+        items: [
+          {
+            id: 'map',
+            name: 'Body condition',
+            section: 'Exterior',
+            condition: 'pass',
+            notes: null,
+            sortOrder: 0,
+            inputType: 'condition_map',
+          },
+        ],
+      } as any,
+      layoutConfig: getDefaultLayout('certificate'),
+      conditionMarks: [mark({ inspectionId: 'insp1', inspectionItemId: 'map' })],
+      bodyType: 'sedan',
+      conditionMapLabels: labels,
+      ...over,
+    }) as any
+
+  it('puts the condition map on the certificate, with its legend', () => {
+    const spec = certificate()
+    expect(ids(spec)).toContain('condition_map')
+    const block = spec.blocks.find((b: any) => b.id === 'condition_map').content
+    expect(block.children.some((c: any) => c.kind === 'drawing')).toBe(true)
+    expect(block.children.some((c: any) => c.kind === 'table')).toBe(true)
+    expect(ids(certificate({ conditionMarks: [] }))).not.toContain('condition_map')
+  })
+
+  it("puts every open mark on the work order, this job's in colour", () => {
+    const spec = buildWorkOrderPrintSpec({
+      data: {
+        id: 'job1',
+        title: 'Job',
+        type: 'repair',
+        serviceDate: new Date('2026-09-24T09:00:00Z'),
+        subtotal: 0,
+        taxRate: 0,
+        taxAmount: 0,
+        totalAmount: 0,
+        cost: 0,
+        invoiceNumber: '1',
+        partItems: [],
+        laborItems: [],
+        customer: { name: 'A' },
+        vehicle: {
+          make: 'Volvo',
+          model: 'V60',
+          year: 2020,
+          vin: null,
+          licensePlate: 'AB 1',
+          mileage: 1,
+          customer: null,
+        },
+      } as unknown as InvoiceData,
+      job: {
+        orderNumber: '1',
+        statusLabel: 'Open',
+        concerns: [],
+        printedAt: new Date('2026-09-25T00:00:00Z'),
+        conditionMarks: [
+          mark({ id: 'own', serviceRecordId: 'job1' }),
+          mark({
+            id: 'earlier',
+            inspectionId: 'x',
+            inspectionItemId: 'y',
+            panel: 'rear_bumper',
+            view: 'rear',
+          }),
+        ],
+        bodyType: 'sedan',
+        conditionMapLabels: labels,
+      },
+      labels: {},
+    }) as any
+    expect(ids(spec)).toContain('condition_map')
+    const table = spec.blocks
+      .find((b: any) => b.id === 'condition_map')
+      .content.children.find((c: any) => c.kind === 'table')
+    // Recorded at the same moment, the earlier visit's mark is listed first, as such.
+    expect(table.rows.map((r: any) => r.area)).toEqual(['Rear bumper (earlier)', 'Left front door'])
+  })
+})

+ 2 - 1
src/__tests__/features/inspections/library-localization.test.ts

@@ -124,7 +124,8 @@ describe('converting an installed checklist', () => {
     const plan = planRelocalization(template, NB, index)
     expect(plan.template?.name).toBeUndefined()
     expect(plan.sections.find((s) => s.id === 's0')?.data.name).toBeUndefined()
-    expect(plan.items.find((i) => i.id === 's0i0')).toBeUndefined()
+    // The name the workshop wrote stays; the guidance it never touched may still move.
+    expect(plan.items.find((i) => i.id === 's0i0')?.data.name).toBeUndefined()
     // The rest of the checklist still moves.
     expect(plan.items.find((i) => i.id === 's0i1')?.data.name).toBe(NB.checks.paint)
   })

+ 1 - 0
src/__tests__/features/invoice-designer/certificate-blocks.test.tsx

@@ -83,6 +83,7 @@ describe('the sections a default certificate prints', () => {
       'test_details',
       'defects',
       'results_table',
+      'condition_map',
       'inspection_photos',
       'notes',
       'attached_documents',

+ 1 - 0
src/__tests__/lib/backup-file-urls.test.ts

@@ -155,6 +155,7 @@ describe('the import route rewrites every restored file column', () => {
     'InspectionItem.imageUrls': /imageUrls: \(\(item\.imageUrls[\s\S]{0,200}?rewriteFileUrl/,
     'InspectionAttachment.fileUrl': /rewriteFileUrl\(file\.fileUrl as string/,
     'VehicleFinding.imageUrls': /imageUrls: \(\(finding\.imageUrls[\s\S]{0,200}?rewriteFileUrl/,
+    'ConditionMark.imageUrls': /imageUrls: \(\(mark\.imageUrls[\s\S]{0,200}?rewriteFileUrl/,
     'StatusReport.videoUrl': /statusReport[\s\S]{0,400}?fields: \['videoUrl'\]/,
     'WhatsappMessage.mediaUrl': /whatsappMessage[\s\S]{0,400}?fields: \['mediaUrl'\]/,
     'AppSetting.value': /const value = \(rewriteFileUrl\(s\.value/,

+ 2 - 0
src/__tests__/lib/files/collect.test.ts

@@ -107,6 +107,7 @@ describe('files that go with a delete', () => {
         serviceRecords: [{ id: 'sr-1' }],
         inspections: [{ id: 'in-1' }],
         findings: [{ imageUrls: ['services/f.jpg'] }],
+        conditionMarks: [{ imageUrls: ['services/dent.jpg'] }],
       },
     ] as never)
 
@@ -115,6 +116,7 @@ describe('files that go with a delete', () => {
     expect(urls).toEqual([
       'vehicles/car.jpg',
       'services/f.jpg',
+      'services/dent.jpg',
       'services/a.jpg',
       'tire-hotel/shared.jpg',
       'services/v.mp4',

+ 14 - 7
src/app/(authenticated)/inspections/[id]/page.tsx

@@ -12,6 +12,7 @@ import {
 } from '@/features/inspections/Components/InspectionPageClient'
 import { PageHeader } from '@/components/page-header'
 import { getAuthContext } from '@/lib/get-auth-context'
+import { loadVehicleConditionMarks } from '@/features/condition-map/Actions/conditionMarkActions'
 import { getFeatures } from '@/lib/features'
 import { redirect } from 'next/navigation'
 
@@ -28,13 +29,18 @@ export default async function InspectionDetailPage({
     redirect('/inspections')
   }
 
-  const [features, defectHistory, technicians, settings, statusReports] = await Promise.all([
-    authContext?.organizationId ? getFeatures(authContext.organizationId) : null,
-    getCommonDefectNotes(id),
-    getInspectionTechnicians(),
-    getDisplaySettings([SETTING_KEYS.WORKSHOP_ADDRESS]),
-    getStatusReportsForInspection(id),
-  ])
+  const hasConditionMap = result.data.items.some((item) => item.inputType === 'condition_map')
+  const [features, defectHistory, technicians, settings, statusReports, conditionMarks] =
+    await Promise.all([
+      authContext?.organizationId ? getFeatures(authContext.organizationId) : null,
+      getCommonDefectNotes(id),
+      getInspectionTechnicians(),
+      getDisplaySettings([SETTING_KEYS.WORKSHOP_ADDRESS]),
+      getStatusReportsForInspection(id),
+      hasConditionMap && authContext?.organizationId
+        ? loadVehicleConditionMarks(authContext.organizationId, result.data.vehicleId)
+        : [],
+    ])
 
   return (
     <>
@@ -59,6 +65,7 @@ export default async function InspectionDetailPage({
           }
           defectHistory={defectHistory.success ? defectHistory.data : {}}
           technicians={technicians.success ? technicians.data : []}
+          conditionMarks={conditionMarks}
           workshopAddress={
             (settings.success && settings.data?.[SETTING_KEYS.WORKSHOP_ADDRESS]) || ''
           }

+ 7 - 0
src/app/(public)/share/inspection/[orgId]/[token]/page.tsx

@@ -12,6 +12,8 @@ import { documentLogoPath } from '@/features/invoice-designer/Lib/documentLogo'
 import { templateConfigFromSource } from '@/features/invoice-designer/Lib/designSource'
 import type { DocumentSpec } from '@/features/invoice-designer/Spec/documentSpec'
 import { buildCertificatePrintSpec } from '@/features/inspections/Pdf/buildCertificatePrint'
+import { loadVehicleConditionMarks } from '@/features/condition-map/Actions/conditionMarkActions'
+import { loadConditionMapLabels } from '@/features/condition-map/Lib/labels'
 import { certificateSignatureDataUri } from '@/features/signatures/Lib/memberSignature.server'
 import {
   certificateDesignSource,
@@ -41,6 +43,7 @@ export default async function PublicInspectionPage({
     include: {
       vehicle: {
         select: {
+          bodyType: true,
           make: true,
           model: true,
           year: true,
@@ -161,8 +164,12 @@ export default async function PublicInspectionPage({
       const photos = item.imageUrls.filter((url) => !isVideo(url)).map((url) => ({ dataUri: url }))
       if (photos.length > 0) itemPhotos[item.id] = photos
     }
+    const hasMap = inspection.items.some((item) => item.inputType === 'condition_map')
     spec = buildCertificatePrintSpec({
       data: inspection,
+      conditionMarks: hasMap ? await loadVehicleConditionMarks(orgId, inspection.vehicleId) : [],
+      bodyType: hasMap ? inspection.vehicle.bodyType : null,
+      conditionMapLabels: hasMap ? await loadConditionMapLabels(locale) : undefined,
       workshop: { ...workshop, slogan: settingsMap['workshop.slogan'] || undefined },
       labels,
       logoDataUri: toPublic(documentLogoPath(settingsMap, 'certificate')) || undefined,

+ 3 - 0
src/app/api/protected/backup/export/route.ts

@@ -181,6 +181,9 @@ export async function POST(request: NextRequest) {
             // three below: every one of them hangs off the vehicle and was
             // restored from a key the export never wrote.
             findings: true,
+            // The condition map's marks: the car's dents and scratches, with
+            // their photos. Deleted with the vehicle on a restore like the rest.
+            conditionMarks: true,
             aiMessages: true,
             recurringInvoices: { include: { templateParts: true, templateLabor: true } },
             serviceRecords: {

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

@@ -1386,6 +1386,68 @@ export async function POST(request: NextRequest) {
         }
       }
 
+      // 10c. The condition map's marks. They hang off the vehicle but name
+      // the inspection check or the job they were drawn on, so they come
+      // after both; a sheet the backup did not carry leaves the mark with no
+      // sheet rather than failing the import. The people who drew and cleared
+      // them have no account on this instance.
+      if (data.vehicles?.length) {
+        const marks = (data.vehicles as Record<string, unknown>[]).flatMap((vehicle) =>
+          ((vehicle.conditionMarks as Record<string, unknown>[] | undefined) ?? []).map((mark) => ({
+            mark,
+            vehicleId: vehicle.id as string,
+          }))
+        )
+        if (marks.length > 0) {
+          const ids = (key: string) =>
+            marks.map(({ mark }) => mark[key]).filter((id): id is string => typeof id === 'string')
+          const [inspectionRows, itemRows, jobRows] = await Promise.all([
+            tx.inspection.findMany({
+              where: { id: { in: ids('inspectionId') }, organizationId },
+              select: { id: true },
+            }),
+            tx.inspectionItem.findMany({
+              where: { id: { in: ids('inspectionItemId') }, inspection: { organizationId } },
+              select: { id: true },
+            }),
+            tx.serviceRecord.findMany({
+              where: { id: { in: ids('serviceRecordId') }, organizationId },
+              select: { id: true },
+            }),
+          ])
+          const restoredInspectionIds = new Set(inspectionRows.map((row) => row.id))
+          const restoredItemIds = new Set(itemRows.map((row) => row.id))
+          const restoredJobIds = new Set(jobRows.map((row) => row.id))
+          await tx.conditionMark.createMany({
+            data: marks.map(({ mark, vehicleId: markVehicleId }) => ({
+              id: mark.id as string,
+              organizationId,
+              vehicleId: markVehicleId,
+              inspectionId: keptReference(mark.inspectionId, restoredInspectionIds),
+              inspectionItemId: keptReference(mark.inspectionItemId, restoredItemIds),
+              serviceRecordId: keptReference(mark.serviceRecordId, restoredJobIds),
+              bodyType: (mark.bodyType as string) || 'sedan',
+              view: (mark.view as string) || 'left',
+              panel: (mark.panel as string) || 'hood',
+              x: typeof mark.x === 'number' ? mark.x : 0.5,
+              y: typeof mark.y === 'number' ? mark.y : 0.5,
+              kind: (mark.kind as string) || 'dent',
+              severity: (mark.severity as string) || 'minor',
+              note: (mark.note as string) || null,
+              imageUrls: ((mark.imageUrls as string[]) || []).map(
+                (url) => rewriteFileUrl(url, ctx.organizationId) ?? url
+              ),
+              recordedAt: toSafeDate(mark.recordedAt as string),
+              recordedById: null,
+              resolvedAt: toSafeDate(mark.resolvedAt as string) ?? null,
+              resolvedById: null,
+              createdAt: toSafeDate(mark.createdAt as string),
+              updatedAt: toSafeDate(mark.updatedAt as string),
+            })),
+          })
+        }
+      }
+
       // 11. Insert audit logs
       if (data.auditLogs?.length) {
         await tx.auditLog.createMany({

+ 1 - 0
src/features/ai/tools/workshop-columns.ts

@@ -41,6 +41,7 @@ export const TABLE_COLUMNS: Readonly<Record<string, readonly string[]>> = {
     'purchaseDate',
     'purchasePrice',
     'imageUrl',
+    'bodyType',
     'isArchived',
     'archiveReason',
     'soldReportedAt',

+ 326 - 0
src/features/condition-map/Actions/conditionMarkActions.ts

@@ -0,0 +1,326 @@
+'use server'
+
+import { revalidatePath } from 'next/cache'
+import { z } from 'zod'
+import { db } from '@/lib/db'
+import { withAuth } from '@/lib/with-auth'
+import { PermissionAction, PermissionSubject } from '@/lib/permissions'
+import { assertOwnUploads } from '@/lib/upload-url'
+import { BODY_TYPES } from '../Lib/drawingTypes'
+import {
+  type ConditionMarkData,
+  markInputSchema,
+  markPatchSchema,
+  numberedMarks,
+} from '../Lib/marks'
+
+/**
+ * The marks on a vehicle's condition map.
+ *
+ * Reads take the vehicle, because the map is the vehicle's: every open mark
+ * from every visit, so a new sheet starts with what is already known. Writes
+ * take the sheet the mark is drawn on, and are allowed to whoever may edit
+ * that sheet: an inspection's marks need the inspections permission, a work
+ * order's the services one.
+ */
+
+const MARK_SELECT = {
+  id: true,
+  vehicleId: true,
+  inspectionId: true,
+  inspectionItemId: true,
+  serviceRecordId: true,
+  bodyType: true,
+  view: true,
+  panel: true,
+  x: true,
+  y: true,
+  kind: true,
+  severity: true,
+  note: true,
+  imageUrls: true,
+  recordedAt: true,
+  resolvedAt: true,
+} as const
+
+function subjectFor(scope: { inspectionId?: string | null; serviceRecordId?: string | null }) {
+  return scope.inspectionId ? PermissionSubject.INSPECTIONS : PermissionSubject.SERVICES
+}
+
+/** Every mark ever drawn on the vehicle, oldest first; resolved ones included as history. */
+export async function listVehicleConditionMarks(vehicleId: string) {
+  return withAuth(
+    async ({ organizationId }): Promise<ConditionMarkData[]> => {
+      const rows = await db.conditionMark.findMany({
+        where: { vehicleId, organizationId },
+        select: MARK_SELECT,
+        orderBy: { recordedAt: 'asc' },
+      })
+      return numberedMarks(rows)
+    },
+    {
+      requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.VEHICLES }],
+    }
+  )
+}
+
+/** The same list for a server component, which already knows the organization. */
+export async function loadVehicleConditionMarks(
+  organizationId: string,
+  vehicleId: string
+): Promise<ConditionMarkData[]> {
+  const rows = await db.conditionMark.findMany({
+    where: { vehicleId, organizationId },
+    select: MARK_SELECT,
+    orderBy: { recordedAt: 'asc' },
+  })
+  return numberedMarks(rows)
+}
+
+/**
+ * Checks that the sheet a mark is drawn on is this workshop's, is this
+ * vehicle's, and is still open: a completed inspection and an issued
+ * invoice keep their marks as they were.
+ */
+async function assertSheetOpen(
+  organizationId: string,
+  scope: {
+    vehicleId: string
+    inspectionId?: string | null
+    inspectionItemId?: string | null
+    serviceRecordId?: string | null
+  }
+) {
+  if (scope.inspectionId) {
+    const inspection = await db.inspection.findFirst({
+      where: { id: scope.inspectionId, organizationId, vehicleId: scope.vehicleId },
+      select: {
+        status: true,
+        items: { where: { id: scope.inspectionItemId ?? '' }, select: { id: true } },
+      },
+    })
+    if (!inspection) throw new Error('Inspection not found')
+    if (inspection.status === 'completed')
+      throw new Error('Reopen the inspection to change its condition map')
+    if (scope.inspectionItemId && inspection.items.length === 0) throw new Error('Check not found')
+    return
+  }
+  if (scope.serviceRecordId) {
+    const job = await db.serviceRecord.findFirst({
+      where: { id: scope.serviceRecordId, organizationId, vehicleId: scope.vehicleId },
+      select: { id: true },
+    })
+    if (!job) throw new Error('Work order not found')
+    return
+  }
+  throw new Error('A mark needs a sheet to be drawn on')
+}
+
+function pathsFor(scope: {
+  vehicleId: string
+  inspectionId?: string | null
+  serviceRecordId?: string | null
+}) {
+  const paths = [`/vehicles/${scope.vehicleId}`]
+  if (scope.inspectionId) paths.push(`/inspections/${scope.inspectionId}`)
+  if (scope.serviceRecordId)
+    paths.push(`/vehicles/${scope.vehicleId}/service/${scope.serviceRecordId}`)
+  return paths
+}
+
+export async function addConditionMark(input: unknown) {
+  const parsed = markInputSchema.parse(input)
+  return withAuth(
+    async ({ organizationId, userId }): Promise<ConditionMarkData> => {
+      await assertSheetOpen(organizationId, parsed)
+      const mark = await db.conditionMark.create({
+        data: {
+          organizationId,
+          vehicleId: parsed.vehicleId,
+          inspectionId: parsed.inspectionId ?? null,
+          inspectionItemId: parsed.inspectionItemId ?? null,
+          serviceRecordId: parsed.serviceRecordId ?? null,
+          bodyType: parsed.bodyType,
+          view: parsed.view,
+          panel: parsed.panel,
+          x: parsed.x,
+          y: parsed.y,
+          kind: parsed.kind,
+          severity: parsed.severity,
+          note: parsed.note?.trim() || null,
+          recordedById: userId,
+        },
+        select: MARK_SELECT,
+      })
+      // The vehicle remembers which drawing it is marked on.
+      await db.vehicle.updateMany({
+        where: { id: parsed.vehicleId, organizationId, bodyType: null },
+        data: { bodyType: parsed.bodyType },
+      })
+      for (const path of pathsFor(parsed)) revalidatePath(path)
+      return mark
+    },
+    {
+      requiredPermissions: [{ action: PermissionAction.UPDATE, subject: subjectFor(parsed) }],
+      audit: ({ result }) => ({
+        action: 'conditionMark.add',
+        entity: 'ConditionMark',
+        entityId: result.id,
+        details: { key: 'condition_mark_add', params: { kind: result.kind, panel: result.panel } },
+        metadata: { vehicleId: result.vehicleId },
+      }),
+    }
+  )
+}
+
+async function ownMark(organizationId: string, id: string) {
+  const mark = await db.conditionMark.findFirst({
+    where: { id, organizationId },
+    select: MARK_SELECT,
+  })
+  if (!mark) throw new Error('Mark not found')
+  return mark
+}
+
+export async function updateConditionMark(id: string, input: unknown) {
+  const patch = markPatchSchema.parse(input)
+  return withAuth(
+    async ({ organizationId }): Promise<ConditionMarkData> => {
+      const mark = await ownMark(organizationId, id)
+      await assertSheetOpen(organizationId, mark)
+      const updated = await db.conditionMark.update({
+        where: { id },
+        data: {
+          ...(patch.kind ? { kind: patch.kind } : {}),
+          ...(patch.severity ? { severity: patch.severity } : {}),
+          ...(patch.note !== undefined ? { note: patch.note?.trim() || null } : {}),
+          ...(patch.x !== undefined ? { x: patch.x } : {}),
+          ...(patch.y !== undefined ? { y: patch.y } : {}),
+          ...(patch.view ? { view: patch.view } : {}),
+          ...(patch.panel ? { panel: patch.panel } : {}),
+        },
+        select: MARK_SELECT,
+      })
+      for (const path of pathsFor(mark)) revalidatePath(path)
+      return updated
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES },
+      ],
+    }
+  )
+}
+
+export async function removeConditionMark(id: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const mark = await ownMark(organizationId, id)
+      await assertSheetOpen(organizationId, mark)
+      await db.conditionMark.delete({ where: { id } })
+      for (const path of pathsFor(mark)) revalidatePath(path)
+      return { id, vehicleId: mark.vehicleId }
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES },
+      ],
+      audit: ({ result }) => ({
+        action: 'conditionMark.remove',
+        entity: 'ConditionMark',
+        entityId: result.id,
+        details: { key: 'condition_mark_remove' },
+        metadata: { vehicleId: result.vehicleId },
+      }),
+    }
+  )
+}
+
+/**
+ * A mark from an earlier visit that is no longer there: repaired, or never
+ * was. It stays as history and stops printing as current. `resolved: false`
+ * takes that back.
+ */
+export async function resolveConditionMark(id: string, resolved: boolean) {
+  return withAuth(
+    async ({ organizationId, userId }): Promise<ConditionMarkData> => {
+      const mark = await ownMark(organizationId, id)
+      const updated = await db.conditionMark.update({
+        where: { id },
+        data: resolved
+          ? { resolvedAt: new Date(), resolvedById: userId }
+          : { resolvedAt: null, resolvedById: null },
+        select: MARK_SELECT,
+      })
+      revalidatePath(`/vehicles/${mark.vehicleId}`)
+      return updated
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES },
+      ],
+    }
+  )
+}
+
+const photosSchema = z.object({ id: z.string(), urls: z.array(z.string()).min(1).max(10) })
+
+export async function addConditionMarkPhotos(input: unknown) {
+  const { id, urls } = photosSchema.parse(input)
+  return withAuth(
+    async ({ organizationId }): Promise<string[]> => {
+      assertOwnUploads(urls, organizationId)
+      const mark = await ownMark(organizationId, id)
+      await assertSheetOpen(organizationId, mark)
+      const next = [...mark.imageUrls, ...urls.filter((u) => !mark.imageUrls.includes(u))]
+      await db.conditionMark.update({ where: { id }, data: { imageUrls: next } })
+      for (const path of pathsFor(mark)) revalidatePath(path)
+      return next
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES },
+      ],
+    }
+  )
+}
+
+export async function removeConditionMarkPhoto(input: unknown) {
+  const { id, url } = z.object({ id: z.string(), url: z.string() }).parse(input)
+  return withAuth(
+    async ({ organizationId }): Promise<string[]> => {
+      const mark = await ownMark(organizationId, id)
+      await assertSheetOpen(organizationId, mark)
+      const next = mark.imageUrls.filter((u) => u !== url)
+      await db.conditionMark.update({ where: { id }, data: { imageUrls: next } })
+      for (const path of pathsFor(mark)) revalidatePath(path)
+      return next
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES },
+      ],
+    }
+  )
+}
+
+/** Which drawing the vehicle is marked on, chosen by hand. */
+export async function setVehicleBodyType(vehicleId: string, bodyType: string) {
+  const body = z.enum(BODY_TYPES).parse(bodyType)
+  return withAuth(
+    async ({ organizationId }) => {
+      const result = await db.vehicle.updateMany({
+        where: { id: vehicleId, organizationId },
+        data: { bodyType: body },
+      })
+      if (result.count === 0) throw new Error('Vehicle not found')
+      revalidatePath(`/vehicles/${vehicleId}`)
+      return { vehicleId, bodyType: body }
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES },
+      ],
+    }
+  )
+}

+ 392 - 0
src/features/condition-map/Components/ConditionMap.tsx

@@ -0,0 +1,392 @@
+'use client'
+
+import {
+  useCallback,
+  useMemo,
+  useRef,
+  useState,
+  type PointerEvent as ReactPointerEvent,
+} from 'react'
+import { useTranslations } from 'next-intl'
+import type { DrawingShape } from '@/features/invoice-designer/Spec/documentSpec'
+import { cn } from '@/lib/utils'
+import { getBodyDrawing } from '../Drawings'
+import {
+  composeViews,
+  locateOnSheet,
+  markGlyph,
+  markPosition,
+  placedLines,
+  placedPanels,
+} from '../Lib/compose'
+import type { BodyType, Panel, View } from '../Lib/drawingTypes'
+import { isPanel, isView, type MarkKind, type MarkSeverity } from '../Lib/marks'
+
+/** A mark as the drawing shows it: where, what, and its number. */
+export interface MapMark {
+  id: string
+  view: string
+  x: number
+  y: number
+  kind: string
+  severity: string
+  number: number
+  previous: boolean
+}
+
+export interface MapTap {
+  view: View
+  panel: Panel
+  x: number
+  y: number
+}
+
+/** The glyph's radius on screen, in sheet units; larger than print, for a finger. */
+const GLYPH = 34
+
+/**
+ * The vehicle's five views on one sheet, with the marks on them.
+ *
+ * Tap a panel to add a mark; drag a mark to move it. Every panel is a path
+ * with its name on it, so a tap records "left front door" as well as where
+ * on the picture, and the legend can say so. One view can be brought up on
+ * its own for a finger on a phone.
+ */
+export function ConditionMap({
+  body,
+  marks,
+  selectedId,
+  focusView,
+  readOnly = false,
+  onTap,
+  onSelect,
+  onMove,
+  className,
+}: {
+  body: BodyType
+  marks: MapMark[]
+  selectedId?: string | null
+  /** Show one view large instead of the whole sheet. */
+  focusView?: View | null
+  readOnly?: boolean
+  onTap?: (tap: MapTap) => void
+  onSelect?: (id: string) => void
+  onMove?: (id: string, to: MapTap) => void
+  className?: string
+}) {
+  const t = useTranslations('conditionMap')
+  const svgRef = useRef<SVGSVGElement>(null)
+  const drawing = useMemo(() => getBodyDrawing(body), [body])
+  const composition = useMemo(() => composeViews(), [])
+  const panels = useMemo(() => placedPanels(drawing, composition), [drawing, composition])
+  const lines = useMemo(() => placedLines(drawing, composition), [drawing, composition])
+  const [hover, setHover] = useState<string | null>(null)
+  const [drag, setDrag] = useState<{ id: string; x: number; y: number } | null>(null)
+  const dragMoved = useRef(false)
+
+  const viewBox = useMemo(() => {
+    if (focusView) {
+      const placed = composition.views.find((v) => v.view === focusView)
+      if (placed) {
+        const pad = 30
+        const { x, y, width, height } = placed.box
+        return `${x - pad} ${y - pad} ${width + pad * 2} ${height + pad * 2}`
+      }
+    }
+    return `0 0 ${composition.width} ${composition.height}`
+  }, [composition, focusView])
+
+  /** A pointer's place in sheet units. */
+  const toSheet = useCallback((event: { clientX: number; clientY: number }) => {
+    const svg = svgRef.current
+    if (!svg) return null
+    const ctm = svg.getScreenCTM()
+    if (!ctm) return null
+    const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(ctm.inverse())
+    return { x: point.x, y: point.y }
+  }, [])
+
+  /** The panel under a screen point, ignoring the marks over it. */
+  const panelAt = useCallback((clientX: number, clientY: number) => {
+    const el = document.elementFromPoint(clientX, clientY) as HTMLElement | SVGElement | null
+    const panel = el?.getAttribute?.('data-panel')
+    const view = el?.getAttribute?.('data-view')
+    return isPanel(panel) && isView(view) ? { panel, view } : null
+  }, [])
+
+  const handlePanelClick = (event: ReactPointerEvent<SVGPathElement>, panel: Panel, view: View) => {
+    if (readOnly || !onTap || drag) return
+    const at = toSheet(event)
+    if (!at) return
+    const located = locateOnSheet(composition, at.x, at.y)
+    if (!located) return
+    onTap({ view, panel, x: located.x, y: located.y })
+  }
+
+  const startDrag = (event: ReactPointerEvent<SVGGElement>, mark: MapMark) => {
+    if (readOnly || mark.previous) {
+      onSelect?.(mark.id)
+      return
+    }
+    event.stopPropagation()
+    const at = toSheet(event)
+    if (!at) return
+    dragMoved.current = false
+    setDrag({ id: mark.id, x: at.x, y: at.y })
+    svgRef.current?.setPointerCapture(event.pointerId)
+  }
+
+  const moveDrag = (event: ReactPointerEvent<SVGSVGElement>) => {
+    if (!drag) return
+    const at = toSheet(event)
+    if (!at) return
+    dragMoved.current = true
+    setDrag({ ...drag, x: at.x, y: at.y })
+  }
+
+  const endDrag = (event: ReactPointerEvent<SVGSVGElement>) => {
+    if (!drag) return
+    const { id } = drag
+    setDrag(null)
+    svgRef.current?.releasePointerCapture(event.pointerId)
+    if (!dragMoved.current) {
+      onSelect?.(id)
+      return
+    }
+    const target = panelAt(event.clientX, event.clientY)
+    const at = toSheet(event)
+    if (!target || !at || !onMove) return
+    const located = locateOnSheet(composition, at.x, at.y)
+    if (!located) return
+    onMove(id, { view: target.view, panel: target.panel, x: located.x, y: located.y })
+  }
+
+  const glyphShapes = (mark: MapMark): DrawingShape[] => {
+    const at = drag?.id === mark.id ? [drag.x, drag.y] : markPosition(composition, mark)
+    if (!at) return []
+    return markGlyph({
+      x: at[0],
+      y: at[1],
+      kind: mark.kind as MarkKind,
+      severity: mark.severity as MarkSeverity,
+      number: mark.number,
+      previous: mark.previous,
+      size: GLYPH,
+    })
+  }
+
+  return (
+    <svg
+      ref={svgRef}
+      viewBox={viewBox}
+      role="img"
+      aria-label={t('title')}
+      className={cn('block h-auto w-full select-none touch-none', className)}
+      onPointerMove={moveDrag}
+      onPointerUp={endDrag}
+      onPointerCancel={() => setDrag(null)}
+    >
+      {panels.map((panel) => (
+        <path
+          key={`${panel.view}-${panel.panel}`}
+          d={panel.d}
+          data-view={panel.view}
+          data-panel={panel.panel}
+          className={cn(
+            'stroke-foreground transition-colors',
+            !readOnly && 'cursor-crosshair',
+            hover === `${panel.view}-${panel.panel}` && !readOnly ? 'fill-primary/15' : 'fill-card'
+          )}
+          strokeWidth={3}
+          strokeLinejoin="round"
+          onPointerEnter={() => setHover(`${panel.view}-${panel.panel}`)}
+          onPointerLeave={() => setHover(null)}
+          onClick={(event) =>
+            handlePanelClick(
+              event as unknown as ReactPointerEvent<SVGPathElement>,
+              panel.panel,
+              panel.view
+            )
+          }
+        >
+          <title>{t(`panels.${panel.panel}`)}</title>
+        </path>
+      ))}
+      {lines.map((d, i) => (
+        <path
+          key={i}
+          d={d}
+          className="pointer-events-none fill-none stroke-foreground"
+          strokeWidth={3}
+          strokeLinecap="round"
+          strokeLinejoin="round"
+        />
+      ))}
+      {!focusView &&
+        composition.views.map((placed) => (
+          <text
+            key={placed.view}
+            x={placed.box.x + placed.box.width / 2}
+            y={placed.box.y + placed.box.height + 46}
+            textAnchor="middle"
+            fontSize={36}
+            className="pointer-events-none fill-muted-foreground"
+          >
+            {t(`views.${placed.view}`)}
+          </text>
+        ))}
+      {marks.map((mark) => {
+        const shapes = glyphShapes(mark)
+        if (shapes.length === 0) return null
+        const selected = selectedId === mark.id
+        return (
+          <g
+            key={mark.id}
+            role="button"
+            tabIndex={0}
+            aria-label={t('markLabel', {
+              n: mark.number,
+              kind: t(`kinds.${mark.kind}`),
+              area: '',
+            })}
+            className={cn(
+              'outline-none',
+              readOnly || mark.previous ? 'cursor-pointer' : 'cursor-grab',
+              drag?.id === mark.id && 'pointer-events-none cursor-grabbing'
+            )}
+            onPointerDown={(event) => startDrag(event, mark)}
+            onKeyDown={(event) => {
+              if (event.key === 'Enter' || event.key === ' ') {
+                event.preventDefault()
+                onSelect?.(mark.id)
+              }
+            }}
+          >
+            {selected && (
+              <circle
+                cx={shapes[0].type === 'circle' ? shapes[0].cx : 0}
+                cy={shapes[0].type === 'circle' ? shapes[0].cy : 0}
+                r={GLYPH * 1.6}
+                className="fill-primary/15 stroke-primary"
+                strokeWidth={4}
+                style={shapes[0].type === 'circle' ? undefined : { display: 'none' }}
+              />
+            )}
+            <SelectionRing mark={mark} shapes={shapes} selected={selected} />
+            {shapes.map((shape, i) => (
+              <Shape key={i} shape={shape} />
+            ))}
+          </g>
+        )
+      })}
+    </svg>
+  )
+}
+
+/** The ring around the selected mark, drawn from the glyph's own centre. */
+function SelectionRing({
+  mark,
+  shapes,
+  selected,
+}: {
+  mark: MapMark
+  shapes: DrawingShape[]
+  selected: boolean
+}) {
+  if (!selected || shapes[0]?.type === 'circle') return null
+  // The number's disc is the last circle; the glyph centre is up-left of it.
+  const disc = [...shapes].reverse().find((s) => s.type === 'circle')
+  if (!disc || disc.type !== 'circle') return null
+  const cx = disc.cx - GLYPH * 1.15
+  const cy = disc.cy + GLYPH * 1.15
+  return (
+    <circle
+      cx={cx}
+      cy={cy}
+      r={GLYPH * 1.6}
+      className="fill-primary/15 stroke-primary"
+      strokeWidth={4}
+      data-mark={mark.id}
+    />
+  )
+}
+
+/** One shape of a glyph as an SVG element. */
+export function Shape({ shape }: { shape: DrawingShape }) {
+  if (shape.type === 'path') {
+    return (
+      <path
+        d={shape.d}
+        stroke={shape.stroke}
+        strokeWidth={shape.strokeWidth}
+        fill={shape.fill ?? 'none'}
+        strokeDasharray={shape.dash?.join(' ')}
+        opacity={shape.opacity}
+        strokeLinejoin="round"
+        strokeLinecap="round"
+      />
+    )
+  }
+  if (shape.type === 'circle') {
+    return (
+      <circle
+        cx={shape.cx}
+        cy={shape.cy}
+        r={shape.r}
+        stroke={shape.stroke}
+        strokeWidth={shape.strokeWidth}
+        fill={shape.fill ?? 'none'}
+        opacity={shape.opacity}
+      />
+    )
+  }
+  return (
+    <text
+      x={shape.x}
+      y={shape.y}
+      fill={shape.fill}
+      textAnchor={shape.anchor ?? 'start'}
+      fontSize={shape.size}
+      fontWeight={shape.bold ? 700 : 400}
+      className="pointer-events-none"
+    >
+      {shape.text}
+    </text>
+  )
+}
+
+/** A kind's glyph on its own, for a chip or a legend row. */
+export function MarkIcon({
+  kind,
+  severity,
+  previous = false,
+  number,
+  size = 18,
+  className,
+}: {
+  kind: MarkKind
+  severity: MarkSeverity
+  previous?: boolean
+  number?: number
+  size?: number
+  className?: string
+}) {
+  const r = 26
+  const shapes = markGlyph({ x: 40, y: 44, kind, severity, number: number ?? 0, previous, size: r })
+  const withNumber = number !== undefined
+  // Without a number, only the glyph itself: the last two shapes are the disc and its digit.
+  const drawn = withNumber ? shapes : shapes.slice(0, -2)
+  return (
+    <svg
+      viewBox={withNumber ? '0 0 100 90' : '10 14 60 60'}
+      width={withNumber ? size * 1.1 : size}
+      height={size}
+      aria-hidden="true"
+      className={cn('inline-block shrink-0', className)}
+    >
+      {drawn.map((shape, i) => (
+        <Shape key={i} shape={shape} />
+      ))}
+    </svg>
+  )
+}

+ 525 - 0
src/features/condition-map/Components/ConditionMapCard.tsx

@@ -0,0 +1,525 @@
+'use client'
+
+import { useMemo, useState, useTransition } from 'react'
+import { useRouter } from 'next/navigation'
+import { useTranslations } from 'next-intl'
+import { Eraser, Loader2 } from 'lucide-react'
+import { toast } from 'sonner'
+import { Button } from '@/components/ui/button'
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+} from '@/components/ui/alert-dialog'
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/components/ui/select'
+import { cn } from '@/lib/utils'
+import {
+  addConditionMark,
+  addConditionMarkPhotos,
+  removeConditionMark,
+  removeConditionMarkPhoto,
+  resolveConditionMark,
+  setVehicleBodyType,
+  updateConditionMark,
+} from '../Actions/conditionMarkActions'
+import { BODY_TYPES, type BodyType, type View, VIEWS } from '../Lib/drawingTypes'
+import {
+  type ConditionMarkData,
+  type MarkKind,
+  type MarkScope,
+  type MarkSeverity,
+  bodyTypeFor,
+  numberedMarks,
+  splitMarks,
+} from '../Lib/marks'
+import { ConditionMap, type MapMark, type MapTap, MarkIcon } from './ConditionMap'
+import { MarkEditor } from './MarkEditor'
+
+/**
+ * The condition map with everything around it: the body type, the view
+ * switcher, the drawing, the legend of this sheet's marks, and the marks
+ * still open from earlier visits waiting to be confirmed or cleared.
+ *
+ * Marks belong to the vehicle. This sheet's own are drawn in colour and
+ * listed as new since the last visit; earlier ones are grey. A tap on the
+ * drawing creates a mark at once (a dent, minor) and opens it to be
+ * described, so a walk-round is tap, describe, next.
+ */
+export function ConditionMapCard({
+  vehicle,
+  scope,
+  initialMarks,
+  readOnly = false,
+  serviceType = 'automotive',
+  compact = false,
+  onCountChange,
+}: {
+  vehicle: { id: string; bodyType: string | null }
+  scope: MarkScope
+  initialMarks: ConditionMarkData[]
+  readOnly?: boolean
+  serviceType?: 'automotive' | 'marine' | string
+  /** Without the card's heading, for a host that has its own. */
+  compact?: boolean
+  /** How many marks this sheet holds, for a host that counts them. */
+  onCountChange?: (own: number, previous: number) => void
+}) {
+  const t = useTranslations('conditionMap')
+  const router = useRouter()
+  const [marks, setMarks] = useState<ConditionMarkData[]>(initialMarks)
+  const [body, setBody] = useState<BodyType>(bodyTypeFor(vehicle, { serviceType }))
+  const [focusView, setFocusView] = useState<View | null>(null)
+  const [editingId, setEditingId] = useState<string | null>(null)
+  const [clearingId, setClearingId] = useState<string | null>(null)
+  const [pending, startTransition] = useTransition()
+
+  const { own, previous } = useMemo(() => splitMarks(marks, scope), [marks, scope])
+  const numbered = useMemo(() => numberedMarks([...previous, ...own]), [previous, own])
+  const numberOf = useMemo(() => new Map(numbered.map((m, i) => [m.id, i + 1])), [numbered])
+  const ownIds = useMemo(() => new Set(own.map((m) => m.id)), [own])
+
+  const mapMarks: MapMark[] = numbered
+    .filter((m) => m.bodyType === body)
+    .map((m) => ({
+      id: m.id,
+      view: m.view,
+      x: m.x,
+      y: m.y,
+      kind: m.kind,
+      severity: m.severity,
+      number: numberOf.get(m.id) ?? 0,
+      previous: !ownIds.has(m.id),
+    }))
+  // Marks drawn on another body type cannot be placed on this drawing; they
+  // still count and are still listed.
+  const elsewhere = numbered.filter((m) => m.bodyType !== body).length
+
+  const report = (next: ConditionMarkData[]) => {
+    const split = splitMarks(next, scope)
+    onCountChange?.(split.own.length, split.previous.length)
+  }
+
+  const replace = (updated: ConditionMarkData) => {
+    setMarks((prev) => {
+      const next = prev.map((m) => (m.id === updated.id ? updated : m))
+      report(next)
+      return next
+    })
+  }
+
+  const handleTap = (tap: MapTap) => {
+    if (readOnly) return
+    startTransition(async () => {
+      const result = await addConditionMark({
+        vehicleId: vehicle.id,
+        ...scope,
+        bodyType: body,
+        view: tap.view,
+        panel: tap.panel,
+        x: tap.x,
+        y: tap.y,
+        kind: 'dent',
+        severity: 'minor',
+      })
+      if (!result.success || !result.data) {
+        toast.error(result.success ? t('saveFailed') : result.error || t('saveFailed'))
+        return
+      }
+      const created = result.data
+      setMarks((prev) => {
+        const next = [...prev, created]
+        report(next)
+        return next
+      })
+      setEditingId(created.id)
+    })
+  }
+
+  const handleMove = (id: string, to: MapTap) => {
+    const before = marks.find((m) => m.id === id)
+    if (!before) return
+    replace({ ...before, view: to.view, panel: to.panel, x: to.x, y: to.y })
+    startTransition(async () => {
+      const result = await updateConditionMark(id, {
+        view: to.view,
+        panel: to.panel,
+        x: to.x,
+        y: to.y,
+      })
+      if (!result.success) {
+        replace(before)
+        toast.error(result.error || t('saveFailed'))
+      }
+    })
+  }
+
+  const handleChange = (patch: {
+    kind?: MarkKind
+    severity?: MarkSeverity
+    note?: string | null
+  }) => {
+    if (!editingId) return
+    const before = marks.find((m) => m.id === editingId)
+    if (!before) return
+    replace({ ...before, ...patch, note: patch.note === undefined ? before.note : patch.note })
+    startTransition(async () => {
+      const result = await updateConditionMark(editingId, patch)
+      if (!result.success) {
+        replace(before)
+        toast.error(result.error || t('saveFailed'))
+      }
+    })
+  }
+
+  const handleRemove = () => {
+    const id = editingId
+    if (!id) return
+    setEditingId(null)
+    const before = marks
+    setMarks((prev) => {
+      const next = prev.filter((m) => m.id !== id)
+      report(next)
+      return next
+    })
+    startTransition(async () => {
+      const result = await removeConditionMark(id)
+      if (!result.success) {
+        setMarks(before)
+        toast.error(result.error || t('saveFailed'))
+      }
+    })
+  }
+
+  const handleAddPhotos = async (files: File[]) => {
+    if (!editingId) return
+    const urls: string[] = []
+    for (const file of files) {
+      const formData = new FormData()
+      formData.append('file', file)
+      try {
+        const res = await fetch('/api/protected/upload/service-files', {
+          method: 'POST',
+          body: formData,
+        })
+        const data = await res.json()
+        if (data.url) urls.push(data.url)
+        else toast.error(data.error || t('uploadFailed'))
+      } catch {
+        toast.error(t('uploadFailed'))
+      }
+    }
+    if (urls.length === 0) return
+    const result = await addConditionMarkPhotos({ id: editingId, urls })
+    if (result.success && result.data) {
+      const before = marks.find((m) => m.id === editingId)
+      if (before) replace({ ...before, imageUrls: result.data })
+      router.refresh()
+    } else {
+      toast.error(result.success ? t('uploadFailed') : result.error || t('uploadFailed'))
+    }
+  }
+
+  const handleRemovePhoto = (url: string) => {
+    if (!editingId) return
+    const before = marks.find((m) => m.id === editingId)
+    if (!before) return
+    replace({ ...before, imageUrls: before.imageUrls.filter((u) => u !== url) })
+    startTransition(async () => {
+      const result = await removeConditionMarkPhoto({ id: editingId, url })
+      if (!result.success) {
+        replace(before)
+        toast.error(result.error || t('saveFailed'))
+      }
+    })
+  }
+
+  const handleClear = () => {
+    const id = clearingId
+    if (!id) return
+    setClearingId(null)
+    const before = marks.find((m) => m.id === id)
+    if (!before) return
+    replace({ ...before, resolvedAt: new Date().toISOString() })
+    startTransition(async () => {
+      const result = await resolveConditionMark(id, true)
+      if (!result.success) {
+        replace(before)
+        toast.error(result.error || t('saveFailed'))
+      }
+    })
+  }
+
+  const handleBody = (next: string) => {
+    if (!BODY_TYPES.includes(next as BodyType)) return
+    setBody(next as BodyType)
+    setFocusView(null)
+    startTransition(async () => {
+      const result = await setVehicleBodyType(vehicle.id, next)
+      if (!result.success) toast.error(result.error || t('saveFailed'))
+    })
+  }
+
+  const editing = editingId ? (marks.find((m) => m.id === editingId) ?? null) : null
+  const clearing = clearingId ? (marks.find((m) => m.id === clearingId) ?? null) : null
+
+  return (
+    <div className="space-y-3" data-testid="condition-map">
+      <div className="flex flex-wrap items-center justify-between gap-2">
+        <div className="min-w-0">
+          {!compact && <h3 className="text-sm font-semibold">{t('title')}</h3>}
+          <p className="text-xs text-muted-foreground">
+            {readOnly ? t('readOnly') : t('addHint')}
+            {pending && (
+              <Loader2 className="ml-1 inline h-3 w-3 animate-spin" aria-label={t('saved')} />
+            )}
+          </p>
+        </div>
+        <div className="flex flex-wrap items-center gap-2">
+          <Select value={body} onValueChange={handleBody} disabled={readOnly}>
+            <SelectTrigger className="h-8 w-40 text-xs" aria-label={t('bodyType')}>
+              <SelectValue />
+            </SelectTrigger>
+            <SelectContent>
+              {BODY_TYPES.map((type) => (
+                <SelectItem key={type} value={type}>
+                  {t(`bodies.${type}`)}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+        </div>
+      </div>
+
+      {/* One view at a time for a finger; the whole sheet for a desk. */}
+      <div className="flex flex-wrap gap-1" role="tablist" aria-label={t('allViews')}>
+        <ViewTab active={focusView === null} onClick={() => setFocusView(null)}>
+          {t('allViews')}
+        </ViewTab>
+        {VIEWS.map((view) => (
+          <ViewTab key={view} active={focusView === view} onClick={() => setFocusView(view)}>
+            {t(`views.${view}`)}
+          </ViewTab>
+        ))}
+      </div>
+
+      <div className="grid gap-4 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
+        <div className="rounded-lg border bg-card p-2">
+          <ConditionMap
+            body={body}
+            marks={mapMarks}
+            selectedId={editingId}
+            focusView={focusView}
+            readOnly={readOnly}
+            onTap={handleTap}
+            onSelect={(id) => setEditingId(id)}
+            onMove={handleMove}
+          />
+        </div>
+
+        <div className="space-y-3">
+          <section aria-label={t('legend')}>
+            <p className="mb-1 text-xs font-medium text-muted-foreground">
+              {own.length > 0 ? t('newSinceLastVisit') : t('legend')} ·{' '}
+              {t('count', { count: own.length })}
+            </p>
+            {own.length === 0 ? (
+              <p className="rounded-md border border-dashed px-3 py-4 text-center text-xs text-muted-foreground">
+                {t('noMarks')}
+              </p>
+            ) : (
+              <ul className="divide-y rounded-md border">
+                {numbered
+                  .filter((m) => ownIds.has(m.id))
+                  .map((mark) => (
+                    <LegendRow
+                      key={mark.id}
+                      mark={mark}
+                      number={numberOf.get(mark.id) ?? 0}
+                      previous={false}
+                      onOpen={() => setEditingId(mark.id)}
+                    />
+                  ))}
+              </ul>
+            )}
+          </section>
+
+          {previous.length > 0 && (
+            <section aria-label={t('previousHeading')}>
+              <p className="mb-1 text-xs font-medium text-muted-foreground">
+                {t('previousHeading')} · {t('previousCount', { count: previous.length })}
+              </p>
+              <p className="mb-1 text-xs text-muted-foreground">{t('previousHint')}</p>
+              <ul className="divide-y rounded-md border">
+                {numbered
+                  .filter((m) => !ownIds.has(m.id))
+                  .map((mark) => (
+                    <LegendRow
+                      key={mark.id}
+                      mark={mark}
+                      number={numberOf.get(mark.id) ?? 0}
+                      previous
+                      onOpen={() => setEditingId(mark.id)}
+                      onClear={readOnly ? undefined : () => setClearingId(mark.id)}
+                    />
+                  ))}
+              </ul>
+            </section>
+          )}
+          {elsewhere > 0 && (
+            <p className="text-xs text-muted-foreground">{t('elsewhere', { count: elsewhere })}</p>
+          )}
+        </div>
+      </div>
+
+      <MarkEditor
+        mark={
+          editing
+            ? {
+                id: editing.id,
+                number: numberOf.get(editing.id) ?? 0,
+                panel: editing.panel,
+                kind: editing.kind,
+                severity: editing.severity,
+                note: editing.note,
+                imageUrls: editing.imageUrls,
+              }
+            : null
+        }
+        open={editing !== null}
+        readOnly={readOnly || (editing ? !ownIds.has(editing.id) : false)}
+        busy={pending}
+        onChange={handleChange}
+        onRemove={handleRemove}
+        onAddPhotos={handleAddPhotos}
+        onRemovePhoto={handleRemovePhoto}
+        onClose={() => setEditingId(null)}
+      />
+
+      <AlertDialog open={clearing !== null} onOpenChange={(open) => !open && setClearingId(null)}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>{t('clearRepaired')}</AlertDialogTitle>
+            <AlertDialogDescription>
+              {clearing &&
+                t('clearBody', {
+                  n: numberOf.get(clearing.id) ?? 0,
+                  area: t(`panels.${clearing.panel}`),
+                })}
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
+            <AlertDialogAction onClick={handleClear}>{t('clearRepaired')}</AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+    </div>
+  )
+}
+
+function ViewTab({
+  active,
+  onClick,
+  children,
+}: {
+  active: boolean
+  onClick: () => void
+  children: React.ReactNode
+}) {
+  return (
+    <button
+      type="button"
+      role="tab"
+      aria-selected={active}
+      onClick={onClick}
+      className={cn(
+        'h-8 rounded-md border px-2.5 text-xs font-medium transition-colors',
+        active
+          ? 'border-primary bg-primary/10 text-foreground'
+          : 'border-input bg-background text-muted-foreground hover:bg-muted'
+      )}
+    >
+      {children}
+    </button>
+  )
+}
+
+function LegendRow({
+  mark,
+  number,
+  previous,
+  onOpen,
+  onClear,
+}: {
+  mark: ConditionMarkData
+  number: number
+  previous: boolean
+  onOpen: () => void
+  onClear?: () => void
+}) {
+  const t = useTranslations('conditionMap')
+  return (
+    <li className="flex items-start gap-2 px-2 py-1.5 text-sm">
+      <button
+        type="button"
+        onClick={onOpen}
+        className="flex min-w-0 flex-1 items-start gap-2 rounded text-left hover:bg-muted/60"
+        aria-label={t('markLabel', {
+          n: number,
+          kind: t(`kinds.${mark.kind}`),
+          area: t(`panels.${mark.panel}`),
+        })}
+      >
+        <MarkIcon
+          kind={mark.kind as MarkKind}
+          severity={mark.severity as MarkSeverity}
+          number={number}
+          previous={previous}
+          size={24}
+          className="mt-0.5"
+        />
+        <span className="min-w-0 flex-1">
+          <span className={cn('block truncate font-medium', previous && 'text-muted-foreground')}>
+            {t('markOn', { kind: t(`kinds.${mark.kind}`), area: t(`panels.${mark.panel}`) })}
+            {mark.severity === 'major' && (
+              <span className="ml-1.5 rounded-full bg-red-600/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-red-700 dark:text-red-300">
+                {t('severities.major')}
+              </span>
+            )}
+          </span>
+          {mark.note && (
+            <span className="block truncate text-xs text-muted-foreground">{mark.note}</span>
+          )}
+          {mark.imageUrls.length > 0 && (
+            <span className="block text-[11px] text-muted-foreground">
+              {t('photoCount', { count: mark.imageUrls.length })}
+            </span>
+          )}
+        </span>
+      </button>
+      {onClear && (
+        <Button
+          type="button"
+          variant="ghost"
+          size="sm"
+          className="h-8 shrink-0 text-xs"
+          onClick={onClear}
+        >
+          <Eraser className="mr-1 h-3.5 w-3.5" aria-hidden="true" />
+          {t('clearRepaired')}
+        </Button>
+      )}
+    </li>
+  )
+}

Vissa filer visades inte eftersom för många filer har ändrats