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>
221 lines
11 KiB
PHP
221 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Tenant\Resources;
|
|
|
|
use App\Filament\Tenant\Resources\WorkOrderResource\Pages;
|
|
use App\Filament\Tenant\Resources\WorkOrderResource\RelationManagers;
|
|
use App\Models\Tenant\Client;
|
|
use App\Models\Tenant\User;
|
|
use App\Models\Tenant\Vehicle;
|
|
use App\Models\Tenant\WorkOrder;
|
|
use App\Tenancy\TenantManager;
|
|
use Filament\Actions;
|
|
use Filament\Forms;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Schemas;
|
|
use Filament\Schemas\Components\Utilities\Get;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
|
|
class WorkOrderResource extends Resource
|
|
{
|
|
protected static ?string $model = WorkOrder::class;
|
|
|
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-wrench-screwdriver';
|
|
|
|
public static function getNavigationLabel(): string
|
|
{
|
|
return __('nav.label.Fișe lucru');
|
|
}
|
|
|
|
public static function getNavigationGroup(): ?string
|
|
{
|
|
return __('nav.group.Service');
|
|
}
|
|
|
|
protected static ?string $modelLabel = null;
|
|
|
|
public static function getModelLabel(): string
|
|
{
|
|
return __('fișă');
|
|
}
|
|
|
|
protected static ?string $pluralModelLabel = null;
|
|
|
|
public static function getPluralModelLabel(): string
|
|
{
|
|
return __('fișe lucru');
|
|
}
|
|
|
|
protected static ?int $navigationSort = 30;
|
|
|
|
public static function getGloballySearchableAttributes(): array
|
|
{
|
|
return ['number', 'complaint', 'diagnosis', 'vehicle.plate', 'vehicle.vin', 'client.name', 'client.phone'];
|
|
}
|
|
|
|
public static function getGlobalSearchResultTitle(\Illuminate\Database\Eloquent\Model $record): string
|
|
{
|
|
return '#' . ($record->number ?? $record->id) . ' · ' . ($record->vehicle?->plate ?? '?');
|
|
}
|
|
|
|
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
|
|
{
|
|
return [
|
|
'Client' => $record->client?->name ?? '—',
|
|
'Status' => $record->status,
|
|
'Total' => number_format((float) $record->total, 2),
|
|
];
|
|
}
|
|
|
|
public static function form(Schema $schema): Schema
|
|
{
|
|
return $schema->components([
|
|
// Antet compact — o singură secțiune densă cu doar esențialul, mereu deschisă.
|
|
Schemas\Components\Section::make(__('Antet fișă'))
|
|
->description(__('Informația de bază — click pe titlul secțiunilor de mai jos pentru detalii.'))
|
|
->compact()
|
|
->columns(4)
|
|
->schema([
|
|
Forms\Components\TextInput::make('number')->label(__('Nr.'))->disabled()->dehydrated(false)->placeholder('auto'),
|
|
Forms\Components\DatePicker::make('opened_at')->label(__('Deschis'))->default(today())->required(),
|
|
Forms\Components\Select::make('status')->options(\App\Support\I18n::opts(WorkOrder::STATUSES))->default('new')->required(),
|
|
Forms\Components\Select::make('urgency')->label(__('Urgență'))
|
|
->options(\App\Support\I18n::opts(\App\Models\Tenant\PricingCoefficient::URGENCY))->default('normal')->required(),
|
|
Forms\Components\Select::make('client_id')->label(__('Client'))
|
|
->options(fn () => Client::pluck('name', 'id'))
|
|
->searchable()->live()->required()->columnSpan(2),
|
|
Forms\Components\Select::make('vehicle_id')->label(__('Auto'))
|
|
->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()
|
|
: [])
|
|
->searchable()->columnSpan(2),
|
|
Forms\Components\Select::make('master_id')->label(__('Maistru'))
|
|
->options(fn () => User::where('status', 'active')->pluck('name', 'id'))
|
|
->searchable()->columnSpan(2),
|
|
Forms\Components\TextInput::make('mileage_in')->label(__('Km intrare'))->numeric(),
|
|
Forms\Components\TextInput::make('mileage_out')->label(__('Km ieșire'))->numeric(),
|
|
]),
|
|
|
|
// Toate secțiunile de detaliu — collapsible și collapsed by default
|
|
Schemas\Components\Section::make(__('Diagnostic'))
|
|
->collapsible()->collapsed()->compact()
|
|
->schema([
|
|
Forms\Components\Textarea::make('complaint')->label(__('Plângere client'))->rows(2)->columnSpanFull(),
|
|
Forms\Components\Textarea::make('diagnosis')->label(__('Diagnostic'))->rows(3)->columnSpanFull(),
|
|
Forms\Components\Textarea::make('recommendations')->label(__('Recomandări'))->rows(2)->columnSpanFull(),
|
|
]),
|
|
Schemas\Components\Section::make(__('Foto'))
|
|
->collapsible()->collapsed()->compact()
|
|
->schema([
|
|
\Filament\Forms\Components\SpatieMediaLibraryFileUpload::make('photos')
|
|
->label(__('Fotografii'))->collection('photos')
|
|
->multiple()->reorderable()->image()->imageEditor()->maxFiles(20)
|
|
->columnSpanFull(),
|
|
]),
|
|
Schemas\Components\Section::make(__('Tracking & ETA'))
|
|
->columns(3)->collapsible()->collapsed()->compact()
|
|
->schema([
|
|
Forms\Components\DateTimePicker::make('eta_at')->label(__('Gata estimat (ETA)'))->seconds(false),
|
|
Forms\Components\TextInput::make('tracking_token')->label(__('Token public'))
|
|
->disabled()->dehydrated(false)->columnSpan(2)
|
|
->helperText(fn (?WorkOrder $record) => $record?->tracking_token
|
|
? 'Link client: ' . $record->trackingUrl()
|
|
: 'Se generează la salvare'),
|
|
]),
|
|
Schemas\Components\Section::make(__('Plată & total'))
|
|
->columns(3)->collapsible()->collapsed()->compact()
|
|
->schema([
|
|
Forms\Components\Select::make('pay_status')->options(\App\Support\I18n::opts(WorkOrder::PAY_STATUSES))->default('unpaid')->required(),
|
|
Forms\Components\TextInput::make('discount_pct')->label(__('Discount %'))->numeric()->default(0),
|
|
Forms\Components\Toggle::make('apply_margin')
|
|
->label(__('Aplică marjă internă'))
|
|
->default(true)
|
|
->helperText(__('On = din prețul manoperelor se scade marja pentru salariu. Off = manopere la cost (salariu pe Total integral).'))
|
|
->visible(fn () => auth()->user()?->canDo(\App\Auth\Permissions::FINANCE_VIEW_INTERNAL_MARGIN) ?? false),
|
|
Forms\Components\TextInput::make('override_margin_pct')->label(__('Marjă internă (%) — override'))
|
|
->numeric()->step(0.01)->minValue(0)->maxValue(90)
|
|
->placeholder(__('Ex: 25'))
|
|
->helperText(__('Doar pentru cazuri speciale. Lasă gol → folosește marja mecanicului.'))
|
|
->visible(fn () => auth()->user()?->canDo(\App\Auth\Permissions::FINANCE_VIEW_INTERNAL_MARGIN) ?? false),
|
|
Forms\Components\TextInput::make('total')->label(__('Total'))->numeric()->disabled()->dehydrated(false),
|
|
Forms\Components\Toggle::make('approved')->label(__('Aprobat de client')),
|
|
Forms\Components\DatePicker::make('closed_at')->label(__('Închis')),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('number')->label(__('Nr.'))->searchable()->sortable(),
|
|
Tables\Columns\TextColumn::make('opened_at')->label(__('Deschis'))->date('d.m.Y')->sortable(),
|
|
Tables\Columns\TextColumn::make('client.name')->label(__('Client'))->searchable(),
|
|
Tables\Columns\TextColumn::make('vehicle.plate')->label(__('Auto'))->placeholder('—'),
|
|
Tables\Columns\TextColumn::make('master.name')->label(__('Maistru'))->placeholder('—'),
|
|
Tables\Columns\TextColumn::make('status')
|
|
->formatStateUsing(fn ($state) => __(WorkOrder::STATUSES[$state] ?? $state))
|
|
->badge()
|
|
->colors([
|
|
'gray' => ['new'],
|
|
'info' => ['diagnosis', 'agreement', 'approved'],
|
|
'warning' => ['in_work', 'awaiting_parts'],
|
|
'success' => ['ready', 'done'],
|
|
'danger' => ['cancelled'],
|
|
]),
|
|
Tables\Columns\TextColumn::make('pay_status')
|
|
->formatStateUsing(fn ($state) => __(WorkOrder::PAY_STATUSES[$state] ?? $state))
|
|
->badge()
|
|
->colors([
|
|
'danger' => ['unpaid'],
|
|
'warning' => ['partial'],
|
|
'success' => ['paid'],
|
|
]),
|
|
Tables\Columns\TextColumn::make('total')->money('MDL')->alignRight()->sortable(),
|
|
])
|
|
->filters([
|
|
Tables\Filters\SelectFilter::make('status')->options(\App\Support\I18n::opts(WorkOrder::STATUSES)),
|
|
Tables\Filters\SelectFilter::make('pay_status')->options(\App\Support\I18n::opts(WorkOrder::PAY_STATUSES)),
|
|
Tables\Filters\SelectFilter::make('master_id')
|
|
->label(__('Maistru'))
|
|
->options(fn () => User::pluck('name', 'id')),
|
|
])
|
|
->actions([
|
|
Actions\Action::make('pdf')
|
|
->label(__('PDF'))
|
|
->icon('heroicon-m-document-magnifying-glass')
|
|
->color('gray')
|
|
->url(fn (WorkOrder $r) => url('/app/work-orders/' . $r->id . '/pdf'))
|
|
->openUrlInNewTab(),
|
|
Actions\EditAction::make(),
|
|
Actions\DeleteAction::make(),
|
|
])
|
|
->emptyStateHeading(__('Nicio fișă de lucru'))
|
|
->emptyStateDescription(__('Crează prima fișă pentru o mașină existentă. Adaugă manopere, piese, plăți — totalul se calculează automat.'))
|
|
->emptyStateIcon('heroicon-o-wrench-screwdriver')
|
|
->defaultSort('opened_at', 'desc');
|
|
}
|
|
|
|
public static function getRelations(): array
|
|
{
|
|
return [
|
|
RelationManagers\WorksRelationManager::class,
|
|
RelationManagers\PartsRelationManager::class,
|
|
RelationManagers\SubcontractJobsRelationManager::class,
|
|
RelationManagers\PaymentsRelationManager::class,
|
|
];
|
|
}
|
|
|
|
public static function getPages(): array
|
|
{
|
|
return [
|
|
'index' => Pages\ListWorkOrders::route('/'),
|
|
'create' => Pages\CreateWorkOrder::route('/create'),
|
|
'edit' => Pages\EditWorkOrder::route('/{record}/edit'),
|
|
];
|
|
}
|
|
}
|