mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 07:31:09 -05:00
320 lines
8.2 KiB
PHP
320 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Enums\ParkStatus;
|
|
use App\Traits\HasLocation;
|
|
use App\Traits\HasSlugHistory;
|
|
use App\Traits\HasParkStatistics;
|
|
use App\Traits\TrackedModel;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use App\Models\Company;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
|
|
class Park extends Model
|
|
{
|
|
use HasFactory, HasSlugHistory, HasParkStatistics, HasLocation;
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
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<string, string>
|
|
*/
|
|
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 owner(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Company::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<string, mixed> $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<int, int> $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();
|
|
});
|
|
}
|
|
} |