Faza 3.2: Service modules — Norme-ore, Tehnicieni, Fișe lucru

Schema:
- users + specialization, color, hourly_rate (pentru maistri)
- labors: catalog manopere standard cu category/ore/preț (RO+RU)
- work_orders: nr unique per tenant, status workflow (9 stări),
  pay_status (3 stări), client/vehicle/master/deal/appointment refs,
  complaint/diagnosis/recommendations, total auto-calculat
- wo_works: manopere per fișă, recalc auto la save/delete
- wo_parts: piese per fișă (free-text deocamdată), discount/total auto

Filament resources (group Service):
- LaborResource: CRUD + grupare pe categorie + filter active
- WorkOrderResource: form complex în 4 secțiuni (antet, diagnostic, plată)
  + 2 RelationManagers (Works, Parts)
- MasterResource: vedere User filtrată role=mechanic, edit specializare/
  culoare calendar/tarif oră

Conversie auto: la adaugare manoperă din catalog Labor,
form populează numele + ore + preț/oră derivat (price/hours).

Number generator pentru WO: format WO-{YY}-{NNNN} per tenant per an,
calculat în CreateWorkOrder via WorkOrder::generateNumber().

Seed extins:
- 3 mecanici (Vasile/Andrei/Nicolae) cu culori + specializări
- 10 manopere standard din prototipul AutoCRM.html
- 1 fișă demo (BMW X5 plăcuțe Brembo) cu 1 manoperă + 1 piesă, total auto
This commit is contained in:
2026-05-06 21:24:07 +00:00
parent c17fb2b413
commit 51a0bab39e
24 changed files with 1112 additions and 172 deletions
@@ -0,0 +1,79 @@
<?php
namespace App\Filament\Tenant\Resources\WorkOrderResource\RelationManagers;
use App\Models\Tenant\Labor;
use App\Models\Tenant\User;
use App\Models\Tenant\WorkOrderWork;
use Filament\Actions;
use Filament\Forms;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
class WorksRelationManager extends RelationManager
{
protected static string $relationship = 'works';
protected static ?string $title = 'Manopere';
public function form(Schema $schema): Schema
{
return $schema->components([
Forms\Components\Select::make('labor_id')
->label('Catalog manoperă')
->options(fn () => Labor::where('is_active', true)
->get()
->mapWithKeys(fn ($l) => [$l->id => "[{$l->category}] {$l->name_ro} ({$l->hours}h)"])
->toArray())
->searchable()
->live()
->afterStateUpdated(function ($state, Set $set) {
if ($state && $labor = Labor::find($state)) {
$set('name', $labor->name_ro);
$set('hours', $labor->hours);
$set('price_per_hour', $labor->hours > 0 ? round($labor->price / max((float) $labor->hours, 1), 2) : 0);
}
})
->columnSpanFull(),
Forms\Components\TextInput::make('name')->label('Nume')->required()->columnSpanFull(),
Forms\Components\TextInput::make('hours')->label('Ore')->numeric()->default(1)->required(),
Forms\Components\TextInput::make('price_per_hour')->label('Preț/h')->numeric()->required(),
Forms\Components\Select::make('master_id')
->label('Maistru')
->options(fn () => User::pluck('name', 'id'))
->searchable(),
Forms\Components\Select::make('status')
->options(WorkOrderWork::STATUSES)
->default('todo')
->required(),
Forms\Components\Textarea::make('notes')->label('Notițe')->columnSpanFull()->rows(2),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('name')
->columns([
Tables\Columns\TextColumn::make('name')->label('Manoperă')->wrap(),
Tables\Columns\TextColumn::make('hours')->label('Ore')->numeric(decimalPlaces: 2)->alignRight(),
Tables\Columns\TextColumn::make('price_per_hour')->label('Preț/h')->money('MDL')->alignRight(),
Tables\Columns\TextColumn::make('total')->label('Total')->money('MDL')->alignRight(),
Tables\Columns\TextColumn::make('master.name')->label('Maistru')->placeholder('—'),
Tables\Columns\TextColumn::make('status')
->formatStateUsing(fn ($s) => WorkOrderWork::STATUSES[$s] ?? $s)
->badge()
->colors(['gray' => ['todo'], 'warning' => ['in_progress'], 'success' => ['done']]),
])
->headerActions([
Actions\CreateAction::make(),
])
->actions([
Actions\EditAction::make(),
Actions\DeleteAction::make(),
]);
}
}