feat(injector-protocols): full module — resource + PDF export via Browsershot

Implements the injector diagnostic protocol module per spec:
- Migration: injector_protocols + injector_protocol_rows with company_id
  (multi-tenant), vehicle_id (not car_id — this CRM uses Vehicle),
  master_id FK to users, snapshot fields (car_model/plate/vin/mileage/year/
  injector_brand) captured at protocol time.
- Models with BelongsToTenant; auto-generate protocol_number as
  PS-{tenantId}-{year}-{seq6}; auto-seed 8 empty rows on create.
- Permissions: injector_protocols.view + injector_protocols.conclude.
  Default roles: owner/admin (both), manager (both), receptionist (view
  only), mechanic (both). Test permission count updated 52→54.
- InjectorProtocolResource under Service nav group with 4 sections:
  data auto+client (with client_id/vehicle_id lookups auto-filling
  snapshot fields), reason checkboxes, 8-row repeater for measurements
  (Repeater with position hidden; fixed 8, non-addable/deletable),
  conclusion + comment + master signature.
- Table with columns, badge filters, PDF action.
- Blade PDF template: pixel-close to reference (graphite/gold/blue
  brand tokens, striped rows, rotated 'ФОРСУНКИ' header).
- Route /app/injector-protocols/{id}/pdf with permission gate,
  streams PDF via new InjectorProtocolPdfService (Browsershot).
- Dockerfile: install nodejs 22 + chromium + noto fonts + puppeteer-core
  in /opt/browsershot; env vars for Browsershot binary paths.
- +71 RU/EN translations covering resource + PDF template.

