Files
thrillwiki_laravel/app/Livewire/ParksListingUniversal.php
pacnpal 97a7682eb7 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.
2025-06-23 21:31:05 -04:00

475 lines
15 KiB
PHP

<?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();
}
}
}