'decimal:7', 'longitude' => 'decimal:7', 'meta' => 'array', 'changed_at' => 'datetime', ]; protected static function boot() { parent::boot(); static::creating(function ($history) { // Calculate durations $delivery = Delivery::find($history->delivery_id); if ($delivery) { // Duration from delivery creation $history->duration_from_start = $delivery->created_at->diffInSeconds(now()); // Duration from previous status $previousHistory = static::where('delivery_id', $history->delivery_id) ->orderBy('changed_at', 'desc') ->first(); if ($previousHistory) { $history->duration_from_previous = $previousHistory->changed_at->diffInSeconds(now()); } } }); } /* |-------------------------------------------------------------------------- | Relationships |-------------------------------------------------------------------------- */ public function delivery(): BelongsTo { return $this->belongsTo(Delivery::class, 'delivery_id'); } public function rider(): BelongsTo { return $this->belongsTo(Rider::class, 'rider_id'); } /* |-------------------------------------------------------------------------- | Scopes |-------------------------------------------------------------------------- */ public function scopeForDelivery($query, int $deliveryId) { return $query->where('delivery_id', $deliveryId); } public function scopeByStatus($query, string $status) { return $query->where('to_status', $status); } public function scopeRecent($query) { return $query->orderBy('changed_at', 'desc'); } /* |-------------------------------------------------------------------------- | Accessors |-------------------------------------------------------------------------- */ public function getFromStatusEnumAttribute(): ?DeliveryStatus { return $this->from_status ? DeliveryStatus::tryFrom($this->from_status) : null; } public function getToStatusEnumAttribute(): DeliveryStatus { return DeliveryStatus::from($this->to_status); } public function getFromStatusLabelAttribute(): ?string { return $this->from_status_enum?->label(); } public function getToStatusLabelAttribute(): string { return $this->to_status_enum->label(); } /* |-------------------------------------------------------------------------- | Methods |-------------------------------------------------------------------------- */ public function getDurationFormatted(): string { if (! $this->duration_from_previous) { return '-'; } $seconds = $this->duration_from_previous; if ($seconds < 60) { return "{$seconds}s"; } $minutes = floor($seconds / 60); $remainingSeconds = $seconds % 60; if ($minutes < 60) { return "{$minutes}m {$remainingSeconds}s"; } $hours = floor($minutes / 60); $remainingMinutes = $minutes % 60; return "{$hours}h {$remainingMinutes}m"; } public function hasLocation(): bool { return $this->latitude !== null && $this->longitude !== null; } public function getCoordinates(): ?array { if (! $this->hasLocation()) { return null; } return [ 'lat' => $this->latitude, 'lng' => $this->longitude, ]; } public static function getTimeline(int $deliveryId): array { return static::forDelivery($deliveryId) ->orderBy('changed_at') ->get() ->map(fn ($history) => [ 'status' => $history->to_status, 'label' => $history->to_status_label, 'color' => $history->to_status_enum->color(), 'icon' => $history->to_status_enum->icon(), 'changed_at' => $history->changed_at->toIso8601String(), 'changed_at_human' => $history->changed_at->diffForHumans(), 'duration' => $history->getDurationFormatted(), 'notes' => $history->notes, 'location' => $history->getCoordinates(), 'changed_by' => $history->changed_by_type, ]) ->toArray(); } }