All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 11:27:53 +00:00
parent 0e85035eff
commit 47eb4f62c1
20 changed files with 1218 additions and 8 deletions
+71 -1
View File
@@ -28,6 +28,7 @@
"2FA dezactivat.": "2FA dezactivat.",
"2FA resetat": "2FA reset",
"58.5h/10": "58.5h/10",
"8 forsunki × 6 măsurători (Înainte / După). Introducerea se face pe rând.": "8 injectors × 6 measurements (Before / After). One row at a time.",
":n poziții importate în Purchase nouă": ":n items imported into a new Purchase",
"> 0 calculează automat prețul client.": "> 0 auto-calculates client price.",
"A1-03": "A1-03",
@@ -260,11 +261,13 @@
"Autentificare 2FA": "2FA authentication",
"Auto": "Vehicle",
"Auto / model": "Vehicle / model",
"Auto CRM": "CRM vehicle",
"Auto-import Excel/CSV": "Auto-import Excel/CSV",
"AutoCRM PSauto · psauto.service.mir.md": "AutoCRM PSauto · psauto.service.mir.md",
"AutoCRM SRL": "AutoCRM SRL",
"Automată": "Automatic",
"Automobile": "Vehicles",
"Autoservis / Client": "Auto service / Client",
"Avans": "Advance",
"Avans achitat": "Advance paid",
"Avg zile": "Avg days",
@@ -363,6 +366,7 @@
"Caută": "Search",
"Caută client, mașină, număr...": "Search client, vehicle, number...",
"Caută în tabel": "Search in table",
"Cauza adresării": "Reason for request",
"Caz asigurare": "Insurance case",
"Caz de asigurare": "Insurance case",
"Ce a spus clientul...": "What the client said...",
@@ -403,6 +407,7 @@
"Client ID": "Client ID",
"Client VIP": "VIP client",
"Client existent": "Existing client",
"Client informat / a luat cunoștință": "Client informed / acknowledged",
"Client legat (CRM)": "Linked client (CRM)",
"Client name": "Client name",
"Client nou": "New client",
@@ -445,6 +450,7 @@
"Comandă nouă #": "New order #",
"Comandă primită": "Order received",
"Combustibil": "Fuel",
"Comentariu / recomandări maistru": "Master comment / recommendations",
"Comenzi": "Orders",
"Comercial": "Commercial",
"Comercial (van/camion)": "Commercial (van/truck)",
@@ -527,6 +533,7 @@
"Creează cerere": "Create lead",
"Creează comandă de aprovizionare": "Create purchase order",
"Creează planuri (ex: Free, Basic, Pro) și atribuie-le companiilor.": "Creează planuri (ex: Free, Basic, Pro) și atribuie-le companiilor.",
"Creează primul protocol de diagnostic forsunki. Numărul se generează automat (PS-{tenant}-{an}-{seq}).": "Create the first injector diagnostic protocol. Number is auto-generated (PS-{tenant}-{year}-{seq}).",
"Creează un bot la @BotFather, lipește token-ul aici și apasă „Setează webhook": "Create a bot with @BotFather, paste the token here and click \"Set webhook\".",
"Creează un bot la @BotFather, lipește token-ul aici și apasă „Setează webhook\". Clienții îți scriu la bot, partajează telefonul, iar codul se leagă automat de fișa lor.": "Create a bot with @BotFather, paste the token here and click \"Set webhook\". Clients write to the bot, share their phone, and the code auto-links to their order.",
"Crescător": "Ascending",
@@ -546,6 +553,7 @@
"Cumulabil = se înmulțește cu alți coeficienți. Necumulabil = doar cel mai mare necumulabil se aplică.": "Stackable = multiplies with other coefficients. Non-stackable = only the largest non-stackable applies.",
"Curier": "Courier",
"Currency": "Currency",
"Curățare": "Cleaning",
"Curăță": "Clear",
"Custom": "Custom",
"Cutie": "Gearbox",
@@ -559,6 +567,7 @@
"DENY — interzice dreptul": "DENY — deny the permission",
"DOT": "DOT",
"DSG": "DSG",
"DUPĂ": "AFTER",
"Da, confirmă": "Yes, confirm",
"Da, șterge": "Yes, delete",
"Dacă e gol, generăm 10 caractere random.": "If empty, we generate 10 random characters.",
@@ -570,6 +579,7 @@
"Data comandă": "Order date",
"Data creării": "Created at",
"Data deschiderii:": "Data deschiderii:",
"Data emiterii": "Issue date",
"Data factură": "Invoice date",
"Data ieșire": "Delivery date",
"Data intrare": "Intake date",
@@ -580,6 +590,7 @@
"Data început": "Start date",
"Data închiderii:": "Close date:",
"Date": "Date",
"Date auto & client": "Vehicle & client data",
"Date generale": "General data",
"Date insuficiente": "Insufficient data",
"Date legale (apar pe facturi)": "Date legale (apar pe facturi)",
@@ -605,6 +616,7 @@
"Deal-uri active": "Active deals",
"Deals": "Deals",
"Debit": "Debit",
"Debit ÎNAINTE": "Flow BEFORE",
"Deblochează": "Unblock",
"Decembrie": "December",
"Decode VIN": "Decode VIN",
@@ -644,6 +656,7 @@
"Deschide panoul pe telefon și acceptă notificările întâi.": "Open the panel on the phone and accept notifications first.",
"Deschide tenantul": "Deschide tenantul",
"Deschide →": "Open →",
"Deschidere ÎNAINTE": "Opening BEFORE",
"Deschis": "Opened",
"Deschis la": "Opened at",
"Descrescător": "Descending",
@@ -682,6 +695,7 @@
"Doar arhivate": "Archived only",
"Doar clienți VIP": "VIP clients only",
"Doar clienții mei": "My clients only",
"Doar dacă maistrul nu e în listă.": "Only if the master is not in the list.",
"Doar la manopere (proprii + subcontract). Baza salariu = preț client × (1 marjă/100). Lasă gol pentru a folosi valoarea implicită a companiei.": "Only for labor (own + subcontract). Salary base = client price × (1 margin/100). Leave empty to use company default.",
"Doar pentru cazuri speciale. Lasă gol → folosește marja mecanicului.": "For special cases only. Leave empty → uses the mechanic's margin.",
"Doar tu ești aici": "Only you are here",
@@ -750,6 +764,7 @@
"Engine": "Engine",
"Eroare": "Error",
"Eroare la recepție": "Receiving error",
"Etanșeitate ÎNAINTE": "Tightness BEFORE",
"Etape": "Stages",
"Etapă": "Stage",
"Etichete piese": "Etichete piese",
@@ -830,6 +845,7 @@
"Fișă asociată": "Linked order",
"Fișă de lucru": "Work order",
"Fișă lucru": "Work order",
"Fișă lucru (opțional)": "Work order (opt.)",
"Folie PPF": "PPF film",
"Folosește AI Assistant": "Use AI Assistant",
"Folosește „Check-in depozit": "Use \"Check-in storage\"",
@@ -840,6 +856,7 @@
"Force delete": "Force delete",
"Force logout": "Force logout",
"Format:": "Format:",
"Forsunka": "Injector",
"Foto": "Photo",
"Foto factură": "Invoice photo",
"Foto piesă": "Part photo",
@@ -942,6 +959,8 @@
"Informația de bază — click pe titlul secțiunilor de mai jos pentru detalii.": "Basic information — click on the section titles below for details.",
"Informație": "Info",
"Injectoare": "Injectors",
"Injectoare noi": "New injectors",
"Injectoarele în normă, apte pentru exploatare": "Injectors OK, fit for use",
"Instrucțiuni adiționale": "Additional instructions",
"Integrări": "Integrations",
"Integrări de plată": "Payment integrations",
@@ -1082,7 +1101,8 @@
"Manual": "Manual",
"Manuală": "Manual",
"Mapare coloane": "Column mapping",
"Marca": "Make",
"Marca": "Brand",
"Marca injectoarelor": "Injector brand",
"Marca/Model": "Make/Model",
"Marca/Model:": "Marca/Model:",
"Marchează Gata": "Mark ready",
@@ -1160,6 +1180,7 @@
"Model Claude": "Claude model",
"Model Gemini": "Gemini model",
"Model OpenAI": "OpenAI model",
"Model auto": "Vehicle model",
"Model implicit": "Default model",
"Model year": "Model year",
"Modele": "Models",
@@ -1178,6 +1199,7 @@
"Mulțumim": "Thank you",
"Mâine": "Tomorrow",
"Mărci auto suportate (separate prin virgulă)": "Supported makes (comma-separated)",
"Măsurători mecanica injectoarelor": "Injector mechanics measurements",
"Name": "Name",
"Neachitat": "Unpaid",
"Necesită acțiune": "Needs action",
@@ -1185,6 +1207,7 @@
"Necesită aprobare client": "Needs client approval",
"Necesită atenție": "Needs attention",
"Necesită cel puțin 2 recepții complete cu data așteptată setată.": "Requires at least 2 full receipts with expected date set.",
"Necesită curățare repetată": "Repeat cleaning needed",
"Necompletat": "Empty",
"Neconfirmat": "Unconfirmed",
"Neconfirmate": "Unconfirmed",
@@ -1238,6 +1261,7 @@
"Niciun mecanic nu a finalizat lucrări în această perioadă.": "No mechanic has completed work in this period.",
"Niciun plan definit": "Niciun plan definit",
"Niciun preț înregistrat": "No prices recorded",
"Niciun protocol": "No protocols",
"Niciun rest de încasat. 🎉": "No balance to collect. 🎉",
"Niciun rezultat pentru": "No results for",
"Niciun set de anvelope": "No tire sets",
@@ -1268,10 +1292,13 @@
"Nou tehnician": "New technician",
"Nouă": "New",
"Nr.": "No.",
"Nr. auto": "Plate",
"Nr. dosar daună": "Damage file no.",
"Nr. factură": "Invoice no.",
"Nr. fișă": "Order no.",
"Nr. injectoare": "Injector count",
"Nr. poliță": "Policy no.",
"Nr. protocol": "Protocol no.",
"Nr. telefon": "Phone number",
"Nr. înmatriculare": "License plate",
"Nr.:": "Nr.:",
@@ -1307,6 +1334,7 @@
"Număr": "Number",
"Numărul de zile gratis după ce un tenant alege acest plan.": "Free trial days when a tenant chooses this plan.",
"OCR eșuat": "OCR failed",
"OK": "OK",
"Observații": "Notes",
"Obține token cu": "Get token with",
"Octombrie": "October",
@@ -1556,6 +1584,7 @@
"Progres": "Progress",
"Progres etapă": "Stage progress",
"Proprietar": "Owner",
"Protocol nou": "New protocol",
"Provider implicit": "Default provider",
"Provider:": "Provider:",
"Public": "Public",
@@ -1616,6 +1645,7 @@
"Recomandare": "Referral",
"Recomandat": "Recommended",
"Recomandate": "Recommended",
"Recomandată înlocuire": "Replacement recommended",
"Recomandă": "Recommend",
"Recomandări": "Recommendations",
"Recomandări AI": "AI recommendations",
@@ -1641,6 +1671,7 @@
"Renunță": "Cancel",
"Reordonează": "Reorder",
"Reparație": "Repair",
"Reparație capitală motor": "Engine overhaul",
"Reparație caroserie": "Body repair",
"Reparații": "Repairs",
"Report": "Report",
@@ -1671,6 +1702,7 @@
"Rezervare": "Reservation",
"Rezervat": "Reserved",
"Rezervă o programare": "Rezervă o programare",
"Rezistență ÎNAINTE": "Resistance BEFORE",
"Rezultat": "Result",
"Rezultate": "Results",
"Rezumat": "Summary",
@@ -1762,6 +1794,7 @@
"Selectează perioadă": "Select period",
"Semnat": "Signed",
"Semnat de": "Signed by",
"Semnat de (manual)": "Signed by (manual)",
"Semnează": "Sign",
"Semnătură": "Signature",
"Semnătură digitală": "Digital signature",
@@ -2094,6 +2127,7 @@
"Venit manopere": "Labor revenue",
"Venituri": "Revenue",
"Verificare": "Verification",
"Verificare / diagnostic": "Check / diagnosis",
"Verificat": "Verified",
"Verifică emailul": "Check your email",
"Verifică și amintește": "Verify and remind",
@@ -2392,6 +2426,8 @@
"programări": "appointments",
"programări active": "active appointments",
"programări neconfirmate &lt; 24h": "unconfirmed appointments < 24h",
"protocoale forsunki": "injector protocols",
"protocol forsunki": "injector protocol",
"rata confirmare": "confirmation rate",
"referral": "Referral",
"reguli markup": "markup rules",
@@ -2446,6 +2482,8 @@
"Începe lucrul": "Start work",
"Închide": "Close",
"Închide fișa": "Close order",
"Închidere KZ ÎNAINTE": "Closing KZ BEFORE",
"Închidere ÎNAINTE": "Closing BEFORE",
"Închis": "Closed",
"Închis (Duminică/sărbătoare)": "Closed (Sunday/holiday)",
"Închis la": "Closed at",
@@ -2453,6 +2491,7 @@
"Încărcare STO": "Workshop load",
"Încărcare celulă": "Cell load",
"Încărcare service": "Workshop load",
"Înlocuire": "Replacement",
"Înregistrare": "Register",
"Înregistrat": "Registered",
"Înregistrează cheltuială": "Record expense",
@@ -2487,10 +2526,41 @@
"șters": "șters",
"Țară": "Country",
"Ține-mă minte": "Remember me",
"Автомобиль": "Vehicle",
"Автосервис / Клиент": "Auto service / Client",
"Время закр. в КЗ, мс": "Closing time (KZ), ms",
"Время закрытия, мс": "Closing time, ms",
"Время открытия, мс": "Opening time, ms",
"Герметичность": "Tightness",
"Год выпуска": "Year",
"Гос. номер": "Plate number",
"ДАННЫЕ АВТОМОБИЛЯ И КЛИЕНТА": "VEHICLE AND CLIENT DATA",
"Дата выдачи": "Issue date",
"ЗАКЛЮЧЕНИЕ": "CONCLUSION",
"Заключение": "Conclusion",
"Заполняйте строки по количеству форсунок; лишние строки оставьте пустыми.": "Fill in rows per injector count; leave extra rows empty.",
"ИЗМЕРЕНИЯ МЕХАНИКИ ФОРСУНОК": "INJECTOR MECHANICS MEASUREMENTS",
"Клиент ознакомлен (подпись)": "Client acknowledged (signature)",
"Кол-во форсунок": "Injector count",
"Комментарий / рекомендации мастера": "Master comment / recommendations",
"Марка форсунок": "Injector brand",
"Мастер (ФИО, подпись)": "Master (name, signature)",
"ПРИЧИНА ОБРАЩЕНИЯ": "REASON FOR REQUEST",
"ПРОТОКОЛ ДИАГНОСТИКИ И ОЧИСТКИ": "DIAGNOSTIC AND CLEANING PROTOCOL",
"Пробег, км": "Mileage, km",
"Расход, мл": "Flow, ml",
"Сопротивление, Ом": "Resistance, Ω",
"Телефон": "Phone",
"ФОРСУНКИ": "INJECTORS",
"ЭЛЕКТРОКЛАПАННЫХ ФОРСУНОК": "OF SOLENOID INJECTORS",
"до": "before",
"после": "after",
"— (multe VIN-uri europene nu respectă checksum-ul NA)": "— (multe VIN-uri europene nu respectă checksum-ul NA)",
"— alege —": "— select —",
"— fără pod —": "— no bay —",
"— niciunul —": "— niciunul —",
"№": "No.",
"№ протокола:": "Protocol no.:",
"️": "️",
"← Săpt. anterioară": "← Prev. week",
"← Înapoi": "← Back",