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 сброшен",
"58.5h/10": "58.5ч/10",
"8 forsunki × 6 măsurători (Înainte / După). Introducerea se face pe rând.": "8 форсунок × 6 измерений (До / После). Ввод — по одной строке.",
":n poziții importate în Purchase nouă": ":n позиций импортировано в новый заказ",
"> 0 calculează automat prețul client.": "> 0 автоматически рассчитывает цену клиента.",
"A1-03": "A1-03",
@@ -260,11 +261,13 @@
"Autentificare 2FA": "2FA аутентификация",
"Auto": "Авто",
"Auto / model": "Авто / модель",
"Auto CRM": "CRM-авто",
"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ă": "Автоматическая",
"Automobile": "Автомобили",
"Autoservis / Client": "Автосервис / Клиент",
"Avans": "Аванс",
"Avans achitat": "Аванс оплачен",
"Avg zile": "Ср. дней",
@@ -363,6 +366,7 @@
"Caută": "Найти",
"Caută client, mașină, număr...": "Поиск клиента, авто, номера...",
"Caută în tabel": "Поиск в таблице",
"Cauza adresării": "Причина обращения",
"Caz asigurare": "Страховой случай",
"Caz de asigurare": "Страховой случай",
"Ce a spus clientul...": "Что сказал клиент...",
@@ -399,10 +403,11 @@
"Client & Auto": "Клиент и авто",
"Client (nume, semnătură):": "Клиент (имя, подпись):",
"Client / Auto": "Client / Auto",
"Client CRM": "Клиент CRM",
"Client CRM": "CRM-клиент",
"Client ID": "ID клиента",
"Client VIP": "VIP-клиент",
"Client existent": "Существующий клиент",
"Client informat / a luat cunoștință": "Клиент проинформирован / ознакомлен",
"Client legat (CRM)": "Привязанный клиент (CRM)",
"Client name": "Имя клиента",
"Client nou": "Новый клиент",
@@ -445,6 +450,7 @@
"Comandă nouă #": "Новый заказ #",
"Comandă primită": "Заказ получен",
"Combustibil": "Топливо",
"Comentariu / recomandări maistru": "Комментарий / рекомендации мастера",
"Comenzi": "Заказы",
"Comercial": "Коммерческий",
"Comercial (van/camion)": "Коммерческий (фургон/грузовик)",
@@ -527,6 +533,7 @@
"Creează cerere": "Создать заявку",
"Creează comandă de aprovizionare": "Создать заказ на закупку",
"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}).": "Создайте первый протокол диагностики форсунок. Номер генерируется автоматически (PS-{тенант}-{год}-{seq}).",
"Creează un bot la @BotFather, lipește token-ul aici și apasă „Setează webhook": "Создайте бота у @BotFather, вставьте токен здесь и нажмите «Настроить 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.": "Создайте бота у @BotFather, вставьте токен здесь и нажмите «Настроить webhook». Клиенты пишут боту, делятся номером, и код автоматически связывается с их нарядом.",
"Crescător": "По возрастанию",
@@ -546,6 +553,7 @@
"Cumulabil = se înmulțește cu alți coeficienți. Necumulabil = doar cel mai mare necumulabil se aplică.": "Накопительный = умножается с другими коэффициентами. Ненакопительный = применяется только наибольший ненакопительный.",
"Curier": "Курьер",
"Currency": "Валюта",
"Curățare": "Чистка",
"Curăță": "Очистить",
"Custom": "Свой",
"Cutie": "Коробка",
@@ -559,6 +567,7 @@
"DENY — interzice dreptul": "DENY — запретить право",
"DOT": "DOT",
"DSG": "DSG",
"DUPĂ": "ПОСЛЕ",
"Da, confirmă": "Да, подтвердить",
"Da, șterge": "Да, удалить",
"Dacă e gol, generăm 10 caractere random.": "Если пусто — сгенерируем 10 случайных символов.",
@@ -570,6 +579,7 @@
"Data comandă": "Дата заказа",
"Data creării": "Дата создания",
"Data deschiderii:": "Data deschiderii:",
"Data emiterii": "Дата выдачи",
"Data factură": "Дата счёта",
"Data ieșire": "Дата выдачи",
"Data intrare": "Дата приёма",
@@ -580,6 +590,7 @@
"Data început": "Дата начала",
"Data închiderii:": "Дата закрытия:",
"Date": "Дата",
"Date auto & client": "Данные авто и клиента",
"Date generale": "Общая информация",
"Date insuficiente": "Недостаточно данных",
"Date legale (apar pe facturi)": "Date legale (apar pe facturi)",
@@ -605,6 +616,7 @@
"Deal-uri active": "Активные сделки",
"Deals": "Сделки",
"Debit": "Дебет",
"Debit ÎNAINTE": "Расход ДО",
"Deblochează": "Разблокировать",
"Decembrie": "Декабрь",
"Decode VIN": "Декодировать VIN",
@@ -644,6 +656,7 @@
"Deschide panoul pe telefon și acceptă notificările întâi.": "Сначала откройте панель на телефоне и разрешите уведомления.",
"Deschide tenantul": "Deschide tenantul",
"Deschide →": "Открыть →",
"Deschidere ÎNAINTE": "Открытие ДО",
"Deschis": "Открыт",
"Deschis la": "Открыт",
"Descrescător": "По убыванию",
@@ -682,6 +695,7 @@
"Doar arhivate": "Только архивные",
"Doar clienți VIP": "Только VIP клиенты",
"Doar clienții mei": "Только мои клиенты",
"Doar dacă maistrul nu e în listă.": "Только если мастер не в списке.",
"Doar la manopere (proprii + subcontract). Baza salariu = preț client × (1 marjă/100). Lasă gol pentru a folosi valoarea implicită a companiei.": "Только для работ (собственных + субподряд). База зарплаты = цена клиенту × (1 − маржа/100). Оставьте пустым для значения по умолчанию компании.",
"Doar pentru cazuri speciale. Lasă gol → folosește marja mecanicului.": "Только для особых случаев. Оставьте пустым → используется маржа механика.",
"Doar tu ești aici": "Только вы здесь",
@@ -750,6 +764,7 @@
"Engine": "Двигатель",
"Eroare": "Ошибка",
"Eroare la recepție": "Ошибка при приёмке",
"Etanșeitate ÎNAINTE": "Герметичность ДО",
"Etape": "Этапы",
"Etapă": "Этап",
"Etichete piese": "Etichete piese",
@@ -830,6 +845,7 @@
"Fișă asociată": "Связанный наряд",
"Fișă de lucru": "Заказ-наряд",
"Fișă lucru": "Заказ-наряд",
"Fișă lucru (opțional)": "Заказ-наряд (опц.)",
"Folie PPF": "Плёнка PPF",
"Folosește AI Assistant": "Использовать AI Assistant",
"Folosește „Check-in depozit": "Используйте «Приёмка на склад»",
@@ -840,6 +856,7 @@
"Force delete": "Удалить безвозвратно",
"Force logout": "Выйти со всех устройств",
"Format:": "Формат:",
"Forsunka": "Форсунка",
"Foto": "Фото",
"Foto factură": "Фото счёта",
"Foto piesă": "Фото запчасти",
@@ -942,6 +959,8 @@
"Informația de bază — click pe titlul secțiunilor de mai jos pentru detalii.": "Основная информация — нажмите на заголовок секции ниже для деталей.",
"Informație": "Информация",
"Injectoare": "Форсунки",
"Injectoare noi": "Новые форсунки",
"Injectoarele în normă, apte pentru exploatare": "Форсунки в норме, годны к эксплуатации",
"Instrucțiuni adiționale": "Дополнительные инструкции",
"Integrări": "Интеграции",
"Integrări de plată": "Платёжные интеграции",
@@ -1083,6 +1102,7 @@
"Manuală": "Ручная",
"Mapare coloane": "Сопоставление колонок",
"Marca": "Марка",
"Marca injectoarelor": "Марка форсунок",
"Marca/Model": "Марка/Модель",
"Marca/Model:": "Marca/Model:",
"Marchează Gata": "Отметить готово",
@@ -1160,6 +1180,7 @@
"Model Claude": "Модель Claude",
"Model Gemini": "Модель Gemini",
"Model OpenAI": "Модель OpenAI",
"Model auto": "Модель авто",
"Model implicit": "Модель по умолчанию",
"Model year": "Модельный год",
"Modele": "Модели",
@@ -1178,6 +1199,7 @@
"Mulțumim": "Спасибо",
"Mâine": "Завтра",
"Mărci auto suportate (separate prin virgulă)": "Поддерживаемые марки (через запятую)",
"Măsurători mecanica injectoarelor": "Измерения механики форсунок",
"Name": "Имя",
"Neachitat": "Не оплачено",
"Necesită acțiune": "Требует действия",
@@ -1185,6 +1207,7 @@
"Necesită aprobare client": "Требуется одобрение клиента",
"Necesită atenție": "Требует внимания",
"Necesită cel puțin 2 recepții complete cu data așteptată setată.": "Требуется минимум 2 полные приёмки с установленной ожидаемой датой.",
"Necesită curățare repetată": "Требуется повторная чистка",
"Necompletat": "Не заполнено",
"Neconfirmat": "Не подтверждено",
"Neconfirmate": "Не подтверждено",
@@ -1238,6 +1261,7 @@
"Niciun mecanic nu a finalizat lucrări în această perioadă.": "Ни один механик не завершил работы за этот период.",
"Niciun plan definit": "Niciun plan definit",
"Niciun preț înregistrat": "Цены не зарегистрированы",
"Niciun protocol": "Нет протоколов",
"Niciun rest de încasat. 🎉": "Нет остатка к получению. 🎉",
"Niciun rezultat pentru": "Нет результатов для",
"Niciun set de anvelope": "Нет комплектов шин",
@@ -1268,10 +1292,13 @@
"Nou tehnician": "Новый техник",
"Nouă": "Новая",
"Nr.": "№",
"Nr. auto": "№ авто",
"Nr. dosar daună": "№ дела о повреждении",
"Nr. factură": "№ счёта",
"Nr. fișă": "№ наряда",
"Nr. injectoare": "Кол-во форсунок",
"Nr. poliță": "№ полиса",
"Nr. protocol": "№ протокола",
"Nr. telefon": "Тел. номер",
"Nr. înmatriculare": "Гос. номер",
"Nr.:": "Nr.:",
@@ -1307,6 +1334,7 @@
"Număr": "Номер",
"Numărul de zile gratis după ce un tenant alege acest plan.": "Дни бесплатного пробного периода при выборе плана.",
"OCR eșuat": "OCR не удалось",
"OK": "OK",
"Observații": "Комментарии",
"Obține token cu": "Получить токен с помощью",
"Octombrie": "Октябрь",
@@ -1556,6 +1584,7 @@
"Progres": "Прогресс",
"Progres etapă": "Прогресс этапа",
"Proprietar": "Владелец",
"Protocol nou": "Новый протокол",
"Provider implicit": "Провайдер по умолчанию",
"Provider:": "Provider:",
"Public": "Публично",
@@ -1616,6 +1645,7 @@
"Recomandare": "Рекомендация",
"Recomandat": "Рекомендуется",
"Recomandate": "Рекомендуемые",
"Recomandată înlocuire": "Рекомендована замена",
"Recomandă": "Рекомендуется",
"Recomandări": "Рекомендации",
"Recomandări AI": "AI-рекомендации",
@@ -1641,6 +1671,7 @@
"Renunță": "Отменить",
"Reordonează": "Изменить порядок",
"Reparație": "Ремонт",
"Reparație capitală motor": "Кап. ремонт двигателя",
"Reparație caroserie": "Ремонт кузова",
"Reparații": "Ремонты",
"Report": "Отчёт",
@@ -1671,6 +1702,7 @@
"Rezervare": "Резерв",
"Rezervat": "Зарезервировано",
"Rezervă o programare": "Rezervă o programare",
"Rezistență ÎNAINTE": "Сопротивление ДО",
"Rezultat": "Результат",
"Rezultate": "Результаты",
"Rezumat": "Резюме",
@@ -1762,6 +1794,7 @@
"Selectează perioadă": "Выберите период",
"Semnat": "Подписано",
"Semnat de": "Подписано",
"Semnat de (manual)": "Подписано (вручную)",
"Semnează": "Подписать",
"Semnătură": "Подпись",
"Semnătură digitală": "Цифровая подпись",
@@ -2094,6 +2127,7 @@
"Venit manopere": "Доход работ",
"Venituri": "Доходы",
"Verificare": "Проверка",
"Verificare / diagnostic": "Проверка / диагностика",
"Verificat": "Проверено",
"Verifică emailul": "Проверьте email",
"Verifică și amintește": "Проверить и напомнить",
@@ -2392,6 +2426,8 @@
"programări": "записи",
"programări active": "активные записи",
"programări neconfirmate &lt; 24h": "неподтверждённые записи < 24ч",
"protocoale forsunki": "протоколы форсунок",
"protocol forsunki": "протокол форсунок",
"rata confirmare": "коэф. подтверждения",
"referral": "Рекомендация",
"reguli markup": "правила наценки",
@@ -2446,6 +2482,8 @@
"Începe lucrul": "Начать работу",
"Închide": "Закрыть",
"Închide fișa": "Закрыть наряд",
"Închidere KZ ÎNAINTE": "Закрытие КЗ ДО",
"Închidere ÎNAINTE": "Закрытие ДО",
"Închis": "Закрыт",
"Închis (Duminică/sărbătoare)": "Закрыто (Воскр./праздник)",
"Închis la": "Закрыт",
@@ -2453,6 +2491,7 @@
"Încărcare STO": "Загрузка СТО",
"Încărcare celulă": "Загрузка ячейки",
"Încărcare service": "Загрузка сервиса",
"Înlocuire": "Замена",
"Înregistrare": "Регистрация",
"Înregistrat": "Зарегистрирован",
"Înregistrează cheltuială": "Регистрация расхода",
@@ -2487,10 +2526,41 @@
"șters": "șters",
"Țară": "Страна",
"Ține-mă minte": "Запомнить меня",
"Автомобиль": "Автомобиль",
"Автосервис / Клиент": "Автосервис / Клиент",
"Время закр. в КЗ, мс": "Время закр. в КЗ, мс",
"Время закрытия, мс": "Время закрытия, мс",
"Время открытия, мс": "Время открытия, мс",
"Герметичность": "Герметичность",
"Год выпуска": "Год выпуска",
"Гос. номер": "Гос. номер",
"ДАННЫЕ АВТОМОБИЛЯ И КЛИЕНТА": "ДАННЫЕ АВТОМОБИЛЯ И КЛИЕНТА",
"Дата выдачи": "Дата выдачи",
"ЗАКЛЮЧЕНИЕ": "ЗАКЛЮЧЕНИЕ",
"Заключение": "Заключение",
"Заполняйте строки по количеству форсунок; лишние строки оставьте пустыми.": "Заполняйте строки по количеству форсунок; лишние строки оставьте пустыми.",
"ИЗМЕРЕНИЯ МЕХАНИКИ ФОРСУНОК": "ИЗМЕРЕНИЯ МЕХАНИКИ ФОРСУНОК",
"Клиент ознакомлен (подпись)": "Клиент ознакомлен (подпись)",
"Кол-во форсунок": "Кол-во форсунок",
"Комментарий / рекомендации мастера": "Комментарий / рекомендации мастера",
"Марка форсунок": "Марка форсунок",
"Мастер (ФИО, подпись)": "Мастер (ФИО, подпись)",
"ПРИЧИНА ОБРАЩЕНИЯ": "ПРИЧИНА ОБРАЩЕНИЯ",
"ПРОТОКОЛ ДИАГНОСТИКИ И ОЧИСТКИ": "ПРОТОКОЛ ДИАГНОСТИКИ И ОЧИСТКИ",
"Пробег, км": "Пробег, км",
"Расход, мл": "Расход, мл",
"Сопротивление, Ом": "Сопротивление, Ом",
"Телефон": "Телефон",
"ФОРСУНКИ": "ФОРСУНКИ",
"ЭЛЕКТРОКЛАПАННЫХ ФОРСУНОК": "ЭЛЕКТРОКЛАПАННЫХ ФОРСУНОК",
"до": "до",
"после": "после",
"— (multe VIN-uri europene nu respectă checksum-ul NA)": "— (multe VIN-uri europene nu respectă checksum-ul NA)",
"— alege —": "— выберите —",
"— fără pod —": "— без поста —",
"— niciunul —": "— niciunul —",
"№": "№",
"№ протокола:": "№ протокола:",
"️": "️",
"← Săpt. anterioară": "← Пред. неделя",
"← Înapoi": "← Назад",