From f5ff3f149a8cdb35bb3f9f02654a5309b2129f73 Mon Sep 17 00:00:00 2001 From: Vasyka Date: Tue, 7 Jul 2026 19:52:30 +0000 Subject: [PATCH] feat(WO edit): manopere/piese first + compact header + collapsible sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three UX changes to /app/work-orders/{id}/edit per user request: 1. Manopere / Piese / Subcontract / Plăți now appear as top-level tabs 2. Header info (Antet, Diagnostic, Foto, Tracking, Plată) is compact and collapsed by default 3. Left navigation column can be narrowed via Filament's built-in sidebar toggle button == Relation managers on top == EditWorkOrder now enables Filament's combined-tabs mode: - hasCombinedRelationManagerTabsWithContent() → true - getContentTabPosition() → ContentTabPosition::After - getContentTabLabel() → "ⓘ Info & antet" Result: the WO edit page opens on the Manopere tab (first tab). Piese, Subcontract, Plăți follow. The full form (Antet, Diagnostic, Foto, Tracking & ETA, Plată & total) is the last tab, opened only when the user needs to change header info. Rationale: mechanics and receptionists spend 90% of their WO edit time in Manopere/Piese, not in the header. Putting those first cuts one scroll on every WO open. == Compact header == WorkOrderResource::form restructured: - "Antet fișă" section: single compact section with 4-column dense grid. Contains only the always-visible essentials: Nr., Deschis, Status, Urgență, Client (span 2), Auto (span 2), Maistru (span 2), Km intrare, Km ieșire. Uses ->compact() to reduce padding. - Diagnostic / Foto / Tracking & ETA / Plată & total: ALL now ->collapsible()->collapsed()->compact(). They appear as closed accordions — visible titles but zero screen space until clicked open. Result: opening the "Info & antet" tab shows a tight 4-column top row + 4 closed accordion titles below. Fits in ~40% the vertical space of the previous layout. == Sidebar collapsible on desktop == TenantPanelProvider ->sidebarCollapsibleOnDesktop() — Filament adds a chevron button that toggles the left nav between full-width labels and icon-only strip. Persisted per user via localStorage. Result: user can narrow the left column with one click when they want more horizontal space for a wide Manopere table. No schema changes. No test changes required (existing WO tests still pass — 303/303). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Tenant/Resources/WorkOrderResource.php | 105 ++++++------------ .../WorkOrderResource/Pages/EditWorkOrder.php | 17 +++ .../Filament/TenantPanelProvider.php | 1 + 3 files changed, 54 insertions(+), 69 deletions(-) diff --git a/app/Filament/Tenant/Resources/WorkOrderResource.php b/app/Filament/Tenant/Resources/WorkOrderResource.php index 3b4e433..36faa46 100644 --- a/app/Filament/Tenant/Resources/WorkOrderResource.php +++ b/app/Filament/Tenant/Resources/WorkOrderResource.php @@ -56,101 +56,68 @@ class WorkOrderResource extends Resource public static function form(Schema $schema): Schema { return $schema->components([ - Schemas\Components\Section::make('Antet') - ->columns(3) + // 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('Generat automat'), - Forms\Components\DatePicker::make('opened_at') - ->label('Deschis') - ->default(today()) - ->required(), - Forms\Components\Select::make('status') - ->options(WorkOrder::STATUSES) - ->default('new') - ->required(), - Forms\Components\Select::make('urgency') - ->label('Urgență') - ->options(\App\Models\Tenant\PricingCoefficient::URGENCY) - ->default('normal') - ->required(), - Forms\Components\Select::make('client_id') - ->label('Client') + 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(WorkOrder::STATUSES)->default('new')->required(), + Forms\Components\Select::make('urgency')->label('Urgență') + ->options(\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(), - Forms\Components\Select::make('vehicle_id') - ->label('Auto') + ->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() + ? Vehicle::where('client_id', $get('client_id'))->get() + ->mapWithKeys(fn ($v) => [$v->id => "{$v->make} {$v->model} {$v->plate}"])->toArray() : []) - ->searchable(), - Forms\Components\Select::make('master_id') - ->label('Maistru') + ->searchable()->columnSpan(2), + Forms\Components\Select::make('master_id')->label('Maistru') ->options(fn () => User::where('status', 'active')->pluck('name', 'id')) - ->searchable(), - Forms\Components\TextInput::make('mileage_in')->label('Km la intrare')->numeric(), - Forms\Components\TextInput::make('mileage_out')->label('Km la ieșire')->numeric(), + ->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() + ->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() + ->collapsible()->collapsed()->compact() ->schema([ \Filament\Forms\Components\SpatieMediaLibraryFileUpload::make('photos') - ->label('Fotografii') - ->collection('photos') - ->multiple() - ->reorderable() - ->image() - ->imageEditor() - ->maxFiles(20) + ->label('Fotografii')->collection('photos') + ->multiple()->reorderable()->image()->imageEditor()->maxFiles(20) ->columnSpanFull(), ]), Schemas\Components\Section::make('Tracking & ETA') - ->columns(3) - ->collapsible() + ->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) + 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) + ->columns(3)->collapsible()->collapsed()->compact() ->schema([ - Forms\Components\Select::make('pay_status') - ->options(WorkOrder::PAY_STATUSES) - ->default('unpaid') - ->required(), + Forms\Components\Select::make('pay_status')->options(WorkOrder::PAY_STATUSES)->default('unpaid')->required(), Forms\Components\TextInput::make('discount_pct')->label('Discount %')->numeric()->default(0), - Forms\Components\TextInput::make('override_margin_pct') - ->label('Marjă internă (%) — override') - ->numeric() - ->step(0.01) - ->minValue(0) - ->maxValue(90) - ->placeholder('Ex: 25 pentru VIP / contract') - ->helperText('Doar pentru cazuri speciale. Lasă gol pentru a folosi marja mecanicului.') + 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'), diff --git a/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php b/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php index c3ccbbc..ba614f3 100644 --- a/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php +++ b/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php @@ -12,6 +12,23 @@ class EditWorkOrder extends EditRecord { protected static string $resource = WorkOrderResource::class; + /** Manopere / Piese / Subcontract / Plăți se afișează SUS ca taburi, apoi antetul. */ + public function hasCombinedRelationManagerTabsWithContent(): bool + { + return true; + } + + public function getContentTabLabel(): ?string + { + return 'ⓘ Info & antet'; + } + + public function getContentTabPosition(): ?\Filament\Resources\Pages\Enums\ContentTabPosition + { + // Puts the form (content) AFTER all relation manager tabs + return \Filament\Resources\Pages\Enums\ContentTabPosition::After; + } + protected function getHeaderActions(): array { return [ diff --git a/app/Providers/Filament/TenantPanelProvider.php b/app/Providers/Filament/TenantPanelProvider.php index 7f6b36a..71de2a9 100644 --- a/app/Providers/Filament/TenantPanelProvider.php +++ b/app/Providers/Filament/TenantPanelProvider.php @@ -33,6 +33,7 @@ class TenantPanelProvider extends PanelProvider ->id('tenant') ->path('app') ->login() + ->sidebarCollapsibleOnDesktop() // permite îngustarea coloanei stânga cu un click ->brandName(fn () => app(\App\Tenancy\TenantManager::class)->current()?->display_name ?? app(\App\Tenancy\TenantManager::class)->current()?->name ?? 'AutoCRM')