mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 05:31:10 -05:00
- 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.
476 lines
15 KiB
PHP
476 lines
15 KiB
PHP
<?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,
|
|
]);
|
|
}
|
|
} |