5a5f43f2de
- WorkOrder and InjectorProtocol PDF routes now respond with Content-Disposition: inline (opens in browser tab so the user can read/print/save from the built-in PDF viewer). ?download=1 forces the classic attachment download. - CalendarBoard exportPdf() switched to the same inline default. - WO edit action + WO table row action + injector protocol actions now use ->url(...)->openUrlInNewTab() instead of streaming the PDF inline into the current tab. - Dashboard Docs tab has both a preview link and a "Descarcă" link. - Vehicle card photo falls back to WO's first uploaded photo when Vehicle model has no MediaLibrary integration of its own. Test CalendarEnhancementsTest updated to assert the new inline response headers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
289 lines
14 KiB
PHP
289 lines
14 KiB
PHP
<?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-magnifying-glass')
|
||
->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'),
|
||
];
|
||
}
|
||
}
|