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,88 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('injector_protocols', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$t->string('protocol_number', 32)->nullable();
|
||||
|
||||
$t->foreignId('client_id')->nullable()->constrained('clients')->nullOnDelete();
|
||||
$t->foreignId('vehicle_id')->nullable()->constrained('vehicles')->nullOnDelete();
|
||||
$t->foreignId('work_order_id')->nullable()->constrained('work_orders')->nullOnDelete();
|
||||
|
||||
// «Автосервис / Клиент» — who delivered the vehicle
|
||||
$t->string('source_type', 20)->default('client'); // 'client' | 'service_partner'
|
||||
$t->string('source_name', 160)->nullable();
|
||||
$t->string('phone', 40)->nullable();
|
||||
|
||||
// vehicle snapshot at protocol time
|
||||
$t->string('car_model', 120)->nullable();
|
||||
$t->string('plate_number', 32)->nullable();
|
||||
$t->string('vin', 32)->nullable();
|
||||
$t->unsignedInteger('mileage')->nullable();
|
||||
$t->unsignedSmallInteger('year')->nullable();
|
||||
$t->string('injector_brand', 60)->nullable();
|
||||
$t->unsignedTinyInteger('injector_count')->default(8);
|
||||
|
||||
// «Причина обращения»
|
||||
$t->boolean('reason_new_injectors')->default(false);
|
||||
$t->boolean('reason_engine_overhaul')->default(false);
|
||||
$t->boolean('reason_check')->default(false);
|
||||
|
||||
// «Заключение»
|
||||
$t->boolean('conclusion_ok')->default(false);
|
||||
$t->boolean('conclusion_needs_cleaning')->default(false);
|
||||
$t->boolean('conclusion_needs_replacement')->default(false);
|
||||
$t->text('master_comment')->nullable();
|
||||
|
||||
// Master = User with role='mechanic' (this CRM uses User, not Employee)
|
||||
$t->foreignId('master_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$t->string('master_name_signed', 120)->nullable();
|
||||
$t->date('issue_date')->nullable();
|
||||
$t->boolean('client_acknowledged')->default(false);
|
||||
|
||||
$t->timestamps();
|
||||
|
||||
$t->unique(['company_id', 'protocol_number']);
|
||||
$t->index(['company_id', 'plate_number']);
|
||||
$t->index(['company_id', 'vin']);
|
||||
});
|
||||
|
||||
Schema::create('injector_protocol_rows', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$t->foreignId('injector_protocol_id')->constrained('injector_protocols')->cascadeOnDelete();
|
||||
$t->unsignedTinyInteger('position'); // 1..8
|
||||
|
||||
$t->decimal('resistance_before', 6, 2)->nullable();
|
||||
$t->decimal('resistance_after', 6, 2)->nullable();
|
||||
$t->decimal('open_time_before', 6, 2)->nullable();
|
||||
$t->decimal('open_time_after', 6, 2)->nullable();
|
||||
$t->decimal('close_time_before', 6, 2)->nullable();
|
||||
$t->decimal('close_time_after', 6, 2)->nullable();
|
||||
$t->decimal('close_time_kz_before', 6, 2)->nullable();
|
||||
$t->decimal('close_time_kz_after', 6, 2)->nullable();
|
||||
$t->decimal('flow_before', 6, 2)->nullable();
|
||||
$t->decimal('flow_after', 6, 2)->nullable();
|
||||
$t->string('tightness_before', 20)->nullable();
|
||||
$t->string('tightness_after', 20)->nullable();
|
||||
|
||||
$t->timestamps();
|
||||
|
||||
$t->unique(['injector_protocol_id', 'position']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('injector_protocol_rows');
|
||||
Schema::dropIfExists('injector_protocols');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user