*/ protected $fillable = [ 'name', 'slug', 'description', 'status', 'opening_date', 'closing_date', 'operating_season', 'size_acres', 'website', 'operator_id', 'total_areas', 'operating_areas', 'closed_areas', 'total_rides', 'total_coasters', 'total_flat_rides', 'total_water_rides', 'total_daily_capacity', 'average_wait_time', 'average_rating', 'total_rides_operated', 'total_rides_retired', 'last_expansion_date', 'last_major_update', 'utilization_rate', 'peak_daily_attendance', 'guest_satisfaction', ]; /** * The attributes that should be cast. * * @var array */ protected $casts = [ 'status' => ParkStatus::class, 'opening_date' => 'date', 'closing_date' => 'date', 'size_acres' => 'decimal:2', 'total_areas' => 'integer', 'operating_areas' => 'integer', 'closed_areas' => 'integer', 'total_rides' => 'integer', 'total_coasters' => 'integer', 'total_flat_rides' => 'integer', 'total_water_rides' => 'integer', 'total_daily_capacity' => 'integer', 'average_wait_time' => 'integer', 'average_rating' => 'decimal:2', 'total_rides_operated' => 'integer', 'total_rides_retired' => 'integer', 'last_expansion_date' => 'date', 'last_major_update' => 'date', 'utilization_rate' => 'decimal:2', 'peak_daily_attendance' => 'integer', 'guest_satisfaction' => 'decimal:2', ]; /** * Get the operator that owns the park. */ public function operator(): BelongsTo { return $this->belongsTo(Operator::class); } /** * Get the owner that owns the park. * @deprecated Use operator() relationship instead until Company model is implemented */ public function owner(): BelongsTo { return $this->belongsTo(Operator::class, 'owner_id'); } /** * Get the areas in the park. */ public function areas(): HasMany { return $this->hasMany(ParkArea::class); } /** * Get the photos for the park. */ public function photos(): MorphMany { return $this->morphMany(Photo::class, 'photoable'); } /** * Get the featured photo for the park. */ public function featuredPhoto() { return $this->photos()->where('is_featured', true)->first() ?? $this->photos()->orderBy('position')->first(); } /** * Get the URL of the featured photo or a default image. */ public function getFeaturedPhotoUrlAttribute(): string { $photo = $this->featuredPhoto(); return $photo ? $photo->url : asset('images/placeholders/default-park.jpg'); } /** * Add a photo to the park. * * @param array $attributes * @return \App\Models\Photo */ public function addPhoto(array $attributes): Photo { // Set position to be the last in the collection if not specified if (!isset($attributes['position'])) { $lastPosition = $this->photos()->max('position') ?? 0; $attributes['position'] = $lastPosition + 1; } // If this is the first photo or is_featured is true, make it featured if ($this->photos()->count() === 0 || ($attributes['is_featured'] ?? false)) { $attributes['is_featured'] = true; } else { $attributes['is_featured'] = false; } return $this->photos()->create($attributes); } /** * Set a photo as the featured photo. * * @param \App\Models\Photo|int $photo * @return bool */ public function setFeaturedPhoto($photo): bool { if (is_numeric($photo)) { $photo = $this->photos()->findOrFail($photo); } return $photo->setAsFeatured(); } /** * Reorder photos. * * @param array $photoIds Ordered array of photo IDs * @return bool */ public function reorderPhotos(array $photoIds): bool { // Begin transaction DB::beginTransaction(); try { foreach ($photoIds as $position => $photoId) { $this->photos()->where('id', $photoId)->update(['position' => $position + 1]); } DB::commit(); return true; } catch (\Exception $e) { DB::rollBack(); return false; } } /** * Get formatted website URL (ensures proper URL format). */ public function getWebsiteUrlAttribute(): string { if (!$this->website) { return ''; } $website = $this->website; if (!str_starts_with($website, 'http://') && !str_starts_with($website, 'https://')) { $website = 'https://' . $website; } return $website; } /** * Get the status display classes for Tailwind CSS. */ public function getStatusClassesAttribute(): string { return $this->status->getStatusClasses(); } /** * Get a formatted display of the park's size. */ public function getSizeDisplayAttribute(): string { return $this->size_acres ? number_format($this->size_acres, 1) . ' acres' : 'Unknown size'; } /** * Get the formatted opening year. */ public function getOpeningYearAttribute(): ?string { return $this->opening_date?->format('Y'); } /** * Get a brief description suitable for cards and previews. */ public function getBriefDescriptionAttribute(): string { $description = $this->description ?? ''; return strlen($description) > 200 ? substr($description, 0, 200) . '...' : $description; } /** * Scope a query to only include operating parks. */ public function scopeOperating($query) { return $query->where('status', ParkStatus::OPERATING); } /** * Scope a query to only include closed parks. */ public function scopeClosed($query) { return $query->whereIn('status', [ ParkStatus::CLOSED_TEMP, ParkStatus::CLOSED_PERM, ParkStatus::DEMOLISHED, ]); } /** * Get the formatted location for the park. */ public function getFormattedLocationAttribute(): string { return $this->formatted_address ?? ''; } /** * Get the absolute URL for the park detail page. */ public function getAbsoluteUrl(): string { return route('parks.show', ['slug' => $this->slug]); } /** * Get a park by its current or historical slug. * * @param string $slug * @return array{0: \App\Models\Park|null, 1: bool} Park and whether a historical slug was used */ public static function getBySlug(string $slug): array { // Try current slug $park = static::where('slug', $slug)->first(); if ($park) { return [$park, false]; } // Try historical slug $slugHistory = SlugHistory::where('slug', $slug) ->where('sluggable_type', static::class) ->latest() ->first(); if ($slugHistory) { $park = static::find($slugHistory->sluggable_id); return [$park, true]; } return [null, false]; } /** * Boot the model. */ protected static function boot() { parent::boot(); static::created(function (Park $park) { $park->operator?->updateStatistics(); }); static::deleted(function (Park $park) { $park->operator?->updateStatistics(); }); } }