'pending', 'paused_seconds_total' => 0, ]; public const STATUSES = [ 'todo' => 'De făcut', 'in_progress' => 'În lucru', 'done' => 'Finalizat', ]; public const MECHANIC_STATUSES = [ 'pending' => 'În așteptare', 'in_progress' => 'În lucru', 'paused' => 'Pe pauză', 'done' => 'Finalizat', 'blocked' => 'Blocat', ]; public const BLOCK_REASONS = [ 'missing_part' => 'Lipsă piesă', 'awaiting_approval' => 'Aștept aprobare client', 'broken_equipment' => 'Echipament defect', 'other' => 'Altă problemă', ]; protected $fillable = [ 'company_id', 'work_order_id', 'labor_id', 'master_id', 'name', 'hours', 'price_per_hour', 'total', 'status', 'notes', 'requires_approval', 'approved_at', 'approval_token', 'declined_at', 'mechanic_status', 'mechanic_started_at', 'mechanic_done_at', 'actual_hours', 'paused_seconds_total', 'paused_at', 'block_reason', 'block_note', ]; protected $casts = [ 'hours' => 'decimal:2', 'price_per_hour' => 'decimal:2', 'total' => 'decimal:2', 'requires_approval' => 'boolean', 'approved_at' => 'datetime', 'declined_at' => 'datetime', 'mechanic_started_at' => 'datetime', 'mechanic_done_at' => 'datetime', 'paused_at' => 'datetime', 'actual_hours' => 'decimal:2', 'paused_seconds_total' => 'integer', ]; // ── State machine ──────────────────────────────────────────── public function start(): void { if ($this->mechanic_status === 'done') return; $this->forceFill([ 'mechanic_status' => 'in_progress', 'mechanic_started_at' => $this->mechanic_started_at ?? now(), 'paused_at' => null, 'block_reason' => null, 'block_note' => null, 'status' => 'in_progress', ])->save(); } public function pause(): void { if ($this->mechanic_status !== 'in_progress') return; $this->forceFill([ 'mechanic_status' => 'paused', 'paused_at' => now(), ])->save(); } public function resume(): void { if ($this->mechanic_status !== 'paused') return; $added = $this->paused_at ? $this->paused_at->diffInSeconds(now()) : 0; $this->forceFill([ 'mechanic_status' => 'in_progress', 'paused_seconds_total' => (int) $this->paused_seconds_total + (int) $added, 'paused_at' => null, ])->save(); } public function markDone(): void { // If currently paused, count up till now as paused time before stopping. if ($this->mechanic_status === 'paused' && $this->paused_at) { $this->paused_seconds_total = (int) $this->paused_seconds_total + (int) $this->paused_at->diffInSeconds(now()); $this->paused_at = null; } $started = $this->mechanic_started_at ?? now(); $endedAt = now(); $elapsedSec = max(0, $started->diffInSeconds($endedAt) - (int) $this->paused_seconds_total); $actualHours = round($elapsedSec / 3600, 2); $this->forceFill([ 'mechanic_status' => 'done', 'mechanic_done_at' => $endedAt, 'actual_hours' => $actualHours, 'status' => 'done', 'block_reason' => null, ])->save(); } public function block(string $reason, ?string $note = null): void { if (! array_key_exists($reason, self::BLOCK_REASONS)) return; $this->forceFill([ 'mechanic_status' => 'blocked', 'block_reason' => $reason, 'block_note' => $note, 'paused_at' => null, ])->save(); } /** 'green' if faster than norm, 'amber' if 30%+ over, 'red' if 100%+ over. */ public function efficiencyClass(): ?string { if ((float) $this->actual_hours <= 0 || (float) $this->hours <= 0) return null; $ratio = (float) $this->actual_hours / (float) $this->hours; return match (true) { $ratio <= 1.0 => 'green', $ratio <= 1.3 => 'amber', default => 'red', }; } public function efficiencyPct(): ?int { if ((float) $this->actual_hours <= 0 || (float) $this->hours <= 0) return null; return (int) round(100 * (float) $this->actual_hours / (float) $this->hours); } public function isPendingApproval(): bool { return $this->requires_approval && $this->approved_at === null && $this->declined_at === null; } public function workOrder(): BelongsTo { return $this->belongsTo(WorkOrder::class); } public function labor(): BelongsTo { return $this->belongsTo(Labor::class); } public function master(): BelongsTo { return $this->belongsTo(User::class, 'master_id'); } protected static function booted(): void { static::saving(function (self $row) { $row->total = round((float) $row->hours * (float) $row->price_per_hour, 2); if ($row->requires_approval && empty($row->approval_token)) { $row->approval_token = \Illuminate\Support\Str::random(24); } }); static::saved(fn (self $row) => $row->workOrder?->recalcTotal()); static::deleted(fn (self $row) => $row->workOrder?->recalcTotal()); } }