diff --git a/Dockerfile b/Dockerfile index 0e470a5..1df698e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,13 +46,32 @@ RUN install-php-extensions \ mbstring \ gmp -# System tools +# System tools + Chromium + Node.js (for spatie/browsershot PDF rendering) RUN apt-get update && apt-get install -y --no-install-recommends \ git \ unzip \ curl \ + ca-certificates \ + gnupg \ + chromium \ + fonts-liberation \ + fonts-noto \ + fonts-noto-cjk \ + && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* +# Puppeteer + Browsershot deps live in a shared node_modules under /app +# so Browsershot can spawn `node`+puppeteer bundled with Chromium bindings. +RUN mkdir -p /opt/browsershot && cd /opt/browsershot \ + && npm install --omit=dev puppeteer-core@23 \ + && chown -R www-data:www-data /opt/browsershot + +ENV BROWSERSHOT_NODE_BINARY=/usr/bin/node \ + BROWSERSHOT_NPM_BINARY=/usr/bin/npm \ + BROWSERSHOT_NODE_MODULE_PATH=/opt/browsershot/node_modules \ + BROWSERSHOT_CHROME_PATH=/usr/bin/chromium + # Copy application code COPY --link . /app # Composer vendor from stage 1 diff --git a/app/Auth/Permissions.php b/app/Auth/Permissions.php index 7472c32..2a358d3 100644 --- a/app/Auth/Permissions.php +++ b/app/Auth/Permissions.php @@ -81,6 +81,10 @@ class Permissions public const AI_ASSISTANT_CONFIGURE_KEYS = 'ai_assistant.configure_keys'; public const ANALYTICS_VIEW = 'analytics.view'; + // Injector diagnostic protocols + public const INJECTOR_PROTOCOLS_VIEW = 'injector_protocols.view'; + public const INJECTOR_PROTOCOLS_CONCLUDE = 'injector_protocols.conclude'; + /** Full list — used by seeder. */ public static function all(): array { @@ -102,6 +106,7 @@ class Permissions self::ADMIN_SETTINGS_EDIT, self::ADMIN_INTEGRATIONS, self::ADMIN_API_TOKENS_MANAGE, self::ADMIN_AUDIT_LOG_VIEW, self::ADMIN_BACKUP_DOWNLOAD, self::AI_ASSISTANT_USE, self::AI_ASSISTANT_CONFIGURE_KEYS, self::ANALYTICS_VIEW, + self::INJECTOR_PROTOCOLS_VIEW, self::INJECTOR_PROTOCOLS_CONCLUDE, ]; } @@ -129,6 +134,7 @@ class Permissions 'admin' => 'Administrare', 'ai_assistant' => 'AI Assistant', 'analytics' => 'Analitică', + 'injector_protocols' => 'Protocol forsunki', ]; } @@ -188,6 +194,8 @@ class Permissions 'ai_assistant.use' => 'Folosește AI Assistant', 'ai_assistant.configure_keys' => 'Configurează chei API pentru AI', 'analytics.view' => 'Vezi rapoarte & analitică', + 'injector_protocols.view' => 'Vezi protocoalele de diagnostic forsunki', + 'injector_protocols.conclude' => 'Completează Заключение (concluzii + semnătură)', ]; } @@ -221,6 +229,7 @@ class Permissions self::SUPPLIERS_VIEW, self::SUPPLIERS_EDIT, self::AI_ASSISTANT_USE, self::ANALYTICS_VIEW, + self::INJECTOR_PROTOCOLS_VIEW, self::INJECTOR_PROTOCOLS_CONCLUDE, ]; // accountant: finance + reporting only @@ -247,6 +256,7 @@ class Permissions self::SALARIES_VIEW_OWN, self::INVENTORY_VIEW, self::AI_ASSISTANT_USE, + self::INJECTOR_PROTOCOLS_VIEW, ]; // mechanic: only own WOs + inventory view @@ -254,6 +264,7 @@ class Permissions self::WORK_ORDERS_VIEW_OWN_ASSIGNED, self::WORK_ORDERS_CHANGE_STATUS, self::INVENTORY_VIEW, self::SALARIES_VIEW_OWN, + self::INJECTOR_PROTOCOLS_VIEW, self::INJECTOR_PROTOCOLS_CONCLUDE, ]; // viewer: read-only diff --git a/app/Filament/Tenant/Resources/InjectorProtocolResource.php b/app/Filament/Tenant/Resources/InjectorProtocolResource.php new file mode 100644 index 0000000..39a8a32 --- /dev/null +++ b/app/Filament/Tenant/Resources/InjectorProtocolResource.php @@ -0,0 +1,288 @@ +user()?->canDo(Permissions::INJECTOR_PROTOCOLS_VIEW) ?? false; + } + + public static function canCreate(): bool + { + return auth()->user()?->canDo(Permissions::INJECTOR_PROTOCOLS_CONCLUDE) ?? false; + } + + public static function canEdit($record): bool + { + return auth()->user()?->canDo(Permissions::INJECTOR_PROTOCOLS_CONCLUDE) ?? false; + } + + public static function form(Schema $schema): Schema + { + return $schema->components([ + Schemas\Components\Section::make(__('Date auto & client')) + ->columns(3) + ->schema([ + Forms\Components\TextInput::make('protocol_number') + ->label(__('Nr. protocol')) + ->disabled() + ->dehydrated(false) + ->placeholder(__('Generat automat')), + Forms\Components\Select::make('source_type') + ->label(__('Sursă')) + ->options(\App\Support\I18n::opts(InjectorProtocol::SOURCE_TYPES)) + ->default('client') + ->required(), + Forms\Components\TextInput::make('source_name') + ->label(__('Autoservis / Client')) + ->required() + ->maxLength(160), + + Forms\Components\Select::make('client_id') + ->label(__('Client CRM')) + ->options(fn () => Client::pluck('name', 'id')) + ->searchable() + ->live() + ->afterStateUpdated(function ($state, Set $set) { + if ($state && $c = Client::find($state)) { + $set('source_name', $c->name); + $set('phone', $c->phone); + } + }), + Forms\Components\Select::make('vehicle_id') + ->label(__('Auto CRM')) + ->options(fn (Get $get) => $get('client_id') + ? Vehicle::where('client_id', $get('client_id')) + ->get()->mapWithKeys(fn ($v) => [$v->id => "{$v->make} {$v->model} {$v->plate}"])->toArray() + : Vehicle::get()->mapWithKeys(fn ($v) => [$v->id => "{$v->make} {$v->model} {$v->plate}"])->toArray()) + ->searchable() + ->live() + ->afterStateUpdated(function ($state, Set $set) { + if ($state && $v = Vehicle::find($state)) { + $set('car_model', trim(($v->make ?? '') . ' ' . ($v->model ?? ''))); + $set('plate_number', $v->plate); + $set('vin', $v->vin); + $set('year', $v->year); + $set('mileage', $v->mileage); + } + }), + Forms\Components\Select::make('work_order_id') + ->label(__('Fișă lucru (opțional)')) + ->options(fn () => WorkOrder::orderBy('id', 'desc')->limit(50) + ->get()->mapWithKeys(fn ($w) => [$w->id => "{$w->number} — " . ($w->client?->name ?? '?')])->toArray()) + ->searchable(), + + Forms\Components\TextInput::make('phone')->label(__('Telefon'))->tel()->maxLength(40), + Forms\Components\TextInput::make('plate_number')->label(__('Nr. înmatriculare'))->maxLength(32), + Forms\Components\TextInput::make('vin')->label(__('VIN'))->maxLength(17), + Forms\Components\TextInput::make('car_model')->label(__('Model auto'))->maxLength(120), + Forms\Components\TextInput::make('year')->label(__('An'))->numeric()->minValue(1950)->maxValue(2100), + Forms\Components\TextInput::make('mileage')->label(__('Kilometraj'))->numeric()->suffix(__('km')), + Forms\Components\TextInput::make('injector_brand')->label(__('Marca injectoarelor'))->maxLength(60), + Forms\Components\TextInput::make('injector_count')->label(__('Nr. injectoare')) + ->numeric()->minValue(1)->maxValue(8)->default(8)->required(), + ]), + + Schemas\Components\Section::make(__('Cauza adresării')) + ->columns(3) + ->schema([ + Forms\Components\Checkbox::make('reason_new_injectors')->label(__('Injectoare noi')), + Forms\Components\Checkbox::make('reason_engine_overhaul')->label(__('Reparație capitală motor')), + Forms\Components\Checkbox::make('reason_check')->label(__('Verificare / diagnostic')), + ]), + + Schemas\Components\Section::make(__('Măsurători mecanica injectoarelor')) + ->description(__('8 forsunki × 6 măsurători (Înainte / După). Introducerea se face pe rând.')) + ->schema([ + Forms\Components\Repeater::make('rows') + ->label('') + ->relationship('rows') + ->schema([ + Forms\Components\TextInput::make('position') + ->label(__('№')) + ->numeric()->minValue(1)->maxValue(8)->required() + ->disabled()->dehydrated(true), + Forms\Components\TextInput::make('resistance_before')->label(__('Rezistență ÎNAINTE'))->numeric()->step(0.01)->suffix('Ω'), + Forms\Components\TextInput::make('resistance_after')->label(__('DUPĂ'))->numeric()->step(0.01)->suffix('Ω'), + Forms\Components\TextInput::make('open_time_before')->label(__('Deschidere ÎNAINTE'))->numeric()->step(0.01)->suffix('ms'), + Forms\Components\TextInput::make('open_time_after')->label(__('DUPĂ'))->numeric()->step(0.01)->suffix('ms'), + Forms\Components\TextInput::make('close_time_before')->label(__('Închidere ÎNAINTE'))->numeric()->step(0.01)->suffix('ms'), + Forms\Components\TextInput::make('close_time_after')->label(__('DUPĂ'))->numeric()->step(0.01)->suffix('ms'), + Forms\Components\TextInput::make('close_time_kz_before')->label(__('Închidere KZ ÎNAINTE'))->numeric()->step(0.01)->suffix('ms'), + Forms\Components\TextInput::make('close_time_kz_after')->label(__('DUPĂ'))->numeric()->step(0.01)->suffix('ms'), + Forms\Components\TextInput::make('flow_before')->label(__('Debit ÎNAINTE'))->numeric()->step(0.01)->suffix(__('ml')), + Forms\Components\TextInput::make('flow_after')->label(__('DUPĂ'))->numeric()->step(0.01)->suffix(__('ml')), + Forms\Components\TextInput::make('tightness_before')->label(__('Etanșeitate ÎNAINTE'))->maxLength(20), + Forms\Components\TextInput::make('tightness_after')->label(__('DUPĂ'))->maxLength(20), + ]) + ->columns(2) + ->itemLabel(fn (array $state) => __('Forsunka') . ' #' . ($state['position'] ?? '?')) + ->addable(false) + ->deletable(false) + ->reorderable(false) + ->collapsed(fn ($record) => (bool) $record), + ]), + + Schemas\Components\Section::make(__('Заключение')) + ->columns(3) + ->schema([ + Forms\Components\Checkbox::make('conclusion_ok') + ->label(__('Injectoarele în normă, apte pentru exploatare')), + Forms\Components\Checkbox::make('conclusion_needs_cleaning') + ->label(__('Necesită curățare repetată')), + Forms\Components\Checkbox::make('conclusion_needs_replacement') + ->label(__('Recomandată înlocuire')), + Forms\Components\Textarea::make('master_comment') + ->label(__('Comentariu / recomandări maistru')) + ->rows(3) + ->columnSpanFull(), + Forms\Components\Select::make('master_id') + ->label(__('Maistru')) + ->options(fn () => User::whereIn('role', ['mechanic', 'admin', 'owner']) + ->pluck('name', 'id')) + ->searchable(), + Forms\Components\TextInput::make('master_name_signed') + ->label(__('Semnat de (manual)')) + ->maxLength(120) + ->helperText(__('Doar dacă maistrul nu e în listă.')), + Forms\Components\DatePicker::make('issue_date') + ->label(__('Data emiterii')) + ->default(today()), + Forms\Components\Checkbox::make('client_acknowledged') + ->label(__('Client informat / a luat cunoștință')) + ->columnSpanFull(), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('protocol_number') + ->label(__('Nr. protocol')) + ->searchable() + ->sortable() + ->copyable() + ->weight('bold'), + Tables\Columns\TextColumn::make('issue_date') + ->label(__('Data')) + ->date('d.m.Y') + ->sortable() + ->placeholder('—'), + Tables\Columns\TextColumn::make('source_name') + ->label(__('Autoservis / Client')) + ->searchable() + ->wrap(), + Tables\Columns\TextColumn::make('plate_number') + ->label(__('Nr. auto')) + ->searchable() + ->placeholder('—'), + Tables\Columns\TextColumn::make('injector_brand') + ->label(__('Marca')) + ->placeholder('—') + ->toggleable(), + Tables\Columns\TextColumn::make('injector_count') + ->label(__('Injectoare')) + ->alignCenter(), + Tables\Columns\IconColumn::make('conclusion_ok') + ->label(__('OK')) + ->boolean() + ->toggleable(), + Tables\Columns\IconColumn::make('conclusion_needs_cleaning') + ->label(__('Curățare')) + ->boolean() + ->toggleable(), + Tables\Columns\IconColumn::make('conclusion_needs_replacement') + ->label(__('Înlocuire')) + ->boolean() + ->toggleable(), + Tables\Columns\TextColumn::make('master.name') + ->label(__('Maistru')) + ->placeholder('—') + ->toggleable(), + Tables\Columns\TextColumn::make('created_at') + ->label(__('Creat')) + ->dateTime('d.m.Y H:i') + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('source_type') + ->label(__('Sursă')) + ->options(\App\Support\I18n::opts(InjectorProtocol::SOURCE_TYPES)), + Tables\Filters\Filter::make('needs_action') + ->label(__('Necesită acțiune')) + ->query(fn ($q) => $q->where(fn ($q) => $q + ->where('conclusion_needs_cleaning', true) + ->orWhere('conclusion_needs_replacement', true))), + ]) + ->actions([ + Actions\Action::make('pdf') + ->label(__('PDF')) + ->icon('heroicon-m-document-arrow-down') + ->color('gray') + ->url(fn (InjectorProtocol $r) => route('tenant.injector-protocols.pdf', ['record' => $r->id])) + ->openUrlInNewTab(), + Actions\EditAction::make(), + Actions\DeleteAction::make(), + ]) + ->emptyStateHeading(__('Niciun protocol')) + ->emptyStateDescription(__('Creează primul protocol de diagnostic forsunki. Numărul se generează automat (PS-{tenant}-{an}-{seq}).')) + ->emptyStateIcon('heroicon-o-beaker') + ->defaultSort('created_at', 'desc'); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListInjectorProtocols::route('/'), + 'create' => Pages\CreateInjectorProtocol::route('/create'), + 'edit' => Pages\EditInjectorProtocol::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/CreateInjectorProtocol.php b/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/CreateInjectorProtocol.php new file mode 100644 index 0000000..b66960e --- /dev/null +++ b/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/CreateInjectorProtocol.php @@ -0,0 +1,11 @@ +label(__('Descarcă PDF')) + ->icon('heroicon-m-document-arrow-down') + ->color('gray') + ->url(fn (InjectorProtocol $record) => route('tenant.injector-protocols.pdf', ['record' => $record->id])) + ->openUrlInNewTab(), + Actions\DeleteAction::make(), + ]; + } +} diff --git a/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/ListInjectorProtocols.php b/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/ListInjectorProtocols.php new file mode 100644 index 0000000..a13e195 --- /dev/null +++ b/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/ListInjectorProtocols.php @@ -0,0 +1,17 @@ +label(__('Protocol nou'))]; + } +} diff --git a/app/Models/Tenant/InjectorProtocol.php b/app/Models/Tenant/InjectorProtocol.php new file mode 100644 index 0000000..68c6c7e --- /dev/null +++ b/app/Models/Tenant/InjectorProtocol.php @@ -0,0 +1,102 @@ + 'Клиент', + '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); + } +} diff --git a/app/Models/Tenant/InjectorProtocolRow.php b/app/Models/Tenant/InjectorProtocolRow.php new file mode 100644 index 0000000..74d402f --- /dev/null +++ b/app/Models/Tenant/InjectorProtocolRow.php @@ -0,0 +1,41 @@ + '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'); + } +} diff --git a/app/Services/InjectorProtocolPdfService.php b/app/Services/InjectorProtocolPdfService.php new file mode 100644 index 0000000..ff942d3 --- /dev/null +++ b/app/Services/InjectorProtocolPdfService.php @@ -0,0 +1,44 @@ + $protocol->load('rows', 'master')])->render(); + + $shot = Browsershot::html($html) + ->format('A4') + ->margins(12, 14, 12, 14) + ->showBackground() + ->waitUntilNetworkIdle(); + + if ($node = env('BROWSERSHOT_NODE_BINARY')) { + $shot->setNodeBinary($node); + } + if ($npm = env('BROWSERSHOT_NPM_BINARY')) { + $shot->setNpmBinary($npm); + } + if ($modules = env('BROWSERSHOT_NODE_MODULE_PATH')) { + $shot->setNodeModulePath($modules); + } + if ($chrome = env('BROWSERSHOT_CHROME_PATH')) { + $shot->setChromePath($chrome); + } + + // Docker sandbox — chromium refuses to run as root without --no-sandbox + $shot->noSandbox(); + + return $shot->pdf(); + } + + public function filename(InjectorProtocol $protocol): string + { + $num = $protocol->protocol_number ?: ('protocol-' . $protocol->id); + return $num . '.pdf'; + } +} diff --git a/composer.json b/composer.json index 8048751..7d5ac5a 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "minishlink/web-push": "^10.0", "phpoffice/phpspreadsheet": "^5.7", "resend/resend-laravel": "^1.4", + "spatie/browsershot": "^5.4", "spatie/laravel-activitylog": "^5.0", "spatie/laravel-medialibrary": "^11.22", "spatie/laravel-permission": "^7.4", diff --git a/composer.lock b/composer.lock index a58c677..5f90ca6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5b5b5d8a2a2a4bac8ef246a2b165992c", + "content-hash": "d8f2a53adf427685095dafb1deff3fa9", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -7380,6 +7380,74 @@ ], "time": "2022-12-17T21:53:22+00:00" }, + { + "name": "spatie/browsershot", + "version": "5.4.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/browsershot.git", + "reference": "dcf7a65fd1d0fc8fd113739b84982377728d0b2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/browsershot/zipball/dcf7a65fd1d0fc8fd113739b84982377728d0b2f", + "reference": "dcf7a65fd1d0fc8fd113739b84982377728d0b2f", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "ext-json": "*", + "php": "^8.2", + "spatie/temporary-directory": "^2.0", + "symfony/process": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "pestphp/pest": "^3.0|^4.0", + "spatie/image": "^3.6", + "spatie/pdf-to-text": "^1.52", + "spatie/phpunit-snapshot-assertions": "^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Browsershot\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://github.com/freekmurze", + "role": "Developer" + } + ], + "description": "Convert a webpage to an image or pdf using headless Chrome", + "homepage": "https://github.com/spatie/browsershot", + "keywords": [ + "chrome", + "convert", + "headless", + "image", + "pdf", + "puppeteer", + "screenshot", + "webpage" + ], + "support": { + "source": "https://github.com/spatie/browsershot/tree/5.4.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-26T13:13:33+00:00" + }, { "name": "spatie/image", "version": "3.9.4", diff --git a/database/migrations/2026_07_26_000002_create_injector_protocols.php b/database/migrations/2026_07_26_000002_create_injector_protocols.php new file mode 100644 index 0000000..2b36084 --- /dev/null +++ b/database/migrations/2026_07_26_000002_create_injector_protocols.php @@ -0,0 +1,88 @@ +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'); + } +}; diff --git a/lang/en.json b/lang/en.json index 05a49fe..d1dc435 100644 --- a/lang/en.json +++ b/lang/en.json @@ -28,6 +28,7 @@ "2FA dezactivat.": "2FA dezactivat.", "2FA resetat": "2FA reset", "5–8.5h/10": "5–8.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 < 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", diff --git a/lang/ru.json b/lang/ru.json index 6e3f188..ff5edc9 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -28,6 +28,7 @@ "2FA dezactivat.": "2FA dezactivat.", "2FA resetat": "2FA сброшен", "5–8.5h/10": "5–8.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 < 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": "← Назад", diff --git a/public/img/injector-protocol/logo-sample.png b/public/img/injector-protocol/logo-sample.png new file mode 100644 index 0000000..245bfd9 Binary files /dev/null and b/public/img/injector-protocol/logo-sample.png differ diff --git a/public/img/injector-protocol/qr-sample.png b/public/img/injector-protocol/qr-sample.png new file mode 100644 index 0000000..e49da12 Binary files /dev/null and b/public/img/injector-protocol/qr-sample.png differ diff --git a/resources/views/pdf/injector-protocol.blade.php b/resources/views/pdf/injector-protocol.blade.php new file mode 100644 index 0000000..404a667 --- /dev/null +++ b/resources/views/pdf/injector-protocol.blade.php @@ -0,0 +1,341 @@ + + +
+ +| {{ __('ФОРСУНКИ') }} | +{{ __('Сопротивление, Ом') }} | +{{ __('Время открытия, мс') }} | +{{ __('Время закрытия, мс') }} | +{{ __('Время закр. в КЗ, мс') }} | +{{ __('Расход, мл') }} | +{{ __('Герметичность') }} | +||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ __('до') }} | {{ __('после') }} | +{{ __('до') }} | {{ __('после') }} | +{{ __('до') }} | {{ __('после') }} | +{{ __('до') }} | {{ __('после') }} | +{{ __('до') }} | {{ __('после') }} | +{{ __('до') }} | {{ __('после') }} | +|
| {{ $i }} | +{{ $row?->resistance_before }} | +{{ $row?->resistance_after }} | +{{ $row?->open_time_before }} | +{{ $row?->open_time_after }} | +{{ $row?->close_time_before }} | +{{ $row?->close_time_after }} | +{{ $row?->close_time_kz_before }} | +{{ $row?->close_time_kz_after }} | +{{ $row?->flow_before }} | +{{ $row?->flow_after }} | +{{ $row?->tightness_before }} | +{{ $row?->tightness_after }} | +