Files
autocrm/resources/views/pdf/injector-protocol.blade.php
T
Vasyka 47eb4f62c1 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>
2026-07-26 11:27:53 +00:00

342 lines
13 KiB
PHP

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="UTF-8">
<title>{{ $protocol->protocol_number ?? 'Protocol' }}</title>
<style>
/* Brand tokens */
:root {
--graphite: #16161A;
--gold: #C9A961;
--blue: #0093D6;
--row-alt: #E9E6DC;
--gray-text: #8A8A8E;
--line: #D8D5CB;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
@page { size: A4; margin: 12mm 14mm; }
html, body {
font-family: 'Roboto', 'Noto Sans', Arial, sans-serif;
color: var(--graphite);
font-size: 10pt;
line-height: 1.35;
}
/* ── HEADER ── */
.hdr { display: flex; align-items: center; gap: 18px; margin-bottom: 8mm; }
.hdr .qr { width: 24mm; height: 24mm; flex: 0 0 24mm; }
.hdr .qr img { width: 100%; height: 100%; object-fit: contain; }
.hdr .brand { flex: 1; }
.hdr .brand-name { font-size: 20pt; font-weight: 800; color: var(--graphite); letter-spacing: -.3px; line-height: 1; }
.hdr .brand-name .accent { color: var(--blue); }
.hdr .brand-tag { color: var(--gray-text); font-size: 9pt; margin-top: 3pt; }
.hdr .brand-contact { font-size: 9pt; margin-top: 5pt; color: #333; }
.hdr .logo { width: 30mm; height: 22mm; flex: 0 0 30mm; }
.hdr .logo img { width: 100%; height: 100%; object-fit: contain; }
/* ── TITLE ── */
.title {
text-align: center;
font-size: 14pt;
font-weight: 800;
letter-spacing: .3px;
color: var(--graphite);
margin: 5mm 0 2mm;
text-transform: uppercase;
}
.title-underline {
width: 42mm; height: 1.4mm; background: var(--gold);
margin: 0 auto 6mm; border-radius: 1mm;
}
/* ── SECTION PLAQUE ── */
.plaque {
display: inline-block;
background: var(--graphite);
color: #fff;
font-weight: 700;
font-size: 9pt;
letter-spacing: .5px;
padding: 2mm 4mm;
text-transform: uppercase;
margin-top: 4mm;
margin-bottom: 2mm;
}
/* ── FIELD GRID (3 cols) ── */
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 3mm 6mm; margin-bottom: 2mm; }
.field { padding-top: 1mm; }
.field-label { font-size: 8.5pt; font-weight: 700; color: var(--graphite); letter-spacing: .2px; }
.field-value {
border-bottom: 1px solid var(--graphite);
min-height: 5mm;
padding: 2pt 0 1pt;
font-size: 10pt;
}
.field-value.number { border-bottom-color: var(--blue); }
/* Field with inline label like «№ протокола:» */
.inline-field { display: flex; align-items: baseline; gap: 3mm; justify-content: flex-end; }
.inline-field .lbl { font-size: 9pt; font-weight: 600; }
.inline-field .val { border-bottom: 1px solid var(--graphite); min-width: 40mm; padding: 1pt 0; }
/* ── CHECKBOX ROW ── */
.cbrow { display: flex; gap: 8mm; flex-wrap: wrap; margin: 3mm 0 4mm; }
.cb { display: flex; align-items: center; gap: 3mm; font-size: 9.5pt; font-weight: 600; }
.cb-box {
width: 3.2mm; height: 3.2mm;
border: 1px solid var(--graphite);
display: inline-block;
position: relative;
}
.cb-box.checked::after {
content: "";
position: absolute;
top: -1mm; left: 0.2mm;
color: var(--graphite);
font-weight: 800;
font-size: 10pt;
}
/* ── MEASUREMENTS TABLE ── */
table.meas { width: 100%; border-collapse: collapse; margin-top: 1mm; table-layout: fixed; }
table.meas th, table.meas td { border: 1px solid var(--line); padding: 1.6mm 1mm; font-size: 8.5pt; text-align: center; vertical-align: middle; }
table.meas thead th {
background: var(--graphite); color: #fff;
font-weight: 700; font-size: 8pt; letter-spacing: .2px;
}
table.meas thead th.subhead {
background: var(--row-alt); color: var(--graphite);
font-weight: 500; font-size: 7.5pt;
}
table.meas th.rot {
width: 8mm;
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 7.5pt;
letter-spacing: 1px;
}
table.meas td.pos {
background: var(--graphite);
color: #fff;
font-weight: 800;
width: 8mm;
}
table.meas tbody td {
background: var(--row-alt);
min-height: 8mm;
height: 8mm;
}
.meas-note { font-style: italic; color: var(--gray-text); font-size: 8.5pt; margin-top: 1.5mm; }
/* ── CONCLUSION ── */
.comment-box {
border: 1px solid var(--line);
min-height: 15mm;
padding: 2mm 3mm;
margin-top: 1.5mm;
font-size: 10pt;
white-space: pre-wrap;
}
.sig-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8mm; margin-top: 5mm; }
.sig { }
.sig .field-label { margin-bottom: 5mm; }
.sig .val { border-bottom: 1px solid var(--graphite); min-height: 6mm; padding-bottom: 1pt; }
.footer {
text-align: center;
color: var(--gray-text);
font-size: 8pt;
margin-top: 6mm;
}
</style>
</head>
<body>
@php
/** @var \App\Models\Tenant\InjectorProtocol $protocol */
$tenant = app(\App\Tenancy\TenantManager::class)->current();
$companyName = $tenant?->display_name ?: ($tenant?->name ?: 'AutoCRM');
$logoUrl = $tenant?->getLogoUrl();
$addressLine = trim(collect([$tenant?->address, $tenant?->phone, $tenant?->email])->filter()->implode(' • '));
$rowsByPos = $protocol->rows->keyBy('position');
$reasons = [
'reason_new_injectors' => __('Injectoare noi'),
'reason_engine_overhaul' => __('Reparație capitală motor'),
'reason_check' => __('Verificare / diagnostic'),
];
$conclusions = [
'conclusion_ok' => __('Injectoarele în normă, apte pentru exploatare'),
'conclusion_needs_cleaning' => __('Necesită curățare repetată'),
'conclusion_needs_replacement' => __('Recomandată înlocuire'),
];
$publicQr = asset('img/injector-protocol/qr-sample.png');
@endphp
<div class="hdr">
<div class="qr">
<img src="{{ $publicQr }}" alt="QR">
</div>
<div class="brand">
@if ($logoUrl)
<div style="font-size:16pt;font-weight:800;line-height:1;">{{ $companyName }}</div>
@else
<div class="brand-name">{{ strtoupper($companyName) }}</div>
@endif
@if ($tenant?->slogan ?? null)
<div class="brand-tag">{{ $tenant->slogan }}</div>
@endif
@if ($addressLine)
<div class="brand-contact">{{ $addressLine }}</div>
@endif
</div>
@if ($logoUrl)
<div class="logo"><img src="{{ $logoUrl }}" alt="logo"></div>
@endif
</div>
<div class="title">{{ __('ПРОТОКОЛ ДИАГНОСТИКИ И ОЧИСТКИ') }}<br>{{ __('ЭЛЕКТРОКЛАПАННЫХ ФОРСУНОК') }}</div>
<div class="title-underline"></div>
{{-- ── SECTION 1: DATA ── --}}
<div style="display:flex;justify-content:space-between;align-items:center;">
<div class="plaque">{{ __('ДАННЫЕ АВТОМОБИЛЯ И КЛИЕНТА') }}</div>
<div class="inline-field">
<span class="lbl">{{ __('№ протокола:') }}</span>
<span class="val number" style="color:var(--blue);font-weight:700;">{{ $protocol->protocol_number ?? '—' }}</span>
</div>
</div>
<div class="grid">
<div class="field">
<div class="field-label">{{ __('Автосервис / Клиент') }}</div>
<div class="field-value">{{ $protocol->source_name }}</div>
</div>
<div class="field">
<div class="field-label">{{ __('Телефон') }}</div>
<div class="field-value">{{ $protocol->phone }}</div>
</div>
<div class="field">
<div class="field-label">{{ __('Автомобиль') }}</div>
<div class="field-value">{{ $protocol->car_model }}</div>
</div>
<div class="field">
<div class="field-label">{{ __('Гос. номер') }}</div>
<div class="field-value">{{ $protocol->plate_number }}</div>
</div>
<div class="field">
<div class="field-label">{{ __('VIN') }}</div>
<div class="field-value" style="font-family:monospace;">{{ $protocol->vin }}</div>
</div>
<div class="field">
<div class="field-label">{{ __('Пробег, км') }}</div>
<div class="field-value">{{ $protocol->mileage ? number_format($protocol->mileage, 0, '.', ' ') : '' }}</div>
</div>
<div class="field">
<div class="field-label">{{ __('Год выпуска') }}</div>
<div class="field-value">{{ $protocol->year }}</div>
</div>
<div class="field">
<div class="field-label">{{ __('Марка форсунок') }}</div>
<div class="field-value">{{ $protocol->injector_brand }}</div>
</div>
<div class="field">
<div class="field-label">{{ __('Кол-во форсунок') }}</div>
<div class="field-value">{{ $protocol->injector_count }}</div>
</div>
</div>
{{-- ── SECTION 2: REASON ── --}}
<div class="plaque">{{ __('ПРИЧИНА ОБРАЩЕНИЯ') }}</div>
<div class="cbrow">
@foreach ($reasons as $field => $label)
<div class="cb">
<span class="cb-box {{ $protocol->{$field} ? 'checked' : '' }}"></span>
<span>{{ $label }}</span>
</div>
@endforeach
</div>
{{-- ── SECTION 3: MEASUREMENTS ── --}}
<div class="plaque">{{ __('ИЗМЕРЕНИЯ МЕХАНИКИ ФОРСУНОК') }}</div>
<table class="meas">
<thead>
<tr>
<th rowspan="2" class="rot">{{ __('ФОРСУНКИ') }}</th>
<th colspan="2">{{ __('Сопротивление, Ом') }}</th>
<th colspan="2">{{ __('Время открытия, мс') }}</th>
<th colspan="2">{{ __('Время закрытия, мс') }}</th>
<th colspan="2">{{ __('Время закр. в КЗ, мс') }}</th>
<th colspan="2">{{ __('Расход, мл') }}</th>
<th colspan="2">{{ __('Герметичность') }}</th>
</tr>
<tr>
<th class="subhead">{{ __('до') }}</th><th class="subhead">{{ __('после') }}</th>
<th class="subhead">{{ __('до') }}</th><th class="subhead">{{ __('после') }}</th>
<th class="subhead">{{ __('до') }}</th><th class="subhead">{{ __('после') }}</th>
<th class="subhead">{{ __('до') }}</th><th class="subhead">{{ __('после') }}</th>
<th class="subhead">{{ __('до') }}</th><th class="subhead">{{ __('после') }}</th>
<th class="subhead">{{ __('до') }}</th><th class="subhead">{{ __('после') }}</th>
</tr>
</thead>
<tbody>
@for ($i = 1; $i <= 8; $i++)
@php $row = $rowsByPos->get($i); @endphp
<tr>
<td class="pos">{{ $i }}</td>
<td>{{ $row?->resistance_before }}</td>
<td>{{ $row?->resistance_after }}</td>
<td>{{ $row?->open_time_before }}</td>
<td>{{ $row?->open_time_after }}</td>
<td>{{ $row?->close_time_before }}</td>
<td>{{ $row?->close_time_after }}</td>
<td>{{ $row?->close_time_kz_before }}</td>
<td>{{ $row?->close_time_kz_after }}</td>
<td>{{ $row?->flow_before }}</td>
<td>{{ $row?->flow_after }}</td>
<td>{{ $row?->tightness_before }}</td>
<td>{{ $row?->tightness_after }}</td>
</tr>
@endfor
</tbody>
</table>
<div class="meas-note">{{ __('Заполняйте строки по количеству форсунок; лишние строки оставьте пустыми.') }}</div>
{{-- ── SECTION 4: CONCLUSION ── --}}
<div class="plaque">{{ __('ЗАКЛЮЧЕНИЕ') }}</div>
<div class="cbrow">
@foreach ($conclusions as $field => $label)
<div class="cb">
<span class="cb-box {{ $protocol->{$field} ? 'checked' : '' }}"></span>
<span>{{ $label }}</span>
</div>
@endforeach
</div>
<div class="field-label" style="margin-top:2mm;">{{ __('Комментарий / рекомендации мастера') }}</div>
<div class="comment-box">{{ $protocol->master_comment }}</div>
<div class="sig-row">
<div class="sig">
<div class="field-label">{{ __('Мастер (ФИО, подпись)') }}</div>
<div class="val">{{ $protocol->master?->name ?: $protocol->master_name_signed }}</div>
</div>
<div class="sig">
<div class="field-label">{{ __('Дата выдачи') }}</div>
<div class="val">{{ $protocol->issue_date?->format('d.m.Y') }}</div>
</div>
</div>
<div class="sig-row">
<div class="sig">
<div class="field-label">{{ __('Клиент ознакомлен (подпись)') }}</div>
<div class="val">@if ($protocol->client_acknowledged) @endif</div>
</div>
<div style="text-align:right;color:var(--gray-text);font-size:9pt;padding-top:8mm;">
{{ parse_url(config('app.url'), PHP_URL_HOST) ?? 'psauto.md' }}
</div>
</div>
</body>
</html>