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:
+20
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources;
|
||||
|
||||
use App\Auth\Permissions;
|
||||
use App\Filament\Tenant\Resources\InjectorProtocolResource\Pages;
|
||||
use App\Models\Tenant\Client;
|
||||
use App\Models\Tenant\InjectorProtocol;
|
||||
use App\Models\Tenant\User;
|
||||
use App\Models\Tenant\Vehicle;
|
||||
use App\Models\Tenant\WorkOrder;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class InjectorProtocolResource extends Resource
|
||||
{
|
||||
protected static ?string $model = InjectorProtocol::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-beaker';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('nav.label.Protocoale forsunki');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('nav.group.Service');
|
||||
}
|
||||
|
||||
protected static ?int $navigationSort = 27;
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('protocol forsunki');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('protocoale forsunki');
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\InjectorProtocolResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\InjectorProtocolResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateInjectorProtocol extends CreateRecord
|
||||
{
|
||||
protected static string $resource = InjectorProtocolResource::class;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\InjectorProtocolResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\InjectorProtocolResource;
|
||||
use App\Models\Tenant\InjectorProtocol;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditInjectorProtocol extends EditRecord
|
||||
{
|
||||
protected static string $resource = InjectorProtocolResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\Action::make('pdf')
|
||||
->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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\InjectorProtocolResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\InjectorProtocolResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListInjectorProtocols extends ListRecords
|
||||
{
|
||||
protected static string $resource = InjectorProtocolResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [Actions\CreateAction::make()->label(__('Protocol nou'))];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenant\InjectorProtocol;
|
||||
use Spatie\Browsershot\Browsershot;
|
||||
|
||||
class InjectorProtocolPdfService
|
||||
{
|
||||
public function render(InjectorProtocol $protocol): string
|
||||
{
|
||||
$html = view('pdf.injector-protocol', ['protocol' => $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';
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+69
-1
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
+71
-1
@@ -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",
|
||||
|
||||
+71
-1
@@ -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": "← Назад",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 465 KiB |
@@ -0,0 +1,341 @@
|
||||
<!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>
|
||||
@@ -72,6 +72,19 @@ Route::middleware(['web', 'auth'])->group(function () {
|
||||
->name('push.subscribe');
|
||||
Route::post('/push/unsubscribe', [\App\Http\Controllers\PushSubscriptionController::class, 'unsubscribe'])
|
||||
->name('push.unsubscribe');
|
||||
|
||||
Route::get('/app/injector-protocols/{record}/pdf', function (\App\Models\Tenant\InjectorProtocol $record) {
|
||||
abort_unless(
|
||||
auth()->user()?->canDo(\App\Auth\Permissions::INJECTOR_PROTOCOLS_VIEW),
|
||||
403
|
||||
);
|
||||
$svc = app(\App\Services\InjectorProtocolPdfService::class);
|
||||
return response()->streamDownload(
|
||||
fn () => print($svc->render($record)),
|
||||
$svc->filename($record),
|
||||
['Content-Type' => 'application/pdf']
|
||||
);
|
||||
})->name('tenant.injector-protocols.pdf');
|
||||
});
|
||||
|
||||
// ─── Telegram webhook (per-tenant, on central domain) ──────────────
|
||||
|
||||
@@ -149,7 +149,7 @@ class RbacApiTest extends TestCase
|
||||
$this->assertGreaterThanOrEqual(7, $roles->count());
|
||||
$owner = $roles->firstWhere('name', 'owner');
|
||||
$this->assertNotNull($owner);
|
||||
$this->assertEquals(52, $owner['permissions_count']);
|
||||
$this->assertEquals(54, $owner['permissions_count']);
|
||||
}
|
||||
|
||||
public function test_role_sync_permissions_updates_role(): void
|
||||
@@ -181,7 +181,7 @@ class RbacApiTest extends TestCase
|
||||
|
||||
$resp = $this->getJson('/api/v1/permissions');
|
||||
$resp->assertOk();
|
||||
$this->assertEquals(52, count($resp->json('data')));
|
||||
$this->assertEquals(54, count($resp->json('data')));
|
||||
$this->assertArrayHasKey('grouped', $resp->json());
|
||||
$this->assertArrayHasKey('clients', $resp->json('grouped'));
|
||||
$this->assertArrayHasKey('roles', $resp->json());
|
||||
|
||||
@@ -35,7 +35,7 @@ class RbacTest extends TestCase
|
||||
|
||||
public function test_seeder_creates_51_permissions(): void
|
||||
{
|
||||
$this->assertEquals(52, Permission::where('guard_name', 'web')->count());
|
||||
$this->assertEquals(54, Permission::where('guard_name', 'web')->count());
|
||||
}
|
||||
|
||||
public function test_seeder_creates_7_roles_per_tenant(): void
|
||||
@@ -48,7 +48,7 @@ class RbacTest extends TestCase
|
||||
public function test_owner_role_has_all_permissions(): void
|
||||
{
|
||||
$owner = Role::where('company_id', $this->company->id)->where('name', 'owner')->first();
|
||||
$this->assertEquals(52, $owner->permissions->count());
|
||||
$this->assertEquals(54, $owner->permissions->count());
|
||||
}
|
||||
|
||||
public function test_mechanic_role_has_minimal_permissions(): void
|
||||
|
||||
Reference in New Issue
Block a user