Add enums for ReviewStatus, TrackMaterial, LaunchType, RideCategory, and RollerCoasterType; implement Designer and RideModel models; create migrations for ride_models and helpful_votes tables; enhance RideGalleryComponent documentation

This commit is contained in:
pacnpal
2025-02-25 20:37:19 -05:00
parent 8951e59f49
commit 64b0e90a27
35 changed files with 3157 additions and 1 deletions

38
app/Enums/LaunchType.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace App\Enums;
enum LaunchType: string
{
case CHAIN = 'CHAIN';
case LSM = 'LSM';
case HYDRAULIC = 'HYDRAULIC';
case GRAVITY = 'GRAVITY';
case OTHER = 'OTHER';
public function label(): string
{
return match($this) {
self::CHAIN => 'Chain Lift',
self::LSM => 'LSM Launch',
self::HYDRAULIC => 'Hydraulic Launch',
self::GRAVITY => 'Gravity',
self::OTHER => 'Other',
};
}
public static function values(): array
{
return array_column(self::cases(), 'value');
}
public static function labels(): array
{
return array_map(fn($case) => $case->label(), self::cases());
}
public static function options(): array
{
return array_combine(self::values(), self::labels());
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Enums;
enum ReviewStatus: string
{
case PENDING = 'pending';
case APPROVED = 'approved';
case REJECTED = 'rejected';
public function label(): string
{
return match($this) {
self::PENDING => 'Pending',
self::APPROVED => 'Approved',
self::REJECTED => 'Rejected',
};
}
public function color(): string
{
return match($this) {
self::PENDING => 'yellow',
self::APPROVED => 'green',
self::REJECTED => 'red',
};
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Enums;
enum RideCategory: string
{
case SELECT = '';
case ROLLER_COASTER = 'RC';
case DARK_RIDE = 'DR';
case FLAT_RIDE = 'FR';
case WATER_RIDE = 'WR';
case TRANSPORT = 'TR';
case OTHER = 'OT';
public function label(): string
{
return match($this) {
self::SELECT => 'Select ride type',
self::ROLLER_COASTER => 'Roller Coaster',
self::DARK_RIDE => 'Dark Ride',
self::FLAT_RIDE => 'Flat Ride',
self::WATER_RIDE => 'Water Ride',
self::TRANSPORT => 'Transport',
self::OTHER => 'Other',
};
}
public static function values(): array
{
return array_column(self::cases(), 'value');
}
public static function labels(): array
{
return array_map(fn($case) => $case->label(), self::cases());
}
public static function options(): array
{
return array_combine(self::values(), self::labels());
}
}

71
app/Enums/RideStatus.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
namespace App\Enums;
enum RideStatus: string
{
case SELECT = '';
case OPERATING = 'OPERATING';
case CLOSED_TEMP = 'CLOSED_TEMP';
case SBNO = 'SBNO';
case CLOSING = 'CLOSING';
case CLOSED_PERM = 'CLOSED_PERM';
case UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION';
case DEMOLISHED = 'DEMOLISHED';
case RELOCATED = 'RELOCATED';
public function label(): string
{
return match($this) {
self::SELECT => 'Select status',
self::OPERATING => 'Operating',
self::CLOSED_TEMP => 'Temporarily Closed',
self::SBNO => 'Standing But Not Operating',
self::CLOSING => 'Closing',
self::CLOSED_PERM => 'Permanently Closed',
self::UNDER_CONSTRUCTION => 'Under Construction',
self::DEMOLISHED => 'Demolished',
self::RELOCATED => 'Relocated',
};
}
public function isPostClosingStatus(): bool
{
return in_array($this, [
self::SBNO,
self::CLOSED_PERM,
]);
}
public static function postClosingStatuses(): array
{
return [
self::SBNO,
self::CLOSED_PERM,
];
}
public static function values(): array
{
return array_column(self::cases(), 'value');
}
public static function labels(): array
{
return array_map(fn($case) => $case->label(), self::cases());
}
public static function options(): array
{
return array_combine(self::values(), self::labels());
}
public static function postClosingOptions(): array
{
$statuses = array_filter(self::cases(), fn($case) => $case->isPostClosingStatus());
return array_combine(
array_column($statuses, 'value'),
array_map(fn($case) => $case->label(), $statuses)
);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Enums;
enum RollerCoasterType: string
{
case SITDOWN = 'SITDOWN';
case INVERTED = 'INVERTED';
case FLYING = 'FLYING';
case STANDUP = 'STANDUP';
case WING = 'WING';
case DIVE = 'DIVE';
case FAMILY = 'FAMILY';
case WILD_MOUSE = 'WILD_MOUSE';
case SPINNING = 'SPINNING';
case FOURTH_DIMENSION = 'FOURTH_DIMENSION';
case OTHER = 'OTHER';
public function label(): string
{
return match($this) {
self::SITDOWN => 'Sit Down',
self::INVERTED => 'Inverted',
self::FLYING => 'Flying',
self::STANDUP => 'Stand Up',
self::WING => 'Wing',
self::DIVE => 'Dive',
self::FAMILY => 'Family',
self::WILD_MOUSE => 'Wild Mouse',
self::SPINNING => 'Spinning',
self::FOURTH_DIMENSION => '4th Dimension',
self::OTHER => 'Other',
};
}
public static function values(): array
{
return array_column(self::cases(), 'value');
}
public static function labels(): array
{
return array_map(fn($case) => $case->label(), self::cases());
}
public static function options(): array
{
return array_combine(self::values(), self::labels());
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Enums;
enum TrackMaterial: string
{
case STEEL = 'STEEL';
case WOOD = 'WOOD';
case HYBRID = 'HYBRID';
public function label(): string
{
return match($this) {
self::STEEL => 'Steel',
self::WOOD => 'Wood',
self::HYBRID => 'Hybrid',
};
}
public static function values(): array
{
return array_column(self::cases(), 'value');
}
public static function labels(): array
{
return array_map(fn($case) => $case->label(), self::cases());
}
public static function options(): array
{
return array_combine(self::values(), self::labels());
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use Livewire\Component;
class RideDetailComponent extends Component
{
/** @var Ride */
public Ride $ride;
/** @var bool */
public bool $showCoasterStats = false;
public function mount(Ride $ride): void
{
$this->ride = $ride->load([
'park',
'parkArea',
'manufacturer',
'designer',
'rideModel',
'coasterStats',
]);
$this->showCoasterStats = $ride->category === 'RC' && $ride->coasterStats !== null;
}
public function toggleCoasterStats(): void
{
$this->showCoasterStats = !$this->showCoasterStats;
}
public function render()
{
return view('livewire.ride-detail');
}
/**
* Get the opening year of the ride.
*/
public function getOpeningYearAttribute(): ?string
{
return $this->ride->opening_date?->format('Y');
}
/**
* Get a formatted date range for the ride's operation.
*/
public function getOperatingPeriodAttribute(): string
{
$start = $this->ride->opening_date?->format('Y');
$end = $this->ride->closing_date?->format('Y');
if (!$start) {
return 'Unknown dates';
}
return $end ? "{$start} - {$end}" : "{$start} - Present";
}
/**
* Get the ride's status badge color classes.
*/
public function getStatusColorClasses(): string
{
return match($this->ride->status) {
'OPERATING' => 'bg-green-100 text-green-800',
'CLOSED_TEMP' => 'bg-yellow-100 text-yellow-800',
'SBNO' => 'bg-red-100 text-red-800',
'CLOSING' => 'bg-orange-100 text-orange-800',
'CLOSED_PERM' => 'bg-gray-100 text-gray-800',
'UNDER_CONSTRUCTION' => 'bg-blue-100 text-blue-800',
'DEMOLISHED' => 'bg-gray-100 text-gray-800',
'RELOCATED' => 'bg-purple-100 text-purple-800',
default => 'bg-gray-100 text-gray-800',
};
}
/**
* Format a measurement value with units.
*/
public function formatMeasurement(?float $value, string $unit): string
{
return $value !== null ? number_format($value, 2) . ' ' . $unit : 'N/A';
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use App\Models\Park;
use App\Models\ParkArea;
use App\Models\RideModel;
use App\Models\Designer;
use App\Models\Manufacturer;
use App\Enums\RideCategory;
use App\Enums\RideStatus;
use Livewire\Component;
use Illuminate\Validation\Rules\Enum;
class RideFormComponent extends Component
{
public ?Ride $ride = null;
/** @var array<string, mixed> */
public array $state = [];
/** @var array<string, mixed>|null */
public ?array $coasterStats = null;
/** @var int|null */
public ?int $parkId = null;
protected function rules(): array
{
return [
'state.name' => ['required', 'string', 'max:255'],
'state.park_id' => ['required', 'exists:parks,id'],
'state.park_area_id' => ['nullable', 'exists:park_areas,id'],
'state.manufacturer_id' => ['nullable', 'exists:manufacturers,id'],
'state.designer_id' => ['nullable', 'exists:designers,id'],
'state.ride_model_id' => ['nullable', 'exists:ride_models,id'],
'state.category' => ['required', new Enum(RideCategory::class)],
'state.status' => ['required', new Enum(RideStatus::class)],
'state.post_closing_status' => ['nullable', new Enum(RideStatus::class)],
'state.description' => ['nullable', 'string'],
'state.opening_date' => ['nullable', 'date'],
'state.closing_date' => ['nullable', 'date'],
'state.status_since' => ['nullable', 'date'],
'state.min_height_in' => ['nullable', 'integer', 'min:0'],
'state.max_height_in' => ['nullable', 'integer', 'min:0'],
'state.capacity_per_hour' => ['nullable', 'integer', 'min:0'],
'state.ride_duration_seconds' => ['nullable', 'integer', 'min:0'],
];
}
public function mount(?Ride $ride = null): void
{
$this->ride = $ride;
if ($ride) {
$this->state = $ride->only([
'name', 'park_id', 'park_area_id', 'manufacturer_id',
'designer_id', 'ride_model_id', 'category', 'status',
'post_closing_status', 'description', 'opening_date',
'closing_date', 'status_since', 'min_height_in',
'max_height_in', 'capacity_per_hour', 'ride_duration_seconds',
]);
$this->parkId = $ride->park_id;
if ($ride->coasterStats) {
$this->coasterStats = $ride->coasterStats->toArray();
}
}
}
public function updated(string $field): void
{
$this->validateOnly($field);
if ($field === 'state.category') {
$this->coasterStats = $this->state['category'] === RideCategory::ROLLER_COASTER->value
? ($this->coasterStats ?? [])
: null;
}
}
public function save(): void
{
$this->validate();
if (!$this->ride) {
$this->ride = new Ride();
}
$this->ride->fill($this->state)->save();
// Handle coaster stats if applicable
if ($this->state['category'] === RideCategory::ROLLER_COASTER->value && $this->coasterStats) {
$this->ride->coasterStats()->updateOrCreate(
['ride_id' => $this->ride->id],
$this->coasterStats
);
} elseif ($this->ride->coasterStats) {
$this->ride->coasterStats->delete();
}
session()->flash('message', 'Ride saved successfully.');
$this->redirect(route('rides.show', $this->ride));
}
public function getParksProperty()
{
return Park::orderBy('name')->get();
}
public function getParkAreasProperty()
{
return $this->parkId
? ParkArea::where('park_id', $this->parkId)->orderBy('name')->get()
: collect();
}
public function getManufacturersProperty()
{
return Manufacturer::orderBy('name')->get();
}
public function getDesignersProperty()
{
return Designer::orderBy('name')->get();
}
public function getRideModelsProperty()
{
return RideModel::orderBy('name')->get();
}
public function render()
{
return view('livewire.ride-form');
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Rule;
class RideGalleryComponent extends Component
{
use WithFileUploads;
/** @var Ride */
public Ride $ride;
/** @var \Illuminate\Http\UploadedFile */
#[Rule('image|max:10240')] // 10MB Max
public $photo;
/** @var string|null */
public ?string $caption = null;
/** @var bool */
public bool $showUploadForm = false;
public function mount(Ride $ride): void
{
$this->ride = $ride;
}
public function toggleUploadForm(): void
{
$this->showUploadForm = !$this->showUploadForm;
$this->reset(['photo', 'caption']);
}
public function save(): void
{
$this->validate([
'photo' => 'required|image|max:10240',
'caption' => 'nullable|string|max:255',
]);
$path = $this->photo->store('ride-photos', 'public');
$this->ride->photos()->create([
'path' => $path,
'caption' => $this->caption,
'uploaded_by' => Auth::id(),
]);
$this->reset(['photo', 'caption']);
$this->showUploadForm = false;
session()->flash('message', 'Photo uploaded successfully.');
}
public function deletePhoto(int $photoId): void
{
$photo = $this->ride->photos()->findOrFail($photoId);
if ($photo->uploaded_by === Auth::id() || Gate::allows('delete-any-photo')) {
Storage::disk('public')->delete($photo->path);
$photo->delete();
session()->flash('message', 'Photo deleted successfully.');
} else {
session()->flash('error', 'You do not have permission to delete this photo.');
}
}
public function setFeaturedPhoto(int $photoId): void
{
if (Gate::allows('edit', $this->ride)) {
$this->ride->featured_photo_id = $photoId;
$this->ride->save();
session()->flash('message', 'Featured photo updated.');
} else {
session()->flash('error', 'You do not have permission to set the featured photo.');
}
}
public function render()
{
return view('livewire.ride-gallery', [
'photos' => $this->ride->photos()->latest()->paginate(12),
]);
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Livewire;
use App\Enums\RideCategory;
use App\Models\Ride;
use Livewire\Component;
use Livewire\WithPagination;
class RideListComponent extends Component
{
use WithPagination;
/** @var string */
public $search = '';
/** @var string */
public $category = '';
/** @var string */
public $sortField = 'name';
/** @var string */
public $sortDirection = 'asc';
/** @var string */
public $viewMode = 'grid';
/**
* Update search term and reset pagination.
*/
public function updatingSearch(): void
{
$this->resetPage();
}
/**
* Update category filter and reset pagination.
*/
public function updatingCategory(): void
{
$this->resetPage();
}
/**
* Sort results by the given field.
*/
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortDirection = 'asc';
}
$this->sortField = $field;
}
/**
* Toggle between grid and list views.
*/
public function toggleView(): void
{
$this->viewMode = $this->viewMode === 'grid' ? 'list' : 'grid';
}
/**
* Get all available ride categories.
*/
public function getCategoriesProperty(): array
{
return RideCategory::options();
}
/**
* Get the filtered and sorted rides query.
*/
private function getRidesQuery()
{
return Ride::query()
->when($this->search, fn($query, $search) =>
$query->where('name', 'like', "%{$search}%")
)
->when($this->category, fn($query, $category) =>
$query->where('category', $category)
)
->orderBy($this->sortField, $this->sortDirection);
}
/**
* Render the component.
*/
public function render()
{
$rides = $this->getRidesQuery()->paginate(12);
return view('livewire.ride-list', [
'rides' => $rides,
]);
}
}

43
app/Models/Designer.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Designer extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'slug',
'bio',
];
/**
* Boot the model.
*/
protected static function boot()
{
parent::boot();
static::creating(function ($designer) {
if (empty($designer->slug)) {
$designer->slug = Str::slug($designer->name);
}
});
}
/**
* Get the rides designed by this designer.
*/
public function rides(): HasMany
{
return $this->hasMany(Ride::class);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class HelpfulVote extends Model
{
protected $fillable = [
'review_id',
'user_id',
];
// Relationships
public function review(): BelongsTo
{
return $this->belongsTo(Review::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
// Helper method to toggle vote
public static function toggle(int $reviewId, int $userId): bool
{
$vote = static::where([
'review_id' => $reviewId,
'user_id' => $userId,
])->first();
if ($vote) {
$vote->delete();
return false;
}
static::create([
'review_id' => $reviewId,
'user_id' => $userId,
]);
return true;
}
}

113
app/Models/Review.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
namespace App\Models;
use App\Enums\ReviewStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Auth;
class Review extends Model
{
protected $fillable = [
'ride_id',
'user_id',
'rating',
'title',
'content',
'status',
'moderated_at',
'moderated_by',
'helpful_votes_count',
];
protected $casts = [
'status' => ReviewStatus::class,
'moderated_at' => 'datetime',
'rating' => 'integer',
'helpful_votes_count' => 'integer',
];
// Relationships
public function ride(): BelongsTo
{
return $this->belongsTo(Ride::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function moderator(): BelongsTo
{
return $this->belongsTo(User::class, 'moderated_by');
}
public function helpfulVotes(): HasMany
{
return $this->hasMany(HelpfulVote::class);
}
// Scopes
public function scopePending($query)
{
return $query->where('status', ReviewStatus::PENDING);
}
public function scopeApproved($query)
{
return $query->where('status', ReviewStatus::APPROVED);
}
public function scopeRejected($query)
{
return $query->where('status', ReviewStatus::REJECTED);
}
public function scopeByRide($query, $rideId)
{
return $query->where('ride_id', $rideId);
}
public function scopeByUser($query, $userId)
{
return $query->where('user_id', $userId);
}
// Methods
public function approve(): bool
{
return $this->moderate(ReviewStatus::APPROVED);
}
public function reject(): bool
{
return $this->moderate(ReviewStatus::REJECTED);
}
public function moderate(ReviewStatus $status, ?int $moderatorId = null): bool
{
return $this->update([
'status' => $status,
'moderated_at' => now(),
'moderated_by' => $moderatorId ?? Auth::id(),
]);
}
public function toggleHelpfulVote(int $userId): bool
{
$vote = $this->helpfulVotes()->where('user_id', $userId)->first();
if ($vote) {
$vote->delete();
$this->decrement('helpful_votes_count');
return false;
}
$this->helpfulVotes()->create(['user_id' => $userId]);
$this->increment('helpful_votes_count');
return true;
}
}

118
app/Models/Ride.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
namespace App\Models;
use App\Enums\RideCategory;
use App\Enums\RideStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Auth;
class Ride extends Model
{
protected $fillable = [
'name',
'description',
'status',
'category',
'opening_date',
'closing_date',
'park_id',
'park_area_id',
'manufacturer_id',
'designer_id',
'ride_model_id',
'min_height_in',
'max_height_in',
'capacity_per_hour',
'ride_duration_seconds',
];
protected $casts = [
'status' => RideStatus::class,
'category' => RideCategory::class,
'opening_date' => 'date',
'closing_date' => 'date',
'min_height_in' => 'integer',
'max_height_in' => 'integer',
'capacity_per_hour' => 'integer',
'ride_duration_seconds' => 'integer',
];
// Base Relationships
public function park(): BelongsTo
{
return $this->belongsTo(Park::class);
}
public function parkArea(): BelongsTo
{
return $this->belongsTo(ParkArea::class);
}
public function manufacturer(): BelongsTo
{
return $this->belongsTo(Manufacturer::class);
}
public function designer(): BelongsTo
{
return $this->belongsTo(Designer::class);
}
public function rideModel(): BelongsTo
{
return $this->belongsTo(RideModel::class);
}
public function coasterStats(): HasOne
{
return $this->hasOne(RollerCoasterStats::class);
}
// Review Relationships
public function reviews(): HasMany
{
return $this->hasMany(Review::class);
}
public function approvedReviews(): HasMany
{
return $this->reviews()->approved();
}
// Review Methods
public function getAverageRatingAttribute(): ?float
{
return $this->approvedReviews()->avg('rating');
}
public function getReviewCountAttribute(): int
{
return $this->approvedReviews()->count();
}
public function canBeReviewedBy(?int $userId): bool
{
if (!$userId) {
return false;
}
return !$this->reviews()
->where('user_id', $userId)
->exists();
}
public function addReview(array $data): Review
{
return $this->reviews()->create([
'user_id' => Auth::id(),
'rating' => $data['rating'],
'title' => $data['title'] ?? null,
'content' => $data['content'],
'status' => ReviewStatus::PENDING,
]);
}
}

58
app/Models/RideModel.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace App\Models;
use App\Enums\RideCategory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class RideModel extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'manufacturer_id',
'description',
'category',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'category' => RideCategory::class,
];
/**
* Get the manufacturer that produces this ride model.
*/
public function manufacturer(): BelongsTo
{
return $this->belongsTo(Manufacturer::class);
}
/**
* Get the rides that are instances of this model.
*/
public function rides(): HasMany
{
return $this->hasMany(Ride::class);
}
/**
* Get the full name of the ride model including manufacturer.
*/
public function getFullNameAttribute(): string
{
return $this->manufacturer
? "{$this->manufacturer->name} {$this->name}"
: $this->name;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Models;
use App\Enums\TrackMaterial;
use App\Enums\RollerCoasterType;
use App\Enums\LaunchType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class RollerCoasterStats extends Model
{
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'ride_id',
'height_ft',
'length_ft',
'speed_mph',
'inversions',
'ride_time_seconds',
'track_type',
'track_material',
'roller_coaster_type',
'max_drop_height_ft',
'launch_type',
'train_style',
'trains_count',
'cars_per_train',
'seats_per_car',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'height_ft' => 'decimal:2',
'length_ft' => 'decimal:2',
'speed_mph' => 'decimal:2',
'max_drop_height_ft' => 'decimal:2',
'track_material' => TrackMaterial::class,
'roller_coaster_type' => RollerCoasterType::class,
'launch_type' => LaunchType::class,
'trains_count' => 'integer',
'cars_per_train' => 'integer',
'seats_per_car' => 'integer',
];
/**
* Get the ride that owns these statistics.
*/
public function ride(): BelongsTo
{
return $this->belongsTo(Ride::class);
}
/**
* Calculate total seating capacity.
*/
public function getTotalSeatsAttribute(): ?int
{
if ($this->trains_count && $this->cars_per_train && $this->seats_per_car) {
return $this->trains_count * $this->cars_per_train * $this->seats_per_car;
}
return null;
}
}