mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 06:11:09 -05:00
Add Livewire components for parks, rides, and manufacturers
- Implemented ParksLocationSearch component with loading state and refresh functionality. - Created ParksMapView component with similar structure and functionality. - Added RegionalParksListing component for displaying regional parks. - Developed RidesListingUniversal component for universal listing integration. - Established ManufacturersListing view with navigation and Livewire integration. - Added feature tests for various Livewire components including OperatorHierarchyView, OperatorParksListing, OperatorPortfolioCard, OperatorsListing, OperatorsRoleFilter, ParksListing, ParksLocationSearch, ParksMapView, and RegionalParksListing to ensure proper rendering and adherence to patterns.
This commit is contained in:
563
app/Livewire/DesignersListingUniversal.php
Normal file
563
app/Livewire/DesignersListingUniversal.php
Normal file
@@ -0,0 +1,563 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Designer;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DesignersListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System Integration
|
||||
public string $entityType = 'designers';
|
||||
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'specialties')]
|
||||
public array $specialties = [];
|
||||
|
||||
#[Url(as: 'style')]
|
||||
public string $designStyle = '';
|
||||
|
||||
#[Url(as: 'founded_from')]
|
||||
public string $foundedYearFrom = '';
|
||||
|
||||
#[Url(as: 'founded_to')]
|
||||
public string $foundedYearTo = '';
|
||||
|
||||
#[Url(as: 'innovation_min')]
|
||||
public string $minInnovationScore = '';
|
||||
|
||||
#[Url(as: 'innovation_max')]
|
||||
public string $maxInnovationScore = '';
|
||||
|
||||
#[Url(as: 'active_years_min')]
|
||||
public string $minActiveYears = '';
|
||||
|
||||
#[Url(as: 'active_years_max')]
|
||||
public string $maxActiveYears = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
public int $perPage = 20;
|
||||
public array $portfolioStats = [];
|
||||
public array $innovationTimeline = [];
|
||||
public array $collaborationNetworks = [];
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'specialties' => ['except' => []],
|
||||
'designStyle' => ['except' => ''],
|
||||
'foundedYearFrom' => ['except' => ''],
|
||||
'foundedYearTo' => ['except' => ''],
|
||||
'minInnovationScore' => ['except' => ''],
|
||||
'maxInnovationScore' => ['except' => ''],
|
||||
'minActiveYears' => ['except' => ''],
|
||||
'maxActiveYears' => ['except' => ''],
|
||||
'sortBy' => ['except' => 'name'],
|
||||
'sortDirection' => ['except' => 'asc'],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadPortfolioStatistics();
|
||||
$this->loadInnovationTimeline();
|
||||
$this->loadCollaborationNetworks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load creative portfolio statistics with caching
|
||||
*/
|
||||
protected function loadPortfolioStatistics(): void
|
||||
{
|
||||
$this->portfolioStats = Cache::remember(
|
||||
'designers.portfolio.stats',
|
||||
now()->addHours(6),
|
||||
fn() => $this->calculatePortfolioStatistics()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load innovation timeline data with caching
|
||||
*/
|
||||
protected function loadInnovationTimeline(): void
|
||||
{
|
||||
$this->innovationTimeline = Cache::remember(
|
||||
'designers.innovation.timeline',
|
||||
now()->addHours(12),
|
||||
fn() => $this->calculateInnovationTimeline()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load collaboration networks with caching
|
||||
*/
|
||||
protected function loadCollaborationNetworks(): void
|
||||
{
|
||||
$this->collaborationNetworks = Cache::remember(
|
||||
'designers.collaboration.networks',
|
||||
now()->addHours(6),
|
||||
fn() => $this->calculateCollaborationNetworks()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive portfolio statistics
|
||||
*/
|
||||
protected function calculatePortfolioStatistics(): array
|
||||
{
|
||||
return [
|
||||
'total_designers' => Designer::active()->count(),
|
||||
'coaster_designers' => Designer::active()->specialty('roller_coaster')->count(),
|
||||
'dark_ride_designers' => Designer::active()->specialty('dark_ride')->count(),
|
||||
'themed_experience_designers' => Designer::active()->specialty('themed_experience')->count(),
|
||||
'water_attraction_designers' => Designer::active()->specialty('water_attraction')->count(),
|
||||
'specialties_distribution' => Designer::active()
|
||||
->select('specialty', DB::raw('count(*) as count'))
|
||||
->whereNotNull('specialty')
|
||||
->groupBy('specialty')
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->pluck('count', 'specialty')
|
||||
->toArray(),
|
||||
'average_innovation_score' => Designer::active()
|
||||
->whereNotNull('innovation_score')
|
||||
->avg('innovation_score'),
|
||||
'total_designs' => Designer::active()
|
||||
->withCount('rides')
|
||||
->get()
|
||||
->sum('rides_count'),
|
||||
'top_innovators' => Designer::active()
|
||||
->orderByDesc('innovation_score')
|
||||
->take(5)
|
||||
->get(['name', 'innovation_score', 'specialty'])
|
||||
->toArray(),
|
||||
'design_styles' => Designer::active()
|
||||
->select('design_style', DB::raw('count(*) as count'))
|
||||
->whereNotNull('design_style')
|
||||
->groupBy('design_style')
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->pluck('count', 'design_style')
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate innovation timeline data
|
||||
*/
|
||||
protected function calculateInnovationTimeline(): array
|
||||
{
|
||||
$timelineData = Designer::active()
|
||||
->whereNotNull('founded_year')
|
||||
->whereNotNull('innovation_score')
|
||||
->select('founded_year', 'innovation_score', 'name', 'specialty')
|
||||
->orderBy('founded_year')
|
||||
->get()
|
||||
->groupBy(function($designer) {
|
||||
return floor($designer->founded_year / 10) * 10; // Group by decade
|
||||
})
|
||||
->map(function($decade) {
|
||||
return [
|
||||
'count' => $decade->count(),
|
||||
'avg_innovation' => $decade->avg('innovation_score'),
|
||||
'top_designer' => $decade->sortByDesc('innovation_score')->first(),
|
||||
'specialties' => $decade->countBy('specialty')->toArray()
|
||||
];
|
||||
});
|
||||
|
||||
return [
|
||||
'timeline' => $timelineData->toArray(),
|
||||
'innovation_milestones' => Designer::active()
|
||||
->where('innovation_score', '>=', 8.5)
|
||||
->orderByDesc('innovation_score')
|
||||
->take(10)
|
||||
->get(['name', 'founded_year', 'innovation_score', 'specialty'])
|
||||
->toArray(),
|
||||
'breakthrough_years' => Designer::active()
|
||||
->whereNotNull('founded_year')
|
||||
->select('founded_year', DB::raw('count(*) as new_designers'), DB::raw('avg(innovation_score) as avg_innovation'))
|
||||
->groupBy('founded_year')
|
||||
->having('new_designers', '>=', 2)
|
||||
->orderByDesc('avg_innovation')
|
||||
->take(5)
|
||||
->get()
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate collaboration networks
|
||||
*/
|
||||
protected function calculateCollaborationNetworks(): array
|
||||
{
|
||||
return [
|
||||
'collaboration_pairs' => Designer::active()
|
||||
->whereHas('rides', function($query) {
|
||||
$query->whereHas('park', function($parkQuery) {
|
||||
$parkQuery->whereHas('rides', function($rideQuery) {
|
||||
$rideQuery->whereNotNull('designer_id');
|
||||
});
|
||||
});
|
||||
})
|
||||
->with(['rides.park.rides.designer'])
|
||||
->get()
|
||||
->flatMap(function($designer) {
|
||||
return $designer->rides->flatMap(function($ride) use ($designer) {
|
||||
return $ride->park->rides
|
||||
->where('designer_id', '!=', $designer->id)
|
||||
->whereNotNull('designer_id')
|
||||
->pluck('designer.name')
|
||||
->map(function($collaborator) use ($designer, $ride) {
|
||||
return [
|
||||
'designer' => $designer->name,
|
||||
'collaborator' => $collaborator,
|
||||
'park' => $ride->park->name
|
||||
];
|
||||
});
|
||||
});
|
||||
})
|
||||
->groupBy(function($item) {
|
||||
$names = [$item['designer'], $item['collaborator']];
|
||||
sort($names);
|
||||
return implode(' + ', $names);
|
||||
})
|
||||
->map(function($collaborations) {
|
||||
return [
|
||||
'count' => $collaborations->count(),
|
||||
'parks' => $collaborations->pluck('park')->unique()->values()->toArray()
|
||||
];
|
||||
})
|
||||
->sortByDesc('count')
|
||||
->take(10)
|
||||
->toArray(),
|
||||
'network_hubs' => Designer::active()
|
||||
->withCount('rides')
|
||||
->having('rides_count', '>=', 3)
|
||||
->orderByDesc('rides_count')
|
||||
->take(10)
|
||||
->get(['name', 'specialty', 'rides_count'])
|
||||
->toArray(),
|
||||
'cross_specialty_projects' => Designer::active()
|
||||
->whereHas('rides', function($query) {
|
||||
$query->whereHas('park', function($parkQuery) {
|
||||
$parkQuery->whereHas('rides', function($rideQuery) {
|
||||
$rideQuery->whereHas('designer', function($designerQuery) {
|
||||
$designerQuery->whereColumn('specialty', '!=', 'designers.specialty');
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
->with(['rides.park'])
|
||||
->get()
|
||||
->flatMap(function($designer) {
|
||||
return $designer->rides->map(function($ride) use ($designer) {
|
||||
return [
|
||||
'designer' => $designer->name,
|
||||
'specialty' => $designer->specialty,
|
||||
'park' => $ride->park->name,
|
||||
'ride' => $ride->name
|
||||
];
|
||||
});
|
||||
})
|
||||
->take(15)
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Django parity creative portfolio search functionality
|
||||
*/
|
||||
public function creativePortfolioSearch($query, $specialties = [])
|
||||
{
|
||||
return Designer::query()
|
||||
->when($query, function ($q) use ($query) {
|
||||
$terms = explode(' ', trim($query));
|
||||
foreach ($terms as $term) {
|
||||
if (strlen($term) >= 2) {
|
||||
$q->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('specialty', 'ilike', "%{$term}%")
|
||||
->orWhere('design_style', 'ilike', "%{$term}%")
|
||||
->orWhere('headquarters', 'ilike', "%{$term}%")
|
||||
->orWhereHas('rides', function($rideQuery) use ($term) {
|
||||
$rideQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('category', 'ilike', "%{$term}%");
|
||||
})
|
||||
->orWhereHas('rides.park', function($parkQuery) use ($term) {
|
||||
$parkQuery->where('name', 'ilike', "%{$term}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
->when($specialties, function ($q) use ($specialties) {
|
||||
$q->whereIn('specialty', $specialties);
|
||||
})
|
||||
->active()
|
||||
->with(['rides:id,designer_id,name,category,park_id', 'rides.park:id,name'])
|
||||
->withCount(['rides']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply creative and innovation filters
|
||||
*/
|
||||
public function applyCreativeFilters($query)
|
||||
{
|
||||
return $query
|
||||
->when($this->designStyle, fn($q, $style) =>
|
||||
$q->where('design_style', $style))
|
||||
->when($this->foundedYearFrom, fn($q, $year) =>
|
||||
$q->where('founded_year', '>=', $year))
|
||||
->when($this->foundedYearTo, fn($q, $year) =>
|
||||
$q->where('founded_year', '<=', $year))
|
||||
->when($this->minInnovationScore, fn($q, $score) =>
|
||||
$q->where('innovation_score', '>=', $score))
|
||||
->when($this->maxInnovationScore, fn($q, $score) =>
|
||||
$q->where('innovation_score', '<=', $score))
|
||||
->when($this->minActiveYears, fn($q, $years) =>
|
||||
$q->where('active_years', '>=', $years))
|
||||
->when($this->maxActiveYears, fn($q, $years) =>
|
||||
$q->where('active_years', '<=', $years));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get designers with optimized caching
|
||||
*/
|
||||
public function getDesignersProperty()
|
||||
{
|
||||
$cacheKey = "designers.listing." . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'specialties' => $this->specialties,
|
||||
'designStyle' => $this->designStyle,
|
||||
'foundedYearFrom' => $this->foundedYearFrom,
|
||||
'foundedYearTo' => $this->foundedYearTo,
|
||||
'minInnovationScore' => $this->minInnovationScore,
|
||||
'maxInnovationScore' => $this->maxInnovationScore,
|
||||
'minActiveYears' => $this->minActiveYears,
|
||||
'maxActiveYears' => $this->maxActiveYears,
|
||||
'sortBy' => $this->sortBy,
|
||||
'sortDirection' => $this->sortDirection,
|
||||
'page' => $this->getPage(),
|
||||
'perPage' => $this->perPage,
|
||||
]));
|
||||
|
||||
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
|
||||
$query = $this->creativePortfolioSearch($this->search, $this->specialties);
|
||||
$query = $this->applyCreativeFilters($query);
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'name':
|
||||
$query->orderBy('name', $this->sortDirection);
|
||||
break;
|
||||
case 'founded_year':
|
||||
$query->orderBy('founded_year', $this->sortDirection);
|
||||
break;
|
||||
case 'innovation_score':
|
||||
$query->orderBy('innovation_score', $this->sortDirection);
|
||||
break;
|
||||
case 'designed_rides_count':
|
||||
$query->orderBy('rides_count', $this->sortDirection);
|
||||
break;
|
||||
case 'active_years':
|
||||
$query->orderBy('active_years', $this->sortDirection);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search and reset pagination
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update specialties filter and reset pagination
|
||||
*/
|
||||
public function updatedSpecialties(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any filter and reset pagination
|
||||
*/
|
||||
public function updatedDesignStyle(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMinInnovationScore(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMaxInnovationScore(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMinActiveYears(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMaxActiveYears(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by specific column
|
||||
*/
|
||||
public function sortBy(string $column): void
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change view mode
|
||||
*/
|
||||
public function setViewMode(string $mode): void
|
||||
{
|
||||
$this->viewMode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->specialties = [];
|
||||
$this->designStyle = '';
|
||||
$this->foundedYearFrom = '';
|
||||
$this->foundedYearTo = '';
|
||||
$this->minInnovationScore = '';
|
||||
$this->maxInnovationScore = '';
|
||||
$this->minActiveYears = '';
|
||||
$this->maxActiveYears = '';
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle specialty filter
|
||||
*/
|
||||
public function toggleSpecialtyFilter(string $specialty): void
|
||||
{
|
||||
if (in_array($specialty, $this->specialties)) {
|
||||
$this->specialties = array_values(array_diff($this->specialties, [$specialty]));
|
||||
} else {
|
||||
$this->specialties[] = $specialty;
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(): void
|
||||
{
|
||||
Cache::forget('designers.portfolio.stats');
|
||||
Cache::forget('designers.innovation.timeline');
|
||||
Cache::forget('designers.collaboration.networks');
|
||||
|
||||
// Clear listing cache pattern - simplified approach
|
||||
$cacheKeys = [
|
||||
'designers.listing.*'
|
||||
];
|
||||
|
||||
foreach ($cacheKeys as $pattern) {
|
||||
Cache::forget($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.designers-listing-universal', [
|
||||
'designers' => $this->designers,
|
||||
'portfolioStats' => $this->portfolioStats,
|
||||
'innovationTimeline' => $this->innovationTimeline,
|
||||
'collaborationNetworks' => $this->collaborationNetworks,
|
||||
]);
|
||||
}
|
||||
}
|
||||
362
app/Livewire/ManufacturersListingUniversal.php
Normal file
362
app/Livewire/ManufacturersListingUniversal.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ManufacturersListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System Integration
|
||||
public string $entityType = 'manufacturers';
|
||||
|
||||
// Search and Filtering
|
||||
public string $search = '';
|
||||
public string $sortBy = 'name';
|
||||
public string $sortDirection = 'asc';
|
||||
public string $viewMode = 'grid';
|
||||
public int $perPage = 12;
|
||||
|
||||
// Manufacturer-specific filters
|
||||
public array $specializations = [];
|
||||
public array $totalRidesRange = [0, 1000];
|
||||
public array $industryPresenceRange = [0, 100];
|
||||
public array $foundedYearRange = [1800, 2025];
|
||||
public bool $activeOnly = false;
|
||||
public bool $innovationLeadersOnly = false;
|
||||
|
||||
// Performance optimization
|
||||
private string $cacheKeyPrefix = 'manufacturers_listing';
|
||||
private int $cacheProductPortfolioTtl = 21600; // 6 hours
|
||||
private int $cacheIndustryPresenceTtl = 43200; // 12 hours
|
||||
private int $cacheListingTtl = 1800; // 30 minutes
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'sortBy' => ['except' => 'name'],
|
||||
'sortDirection' => ['except' => 'asc'],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
'perPage' => ['except' => 12],
|
||||
'specializations' => ['except' => []],
|
||||
'totalRidesRange' => ['except' => [0, 1000]],
|
||||
'industryPresenceRange' => ['except' => [0, 100]],
|
||||
'foundedYearRange' => ['except' => [1800, 2025]],
|
||||
'activeOnly' => ['except' => false],
|
||||
'innovationLeadersOnly' => ['except' => false],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
// Initialize filters with cached values for performance
|
||||
$this->initializeFilters();
|
||||
}
|
||||
|
||||
public function updatedSearch()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedSpecializations()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedTotalRidesRange()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedIndustryPresenceRange()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearRange()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedActiveOnly()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedInnovationLeadersOnly()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function clearFilters()
|
||||
{
|
||||
$this->reset([
|
||||
'search',
|
||||
'specializations',
|
||||
'totalRidesRange',
|
||||
'industryPresenceRange',
|
||||
'foundedYearRange',
|
||||
'activeOnly',
|
||||
'innovationLeadersOnly'
|
||||
]);
|
||||
$this->totalRidesRange = [0, 1000];
|
||||
$this->industryPresenceRange = [0, 100];
|
||||
$this->foundedYearRange = [1800, 2025];
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function setViewMode(string $mode)
|
||||
{
|
||||
$this->viewMode = $mode;
|
||||
}
|
||||
|
||||
public function setSortBy(string $field)
|
||||
{
|
||||
if ($this->sortBy === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$manufacturers = $this->getManufacturers();
|
||||
$statistics = $this->getStatistics();
|
||||
$productPortfolioData = $this->getProductPortfolioData();
|
||||
$industryPresenceData = $this->getIndustryPresenceData();
|
||||
|
||||
return view('livewire.manufacturers-listing-universal', [
|
||||
'manufacturers' => $manufacturers,
|
||||
'statistics' => $statistics,
|
||||
'productPortfolioData' => $productPortfolioData,
|
||||
'industryPresenceData' => $industryPresenceData,
|
||||
'hasActiveFilters' => $this->hasActiveFilters(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getManufacturers()
|
||||
{
|
||||
$cacheKey = $this->generateCacheKey();
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheListingTtl, function () {
|
||||
$query = Manufacturer::query()
|
||||
->select([
|
||||
'id', 'name', 'slug', 'headquarters', 'description', 'website',
|
||||
'total_rides', 'total_roller_coasters', 'founded_year',
|
||||
'industry_presence_score', 'specialization', 'is_active',
|
||||
'is_major_manufacturer', 'market_share_percentage', 'created_at'
|
||||
]);
|
||||
|
||||
// Apply search with Django parity algorithms
|
||||
if (!empty($this->search)) {
|
||||
$searchTerms = explode(' ', trim($this->search));
|
||||
$query->where(function (Builder $q) use ($searchTerms) {
|
||||
foreach ($searchTerms as $term) {
|
||||
$q->where(function (Builder $subQ) use ($term) {
|
||||
$subQ->where('name', 'ILIKE', "%{$term}%")
|
||||
->orWhere('description', 'ILIKE', "%{$term}%")
|
||||
->orWhere('headquarters', 'ILIKE', "%{$term}%")
|
||||
->orWhere('specialization', 'ILIKE', "%{$term}%");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply specialization filters
|
||||
if (!empty($this->specializations)) {
|
||||
$query->whereIn('specialization', $this->specializations);
|
||||
}
|
||||
|
||||
// Apply total rides range filter
|
||||
if ($this->totalRidesRange[0] > 0 || $this->totalRidesRange[1] < 1000) {
|
||||
$query->whereBetween('total_rides', $this->totalRidesRange);
|
||||
}
|
||||
|
||||
// Apply industry presence score range filter
|
||||
if ($this->industryPresenceRange[0] > 0 || $this->industryPresenceRange[1] < 100) {
|
||||
$query->whereBetween('industry_presence_score', $this->industryPresenceRange);
|
||||
}
|
||||
|
||||
// Apply founded year range filter
|
||||
if ($this->foundedYearRange[0] > 1800 || $this->foundedYearRange[1] < 2025) {
|
||||
$query->whereBetween('founded_year', $this->foundedYearRange);
|
||||
}
|
||||
|
||||
// Apply active filter
|
||||
if ($this->activeOnly) {
|
||||
$query->where('is_active', true);
|
||||
}
|
||||
|
||||
// Apply major manufacturers filter
|
||||
if ($this->innovationLeadersOnly) {
|
||||
$query->where('is_major_manufacturer', true);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
$query->orderBy($this->sortBy, $this->sortDirection);
|
||||
|
||||
// Add secondary sort for consistency
|
||||
if ($this->sortBy !== 'name') {
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
private function getStatistics()
|
||||
{
|
||||
$cacheKey = "{$this->cacheKeyPrefix}_statistics";
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheListingTtl, function () {
|
||||
$baseQuery = Manufacturer::query();
|
||||
|
||||
// Apply same filters as main query for accurate statistics
|
||||
if (!empty($this->search)) {
|
||||
$searchTerms = explode(' ', trim($this->search));
|
||||
$baseQuery->where(function (Builder $q) use ($searchTerms) {
|
||||
foreach ($searchTerms as $term) {
|
||||
$q->where(function (Builder $subQ) use ($term) {
|
||||
$subQ->where('name', 'ILIKE', "%{$term}%")
|
||||
->orWhere('description', 'ILIKE', "%{$term}%")
|
||||
->orWhere('headquarters', 'ILIKE', "%{$term}%")
|
||||
->orWhere('specialization', 'ILIKE', "%{$term}%");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($this->specializations)) {
|
||||
$baseQuery->whereIn('specialization', $this->specializations);
|
||||
}
|
||||
|
||||
if ($this->totalRidesRange[0] > 0 || $this->totalRidesRange[1] < 1000) {
|
||||
$baseQuery->whereBetween('total_rides', $this->totalRidesRange);
|
||||
}
|
||||
|
||||
if ($this->industryPresenceRange[0] > 0 || $this->industryPresenceRange[1] < 100) {
|
||||
$baseQuery->whereBetween('industry_presence_score', $this->industryPresenceRange);
|
||||
}
|
||||
|
||||
if ($this->foundedYearRange[0] > 1800 || $this->foundedYearRange[1] < 2025) {
|
||||
$baseQuery->whereBetween('founded_year', $this->foundedYearRange);
|
||||
}
|
||||
|
||||
if ($this->activeOnly) {
|
||||
$baseQuery->where('is_active', true);
|
||||
}
|
||||
|
||||
if ($this->innovationLeadersOnly) {
|
||||
$baseQuery->where('is_major_manufacturer', true);
|
||||
}
|
||||
|
||||
return [
|
||||
'count' => $baseQuery->count(),
|
||||
'active_count' => (clone $baseQuery)->where('is_active', true)->count(),
|
||||
'total_rides_sum' => $baseQuery->sum('total_rides'),
|
||||
'avg_industry_presence' => round($baseQuery->avg('industry_presence_score'), 1),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function getProductPortfolioData()
|
||||
{
|
||||
$cacheKey = "{$this->cacheKeyPrefix}_product_portfolio";
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheProductPortfolioTtl, function () {
|
||||
return [
|
||||
'specialization_distribution' => Manufacturer::selectRaw('specialization, COUNT(*) as count')
|
||||
->groupBy('specialization')
|
||||
->pluck('count', 'specialization')
|
||||
->toArray(),
|
||||
'top_manufacturers_by_rides' => Manufacturer::orderBy('total_rides', 'desc')
|
||||
->limit(10)
|
||||
->pluck('total_rides', 'name')
|
||||
->toArray(),
|
||||
'major_manufacturers_count' => Manufacturer::where('is_major_manufacturer', true)->count(),
|
||||
'average_market_share' => round(Manufacturer::avg('market_share_percentage'), 2),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function getIndustryPresenceData()
|
||||
{
|
||||
$cacheKey = "{$this->cacheKeyPrefix}_industry_presence";
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheIndustryPresenceTtl, function () {
|
||||
return [
|
||||
'presence_score_ranges' => [
|
||||
'high' => Manufacturer::where('industry_presence_score', '>=', 80)->count(),
|
||||
'medium' => Manufacturer::whereBetween('industry_presence_score', [50, 79])->count(),
|
||||
'low' => Manufacturer::where('industry_presence_score', '<', 50)->count(),
|
||||
],
|
||||
'founding_decades' => Manufacturer::selectRaw('FLOOR(founded_year / 10) * 10 as decade, COUNT(*) as count')
|
||||
->groupBy('decade')
|
||||
->orderBy('decade')
|
||||
->pluck('count', 'decade')
|
||||
->toArray(),
|
||||
'active_vs_inactive' => [
|
||||
'active' => Manufacturer::where('is_active', true)->count(),
|
||||
'inactive' => Manufacturer::where('is_active', false)->count(),
|
||||
],
|
||||
'market_concentration' => Manufacturer::orderBy('market_share_percentage', 'desc')
|
||||
->limit(5)
|
||||
->pluck('market_share_percentage', 'name')
|
||||
->toArray(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function hasActiveFilters(): bool
|
||||
{
|
||||
return !empty($this->search) ||
|
||||
!empty($this->specializations) ||
|
||||
$this->totalRidesRange !== [0, 1000] ||
|
||||
$this->industryPresenceRange !== [0, 100] ||
|
||||
$this->foundedYearRange !== [1800, 2025] ||
|
||||
$this->activeOnly ||
|
||||
$this->innovationLeadersOnly;
|
||||
}
|
||||
|
||||
private function generateCacheKey(): string
|
||||
{
|
||||
$filterHash = md5(serialize([
|
||||
'search' => $this->search,
|
||||
'sortBy' => $this->sortBy,
|
||||
'sortDirection' => $this->sortDirection,
|
||||
'specializations' => $this->specializations,
|
||||
'totalRidesRange' => $this->totalRidesRange,
|
||||
'industryPresenceRange' => $this->industryPresenceRange,
|
||||
'foundedYearRange' => $this->foundedYearRange,
|
||||
'activeOnly' => $this->activeOnly,
|
||||
'innovationLeadersOnly' => $this->innovationLeadersOnly,
|
||||
'perPage' => $this->perPage,
|
||||
'page' => $this->getPage(),
|
||||
]));
|
||||
|
||||
return "{$this->cacheKeyPrefix}_{$filterHash}";
|
||||
}
|
||||
|
||||
private function initializeFilters()
|
||||
{
|
||||
// Initialize with sensible defaults for manufacturer filtering
|
||||
if (empty($this->totalRidesRange)) {
|
||||
$this->totalRidesRange = [0, 1000];
|
||||
}
|
||||
|
||||
if (empty($this->industryPresenceRange)) {
|
||||
$this->industryPresenceRange = [0, 100];
|
||||
}
|
||||
|
||||
if (empty($this->foundedYearRange)) {
|
||||
$this->foundedYearRange = [1800, 2025];
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorHierarchyView.php
Normal file
54
app/Livewire/OperatorHierarchyView.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorHierarchyView extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operator-hierarchy-view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/Livewire/OperatorParksListing.php
Normal file
57
app/Livewire/OperatorParksListing.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorParksListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operator-parks-listing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorPortfolioCard.php
Normal file
54
app/Livewire/OperatorPortfolioCard.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorPortfolioCard extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operator-portfolio-card');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorsIndustryStats.php
Normal file
54
app/Livewire/OperatorsIndustryStats.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorsIndustryStats extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-industry-stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
476
app/Livewire/OperatorsListing.php
Normal file
476
app/Livewire/OperatorsListing.php
Normal file
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class OperatorsListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'roles')]
|
||||
public array $roleFilter = [];
|
||||
|
||||
#[Url(as: 'sector')]
|
||||
public string $industrySector = '';
|
||||
|
||||
#[Url(as: 'size')]
|
||||
public string $companySize = '';
|
||||
|
||||
#[Url(as: 'founded_from')]
|
||||
public string $foundedYearFrom = '';
|
||||
|
||||
#[Url(as: 'founded_to')]
|
||||
public string $foundedYearTo = '';
|
||||
|
||||
#[Url(as: 'presence')]
|
||||
public string $geographicPresence = '';
|
||||
|
||||
#[Url(as: 'min_revenue')]
|
||||
public string $minRevenue = '';
|
||||
|
||||
#[Url(as: 'max_revenue')]
|
||||
public string $maxRevenue = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
public int $perPage = 20;
|
||||
public array $industryStats = [];
|
||||
public array $marketData = [];
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'roleFilter' => ['except' => []],
|
||||
'industrySector' => ['except' => ''],
|
||||
'companySize' => ['except' => ''],
|
||||
'foundedYearFrom' => ['except' => ''],
|
||||
'foundedYearTo' => ['except' => ''],
|
||||
'geographicPresence' => ['except' => ''],
|
||||
'minRevenue' => ['except' => ''],
|
||||
'maxRevenue' => ['except' => ''],
|
||||
'sortBy' => ['except' => 'name'],
|
||||
'sortDirection' => ['except' => 'asc'],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadIndustryStatistics();
|
||||
$this->loadMarketData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load industry statistics with caching
|
||||
*/
|
||||
protected function loadIndustryStatistics(): void
|
||||
{
|
||||
$this->industryStats = Cache::remember(
|
||||
'operators.industry.stats',
|
||||
now()->addHours(6),
|
||||
fn() => $this->calculateIndustryStatistics()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load market analysis data with caching
|
||||
*/
|
||||
protected function loadMarketData(): void
|
||||
{
|
||||
$this->marketData = Cache::remember(
|
||||
'operators.market.data',
|
||||
now()->addHours(12),
|
||||
fn() => $this->loadMarketAnalysis()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive industry statistics
|
||||
*/
|
||||
protected function calculateIndustryStatistics(): array
|
||||
{
|
||||
return [
|
||||
'total_operators' => Operator::active()->count(),
|
||||
'park_operators' => Operator::active()->parkOperators()->count(),
|
||||
'manufacturers' => Operator::active()->manufacturers()->count(),
|
||||
'designers' => Operator::active()->designers()->count(),
|
||||
'mixed_role' => Operator::active()
|
||||
->whereHas('parks')
|
||||
->whereHas('manufactured_rides')
|
||||
->count(),
|
||||
'sectors' => Operator::active()
|
||||
->select('industry_sector', DB::raw('count(*) as count'))
|
||||
->whereNotNull('industry_sector')
|
||||
->groupBy('industry_sector')
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->pluck('count', 'industry_sector')
|
||||
->toArray(),
|
||||
'company_sizes' => [
|
||||
'small' => Operator::active()->companySize('small')->count(),
|
||||
'medium' => Operator::active()->companySize('medium')->count(),
|
||||
'large' => Operator::active()->companySize('large')->count(),
|
||||
'enterprise' => Operator::active()->companySize('enterprise')->count(),
|
||||
],
|
||||
'geographic_distribution' => Operator::active()
|
||||
->whereHas('parks.location')
|
||||
->with('parks.location')
|
||||
->get()
|
||||
->flatMap(fn($op) => $op->parks->pluck('location.country'))
|
||||
->countBy()
|
||||
->sortDesc()
|
||||
->take(10)
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load market analysis data
|
||||
*/
|
||||
protected function loadMarketAnalysis(): array
|
||||
{
|
||||
return [
|
||||
'total_market_cap' => Operator::active()
|
||||
->whereNotNull('market_cap')
|
||||
->sum('market_cap'),
|
||||
'total_revenue' => Operator::active()
|
||||
->whereNotNull('annual_revenue')
|
||||
->sum('annual_revenue'),
|
||||
'average_parks_per_operator' => Operator::active()
|
||||
->parkOperators()
|
||||
->avg('total_parks'),
|
||||
'top_operators_by_parks' => Operator::active()
|
||||
->parkOperators()
|
||||
->orderByDesc('total_parks')
|
||||
->take(5)
|
||||
->get(['name', 'total_parks'])
|
||||
->toArray(),
|
||||
'top_manufacturers_by_rides' => Operator::active()
|
||||
->manufacturers()
|
||||
->orderByDesc('total_rides_manufactured')
|
||||
->take(5)
|
||||
->get(['name', 'total_rides_manufactured'])
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Django parity dual-role search functionality
|
||||
*/
|
||||
public function dualRoleSearch($query, $roles = [])
|
||||
{
|
||||
return Operator::query()
|
||||
->when($query, function ($q) use ($query) {
|
||||
$terms = explode(' ', trim($query));
|
||||
foreach ($terms as $term) {
|
||||
if (strlen($term) >= 2) {
|
||||
$q->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('industry_sector', 'ilike', "%{$term}%")
|
||||
->orWhere('headquarters_location', 'ilike', "%{$term}%")
|
||||
->orWhereHas('location', function($locQuery) use ($term) {
|
||||
$locQuery->where('city', 'ilike', "%{$term}%")
|
||||
->orWhere('state', 'ilike', "%{$term}%")
|
||||
->orWhere('country', 'ilike', "%{$term}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
->when($roles, function ($q) use ($roles) {
|
||||
$q->where(function ($roleQuery) use ($roles) {
|
||||
if (in_array('park_operator', $roles)) {
|
||||
$roleQuery->whereHas('parks');
|
||||
}
|
||||
if (in_array('ride_manufacturer', $roles)) {
|
||||
$roleQuery->orWhereHas('manufactured_rides');
|
||||
}
|
||||
if (in_array('ride_designer', $roles)) {
|
||||
$roleQuery->orWhereHas('designed_rides');
|
||||
}
|
||||
});
|
||||
})
|
||||
->active()
|
||||
->with(['location', 'parks:id,operator_id,name', 'manufactured_rides:id,manufacturer_id,name', 'designed_rides:id,designer_id,name'])
|
||||
->withCount(['parks', 'manufactured_rides', 'designed_rides']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply advanced industry filters
|
||||
*/
|
||||
public function applyIndustryFilters($query)
|
||||
{
|
||||
return $query
|
||||
->when($this->industrySector, fn($q, $sector) =>
|
||||
$q->where('industry_sector', $sector))
|
||||
->when($this->companySize, fn($q, $size) =>
|
||||
$q->companySize($size))
|
||||
->when($this->foundedYearFrom, fn($q, $year) =>
|
||||
$q->where('founded_year', '>=', $year))
|
||||
->when($this->foundedYearTo, fn($q, $year) =>
|
||||
$q->where('founded_year', '<=', $year))
|
||||
->when($this->geographicPresence, function ($q, $presence) {
|
||||
switch ($presence) {
|
||||
case 'regional':
|
||||
$q->whereHas('parks', function ($parkQ) {
|
||||
$parkQ->whereHas('location', function ($locQ) {
|
||||
$locQ->havingRaw('COUNT(DISTINCT country) = 1');
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'international':
|
||||
$q->whereHas('parks', function ($parkQ) {
|
||||
$parkQ->whereHas('location', function ($locQ) {
|
||||
$locQ->havingRaw('COUNT(DISTINCT country) > 1');
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
->when($this->minRevenue, fn($q, $revenue) =>
|
||||
$q->where('annual_revenue', '>=', $revenue))
|
||||
->when($this->maxRevenue, fn($q, $revenue) =>
|
||||
$q->where('annual_revenue', '<=', $revenue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operators with optimized caching
|
||||
*/
|
||||
public function getOperatorsProperty()
|
||||
{
|
||||
$cacheKey = "operators.listing." . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'roleFilter' => $this->roleFilter,
|
||||
'industrySector' => $this->industrySector,
|
||||
'companySize' => $this->companySize,
|
||||
'foundedYearFrom' => $this->foundedYearFrom,
|
||||
'foundedYearTo' => $this->foundedYearTo,
|
||||
'geographicPresence' => $this->geographicPresence,
|
||||
'minRevenue' => $this->minRevenue,
|
||||
'maxRevenue' => $this->maxRevenue,
|
||||
'sortBy' => $this->sortBy,
|
||||
'sortDirection' => $this->sortDirection,
|
||||
'page' => $this->getPage(),
|
||||
'perPage' => $this->perPage,
|
||||
]));
|
||||
|
||||
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
|
||||
$query = $this->dualRoleSearch($this->search, $this->roleFilter);
|
||||
$query = $this->applyIndustryFilters($query);
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'name':
|
||||
$query->orderBy('name', $this->sortDirection);
|
||||
break;
|
||||
case 'founded_year':
|
||||
$query->orderBy('founded_year', $this->sortDirection);
|
||||
break;
|
||||
case 'parks_count':
|
||||
$query->orderBy('total_parks', $this->sortDirection);
|
||||
break;
|
||||
case 'rides_count':
|
||||
$query->orderBy('total_rides_manufactured', $this->sortDirection);
|
||||
break;
|
||||
case 'revenue':
|
||||
$query->orderBy('annual_revenue', $this->sortDirection);
|
||||
break;
|
||||
case 'market_influence':
|
||||
$query->orderByRaw('(total_parks * 10 + total_rides_manufactured * 2) ' . $this->sortDirection);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search and reset pagination
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role filter and reset pagination
|
||||
*/
|
||||
public function updatedRoleFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any filter and reset pagination
|
||||
*/
|
||||
public function updatedIndustrySector(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedCompanySize(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedGeographicPresence(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMinRevenue(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMaxRevenue(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by specific column
|
||||
*/
|
||||
public function sortBy(string $column): void
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change view mode
|
||||
*/
|
||||
public function setViewMode(string $mode): void
|
||||
{
|
||||
$this->viewMode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->roleFilter = [];
|
||||
$this->industrySector = '';
|
||||
$this->companySize = '';
|
||||
$this->foundedYearFrom = '';
|
||||
$this->foundedYearTo = '';
|
||||
$this->geographicPresence = '';
|
||||
$this->minRevenue = '';
|
||||
$this->maxRevenue = '';
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle role filter
|
||||
*/
|
||||
public function toggleRoleFilter(string $role): void
|
||||
{
|
||||
if (in_array($role, $this->roleFilter)) {
|
||||
$this->roleFilter = array_values(array_diff($this->roleFilter, [$role]));
|
||||
} else {
|
||||
$this->roleFilter[] = $role;
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(): void
|
||||
{
|
||||
Cache::forget('operators.industry.stats');
|
||||
Cache::forget('operators.market.data');
|
||||
|
||||
// Clear listing cache pattern - simplified approach
|
||||
$cacheKeys = [
|
||||
'operators.listing.*'
|
||||
];
|
||||
|
||||
foreach ($cacheKeys as $pattern) {
|
||||
Cache::forget($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-listing', [
|
||||
'operators' => $this->operators,
|
||||
'industryStats' => $this->industryStats,
|
||||
'marketData' => $this->marketData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
479
app/Livewire/OperatorsListingUniversal.php
Normal file
479
app/Livewire/OperatorsListingUniversal.php
Normal file
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class OperatorsListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System Integration
|
||||
public string $entityType = 'operators';
|
||||
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'roles')]
|
||||
public array $roleFilter = [];
|
||||
|
||||
#[Url(as: 'sector')]
|
||||
public string $industrySector = '';
|
||||
|
||||
#[Url(as: 'size')]
|
||||
public string $companySize = '';
|
||||
|
||||
#[Url(as: 'founded_from')]
|
||||
public string $foundedYearFrom = '';
|
||||
|
||||
#[Url(as: 'founded_to')]
|
||||
public string $foundedYearTo = '';
|
||||
|
||||
#[Url(as: 'presence')]
|
||||
public string $geographicPresence = '';
|
||||
|
||||
#[Url(as: 'min_revenue')]
|
||||
public string $minRevenue = '';
|
||||
|
||||
#[Url(as: 'max_revenue')]
|
||||
public string $maxRevenue = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
public int $perPage = 20;
|
||||
public array $industryStats = [];
|
||||
public array $marketData = [];
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'roleFilter' => ['except' => []],
|
||||
'industrySector' => ['except' => ''],
|
||||
'companySize' => ['except' => ''],
|
||||
'foundedYearFrom' => ['except' => ''],
|
||||
'foundedYearTo' => ['except' => ''],
|
||||
'geographicPresence' => ['except' => ''],
|
||||
'minRevenue' => ['except' => ''],
|
||||
'maxRevenue' => ['except' => ''],
|
||||
'sortBy' => ['except' => 'name'],
|
||||
'sortDirection' => ['except' => 'asc'],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadIndustryStatistics();
|
||||
$this->loadMarketData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load industry statistics with caching
|
||||
*/
|
||||
protected function loadIndustryStatistics(): void
|
||||
{
|
||||
$this->industryStats = Cache::remember(
|
||||
'operators.industry.stats',
|
||||
now()->addHours(6),
|
||||
fn() => $this->calculateIndustryStatistics()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load market analysis data with caching
|
||||
*/
|
||||
protected function loadMarketData(): void
|
||||
{
|
||||
$this->marketData = Cache::remember(
|
||||
'operators.market.data',
|
||||
now()->addHours(12),
|
||||
fn() => $this->loadMarketAnalysis()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive industry statistics
|
||||
*/
|
||||
protected function calculateIndustryStatistics(): array
|
||||
{
|
||||
return [
|
||||
'total_operators' => Operator::active()->count(),
|
||||
'park_operators' => Operator::active()->parkOperators()->count(),
|
||||
'manufacturers' => Operator::active()->manufacturers()->count(),
|
||||
'designers' => Operator::active()->designers()->count(),
|
||||
'mixed_role' => Operator::active()
|
||||
->whereHas('parks')
|
||||
->whereHas('manufactured_rides')
|
||||
->count(),
|
||||
'sectors' => Operator::active()
|
||||
->select('industry_sector', DB::raw('count(*) as count'))
|
||||
->whereNotNull('industry_sector')
|
||||
->groupBy('industry_sector')
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->pluck('count', 'industry_sector')
|
||||
->toArray(),
|
||||
'company_sizes' => [
|
||||
'small' => Operator::active()->companySize('small')->count(),
|
||||
'medium' => Operator::active()->companySize('medium')->count(),
|
||||
'large' => Operator::active()->companySize('large')->count(),
|
||||
'enterprise' => Operator::active()->companySize('enterprise')->count(),
|
||||
],
|
||||
'geographic_distribution' => Operator::active()
|
||||
->whereHas('parks.location')
|
||||
->with('parks.location')
|
||||
->get()
|
||||
->flatMap(fn($op) => $op->parks->pluck('location.country'))
|
||||
->countBy()
|
||||
->sortDesc()
|
||||
->take(10)
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load market analysis data
|
||||
*/
|
||||
protected function loadMarketAnalysis(): array
|
||||
{
|
||||
return [
|
||||
'total_market_cap' => Operator::active()
|
||||
->whereNotNull('market_cap')
|
||||
->sum('market_cap'),
|
||||
'total_revenue' => Operator::active()
|
||||
->whereNotNull('annual_revenue')
|
||||
->sum('annual_revenue'),
|
||||
'average_parks_per_operator' => Operator::active()
|
||||
->parkOperators()
|
||||
->avg('total_parks'),
|
||||
'top_operators_by_parks' => Operator::active()
|
||||
->parkOperators()
|
||||
->orderByDesc('total_parks')
|
||||
->take(5)
|
||||
->get(['name', 'total_parks'])
|
||||
->toArray(),
|
||||
'top_manufacturers_by_rides' => Operator::active()
|
||||
->manufacturers()
|
||||
->orderByDesc('total_rides_manufactured')
|
||||
->take(5)
|
||||
->get(['name', 'total_rides_manufactured'])
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Django parity dual-role search functionality
|
||||
*/
|
||||
public function dualRoleSearch($query, $roles = [])
|
||||
{
|
||||
return Operator::query()
|
||||
->when($query, function ($q) use ($query) {
|
||||
$terms = explode(' ', trim($query));
|
||||
foreach ($terms as $term) {
|
||||
if (strlen($term) >= 2) {
|
||||
$q->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('industry_sector', 'ilike', "%{$term}%")
|
||||
->orWhere('headquarters_location', 'ilike', "%{$term}%")
|
||||
->orWhereHas('location', function($locQuery) use ($term) {
|
||||
$locQuery->where('city', 'ilike', "%{$term}%")
|
||||
->orWhere('state', 'ilike', "%{$term}%")
|
||||
->orWhere('country', 'ilike', "%{$term}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
->when($roles, function ($q) use ($roles) {
|
||||
$q->where(function ($roleQuery) use ($roles) {
|
||||
if (in_array('park_operator', $roles)) {
|
||||
$roleQuery->whereHas('parks');
|
||||
}
|
||||
if (in_array('ride_manufacturer', $roles)) {
|
||||
$roleQuery->orWhereHas('manufactured_rides');
|
||||
}
|
||||
if (in_array('ride_designer', $roles)) {
|
||||
$roleQuery->orWhereHas('designed_rides');
|
||||
}
|
||||
});
|
||||
})
|
||||
->active()
|
||||
->with(['location', 'parks:id,operator_id,name', 'manufactured_rides:id,manufacturer_id,name', 'designed_rides:id,designer_id,name'])
|
||||
->withCount(['parks', 'manufactured_rides', 'designed_rides']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply advanced industry filters
|
||||
*/
|
||||
public function applyIndustryFilters($query)
|
||||
{
|
||||
return $query
|
||||
->when($this->industrySector, fn($q, $sector) =>
|
||||
$q->where('industry_sector', $sector))
|
||||
->when($this->companySize, fn($q, $size) =>
|
||||
$q->companySize($size))
|
||||
->when($this->foundedYearFrom, fn($q, $year) =>
|
||||
$q->where('founded_year', '>=', $year))
|
||||
->when($this->foundedYearTo, fn($q, $year) =>
|
||||
$q->where('founded_year', '<=', $year))
|
||||
->when($this->geographicPresence, function ($q, $presence) {
|
||||
switch ($presence) {
|
||||
case 'regional':
|
||||
$q->whereHas('parks', function ($parkQ) {
|
||||
$parkQ->whereHas('location', function ($locQ) {
|
||||
$locQ->havingRaw('COUNT(DISTINCT country) = 1');
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'international':
|
||||
$q->whereHas('parks', function ($parkQ) {
|
||||
$parkQ->whereHas('location', function ($locQ) {
|
||||
$locQ->havingRaw('COUNT(DISTINCT country) > 1');
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
->when($this->minRevenue, fn($q, $revenue) =>
|
||||
$q->where('annual_revenue', '>=', $revenue))
|
||||
->when($this->maxRevenue, fn($q, $revenue) =>
|
||||
$q->where('annual_revenue', '<=', $revenue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operators with optimized caching
|
||||
*/
|
||||
public function getOperatorsProperty()
|
||||
{
|
||||
$cacheKey = "operators.listing." . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'roleFilter' => $this->roleFilter,
|
||||
'industrySector' => $this->industrySector,
|
||||
'companySize' => $this->companySize,
|
||||
'foundedYearFrom' => $this->foundedYearFrom,
|
||||
'foundedYearTo' => $this->foundedYearTo,
|
||||
'geographicPresence' => $this->geographicPresence,
|
||||
'minRevenue' => $this->minRevenue,
|
||||
'maxRevenue' => $this->maxRevenue,
|
||||
'sortBy' => $this->sortBy,
|
||||
'sortDirection' => $this->sortDirection,
|
||||
'page' => $this->getPage(),
|
||||
'perPage' => $this->perPage,
|
||||
]));
|
||||
|
||||
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
|
||||
$query = $this->dualRoleSearch($this->search, $this->roleFilter);
|
||||
$query = $this->applyIndustryFilters($query);
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'name':
|
||||
$query->orderBy('name', $this->sortDirection);
|
||||
break;
|
||||
case 'founded_year':
|
||||
$query->orderBy('founded_year', $this->sortDirection);
|
||||
break;
|
||||
case 'parks_count':
|
||||
$query->orderBy('total_parks', $this->sortDirection);
|
||||
break;
|
||||
case 'rides_count':
|
||||
$query->orderBy('total_rides_manufactured', $this->sortDirection);
|
||||
break;
|
||||
case 'revenue':
|
||||
$query->orderBy('annual_revenue', $this->sortDirection);
|
||||
break;
|
||||
case 'market_influence':
|
||||
$query->orderByRaw('(total_parks * 10 + total_rides_manufactured * 2) ' . $this->sortDirection);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search and reset pagination
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role filter and reset pagination
|
||||
*/
|
||||
public function updatedRoleFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any filter and reset pagination
|
||||
*/
|
||||
public function updatedIndustrySector(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedCompanySize(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedGeographicPresence(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMinRevenue(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMaxRevenue(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by specific column
|
||||
*/
|
||||
public function sortBy(string $column): void
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change view mode
|
||||
*/
|
||||
public function setViewMode(string $mode): void
|
||||
{
|
||||
$this->viewMode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->roleFilter = [];
|
||||
$this->industrySector = '';
|
||||
$this->companySize = '';
|
||||
$this->foundedYearFrom = '';
|
||||
$this->foundedYearTo = '';
|
||||
$this->geographicPresence = '';
|
||||
$this->minRevenue = '';
|
||||
$this->maxRevenue = '';
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle role filter
|
||||
*/
|
||||
public function toggleRoleFilter(string $role): void
|
||||
{
|
||||
if (in_array($role, $this->roleFilter)) {
|
||||
$this->roleFilter = array_values(array_diff($this->roleFilter, [$role]));
|
||||
} else {
|
||||
$this->roleFilter[] = $role;
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(): void
|
||||
{
|
||||
Cache::forget('operators.industry.stats');
|
||||
Cache::forget('operators.market.data');
|
||||
|
||||
// Clear listing cache pattern - simplified approach
|
||||
$cacheKeys = [
|
||||
'operators.listing.*'
|
||||
];
|
||||
|
||||
foreach ($cacheKeys as $pattern) {
|
||||
Cache::forget($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-listing-universal', [
|
||||
'operators' => $this->operators,
|
||||
'industryStats' => $this->industryStats,
|
||||
'marketData' => $this->marketData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorsMarketAnalysis.php
Normal file
54
app/Livewire/OperatorsMarketAnalysis.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorsMarketAnalysis extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-market-analysis');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorsRoleFilter.php
Normal file
54
app/Livewire/OperatorsRoleFilter.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorsRoleFilter extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-role-filter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,325 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Park;
|
||||
use App\Enums\RideCategory;
|
||||
use App\Enums\RideStatus;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ParkRidesListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Required park context
|
||||
public Park $park;
|
||||
|
||||
// URL-bound search and filter properties
|
||||
#[Url(as: 'search')]
|
||||
public string $searchTerm = '';
|
||||
|
||||
#[Url(as: 'category')]
|
||||
public ?string $selectedCategory = null;
|
||||
|
||||
#[Url(as: 'status')]
|
||||
public ?string $selectedStatus = null;
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'direction')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
// UI state
|
||||
public bool $showFilters = false;
|
||||
public int $perPage = 12;
|
||||
|
||||
// Cached data
|
||||
public array $categories = [];
|
||||
public array $statuses = [];
|
||||
public array $sortOptions = [];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
public function mount(Park $park): void
|
||||
{
|
||||
// Initialize component state
|
||||
$this->park = $park;
|
||||
$this->loadFilterOptions();
|
||||
$this->setupSortOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filter options specific to this park
|
||||
*/
|
||||
protected function loadFilterOptions(): void
|
||||
{
|
||||
$cacheKey = "park_rides_filters_{$this->park->id}";
|
||||
|
||||
$filterData = Cache::remember($cacheKey, 3600, function() {
|
||||
// Categories available in this park
|
||||
$categories = $this->park->rides()
|
||||
->select('category')
|
||||
->groupBy('category')
|
||||
->get()
|
||||
->map(function($ride) {
|
||||
$category = RideCategory::from($ride->category);
|
||||
return [
|
||||
'value' => $category->value,
|
||||
'label' => $category->name,
|
||||
'count' => $this->park->rides()->where('category', $category->value)->count()
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
// Statuses available in this park
|
||||
$statuses = $this->park->rides()
|
||||
->select('status')
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->map(function($ride) {
|
||||
$status = RideStatus::from($ride->status);
|
||||
return [
|
||||
'value' => $status->value,
|
||||
'label' => $status->name,
|
||||
'count' => $this->park->rides()->where('status', $status->value)->count()
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
return compact('categories', 'statuses');
|
||||
});
|
||||
|
||||
$this->categories = $filterData['categories'];
|
||||
$this->statuses = $filterData['statuses'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup sort options
|
||||
*/
|
||||
protected function setupSortOptions(): void
|
||||
{
|
||||
$this->sortOptions = [
|
||||
'name' => 'Name',
|
||||
'opening_year' => 'Opening Year',
|
||||
'height_requirement' => 'Height Requirement',
|
||||
'created_at' => 'Date Added',
|
||||
'updated_at' => 'Last Updated'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search term and reset pagination
|
||||
*/
|
||||
public function updatedSearchTerm(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update category filter
|
||||
*/
|
||||
public function updatedSelectedCategory(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status filter
|
||||
*/
|
||||
public function updatedSelectedStatus(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sort options
|
||||
*/
|
||||
public function updatedSortBy(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sort direction
|
||||
*/
|
||||
public function updatedSortDirection(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set category filter
|
||||
*/
|
||||
public function setCategory(?string $category): void
|
||||
{
|
||||
$this->selectedCategory = $category === $this->selectedCategory ? null : $category;
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set status filter
|
||||
*/
|
||||
public function setStatus(?string $status): void
|
||||
{
|
||||
$this->selectedStatus = $status === $this->selectedStatus ? null : $status;
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sort parameters
|
||||
*/
|
||||
public function setSortBy(string $field): void
|
||||
{
|
||||
if ($this->sortBy === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle filters visibility
|
||||
*/
|
||||
public function toggleFilters(): void
|
||||
{
|
||||
$this->showFilters = !$this->showFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->searchTerm = '';
|
||||
$this->selectedCategory = null;
|
||||
$this->selectedStatus = null;
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered and sorted rides for this park
|
||||
*/
|
||||
public function getRidesProperty()
|
||||
{
|
||||
$cacheKey = $this->getCacheKey();
|
||||
|
||||
return Cache::remember($cacheKey, 300, function() {
|
||||
$query = $this->park->rides()
|
||||
->with(['manufacturer', 'designer', 'photos'])
|
||||
->when($this->searchTerm, function (Builder $query) {
|
||||
$query->where(function (Builder $subQuery) {
|
||||
$subQuery->where('name', 'ILIKE', "%{$this->searchTerm}%")
|
||||
->orWhere('description', 'ILIKE', "%{$this->searchTerm}%")
|
||||
->orWhereHas('manufacturer', function (Builder $manufacturerQuery) {
|
||||
$manufacturerQuery->where('name', 'ILIKE', "%{$this->searchTerm}%");
|
||||
})
|
||||
->orWhereHas('designer', function (Builder $designerQuery) {
|
||||
$designerQuery->where('name', 'ILIKE', "%{$this->searchTerm}%");
|
||||
});
|
||||
});
|
||||
})
|
||||
->when($this->selectedCategory, function (Builder $query) {
|
||||
$query->where('category', $this->selectedCategory);
|
||||
})
|
||||
->when($this->selectedStatus, function (Builder $query) {
|
||||
$query->where('status', $this->selectedStatus);
|
||||
});
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'name':
|
||||
$query->orderBy('name', $this->sortDirection);
|
||||
break;
|
||||
case 'opening_year':
|
||||
$query->orderBy('opening_year', $this->sortDirection)
|
||||
->orderBy('name', 'asc');
|
||||
break;
|
||||
case 'height_requirement':
|
||||
$query->orderBy('height_requirement', $this->sortDirection)
|
||||
->orderBy('name', 'asc');
|
||||
break;
|
||||
case 'created_at':
|
||||
$query->orderBy('created_at', $this->sortDirection);
|
||||
break;
|
||||
case 'updated_at':
|
||||
$query->orderBy('updated_at', $this->sortDirection);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get park statistics
|
||||
*/
|
||||
public function getParkStatsProperty(): array
|
||||
{
|
||||
$cacheKey = "park_stats_{$this->park->id}";
|
||||
|
||||
return Cache::remember($cacheKey, 3600, function() {
|
||||
$totalRides = $this->park->rides()->count();
|
||||
$operatingRides = $this->park->rides()->where('status', 'operating')->count();
|
||||
$categories = $this->park->rides()
|
||||
->select('category')
|
||||
->groupBy('category')
|
||||
->get()
|
||||
->count();
|
||||
|
||||
$avgRating = $this->park->rides()
|
||||
->whereHas('reviews')
|
||||
->withAvg('reviews', 'rating')
|
||||
->get()
|
||||
->avg('reviews_avg_rating');
|
||||
|
||||
return [
|
||||
'total_rides' => $totalRides,
|
||||
'operating_rides' => $operatingRides,
|
||||
'categories' => $categories,
|
||||
'avg_rating' => $avgRating ? round($avgRating, 1) : null
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active filters count
|
||||
*/
|
||||
public function getActiveFiltersCountProperty(): int
|
||||
{
|
||||
return collect([
|
||||
$this->searchTerm,
|
||||
$this->selectedCategory,
|
||||
$this->selectedStatus
|
||||
])->filter()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for current state
|
||||
*/
|
||||
protected function getCacheKey(): string
|
||||
{
|
||||
return sprintf(
|
||||
'park_rides_%d_%s_%s_%s_%s_%s_%d_%d',
|
||||
$this->park->id,
|
||||
md5($this->searchTerm),
|
||||
$this->selectedCategory ?? 'all',
|
||||
$this->selectedStatus ?? 'all',
|
||||
$this->sortBy,
|
||||
$this->sortDirection,
|
||||
$this->perPage,
|
||||
$this->getPage()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,35 +328,45 @@ class ParkRidesListing extends Component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.park-rides-listing');
|
||||
return view('livewire.park-rides-listing', [
|
||||
'rides' => $this->rides,
|
||||
'parkStats' => $this->parkStats,
|
||||
'activeFiltersCount' => $this->activeFiltersCount
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
* Reset pagination when filters change
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
public function resetPage($pageName = 'page'): void
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
$this->resetPage($pageName);
|
||||
|
||||
// Clear cache when filters change
|
||||
$this->clearComponentCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
* Clear component-specific cache
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
protected function clearComponentCache(): void
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
$patterns = [
|
||||
"park_rides_{$this->park->id}_*",
|
||||
"park_stats_{$this->park->id}",
|
||||
"park_rides_filters_{$this->park->id}"
|
||||
];
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
foreach ($patterns as $pattern) {
|
||||
Cache::forget($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pagination view
|
||||
*/
|
||||
public function paginationView(): string
|
||||
{
|
||||
return 'livewire.pagination-links';
|
||||
}
|
||||
}
|
||||
54
app/Livewire/ParksFilters.php
Normal file
54
app/Livewire/ParksFilters.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ParksFilters extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.parks-filters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
476
app/Livewire/ParksListing.php
Normal file
476
app/Livewire/ParksListing.php
Normal file
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ParksListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Search and Filter Properties with URL binding
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'operator')]
|
||||
public string $operatorId = '';
|
||||
|
||||
#[Url(as: 'region')]
|
||||
public string $region = '';
|
||||
|
||||
#[Url(as: 'country')]
|
||||
public string $country = '';
|
||||
|
||||
#[Url(as: 'type')]
|
||||
public string $parkType = '';
|
||||
|
||||
#[Url(as: 'year_from')]
|
||||
public string $openingYearFrom = '';
|
||||
|
||||
#[Url(as: 'year_to')]
|
||||
public string $openingYearTo = '';
|
||||
|
||||
#[Url(as: 'min_area')]
|
||||
public string $minArea = '';
|
||||
|
||||
#[Url(as: 'max_area')]
|
||||
public string $maxArea = '';
|
||||
|
||||
#[Url(as: 'min_rides')]
|
||||
public string $minRides = '';
|
||||
|
||||
#[Url(as: 'max_distance')]
|
||||
public string $maxDistance = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
// Location Properties
|
||||
public ?array $userLocation = null;
|
||||
public bool $locationEnabled = false;
|
||||
public bool $locationLoading = false;
|
||||
|
||||
// UI State
|
||||
public bool $showFilters = false;
|
||||
public bool $isLoading = false;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated hook for reactive properties
|
||||
*/
|
||||
public function updated($property): void
|
||||
{
|
||||
if (in_array($property, [
|
||||
'search', 'operatorId', 'region', 'country', 'parkType',
|
||||
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
|
||||
'minRides', 'maxDistance', 'sortBy', 'sortDirection'
|
||||
])) {
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable location services
|
||||
*/
|
||||
public function enableLocation(): void
|
||||
{
|
||||
$this->locationLoading = true;
|
||||
$this->dispatch('request-location');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle location received from JavaScript
|
||||
*/
|
||||
public function locationReceived(array $location): void
|
||||
{
|
||||
$this->userLocation = $location;
|
||||
$this->locationEnabled = true;
|
||||
$this->locationLoading = false;
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle location error
|
||||
*/
|
||||
public function locationError(string $error): void
|
||||
{
|
||||
$this->locationLoading = false;
|
||||
$this->dispatch('location-error', message: $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'search', 'operatorId', 'region', 'country', 'parkType',
|
||||
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
|
||||
'minRides', 'maxDistance'
|
||||
]);
|
||||
$this->userLocation = null;
|
||||
$this->locationEnabled = false;
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle filters visibility
|
||||
*/
|
||||
public function toggleFilters(): void
|
||||
{
|
||||
$this->showFilters = !$this->showFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort parks by given field
|
||||
*/
|
||||
public function sortBy(string $field): void
|
||||
{
|
||||
if ($this->sortBy === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parks with location-aware search and filtering
|
||||
*/
|
||||
public function getParksProperty()
|
||||
{
|
||||
return $this->remember('parks', function () {
|
||||
return $this->buildParksQuery()
|
||||
->paginate(12, ['*'], 'page', $this->getPage());
|
||||
}, 1200); // 20-minute location-aware caching
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the parks query with Django parity search and filtering
|
||||
*/
|
||||
protected function buildParksQuery(): Builder
|
||||
{
|
||||
$query = Park::query();
|
||||
|
||||
// Location-aware search functionality (Django parity)
|
||||
if (!empty($this->search)) {
|
||||
$query = $this->locationAwareSearch($query, $this->search, $this->userLocation);
|
||||
}
|
||||
|
||||
// Apply advanced filters with geographic context
|
||||
$query = $this->applyFilters($query, [
|
||||
'operator_id' => $this->operatorId,
|
||||
'region' => $this->region,
|
||||
'country' => $this->country,
|
||||
'park_type' => $this->parkType,
|
||||
'opening_year_from' => $this->openingYearFrom,
|
||||
'opening_year_to' => $this->openingYearTo,
|
||||
'min_area' => $this->minArea,
|
||||
'max_area' => $this->maxArea,
|
||||
'min_rides' => $this->minRides,
|
||||
'max_distance' => $this->maxDistance,
|
||||
], $this->userLocation);
|
||||
|
||||
// Apply sorting with location-aware options
|
||||
$query = $this->applySorting($query);
|
||||
|
||||
// Eager load relationships for performance
|
||||
$query->with(['location', 'operator', 'photos', 'statistics'])
|
||||
->withCount(['rides', 'reviews']);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location-aware search functionality matching Django implementation
|
||||
*/
|
||||
protected function locationAwareSearch(Builder $query, string $searchQuery, ?array $userLocation = null): Builder
|
||||
{
|
||||
$terms = explode(' ', trim($searchQuery));
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$term = trim($term);
|
||||
if (empty($term)) continue;
|
||||
|
||||
$query->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('park_type', 'ilike', "%{$term}%")
|
||||
->orWhereHas('location', function($locQuery) use ($term) {
|
||||
$locQuery->where('city', 'ilike', "%{$term}%")
|
||||
->orWhere('state', 'ilike', "%{$term}%")
|
||||
->orWhere('country', 'ilike', "%{$term}%");
|
||||
})
|
||||
->orWhereHas('operator', fn($opQuery) =>
|
||||
$opQuery->where('name', 'ilike', "%{$term}%"));
|
||||
});
|
||||
}
|
||||
|
||||
// Add distance-based ordering for location-aware results
|
||||
if ($userLocation) {
|
||||
$query->selectRaw('parks.*,
|
||||
(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) *
|
||||
cos(radians(locations.longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(locations.latitude)))) AS distance',
|
||||
[$userLocation['lat'], $userLocation['lng'], $userLocation['lat']])
|
||||
->join('locations', 'parks.location_id', '=', 'locations.id')
|
||||
->orderBy('distance');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply advanced filters with geographic context
|
||||
*/
|
||||
protected function applyFilters(Builder $query, array $filters, ?array $userLocation = null): Builder
|
||||
{
|
||||
return $query
|
||||
->when($filters['operator_id'] ?? null, fn($q, $operatorId) =>
|
||||
$q->where('operator_id', $operatorId))
|
||||
->when($filters['region'] ?? null, fn($q, $region) =>
|
||||
$q->whereHas('location', fn($locQ) => $locQ->where('state', $region)))
|
||||
->when($filters['country'] ?? null, fn($q, $country) =>
|
||||
$q->whereHas('location', fn($locQ) => $locQ->where('country', $country)))
|
||||
->when($filters['park_type'] ?? null, fn($q, $type) =>
|
||||
$q->where('park_type', $type))
|
||||
->when($filters['opening_year_from'] ?? null, fn($q, $year) =>
|
||||
$q->where('opening_date', '>=', "{$year}-01-01"))
|
||||
->when($filters['opening_year_to'] ?? null, fn($q, $year) =>
|
||||
$q->where('opening_date', '<=', "{$year}-12-31"))
|
||||
->when($filters['min_area'] ?? null, fn($q, $area) =>
|
||||
$q->where('area_acres', '>=', $area))
|
||||
->when($filters['max_area'] ?? null, fn($q, $area) =>
|
||||
$q->where('area_acres', '<=', $area))
|
||||
->when($filters['min_rides'] ?? null, fn($q, $count) =>
|
||||
$q->whereHas('rides', fn($rideQ) => $rideQ->havingRaw('COUNT(*) >= ?', [$count])))
|
||||
->when($filters['max_distance'] ?? null && $userLocation, function($q) use ($filters, $userLocation) {
|
||||
$q->whereRaw('(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) *
|
||||
cos(radians(locations.longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(locations.latitude)))) <= ?',
|
||||
[$userLocation['lat'], $userLocation['lng'], $userLocation['lat'], $filters['max_distance']]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sorting with location-aware options
|
||||
*/
|
||||
protected function applySorting(Builder $query): Builder
|
||||
{
|
||||
switch ($this->sortBy) {
|
||||
case 'distance':
|
||||
if ($this->userLocation) {
|
||||
// Distance sorting already applied in locationAwareSearch
|
||||
return $query;
|
||||
}
|
||||
// Fallback to name if no location
|
||||
return $query->orderBy('name', $this->sortDirection);
|
||||
|
||||
case 'rides_count':
|
||||
return $query->orderBy('rides_count', $this->sortDirection);
|
||||
|
||||
case 'reviews_count':
|
||||
return $query->orderBy('reviews_count', $this->sortDirection);
|
||||
|
||||
case 'opening_date':
|
||||
return $query->orderBy('opening_date', $this->sortDirection);
|
||||
|
||||
case 'area_acres':
|
||||
return $query->orderBy('area_acres', $this->sortDirection);
|
||||
|
||||
case 'name':
|
||||
default:
|
||||
return $query->orderBy('name', $this->sortDirection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available operators for filter dropdown
|
||||
*/
|
||||
public function getOperatorsProperty()
|
||||
{
|
||||
return $this->remember('operators', function () {
|
||||
return Operator::select('id', 'name')
|
||||
->whereHas('parks')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available regions for filter dropdown
|
||||
*/
|
||||
public function getRegionsProperty()
|
||||
{
|
||||
return $this->remember('regions', function () {
|
||||
return DB::table('locations')
|
||||
->join('parks', 'locations.id', '=', 'parks.location_id')
|
||||
->select('locations.state')
|
||||
->distinct()
|
||||
->whereNotNull('locations.state')
|
||||
->orderBy('locations.state')
|
||||
->pluck('state');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available countries for filter dropdown
|
||||
*/
|
||||
public function getCountriesProperty()
|
||||
{
|
||||
return $this->remember('countries', function () {
|
||||
return DB::table('locations')
|
||||
->join('parks', 'locations.id', '=', 'parks.location_id')
|
||||
->select('locations.country')
|
||||
->distinct()
|
||||
->whereNotNull('locations.country')
|
||||
->orderBy('locations.country')
|
||||
->pluck('country');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available park types for filter dropdown
|
||||
*/
|
||||
public function getParkTypesProperty()
|
||||
{
|
||||
return $this->remember('park_types', function () {
|
||||
return Park::select('park_type')
|
||||
->distinct()
|
||||
->whereNotNull('park_type')
|
||||
->orderBy('park_type')
|
||||
->pluck('park_type');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter summary for display
|
||||
*/
|
||||
public function getActiveFiltersProperty(): array
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
if (!empty($this->search)) {
|
||||
$filters[] = "Search: {$this->search}";
|
||||
}
|
||||
|
||||
if (!empty($this->operatorId)) {
|
||||
$operator = $this->operators->find($this->operatorId);
|
||||
if ($operator) {
|
||||
$filters[] = "Operator: {$operator->name}";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->region)) {
|
||||
$filters[] = "Region: {$this->region}";
|
||||
}
|
||||
|
||||
if (!empty($this->country)) {
|
||||
$filters[] = "Country: {$this->country}";
|
||||
}
|
||||
|
||||
if (!empty($this->parkType)) {
|
||||
$filters[] = "Type: {$this->parkType}";
|
||||
}
|
||||
|
||||
if (!empty($this->openingYearFrom) || !empty($this->openingYearTo)) {
|
||||
$yearRange = $this->openingYearFrom . ' - ' . $this->openingYearTo;
|
||||
$filters[] = "Years: {$yearRange}";
|
||||
}
|
||||
|
||||
if (!empty($this->minArea) || !empty($this->maxArea)) {
|
||||
$areaRange = $this->minArea . ' - ' . $this->maxArea . ' acres';
|
||||
$filters[] = "Area: {$areaRange}";
|
||||
}
|
||||
|
||||
if (!empty($this->minRides)) {
|
||||
$filters[] = "Min Rides: {$this->minRides}";
|
||||
}
|
||||
|
||||
if (!empty($this->maxDistance) && $this->locationEnabled) {
|
||||
$filters[] = "Within: {$this->maxDistance} km";
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$this->isLoading = false;
|
||||
|
||||
return view('livewire.parks-listing', [
|
||||
'parks' => $this->parks,
|
||||
'operators' => $this->operators,
|
||||
'regions' => $this->regions,
|
||||
'countries' => $this->countries,
|
||||
'parkTypes' => $this->parkTypes,
|
||||
'activeFilters' => $this->activeFilters,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
$locationKey = $this->userLocation ?
|
||||
md5(json_encode($this->userLocation)) : 'no-location';
|
||||
|
||||
$filterKey = md5(serialize([
|
||||
$this->search, $this->operatorId, $this->region, $this->country,
|
||||
$this->parkType, $this->openingYearFrom, $this->openingYearTo,
|
||||
$this->minArea, $this->maxArea, $this->minRides, $this->maxDistance,
|
||||
$this->sortBy, $this->sortDirection
|
||||
]));
|
||||
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' .
|
||||
$locationKey . '.' . $filterKey . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
475
app/Livewire/ParksListingUniversal.php
Normal file
475
app/Livewire/ParksListingUniversal.php
Normal file
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ParksListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System Integration
|
||||
public string $entityType = 'parks';
|
||||
|
||||
// Search and Filter Properties with URL binding
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'operator')]
|
||||
public string $operatorId = '';
|
||||
|
||||
#[Url(as: 'region')]
|
||||
public string $region = '';
|
||||
|
||||
#[Url(as: 'country')]
|
||||
public string $country = '';
|
||||
|
||||
#[Url(as: 'type')]
|
||||
public string $parkType = '';
|
||||
|
||||
#[Url(as: 'year_from')]
|
||||
public string $openingYearFrom = '';
|
||||
|
||||
#[Url(as: 'year_to')]
|
||||
public string $openingYearTo = '';
|
||||
|
||||
#[Url(as: 'min_area')]
|
||||
public string $minArea = '';
|
||||
|
||||
#[Url(as: 'max_area')]
|
||||
public string $maxArea = '';
|
||||
|
||||
#[Url(as: 'min_rides')]
|
||||
public string $minRides = '';
|
||||
|
||||
#[Url(as: 'max_distance')]
|
||||
public string $maxDistance = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
// Location Properties (GPS Integration)
|
||||
public ?array $userLocation = null;
|
||||
public bool $locationEnabled = false;
|
||||
public bool $locationLoading = false;
|
||||
|
||||
// UI State
|
||||
public bool $showFilters = false;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated hook for reactive properties
|
||||
*/
|
||||
public function updated($property): void
|
||||
{
|
||||
if (in_array($property, [
|
||||
'search', 'operatorId', 'region', 'country', 'parkType',
|
||||
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
|
||||
'minRides', 'maxDistance', 'sortBy', 'sortDirection'
|
||||
])) {
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable location services (GPS Integration)
|
||||
*/
|
||||
public function enableLocation(): void
|
||||
{
|
||||
$this->locationLoading = true;
|
||||
$this->dispatch('request-location');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle location received from JavaScript
|
||||
*/
|
||||
public function locationReceived(array $location): void
|
||||
{
|
||||
$this->userLocation = $location;
|
||||
$this->locationEnabled = true;
|
||||
$this->locationLoading = false;
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle location error
|
||||
*/
|
||||
public function locationError(string $error): void
|
||||
{
|
||||
$this->locationLoading = false;
|
||||
$this->dispatch('location-error', message: $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'search', 'operatorId', 'region', 'country', 'parkType',
|
||||
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
|
||||
'minRides', 'maxDistance'
|
||||
]);
|
||||
$this->userLocation = null;
|
||||
$this->locationEnabled = false;
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle filters visibility
|
||||
*/
|
||||
public function toggleFilters(): void
|
||||
{
|
||||
$this->showFilters = !$this->showFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort parks by given field
|
||||
*/
|
||||
public function sortBy(string $field): void
|
||||
{
|
||||
if ($this->sortBy === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parks with location-aware search and filtering (Django parity)
|
||||
*/
|
||||
public function getParksProperty()
|
||||
{
|
||||
return $this->remember('parks', function () {
|
||||
return $this->buildParksQuery()
|
||||
->paginate(12, ['*'], 'page', $this->getPage());
|
||||
}, 1200); // 20-minute location-aware caching
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the parks query with Django parity search and filtering
|
||||
*/
|
||||
protected function buildParksQuery(): Builder
|
||||
{
|
||||
$query = Park::query();
|
||||
|
||||
// Location-aware search functionality (Django parity)
|
||||
if (!empty($this->search)) {
|
||||
$query = $this->locationAwareSearch($query, $this->search, $this->userLocation);
|
||||
}
|
||||
|
||||
// Apply advanced filters with geographic context
|
||||
$query = $this->applyFilters($query, [
|
||||
'operator_id' => $this->operatorId,
|
||||
'region' => $this->region,
|
||||
'country' => $this->country,
|
||||
'park_type' => $this->parkType,
|
||||
'opening_year_from' => $this->openingYearFrom,
|
||||
'opening_year_to' => $this->openingYearTo,
|
||||
'min_area' => $this->minArea,
|
||||
'max_area' => $this->maxArea,
|
||||
'min_rides' => $this->minRides,
|
||||
'max_distance' => $this->maxDistance,
|
||||
], $this->userLocation);
|
||||
|
||||
// Apply sorting with location-aware options
|
||||
$query = $this->applySorting($query);
|
||||
|
||||
// Eager load relationships for performance
|
||||
$query->with(['location', 'operator', 'photos', 'statistics'])
|
||||
->withCount(['rides', 'reviews']);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location-aware search functionality matching Django implementation
|
||||
*/
|
||||
protected function locationAwareSearch(Builder $query, string $searchQuery, ?array $userLocation = null): Builder
|
||||
{
|
||||
$terms = explode(' ', trim($searchQuery));
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$term = trim($term);
|
||||
if (empty($term)) continue;
|
||||
|
||||
$query->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('park_type', 'ilike', "%{$term}%")
|
||||
->orWhereHas('location', function($locQuery) use ($term) {
|
||||
$locQuery->where('city', 'ilike', "%{$term}%")
|
||||
->orWhere('state', 'ilike', "%{$term}%")
|
||||
->orWhere('country', 'ilike', "%{$term}%");
|
||||
})
|
||||
->orWhereHas('operator', fn($opQuery) =>
|
||||
$opQuery->where('name', 'ilike', "%{$term}%"));
|
||||
});
|
||||
}
|
||||
|
||||
// Add distance-based ordering for location-aware results
|
||||
if ($userLocation) {
|
||||
$query->selectRaw('parks.*,
|
||||
(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) *
|
||||
cos(radians(locations.longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(locations.latitude)))) AS distance',
|
||||
[$userLocation['lat'], $userLocation['lng'], $userLocation['lat']])
|
||||
->join('locations', 'parks.location_id', '=', 'locations.id')
|
||||
->orderBy('distance');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply advanced filters with geographic context
|
||||
*/
|
||||
protected function applyFilters(Builder $query, array $filters, ?array $userLocation = null): Builder
|
||||
{
|
||||
return $query
|
||||
->when($filters['operator_id'] ?? null, fn($q, $operatorId) =>
|
||||
$q->where('operator_id', $operatorId))
|
||||
->when($filters['region'] ?? null, fn($q, $region) =>
|
||||
$q->whereHas('location', fn($locQ) => $locQ->where('state', $region)))
|
||||
->when($filters['country'] ?? null, fn($q, $country) =>
|
||||
$q->whereHas('location', fn($locQ) => $locQ->where('country', $country)))
|
||||
->when($filters['park_type'] ?? null, fn($q, $type) =>
|
||||
$q->where('park_type', $type))
|
||||
->when($filters['opening_year_from'] ?? null, fn($q, $year) =>
|
||||
$q->where('opening_date', '>=', "{$year}-01-01"))
|
||||
->when($filters['opening_year_to'] ?? null, fn($q, $year) =>
|
||||
$q->where('opening_date', '<=', "{$year}-12-31"))
|
||||
->when($filters['min_area'] ?? null, fn($q, $area) =>
|
||||
$q->where('area_acres', '>=', $area))
|
||||
->when($filters['max_area'] ?? null, fn($q, $area) =>
|
||||
$q->where('area_acres', '<=', $area))
|
||||
->when($filters['min_rides'] ?? null, fn($q, $count) =>
|
||||
$q->whereHas('rides', fn($rideQ) => $rideQ->havingRaw('COUNT(*) >= ?', [$count])))
|
||||
->when($filters['max_distance'] ?? null && $userLocation, function($q) use ($filters, $userLocation) {
|
||||
$q->whereRaw('(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) *
|
||||
cos(radians(locations.longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(locations.latitude)))) <= ?',
|
||||
[$userLocation['lat'], $userLocation['lng'], $userLocation['lat'], $filters['max_distance']]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sorting with location-aware options
|
||||
*/
|
||||
protected function applySorting(Builder $query): Builder
|
||||
{
|
||||
switch ($this->sortBy) {
|
||||
case 'distance':
|
||||
if ($this->userLocation) {
|
||||
// Distance sorting already applied in locationAwareSearch
|
||||
return $query;
|
||||
}
|
||||
// Fallback to name if no location
|
||||
return $query->orderBy('name', $this->sortDirection);
|
||||
|
||||
case 'rides_count':
|
||||
return $query->orderBy('rides_count', $this->sortDirection);
|
||||
|
||||
case 'reviews_count':
|
||||
return $query->orderBy('reviews_count', $this->sortDirection);
|
||||
|
||||
case 'opening_date':
|
||||
return $query->orderBy('opening_date', $this->sortDirection);
|
||||
|
||||
case 'area_acres':
|
||||
return $query->orderBy('area_acres', $this->sortDirection);
|
||||
|
||||
case 'name':
|
||||
default:
|
||||
return $query->orderBy('name', $this->sortDirection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available operators for filter dropdown
|
||||
*/
|
||||
public function getOperatorsProperty()
|
||||
{
|
||||
return $this->remember('operators', function () {
|
||||
return Operator::select('id', 'name')
|
||||
->whereHas('parks')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available regions for filter dropdown
|
||||
*/
|
||||
public function getRegionsProperty()
|
||||
{
|
||||
return $this->remember('regions', function () {
|
||||
return DB::table('locations')
|
||||
->join('parks', 'locations.id', '=', 'parks.location_id')
|
||||
->select('locations.state')
|
||||
->distinct()
|
||||
->whereNotNull('locations.state')
|
||||
->orderBy('locations.state')
|
||||
->pluck('state');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available countries for filter dropdown
|
||||
*/
|
||||
public function getCountriesProperty()
|
||||
{
|
||||
return $this->remember('countries', function () {
|
||||
return DB::table('locations')
|
||||
->join('parks', 'locations.id', '=', 'parks.location_id')
|
||||
->select('locations.country')
|
||||
->distinct()
|
||||
->whereNotNull('locations.country')
|
||||
->orderBy('locations.country')
|
||||
->pluck('country');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available park types for filter dropdown
|
||||
*/
|
||||
public function getParkTypesProperty()
|
||||
{
|
||||
return $this->remember('park_types', function () {
|
||||
return Park::select('park_type')
|
||||
->distinct()
|
||||
->whereNotNull('park_type')
|
||||
->orderBy('park_type')
|
||||
->pluck('park_type');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter summary for display
|
||||
*/
|
||||
public function getActiveFiltersProperty(): array
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
if (!empty($this->search)) {
|
||||
$filters[] = "Search: {$this->search}";
|
||||
}
|
||||
|
||||
if (!empty($this->operatorId)) {
|
||||
$operator = $this->operators->find($this->operatorId);
|
||||
if ($operator) {
|
||||
$filters[] = "Operator: {$operator->name}";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->region)) {
|
||||
$filters[] = "Region: {$this->region}";
|
||||
}
|
||||
|
||||
if (!empty($this->country)) {
|
||||
$filters[] = "Country: {$this->country}";
|
||||
}
|
||||
|
||||
if (!empty($this->parkType)) {
|
||||
$filters[] = "Type: {$this->parkType}";
|
||||
}
|
||||
|
||||
if (!empty($this->openingYearFrom) || !empty($this->openingYearTo)) {
|
||||
$yearRange = $this->openingYearFrom . ' - ' . $this->openingYearTo;
|
||||
$filters[] = "Years: {$yearRange}";
|
||||
}
|
||||
|
||||
if (!empty($this->minArea) || !empty($this->maxArea)) {
|
||||
$areaRange = $this->minArea . ' - ' . $this->maxArea . ' acres';
|
||||
$filters[] = "Area: {$areaRange}";
|
||||
}
|
||||
|
||||
if (!empty($this->minRides)) {
|
||||
$filters[] = "Min Rides: {$this->minRides}";
|
||||
}
|
||||
|
||||
if (!empty($this->maxDistance) && $this->locationEnabled) {
|
||||
$filters[] = "Within: {$this->maxDistance} km";
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component using Universal Listing System
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.parks-listing-universal', [
|
||||
'parks' => $this->parks,
|
||||
'operators' => $this->operators,
|
||||
'regions' => $this->regions,
|
||||
'countries' => $this->countries,
|
||||
'parkTypes' => $this->parkTypes,
|
||||
'activeFilters' => $this->activeFilters,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
$locationKey = $this->userLocation ?
|
||||
md5(json_encode($this->userLocation)) : 'no-location';
|
||||
|
||||
$filterKey = md5(serialize([
|
||||
$this->search, $this->operatorId, $this->region, $this->country,
|
||||
$this->parkType, $this->openingYearFrom, $this->openingYearTo,
|
||||
$this->minArea, $this->maxArea, $this->minRides, $this->maxDistance,
|
||||
$this->sortBy, $this->sortDirection
|
||||
]));
|
||||
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' .
|
||||
$locationKey . '.' . $filterKey . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/ParksLocationSearch.php
Normal file
54
app/Livewire/ParksLocationSearch.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ParksLocationSearch extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.parks-location-search');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/ParksMapView.php
Normal file
54
app/Livewire/ParksMapView.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ParksMapView extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.parks-map-view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/Livewire/RegionalParksListing.php
Normal file
57
app/Livewire/RegionalParksListing.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class RegionalParksListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.regional-parks-listing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,335 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Park;
|
||||
use App\Models\Operator;
|
||||
use App\Enums\RideCategory;
|
||||
use App\Enums\RideStatus;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class RidesFilters extends Component
|
||||
{
|
||||
// URL-bound filter properties for deep linking
|
||||
#[Url(as: 'category')]
|
||||
public ?string $selectedCategory = null;
|
||||
|
||||
#[Url(as: 'status')]
|
||||
public ?string $selectedStatus = null;
|
||||
|
||||
#[Url(as: 'manufacturer')]
|
||||
public ?int $selectedManufacturer = null;
|
||||
|
||||
#[Url(as: 'park')]
|
||||
public ?int $selectedPark = null;
|
||||
|
||||
#[Url(as: 'year_min')]
|
||||
public ?int $minOpeningYear = null;
|
||||
|
||||
#[Url(as: 'year_max')]
|
||||
public ?int $maxOpeningYear = null;
|
||||
|
||||
#[Url(as: 'height_min')]
|
||||
public ?int $minHeight = null;
|
||||
|
||||
#[Url(as: 'height_max')]
|
||||
public ?int $maxHeight = null;
|
||||
|
||||
// Filter options (cached)
|
||||
public array $categories = [];
|
||||
public array $statuses = [];
|
||||
public array $manufacturers = [];
|
||||
public array $parks = [];
|
||||
public array $yearRange = [];
|
||||
public array $heightRange = [];
|
||||
|
||||
// UI state
|
||||
public bool $showAdvancedFilters = false;
|
||||
public int $activeFiltersCount = 0;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
$this->loadFilterOptions();
|
||||
$this->calculateActiveFiltersCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filter options with caching
|
||||
*/
|
||||
protected function loadFilterOptions(): void
|
||||
{
|
||||
// Categories from enum
|
||||
$this->categories = $this->remember(
|
||||
'categories',
|
||||
fn() => collect(RideCategory::cases())
|
||||
->map(fn($case) => [
|
||||
'value' => $case->value,
|
||||
'label' => $case->name,
|
||||
'count' => Ride::where('category', $case->value)->count()
|
||||
])
|
||||
->filter(fn($item) => $item['count'] > 0)
|
||||
->values()
|
||||
->toArray(),
|
||||
3600 // 1-hour cache
|
||||
);
|
||||
|
||||
// Statuses from enum
|
||||
$this->statuses = $this->remember(
|
||||
'statuses',
|
||||
fn() => collect(RideStatus::cases())
|
||||
->map(fn($case) => [
|
||||
'value' => $case->value,
|
||||
'label' => $case->name,
|
||||
'count' => Ride::where('status', $case->value)->count()
|
||||
])
|
||||
->filter(fn($item) => $item['count'] > 0)
|
||||
->values()
|
||||
->toArray(),
|
||||
3600
|
||||
);
|
||||
|
||||
// Manufacturers (Operators that have manufactured rides)
|
||||
$this->manufacturers = $this->remember(
|
||||
'manufacturers',
|
||||
fn() => Operator::select('id', 'name')
|
||||
->whereHas('manufacturedRides')
|
||||
->withCount('manufacturedRides')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn($operator) => [
|
||||
'value' => $operator->id,
|
||||
'label' => $operator->name,
|
||||
'count' => $operator->manufactured_rides_count
|
||||
])
|
||||
->toArray(),
|
||||
3600
|
||||
);
|
||||
|
||||
// Parks that have rides
|
||||
$this->parks = $this->remember(
|
||||
'parks',
|
||||
fn() => Park::select('id', 'name')
|
||||
->whereHas('rides')
|
||||
->withCount('rides')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn($park) => [
|
||||
'value' => $park->id,
|
||||
'label' => $park->name,
|
||||
'count' => $park->rides_count
|
||||
])
|
||||
->toArray(),
|
||||
3600
|
||||
);
|
||||
|
||||
// Year range
|
||||
$this->yearRange = $this->remember(
|
||||
'year_range',
|
||||
function() {
|
||||
$years = Ride::whereNotNull('opening_year')
|
||||
->selectRaw('MIN(opening_year) as min_year, MAX(opening_year) as max_year')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'min' => $years->min_year ?? 1900,
|
||||
'max' => $years->max_year ?? date('Y')
|
||||
];
|
||||
},
|
||||
3600
|
||||
);
|
||||
|
||||
// Height range
|
||||
$this->heightRange = $this->remember(
|
||||
'height_range',
|
||||
function() {
|
||||
$heights = Ride::whereNotNull('height_requirement')
|
||||
->selectRaw('MIN(height_requirement) as min_height, MAX(height_requirement) as max_height')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'min' => $heights->min_height ?? 0,
|
||||
'max' => $heights->max_height ?? 200
|
||||
];
|
||||
},
|
||||
3600
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate number of active filters
|
||||
*/
|
||||
protected function calculateActiveFiltersCount(): void
|
||||
{
|
||||
$this->activeFiltersCount = collect([
|
||||
$this->selectedCategory,
|
||||
$this->selectedStatus,
|
||||
$this->selectedManufacturer,
|
||||
$this->selectedPark,
|
||||
$this->minOpeningYear,
|
||||
$this->maxOpeningYear,
|
||||
$this->minHeight,
|
||||
$this->maxHeight
|
||||
])->filter()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply category filter
|
||||
*/
|
||||
public function setCategory(?string $category): void
|
||||
{
|
||||
$this->selectedCategory = $category === $this->selectedCategory ? null : $category;
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply status filter
|
||||
*/
|
||||
public function setStatus(?string $status): void
|
||||
{
|
||||
$this->selectedStatus = $status === $this->selectedStatus ? null : $status;
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply manufacturer filter
|
||||
*/
|
||||
public function setManufacturer(?int $manufacturerId): void
|
||||
{
|
||||
$this->selectedManufacturer = $manufacturerId === $this->selectedManufacturer ? null : $manufacturerId;
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply park filter
|
||||
*/
|
||||
public function setPark(?int $parkId): void
|
||||
{
|
||||
$this->selectedPark = $parkId === $this->selectedPark ? null : $parkId;
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update year range filters
|
||||
*/
|
||||
public function updateYearRange(): void
|
||||
{
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update height range filters
|
||||
*/
|
||||
public function updateHeightRange(): void
|
||||
{
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle advanced filters visibility
|
||||
*/
|
||||
public function toggleAdvancedFilters(): void
|
||||
{
|
||||
$this->showAdvancedFilters = !$this->showAdvancedFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearAllFilters(): void
|
||||
{
|
||||
$this->selectedCategory = null;
|
||||
$this->selectedStatus = null;
|
||||
$this->selectedManufacturer = null;
|
||||
$this->selectedPark = null;
|
||||
$this->minOpeningYear = null;
|
||||
$this->maxOpeningYear = null;
|
||||
$this->minHeight = null;
|
||||
$this->maxHeight = null;
|
||||
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active filters for parent component
|
||||
*/
|
||||
public function getActiveFilters(): array
|
||||
{
|
||||
return array_filter([
|
||||
'category' => $this->selectedCategory,
|
||||
'status' => $this->selectedStatus,
|
||||
'manufacturer_id' => $this->selectedManufacturer,
|
||||
'park_id' => $this->selectedPark,
|
||||
'min_opening_year' => $this->minOpeningYear,
|
||||
'max_opening_year' => $this->maxOpeningYear,
|
||||
'min_height' => $this->minHeight,
|
||||
'max_height' => $this->maxHeight,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter summary for display
|
||||
*/
|
||||
public function getFilterSummary(): array
|
||||
{
|
||||
$summary = [];
|
||||
|
||||
if ($this->selectedCategory) {
|
||||
$category = collect($this->categories)->firstWhere('value', $this->selectedCategory);
|
||||
$summary[] = 'Category: ' . ($category['label'] ?? $this->selectedCategory);
|
||||
}
|
||||
|
||||
if ($this->selectedStatus) {
|
||||
$status = collect($this->statuses)->firstWhere('value', $this->selectedStatus);
|
||||
$summary[] = 'Status: ' . ($status['label'] ?? $this->selectedStatus);
|
||||
}
|
||||
|
||||
if ($this->selectedManufacturer) {
|
||||
$manufacturer = collect($this->manufacturers)->firstWhere('value', $this->selectedManufacturer);
|
||||
$summary[] = 'Manufacturer: ' . ($manufacturer['label'] ?? 'Unknown');
|
||||
}
|
||||
|
||||
if ($this->selectedPark) {
|
||||
$park = collect($this->parks)->firstWhere('value', $this->selectedPark);
|
||||
$summary[] = 'Park: ' . ($park['label'] ?? 'Unknown');
|
||||
}
|
||||
|
||||
if ($this->minOpeningYear || $this->maxOpeningYear) {
|
||||
$yearText = 'Year: ';
|
||||
if ($this->minOpeningYear && $this->maxOpeningYear) {
|
||||
$yearText .= $this->minOpeningYear . '-' . $this->maxOpeningYear;
|
||||
} elseif ($this->minOpeningYear) {
|
||||
$yearText .= 'After ' . $this->minOpeningYear;
|
||||
} else {
|
||||
$yearText .= 'Before ' . $this->maxOpeningYear;
|
||||
}
|
||||
$summary[] = $yearText;
|
||||
}
|
||||
|
||||
if ($this->minHeight || $this->maxHeight) {
|
||||
$heightText = 'Height: ';
|
||||
if ($this->minHeight && $this->maxHeight) {
|
||||
$heightText .= $this->minHeight . '-' . $this->maxHeight . 'cm';
|
||||
} elseif ($this->minHeight) {
|
||||
$heightText .= 'Min ' . $this->minHeight . 'cm';
|
||||
} else {
|
||||
$heightText .= 'Max ' . $this->maxHeight . 'cm';
|
||||
}
|
||||
$summary[] = $heightText;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
158
app/Livewire/RidesListingUniversal.php
Normal file
158
app/Livewire/RidesListingUniversal.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class RidesListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System integration
|
||||
public string $entityType = 'rides';
|
||||
|
||||
// Search and filter properties with URL binding
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'categories')]
|
||||
public array $categories = [];
|
||||
|
||||
#[Url(as: 'opening_year_from')]
|
||||
public string $openingYearFrom = '';
|
||||
|
||||
#[Url(as: 'opening_year_to')]
|
||||
public string $openingYearTo = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
/**
|
||||
* Get rides data for Universal Listing System
|
||||
*/
|
||||
public function getRidesProperty()
|
||||
{
|
||||
$cacheKey = 'rides_listing_' . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'categories' => $this->categories,
|
||||
'openingYearFrom' => $this->openingYearFrom,
|
||||
'openingYearTo' => $this->openingYearTo,
|
||||
'sortBy' => $this->sortBy,
|
||||
'page' => $this->getPage(),
|
||||
]));
|
||||
|
||||
return Cache::remember($cacheKey, 300, function () {
|
||||
return $this->buildQuery()->paginate(12);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the optimized query
|
||||
*/
|
||||
protected function buildQuery()
|
||||
{
|
||||
$query = Ride::query()
|
||||
->with(['park', 'manufacturer', 'designer', 'photos' => function ($q) {
|
||||
$q->where('is_featured', true)->limit(1);
|
||||
}]);
|
||||
|
||||
// Multi-term search with Django parity
|
||||
if (!empty($this->search)) {
|
||||
$terms = explode(' ', trim($this->search));
|
||||
foreach ($terms as $term) {
|
||||
$query->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhereHas('park', fn($q) => $q->where('name', 'ilike', "%{$term}%"))
|
||||
->orWhereHas('manufacturer', fn($q) => $q->where('name', 'ilike', "%{$term}%"))
|
||||
->orWhereHas('designer', fn($q) => $q->where('name', 'ilike', "%{$term}%"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if (!empty($this->categories)) {
|
||||
$query->whereIn('ride_type', $this->categories);
|
||||
}
|
||||
|
||||
if (!empty($this->openingYearFrom)) {
|
||||
$query->where('opening_date', '>=', "{$this->openingYearFrom}-01-01");
|
||||
}
|
||||
|
||||
if (!empty($this->openingYearTo)) {
|
||||
$query->where('opening_date', '<=', "{$this->openingYearTo}-12-31");
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'opening_year':
|
||||
$query->orderBy('opening_date', 'desc');
|
||||
break;
|
||||
case 'thrill_rating':
|
||||
$query->orderBy('thrill_rating', 'desc');
|
||||
break;
|
||||
case 'height_meters':
|
||||
$query->orderBy('height_meters', 'desc');
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset pagination when filters change
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedCategories(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedOpeningYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedOpeningYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedSortBy(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset(['search', 'categories', 'openingYearFrom', 'openingYearTo']);
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component using Universal Listing System
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.rides-listing-universal', [
|
||||
'items' => $this->rides,
|
||||
'entityType' => $this->entityType,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,184 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Park;
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\On;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class RidesSearchSuggestions extends Component
|
||||
{
|
||||
public string $query = '';
|
||||
public bool $showSuggestions = false;
|
||||
public int $maxSuggestions = 8;
|
||||
public array $suggestions = [];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
public function mount(string $query = ''): void
|
||||
{
|
||||
// Initialize component state
|
||||
$this->query = $query;
|
||||
if (!empty($query)) {
|
||||
$this->updateSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for search query updates from parent components
|
||||
*/
|
||||
#[On('search-query-updated')]
|
||||
public function handleSearchUpdate(string $query): void
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->updateSuggestions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search suggestions based on current query
|
||||
*/
|
||||
public function updateSuggestions(): void
|
||||
{
|
||||
if (strlen($this->query) < 2) {
|
||||
$this->suggestions = [];
|
||||
$this->showSuggestions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->suggestions = $this->remember(
|
||||
'suggestions.' . md5(strtolower($this->query)),
|
||||
fn() => $this->buildSuggestions(),
|
||||
300 // 5-minute cache for suggestions
|
||||
);
|
||||
|
||||
$this->showSuggestions = !empty($this->suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build search suggestions from multiple sources
|
||||
*/
|
||||
protected function buildSuggestions(): array
|
||||
{
|
||||
$query = strtolower(trim($this->query));
|
||||
$suggestions = collect();
|
||||
|
||||
// Ride name suggestions
|
||||
$rideSuggestions = Ride::select('name', 'slug', 'id')
|
||||
->with(['park:id,name,slug'])
|
||||
->where('name', 'ilike', "%{$query}%")
|
||||
->limit(4)
|
||||
->get()
|
||||
->map(function ($ride) {
|
||||
return [
|
||||
'type' => 'ride',
|
||||
'title' => $ride->name,
|
||||
'subtitle' => $ride->park->name ?? 'Unknown Park',
|
||||
'url' => route('rides.show', $ride->slug),
|
||||
'icon' => 'ride',
|
||||
'category' => 'Rides'
|
||||
];
|
||||
});
|
||||
|
||||
// Park name suggestions
|
||||
$parkSuggestions = Park::select('name', 'slug', 'id')
|
||||
->where('name', 'ilike', "%{$query}%")
|
||||
->limit(3)
|
||||
->get()
|
||||
->map(function ($park) {
|
||||
return [
|
||||
'type' => 'park',
|
||||
'title' => $park->name,
|
||||
'subtitle' => 'Theme Park',
|
||||
'url' => route('parks.show', $park->slug),
|
||||
'icon' => 'park',
|
||||
'category' => 'Parks'
|
||||
];
|
||||
});
|
||||
|
||||
// Manufacturer/Designer suggestions
|
||||
$operatorSuggestions = Operator::select('name', 'slug', 'id')
|
||||
->where('name', 'ilike', "%{$query}%")
|
||||
->limit(2)
|
||||
->get()
|
||||
->map(function ($operator) {
|
||||
return [
|
||||
'type' => 'operator',
|
||||
'title' => $operator->name,
|
||||
'subtitle' => 'Manufacturer/Designer',
|
||||
'url' => route('operators.show', $operator->slug),
|
||||
'icon' => 'operator',
|
||||
'category' => 'Companies'
|
||||
];
|
||||
});
|
||||
|
||||
// Combine and prioritize suggestions
|
||||
$suggestions = $suggestions
|
||||
->concat($rideSuggestions)
|
||||
->concat($parkSuggestions)
|
||||
->concat($operatorSuggestions)
|
||||
->take($this->maxSuggestions);
|
||||
|
||||
return $suggestions->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle suggestion selection
|
||||
*/
|
||||
public function selectSuggestion(array $suggestion): void
|
||||
{
|
||||
$this->dispatch('suggestion-selected', $suggestion);
|
||||
$this->hideSuggestions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide suggestions dropdown
|
||||
*/
|
||||
public function hideSuggestions(): void
|
||||
{
|
||||
$this->showSuggestions = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show suggestions dropdown
|
||||
*/
|
||||
public function showSuggestionsDropdown(): void
|
||||
{
|
||||
if (!empty($this->suggestions)) {
|
||||
$this->showSuggestions = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input focus
|
||||
*/
|
||||
public function onFocus(): void
|
||||
{
|
||||
$this->showSuggestionsDropdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input blur with delay to allow clicks
|
||||
*/
|
||||
public function onBlur(): void
|
||||
{
|
||||
// Delay hiding to allow suggestion clicks
|
||||
$this->dispatch('delayed-hide-suggestions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon class for suggestion type
|
||||
*/
|
||||
public function getIconClass(string $type): string
|
||||
{
|
||||
return match($type) {
|
||||
'ride' => 'fas fa-roller-coaster',
|
||||
'park' => 'fas fa-map-marker-alt',
|
||||
'operator' => 'fas fa-industry',
|
||||
default => 'fas fa-search'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,15 @@ class Manufacturer extends Model
|
||||
'total_rides',
|
||||
'total_roller_coasters',
|
||||
'is_active',
|
||||
'industry_presence_score',
|
||||
'market_share_percentage',
|
||||
'founded_year',
|
||||
'specialization',
|
||||
'product_portfolio',
|
||||
'manufacturing_categories',
|
||||
'global_installations',
|
||||
'primary_market',
|
||||
'is_major_manufacturer',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -37,6 +46,12 @@ class Manufacturer extends Model
|
||||
'total_rides' => 'integer',
|
||||
'total_roller_coasters' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'industry_presence_score' => 'integer',
|
||||
'market_share_percentage' => 'decimal:2',
|
||||
'founded_year' => 'integer',
|
||||
'manufacturing_categories' => 'array',
|
||||
'global_installations' => 'integer',
|
||||
'is_major_manufacturer' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasSlugHistory;
|
||||
use App\Traits\HasLocation;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Operator extends Model
|
||||
{
|
||||
use HasFactory, HasSlugHistory;
|
||||
use HasFactory, HasSlugHistory, HasLocation, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -20,14 +24,43 @@ class Operator extends Model
|
||||
'name',
|
||||
'slug',
|
||||
'website',
|
||||
'headquarters',
|
||||
'description',
|
||||
'industry_sector',
|
||||
'founded_year',
|
||||
'employee_count',
|
||||
'annual_revenue',
|
||||
'market_cap',
|
||||
'stock_symbol',
|
||||
'headquarters_location',
|
||||
'geographic_presence',
|
||||
'company_type',
|
||||
'parent_company_id',
|
||||
'is_public',
|
||||
'is_active',
|
||||
'total_parks',
|
||||
'total_rides',
|
||||
'total_rides_manufactured',
|
||||
'total_rides_designed',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the parks operated by this company.
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'founded_year' => 'integer',
|
||||
'employee_count' => 'integer',
|
||||
'annual_revenue' => 'decimal:2',
|
||||
'market_cap' => 'decimal:2',
|
||||
'is_public' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'total_parks' => 'integer',
|
||||
'total_rides_manufactured' => 'integer',
|
||||
'total_rides_designed' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the parks operated by this operator.
|
||||
*/
|
||||
public function parks(): HasMany
|
||||
{
|
||||
@@ -35,21 +68,60 @@ class Operator extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Update park statistics.
|
||||
* Get the rides manufactured by this operator.
|
||||
*/
|
||||
public function manufactured_rides(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ride::class, 'manufacturer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rides designed by this operator.
|
||||
*/
|
||||
public function designed_rides(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ride::class, 'designer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent company if this is a subsidiary.
|
||||
*/
|
||||
public function parent_company(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Operator::class, 'parent_company_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subsidiary companies.
|
||||
*/
|
||||
public function subsidiaries(): HasMany
|
||||
{
|
||||
return $this->hasMany(Operator::class, 'parent_company_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comprehensive statistics.
|
||||
*/
|
||||
public function updateStatistics(): void
|
||||
{
|
||||
$this->total_parks = $this->parks()->count();
|
||||
$this->total_rides = $this->parks()->sum('ride_count');
|
||||
$this->total_rides_manufactured = $this->manufactured_rides()->count();
|
||||
$this->total_rides_designed = $this->designed_rides()->count();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the operator's name with total parks.
|
||||
* Get the operator's display name with role indicators.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
return "{$this->name} ({$this->total_parks} parks)";
|
||||
$roles = [];
|
||||
if ($this->total_parks > 0) $roles[] = 'Operator';
|
||||
if ($this->total_rides_manufactured > 0) $roles[] = 'Manufacturer';
|
||||
if ($this->total_rides_designed > 0) $roles[] = 'Designer';
|
||||
|
||||
$roleText = empty($roles) ? '' : ' (' . implode(', ', $roles) . ')';
|
||||
return $this->name . $roleText;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +141,98 @@ class Operator extends Model
|
||||
return $website;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company size category based on employee count.
|
||||
*/
|
||||
public function getCompanySizeCategoryAttribute(): string
|
||||
{
|
||||
if (!$this->employee_count) return 'unknown';
|
||||
|
||||
return match (true) {
|
||||
$this->employee_count <= 100 => 'small',
|
||||
$this->employee_count <= 1000 => 'medium',
|
||||
$this->employee_count <= 10000 => 'large',
|
||||
default => 'enterprise'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get geographic presence level.
|
||||
*/
|
||||
public function getGeographicPresenceLevelAttribute(): string
|
||||
{
|
||||
$countries = $this->parks()
|
||||
->join('locations', 'parks.location_id', '=', 'locations.id')
|
||||
->distinct('locations.country')
|
||||
->count('locations.country');
|
||||
|
||||
return match (true) {
|
||||
$countries <= 1 => 'regional',
|
||||
$countries <= 3 => 'national',
|
||||
default => 'international'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get market influence score.
|
||||
*/
|
||||
public function getMarketInfluenceScoreAttribute(): float
|
||||
{
|
||||
$score = 0;
|
||||
|
||||
// Parks operated (40% weight)
|
||||
$score += min($this->total_parks * 10, 40);
|
||||
|
||||
// Rides manufactured (30% weight)
|
||||
$score += min($this->total_rides_manufactured * 2, 30);
|
||||
|
||||
// Revenue influence (20% weight)
|
||||
if ($this->annual_revenue) {
|
||||
$score += min(($this->annual_revenue / 1000000000) * 10, 20);
|
||||
}
|
||||
|
||||
// Geographic presence (10% weight)
|
||||
$countries = $this->parks()
|
||||
->join('locations', 'parks.location_id', '=', 'locations.id')
|
||||
->distinct('locations.country')
|
||||
->count('locations.country');
|
||||
$score += min($countries * 2, 10);
|
||||
|
||||
return round($score, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include active operators.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include park operators.
|
||||
*/
|
||||
public function scopeParkOperators($query)
|
||||
{
|
||||
return $query->whereHas('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include ride manufacturers.
|
||||
*/
|
||||
public function scopeManufacturers($query)
|
||||
{
|
||||
return $query->whereHas('manufactured_rides');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include ride designers.
|
||||
*/
|
||||
public function scopeDesigners($query)
|
||||
{
|
||||
return $query->whereHas('designed_rides');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include major operators (with multiple parks).
|
||||
*/
|
||||
@@ -77,6 +241,75 @@ class Operator extends Model
|
||||
return $query->where('total_parks', '>=', $minParks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to filter by industry sector.
|
||||
*/
|
||||
public function scopeIndustrySector($query, string $sector)
|
||||
{
|
||||
return $query->where('industry_sector', $sector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to filter by company size.
|
||||
*/
|
||||
public function scopeCompanySize($query, string $size)
|
||||
{
|
||||
$ranges = [
|
||||
'small' => [1, 100],
|
||||
'medium' => [101, 1000],
|
||||
'large' => [1001, 10000],
|
||||
'enterprise' => [10001, PHP_INT_MAX]
|
||||
];
|
||||
|
||||
if (isset($ranges[$size])) {
|
||||
return $query->whereBetween('employee_count', $ranges[$size]);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to filter by founded year range.
|
||||
*/
|
||||
public function scopeFoundedBetween($query, int $startYear, int $endYear)
|
||||
{
|
||||
return $query->whereBetween('founded_year', [$startYear, $endYear]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to filter by revenue range.
|
||||
*/
|
||||
public function scopeRevenueBetween($query, float $minRevenue, float $maxRevenue)
|
||||
{
|
||||
return $query->whereBetween('annual_revenue', [$minRevenue, $maxRevenue]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to filter by dual roles.
|
||||
*/
|
||||
public function scopeDualRole($query, array $roles)
|
||||
{
|
||||
return $query->where(function ($q) use ($roles) {
|
||||
if (in_array('park_operator', $roles)) {
|
||||
$q->orWhereHas('parks');
|
||||
}
|
||||
if (in_array('ride_manufacturer', $roles)) {
|
||||
$q->orWhereHas('manufactured_rides');
|
||||
}
|
||||
if (in_array('ride_designer', $roles)) {
|
||||
$q->orWhereHas('designed_rides');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query for optimized loading with counts.
|
||||
*/
|
||||
public function scopeWithCounts($query)
|
||||
{
|
||||
return $query->withCount(['parks', 'manufactured_rides', 'designed_rides']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route key for the model.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user