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::orderBy('name')->pluck('name', 'id')) ->searchable()->live()->required()->columnSpan(2) ->createOptionForm([ Forms\Components\TextInput::make('name')->label(__('Nume'))->required(), Forms\Components\TextInput::make('phone')->label(__('Telefon')), Forms\Components\TextInput::make('email')->label(__('Email'))->email(), ]) ->createOptionUsing(fn (array $data) => Client::create($data)->id), 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) ->createOptionForm([ Forms\Components\TextInput::make('make')->label(__('Marcă'))->required(), Forms\Components\TextInput::make('model')->label(__('Model'))->required(), Forms\Components\TextInput::make('plate')->label(__('Nr. înmatriculare')), Forms\Components\TextInput::make('year')->label(__('An'))->numeric(), Forms\Components\TextInput::make('vin')->label(__('VIN')), ]) ->createOptionUsing(function (array $data, Get $get) { $data['client_id'] = $get('client_id'); return Vehicle::create($data)->id; }), Forms\Components\Select::make('master_id')->label(__('Maistru')) ->options(fn () => User::where('status', 'active') ->whereIn('role', ['mechanic', 'master']) ->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) ->live(onBlur: true) ->afterStateUpdated(function ($state, callable $set, $record) { if (! $record) return; $works = (float) $record->works()->sum('total'); $parts = (float) $record->parts()->sum('total'); $sub = (float) $record->subcontractJobs() ->where('status', '!=', 'cancelled')->sum('client_price'); $disc = min(100, max(0, (float) $state)); $set('total', round(($works + $parts + $sub) * (1 - $disc / 100), 2)); }), 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')), ]) // Row click opens the dashboard (single hub) instead of the raw edit form. ->recordUrl(fn (WorkOrder $r) => url('/app/work-orders/' . $r->id . '/dashboard')) ->actions([ Actions\Action::make('pdf') ->label(__('PDF')) ->icon('heroicon-m-document-magnifying-glass') ->color('gray') ->modalHeading(fn (WorkOrder $r) => __('Factură') . ' — WO #' . $r->number) ->modalSubmitAction(false) ->modalCancelActionLabel(__('Închide')) ->modalWidth('7xl') ->modalContent(fn (WorkOrder $r) => view('filament.tenant.pdf-preview', [ 'pdfUrl' => url('/app/work-orders/' . $r->id . '/pdf'), 'downloadUrl' => url('/app/work-orders/' . $r->id . '/pdf?download=1'), ])), 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'), ]; } /** * Mechanics (role='mechanic') see only WOs assigned to them. * All other roles see everything in the current tenant. * Uses role column directly — no Spatie dependency — to avoid * team_id misconfiguration causing wrong results. */ public static function getEloquentQuery(): Builder { $query = parent::getEloquentQuery(); $user = auth()->user(); if (! $user) return $query->whereRaw('0 = 1'); if ($user->role === 'mechanic') { return $query->where('master_id', $user->id); } return $query; } }