'Nou', 'diagnosis' => 'Diagnosticare', 'agreement' => 'Aprobare client', 'approved' => 'Aprobat', 'in_work' => 'În lucru', 'awaiting_parts' => 'Așteaptă piese', 'ready' => 'Gata de ridicare', 'done' => 'Predat', 'cancelled' => 'Anulat', ]; public const PAY_STATUSES = [ 'unpaid' => 'Neplătit', 'partial' => 'Parțial', 'paid' => 'Plătit', ]; protected $fillable = [ 'company_id', 'number', 'client_id', 'vehicle_id', 'master_id', 'deal_id', 'appointment_id', 'opened_at', 'closed_at', 'mileage_in', 'mileage_out', 'complaint', 'diagnosis', 'recommendations', 'status', 'pay_status', 'approved', 'approved_at', 'discount_pct', 'total', 'eta_at', 'tracking_token', ]; protected $casts = [ 'opened_at' => 'date', 'closed_at' => 'date', 'approved_at' => 'datetime', 'eta_at' => 'datetime', 'approved' => 'boolean', 'discount_pct' => 'decimal:2', 'total' => 'decimal:2', ]; public function registerMediaCollections(): void { $this->addMediaCollection('photos'); } public function trackingUrl(): string { return url('/t/' . $this->tracking_token); } public function client(): BelongsTo { return $this->belongsTo(Client::class); } public function vehicle(): BelongsTo { return $this->belongsTo(Vehicle::class); } public function master(): BelongsTo { return $this->belongsTo(User::class, 'master_id'); } public function works(): HasMany { return $this->hasMany(WorkOrderWork::class); } public function parts(): HasMany { return $this->hasMany(WorkOrderPart::class); } public function payments(): HasMany { return $this->hasMany(Payment::class); } public function paidAmount(): float { return (float) $this->payments()->sum('amount'); } public function balanceDue(): float { return max(0.0, (float) $this->total - $this->paidAmount()); } public function recalcTotal(): void { $worksTotal = $this->works()->sum('total'); $partsTotal = $this->parts()->sum('total'); $sub = (float) $worksTotal + (float) $partsTotal; $disc = (float) $this->discount_pct; $this->total = round($sub * (1 - $disc / 100), 2); $this->save(); } public static function generateNumber(int $companyId): string { $year = date('y'); $count = static::withoutGlobalScopes() ->where('company_id', $companyId) ->whereYear('created_at', date('Y')) ->count(); return sprintf('WO-%s-%04d', $year, $count + 1); } /** Auto-send 'ready' email + broadcast WS event on status change. */ protected static function booted(): void { static::creating(function (self $wo) { if (empty($wo->tracking_token)) { $wo->tracking_token = \Illuminate\Support\Str::random(24); } }); static::updated(function (self $wo) { if ( $wo->wasChanged('status') && $wo->status === 'ready' && $wo->getOriginal('status') !== 'ready' ) { app(\App\Services\NotificationDispatcher::class)->workOrderReady($wo); } // Warehouse lifecycle: status=done → consume reservations into issues; // status=cancelled → release reservations. if ($wo->wasChanged('status')) { $svc = app(\App\Services\Warehouse\WarehouseService::class); if ($wo->status === 'done' && $wo->getOriginal('status') !== 'done') { $svc->consume($wo); } if ($wo->status === 'cancelled' && $wo->getOriginal('status') !== 'cancelled') { foreach ($wo->parts as $wop) { $svc->release($wop); } } } // Broadcast real-time update on any field change (skip if broadcasting=log). if (config('broadcasting.default') !== 'log') { try { $company = \App\Models\Central\Company::withoutGlobalScopes()->find($wo->company_id); if ($company) { \App\Events\WorkOrderUpdated::dispatch($wo, $company->slug); } } catch (\Throwable $e) { \Illuminate\Support\Facades\Log::debug('WO broadcast skipped: ' . $e->getMessage()); } } }); } }