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:
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class InjectorProtocol extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
public const SOURCE_TYPES = [
|
||||
'client' => 'Клиент',
|
||||
'service_partner' => 'Автосервис',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'protocol_number',
|
||||
'client_id', 'vehicle_id', 'work_order_id',
|
||||
'source_type', 'source_name', 'phone',
|
||||
'car_model', 'plate_number', 'vin', 'mileage', 'year',
|
||||
'injector_brand', 'injector_count',
|
||||
'reason_new_injectors', 'reason_engine_overhaul', 'reason_check',
|
||||
'conclusion_ok', 'conclusion_needs_cleaning', 'conclusion_needs_replacement',
|
||||
'master_comment', 'master_id', 'master_name_signed',
|
||||
'issue_date', 'client_acknowledged',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'reason_new_injectors' => 'boolean',
|
||||
'reason_engine_overhaul' => 'boolean',
|
||||
'reason_check' => 'boolean',
|
||||
'conclusion_ok' => 'boolean',
|
||||
'conclusion_needs_cleaning' => 'boolean',
|
||||
'conclusion_needs_replacement' => 'boolean',
|
||||
'client_acknowledged' => 'boolean',
|
||||
'issue_date' => 'date',
|
||||
'mileage' => 'integer',
|
||||
'year' => 'integer',
|
||||
'injector_count' => 'integer',
|
||||
];
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function vehicle(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Vehicle::class);
|
||||
}
|
||||
|
||||
public function workOrder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkOrder::class);
|
||||
}
|
||||
|
||||
public function master(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'master_id');
|
||||
}
|
||||
|
||||
public function rows(): HasMany
|
||||
{
|
||||
return $this->hasMany(InjectorProtocolRow::class)->orderBy('position');
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (InjectorProtocol $p) {
|
||||
$p->protocol_number ??= static::generateNumber();
|
||||
});
|
||||
|
||||
static::created(function (InjectorProtocol $p) {
|
||||
// Auto-seed empty rows for each injector position (1..N)
|
||||
$count = max(1, min(8, (int) $p->injector_count));
|
||||
for ($i = 1; $i <= $count; $i++) {
|
||||
$p->rows()->create([
|
||||
'company_id' => $p->company_id,
|
||||
'position' => $i,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** PS-{tenantId}-{YYYY}-{seq6} — per-tenant, per-year sequence. */
|
||||
public static function generateNumber(): string
|
||||
{
|
||||
$companyId = app(TenantManager::class)->current()?->id
|
||||
?? auth()->user()?->company_id
|
||||
?? 0;
|
||||
$year = now()->year;
|
||||
$seq = static::withoutGlobalScopes()
|
||||
->where('company_id', $companyId)
|
||||
->whereYear('created_at', $year)
|
||||
->count() + 1;
|
||||
return sprintf('PS-%d-%d-%06d', $companyId, $year, $seq);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class InjectorProtocolRow extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'injector_protocol_id', 'position',
|
||||
'resistance_before', 'resistance_after',
|
||||
'open_time_before', 'open_time_after',
|
||||
'close_time_before', 'close_time_after',
|
||||
'close_time_kz_before', 'close_time_kz_after',
|
||||
'flow_before', 'flow_after',
|
||||
'tightness_before', 'tightness_after',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'position' => 'integer',
|
||||
'resistance_before' => 'decimal:2',
|
||||
'resistance_after' => 'decimal:2',
|
||||
'open_time_before' => 'decimal:2',
|
||||
'open_time_after' => 'decimal:2',
|
||||
'close_time_before' => 'decimal:2',
|
||||
'close_time_after' => 'decimal:2',
|
||||
'close_time_kz_before' => 'decimal:2',
|
||||
'close_time_kz_after' => 'decimal:2',
|
||||
'flow_before' => 'decimal:2',
|
||||
'flow_after' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function protocol(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(InjectorProtocol::class, 'injector_protocol_id');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user