Files
thrillwiki_laravel/memory-bank/prompts/OperatorsListingPagePrompt.md

23 KiB

Operators Listing Page Implementation Prompt

Django Parity Reference

Django Implementation: companies/views.py - CompanyListView & ManufacturerListView (lines 62-126) Django Template: companies/templates/companies/company_list.html Django Features: Dual-role filtering (park operators vs ride manufacturers), industry statistics, portfolio showcases, corporate hierarchy display, market analysis

Core Implementation Requirements

Laravel/Livewire Architecture

Generate the operators listing system using ThrillWiki's custom generators:

# Generate unified operators listing with dual-role support
php artisan make:thrillwiki-livewire OperatorsListing --paginated --cached --with-tests

# Generate role-specific filtering component
php artisan make:thrillwiki-livewire OperatorsRoleFilter --reusable --with-tests

# Generate portfolio showcase component
php artisan make:thrillwiki-livewire OperatorPortfolioCard --reusable --with-tests

# Generate industry statistics dashboard
php artisan make:thrillwiki-livewire OperatorsIndustryStats --reusable --cached

# Generate corporate hierarchy visualization
php artisan make:thrillwiki-livewire OperatorHierarchyView --reusable --with-tests

# Generate market analysis component
php artisan make:thrillwiki-livewire OperatorsMarketAnalysis --reusable --cached

Django Parity Features

1. Dual-Role Search Functionality

Django Implementation: Multi-role search across:

  • Operator name (name__icontains)
  • Company description (description__icontains)
  • Founded year range (founded_year__range)
  • Headquarters location (headquarters__city__icontains)
  • Role-specific filtering (park_operator, ride_manufacturer, or both)
  • Industry sector (industry_sector__icontains)

Laravel Implementation:

public function dualRoleSearch($query, $roles = [])
{
    return Operator::query()
        ->when($query, function ($q) use ($query) {
            $terms = explode(' ', $query);
            foreach ($terms as $term) {
                $q->where(function ($subQuery) use ($term) {
                    $subQuery->where('name', 'ilike', "%{$term}%")
                            ->orWhere('description', 'ilike', "%{$term}%")
                            ->orWhere('industry_sector', '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->whereExists(function ($exists) {
                        $exists->select(DB::raw(1))
                               ->from('parks')
                               ->whereRaw('parks.operator_id = operators.id');
                    });
                }
                if (in_array('ride_manufacturer', $roles)) {
                    $roleQuery->orWhereExists(function ($exists) {
                        $exists->select(DB::raw(1))
                               ->from('rides')
                               ->whereRaw('rides.manufacturer_id = operators.id');
                    });
                }
                if (in_array('ride_designer', $roles)) {
                    $roleQuery->orWhereExists(function ($exists) {
                        $exists->select(DB::raw(1))
                               ->from('rides')
                               ->whereRaw('rides.designer_id = operators.id');
                    });
                }
            });
        })
        ->with(['location', 'parks', 'manufactured_rides', 'designed_rides'])
        ->withCount(['parks', 'manufactured_rides', 'designed_rides']);
}

2. Advanced Industry Filtering

Django Filters:

  • Role type (park_operator, manufacturer, designer, mixed)
  • Industry sector (entertainment, manufacturing, technology)
  • Company size (small, medium, large, enterprise)
  • Founded year range
  • Geographic presence (regional, national, international)
  • Market capitalization range
  • Annual revenue range

Laravel Filters Implementation:

public function applyIndustryFilters($query, $filters)
{
    return $query
        ->when($filters['role_type'] ?? null, function ($q, $roleType) {
            switch ($roleType) {
                case 'park_operator_only':
                    $q->whereHas('parks')->whereDoesntHave('manufactured_rides');
                    break;
                case 'manufacturer_only':
                    $q->whereHas('manufactured_rides')->whereDoesntHave('parks');
                    break;
                case 'mixed':
                    $q->whereHas('parks')->whereHas('manufactured_rides');
                    break;
                case 'designer':
                    $q->whereHas('designed_rides');
                    break;
            }
        })
        ->when($filters['industry_sector'] ?? null, fn($q, $sector) => 
            $q->where('industry_sector', $sector))
        ->when($filters['company_size'] ?? null, function ($q, $size) {
            $ranges = [
                'small' => [1, 100],
                'medium' => [101, 1000],
                'large' => [1001, 10000],
                'enterprise' => [10001, PHP_INT_MAX]
            ];
            if (isset($ranges[$size])) {
                $q->whereBetween('employee_count', $ranges[$size]);
            }
        })
        ->when($filters['founded_year_from'] ?? null, fn($q, $year) => 
            $q->where('founded_year', '>=', $year))
        ->when($filters['founded_year_to'] ?? null, fn($q, $year) => 
            $q->where('founded_year', '<=', $year))
        ->when($filters['geographic_presence'] ?? null, 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($filters['min_revenue'] ?? null, fn($q, $revenue) => 
            $q->where('annual_revenue', '>=', $revenue))
        ->when($filters['max_revenue'] ?? null, fn($q, $revenue) => 
            $q->where('annual_revenue', '<=', $revenue));
}

3. Portfolio and Statistics Display

Portfolio Metrics:

  • Total parks operated
  • Total rides manufactured/designed
  • Geographic reach (countries, continents)
  • Market share analysis
  • Revenue and financial metrics
  • Industry influence score

Screen-Agnostic Design Implementation

Mobile Layout (320px - 767px)

  • Corporate Cards: Compact operator cards with key metrics
  • Role Badges: Visual indicators for operator/manufacturer/designer roles
  • Portfolio Highlights: Key statistics prominently displayed
  • Industry Filters: Simplified filtering for mobile users

Mobile Component Structure:

<div class="operators-mobile-layout">
    <!-- Industry Search Bar -->
    <div class="sticky top-0 bg-white dark:bg-gray-900 z-20 p-4">
        <livewire:operators-industry-search />
        <div class="flex items-center mt-2 space-x-2">
            <button wire:click="filterByRole('park_operator')" 
                    class="flex items-center space-x-1 px-3 py-1 {{ $activeRole === 'park_operator' ? 'bg-blue-500 text-white' : 'bg-blue-100 dark:bg-blue-900' }} rounded-full">
                <span class="text-sm">Operators</span>
            </button>
            <button wire:click="filterByRole('manufacturer')" 
                    class="flex items-center space-x-1 px-3 py-1 {{ $activeRole === 'manufacturer' ? 'bg-green-500 text-white' : 'bg-green-100 dark:bg-green-900' }} rounded-full">
                <span class="text-sm">Manufacturers</span>
            </button>
        </div>
    </div>
    
    <!-- Industry Statistics Banner -->
    <div class="bg-gradient-to-r from-blue-500 to-purple-600 text-white p-4 m-4 rounded-lg">
        <livewire:operators-industry-stats :compact="true" />
    </div>
    
    <!-- Quick Filters -->
    <div class="horizontal-scroll p-4 pb-2">
        <livewire:operators-quick-filters />
    </div>
    
    <!-- Operator Cards -->
    <div class="space-y-4 p-4">
        @foreach($operators as $operator)
            <livewire:operator-mobile-card :operator="$operator" :show-portfolio="true" :key="$operator->id" />
        @endforeach
    </div>
    
    <!-- Mobile Pagination -->
    <div class="sticky bottom-0 bg-white dark:bg-gray-900 p-4">
        {{ $operators->links('pagination.mobile') }}
    </div>
</div>

Tablet Layout (768px - 1023px)

  • Dual-Pane Layout: Filter sidebar + operator grid
  • Portfolio Showcases: Detailed portfolio cards for each operator
  • Industry Dashboard: Real-time industry statistics and trends
  • Comparison Mode: Side-by-side operator comparisons

Tablet Component Structure:

<div class="operators-tablet-layout flex h-screen">
    <!-- Industry Filter Sidebar -->
    <div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
        <div class="p-6">
            <livewire:operators-industry-search :advanced="true" />
            <div class="mt-6">
                <livewire:operators-role-filter :expanded="true" />
            </div>
            <div class="mt-6">
                <livewire:operators-industry-filters :show-financial="true" />
            </div>
            <div class="mt-6">
                <livewire:operators-industry-stats :detailed="true" />
            </div>
        </div>
    </div>
    
    <!-- Main Content Area -->
    <div class="flex-1 flex flex-col">
        <!-- Industry Header -->
        <div class="bg-white dark:bg-gray-900 p-4 border-b border-gray-200 dark:border-gray-700">
            <div class="flex items-center justify-between">
                <div class="flex items-center space-x-4">
                    <h2 class="text-xl font-semibold">{{ $operators->total() }} Industry Leaders</h2>
                    <livewire:operators-market-overview />
                </div>
                <div class="flex items-center space-x-2">
                    <livewire:operators-sort-selector />
                    <livewire:operators-view-toggle />
                </div>
            </div>
        </div>
        
        <!-- Content Grid -->
        <div class="flex-1 overflow-y-auto p-6">
            @if($view === 'grid')
                <div class="grid grid-cols-2 gap-6">
                    @foreach($operators as $operator)
                        <livewire:operator-tablet-card :operator="$operator" :detailed="true" :key="$operator->id" />
                    @endforeach
                </div>
            @elseif($view === 'portfolio')
                <div class="space-y-6">
                    @foreach($operators as $operator)
                        <livewire:operator-portfolio-showcase :operator="$operator" :key="$operator->id" />
                    @endforeach
                </div>
            @else
                <livewire:operators-market-analysis :operators="$operators" />
            @endif
            
            <div class="mt-6">
                {{ $operators->links() }}
            </div>
        </div>
    </div>
</div>

Desktop Layout (1024px - 1919px)

  • Three-Pane Layout: Filters + main content + industry insights
  • Advanced Analytics: Market share analysis and industry trends
  • Corporate Hierarchies: Visual representation of corporate structures
  • Portfolio Deep Dives: Comprehensive portfolio analysis

Desktop Component Structure:

<div class="operators-desktop-layout flex h-screen">
    <!-- Advanced Filter Sidebar -->
    <div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
        <div class="p-6">
            <livewire:operators-industry-search :advanced="true" :autocomplete="true" />
            <div class="mt-6">
                <livewire:operators-role-filter :advanced="true" :show-statistics="true" />
            </div>
            <div class="mt-6">
                <livewire:operators-industry-filters :advanced="true" :show-financial="true" />
            </div>
        </div>
    </div>
    
    <!-- Main Content -->
    <div class="flex-1 flex flex-col">
        <!-- Industry Dashboard Header -->
        <div class="bg-white dark:bg-gray-900 p-6 border-b border-gray-200 dark:border-gray-700">
            <div class="flex items-center justify-between mb-4">
                <div class="flex items-center space-x-6">
                    <h1 class="text-2xl font-bold">{{ $operators->total() }} Industry Operators</h1>
                    <livewire:operators-market-summary />
                </div>
                <div class="flex items-center space-x-4">
                    <livewire:operators-sort-selector :advanced="true" />
                    <livewire:operators-view-selector />
                    <livewire:operators-export-options />
                </div>
            </div>
            <livewire:operators-advanced-search />
        </div>
        
        <!-- Content Area -->
        <div class="flex-1 overflow-y-auto">
            @if($view === 'grid')
                <div class="p-6">
                    <div class="grid grid-cols-3 xl:grid-cols-4 gap-6">
                        @foreach($operators as $operator)
                            <livewire:operator-desktop-card :operator="$operator" :comprehensive="true" :key="$operator->id" />
                        @endforeach
                    </div>
                    <div class="mt-8">
                        {{ $operators->links('pagination.desktop') }}
                    </div>
                </div>
            @elseif($view === 'portfolio')
                <div class="p-6 space-y-8">
                    @foreach($operators as $operator)
                        <livewire:operator-portfolio-detailed :operator="$operator" :key="$operator->id" />
                    @endforeach
                </div>
            @elseif($view === 'hierarchy')
                <div class="p-6">
                    <livewire:operators-hierarchy-visualization :operators="$operators" />
                </div>
            @else
                <div class="p-6">
                    <livewire:operators-market-dashboard :operators="$operators" />
                </div>
            @endif
        </div>
    </div>
    
    <!-- Industry Insights Panel -->
    <div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
        <div class="p-6">
            <livewire:operators-industry-insights />
            <div class="mt-6">
                <livewire:operators-market-trends />
            </div>
            <div class="mt-6">
                <livewire:operators-recent-activity />
            </div>
        </div>
    </div>
</div>

Large Screen Layout (1920px+)

  • Dashboard-Style Interface: Comprehensive industry analytics
  • Multi-Panel Views: Simultaneous portfolio and market analysis
  • Advanced Visualizations: Corporate network maps and market dynamics
  • Real-Time Market Data: Live industry statistics and trends

Performance Optimization Strategy

Industry-Specific Caching

public function mount()
{
    $this->industryStats = Cache::remember(
        'operators.industry.stats',
        now()->addHours(6),
        fn() => $this->calculateIndustryStatistics()
    );
    
    $this->marketData = Cache::remember(
        'operators.market.data',
        now()->addHours(12),
        fn() => $this->loadMarketAnalysis()
    );
}

public function getOperatorsProperty()
{
    $cacheKey = "operators.listing." . md5(serialize([
        'search' => $this->search,
        'filters' => $this->filters,
        'role_filter' => $this->roleFilter,
        'sort' => $this->sort,
        'page' => $this->page
    ]));
    
    return Cache::remember($cacheKey, now()->addMinutes(30), function() {
        return $this->dualRoleSearch($this->search, $this->roleFilter)
            ->applyIndustryFilters($this->filters)
            ->orderBy($this->sort['column'], $this->sort['direction'])
            ->paginate(20);
    });
}

Financial Data Optimization

// Optimized query for financial and portfolio data
public function optimizedFinancialQuery()
{
    return Operator::select([
        'operators.*',
        DB::raw('COALESCE(parks_count.count, 0) as parks_count'),
        DB::raw('COALESCE(rides_count.count, 0) as manufactured_rides_count'),
        DB::raw('COALESCE(designed_rides_count.count, 0) as designed_rides_count'),
        DB::raw('CASE 
            WHEN annual_revenue > 10000000000 THEN "enterprise"
            WHEN annual_revenue > 1000000000 THEN "large"
            WHEN annual_revenue > 100000000 THEN "medium"
            ELSE "small"
        END as company_size_category')
    ])
    ->leftJoin(DB::raw('(SELECT operator_id, COUNT(*) as count FROM parks GROUP BY operator_id) as parks_count'), 
              'operators.id', '=', 'parks_count.operator_id')
    ->leftJoin(DB::raw('(SELECT manufacturer_id, COUNT(*) as count FROM rides GROUP BY manufacturer_id) as rides_count'), 
              'operators.id', '=', 'rides_count.manufacturer_id')
    ->leftJoin(DB::raw('(SELECT designer_id, COUNT(*) as count FROM rides GROUP BY designer_id) as designed_rides_count'), 
              'operators.id', '=', 'designed_rides_count.designer_id')
    ->with([
        'location:id,city,state,country',
        'parks:id,operator_id,name,opening_date',
        'manufactured_rides:id,manufacturer_id,name,ride_type',
        'designed_rides:id,designer_id,name,ride_type'
    ]);
}

Component Reuse Strategy

Shared Components

  • OperatorsRoleFilter: Multi-role filtering with statistics
  • OperatorPortfolioCard: Comprehensive portfolio display
  • OperatorsIndustryStats: Real-time industry analytics
  • OperatorFinancialMetrics: Financial performance indicators

Context Variations

  • ParkOperatorsListing: Park operators only with park portfolios
  • ManufacturersListing: Ride manufacturers with product catalogs
  • DesignersListing: Ride designers with design portfolios
  • CorporateGroupsListing: Corporate hierarchies and subsidiaries

Testing Requirements

Feature Tests

/** @test */
public function can_filter_operators_by_dual_roles()
{
    $pureOperator = Operator::factory()->create(['name' => 'Disney Parks']);
    $pureOperator->parks()->create(['name' => 'Magic Kingdom']);
    
    $pureManufacturer = Operator::factory()->create(['name' => 'Intamin']);
    $pureManufacturer->manufactured_rides()->create(['name' => 'Millennium Force']);
    
    $mixedOperator = Operator::factory()->create(['name' => 'Universal']);
    $mixedOperator->parks()->create(['name' => 'Universal Studios']);
    $mixedOperator->manufactured_rides()->create(['name' => 'Custom Ride']);
    
    Livewire::test(OperatorsListing::class)
        ->set('roleFilter', ['park_operator'])
        ->assertSee($pureOperator->name)
        ->assertSee($mixedOperator->name)
        ->assertDontSee($pureManufacturer->name);
}

/** @test */
public function calculates_industry_statistics_correctly()
{
    Operator::factory()->count(10)->create(['industry_sector' => 'entertainment']);
    Operator::factory()->count(5)->create(['industry_sector' => 'manufacturing']);
    
    $component = Livewire::test(OperatorsListing::class);
    $stats = $component->get('industryStats');
    
    $this->assertEquals(15, $stats['total_operators']);
    $this->assertEquals(10, $stats['entertainment_operators']);
    $this->assertEquals(5, $stats['manufacturing_operators']);
}

/** @test */
public function maintains_django_parity_performance_with_portfolio_data()
{
    Operator::factory()->count(50)->create();
    
    $start = microtime(true);
    Livewire::test(OperatorsListing::class);
    $end = microtime(true);
    
    $this->assertLessThan(0.5, $end - $start); // < 500ms with portfolio data
}

Financial Data Tests

/** @test */
public function categorizes_company_size_correctly()
{
    $enterprise = Operator::factory()->create(['annual_revenue' => 15000000000]);
    $large = Operator::factory()->create(['annual_revenue' => 5000000000]);
    $medium = Operator::factory()->create(['annual_revenue' => 500000000]);
    $small = Operator::factory()->create(['annual_revenue' => 50000000]);
    
    Livewire::test(OperatorsListing::class)
        ->set('filters.company_size', 'enterprise')
        ->assertSee($enterprise->name)
        ->assertDontSee($large->name);
}

/** @test */
public function handles_portfolio_metrics_calculation()
{
    $operator = Operator::factory()->create();
    $operator->parks()->createMany(3, ['name' => 'Test Park']);
    $operator->manufactured_rides()->createMany(5, ['name' => 'Test Ride']);
    
    $component = Livewire::test(OperatorsListing::class);
    $portfolioData = $component->get('operators')->first();
    
    $this->assertEquals(3, $portfolioData->parks_count);
    $this->assertEquals(5, $portfolioData->manufactured_rides_count);
}

Performance Targets

Universal Performance Standards with Financial Data

  • Initial Load: < 500ms (including industry statistics)
  • Portfolio Calculation: < 200ms for 100 operators
  • Financial Filtering: < 150ms with complex criteria
  • Market Analysis: < 1 second for trend calculations
  • Industry Statistics: < 100ms (cached)

Industry-Specific Caching Strategy

  • Market Data Cache: 12 hours (financial markets change)
  • Industry Statistics: 6 hours (relatively stable)
  • Portfolio Metrics: 1 hour (operational data)
  • Company Profiles: 24 hours (corporate data stable)

Success Criteria Checklist

Django Parity Verification

  • Dual-role filtering matches Django behavior exactly
  • Industry statistics calculated identically to Django
  • Portfolio metrics match Django calculations
  • Financial filtering provides same results as Django
  • Corporate hierarchy display matches Django structure

Screen-Agnostic Compliance

  • Mobile layout optimized for corporate data consumption
  • Tablet layout provides effective portfolio comparisons
  • Desktop layout maximizes industry analytics
  • Large screen layout provides comprehensive market view
  • All layouts handle complex financial data gracefully

Performance Benchmarks

  • Initial load under 500ms including portfolio data
  • Financial calculations under 200ms
  • Industry statistics under 100ms (cached)
  • Market analysis under 1 second
  • Portfolio caching reduces server load by 60%

Industry Feature Completeness

  • Dual-role filtering works across all operator types
  • Financial metrics display accurately
  • Portfolio showcases provide comprehensive overviews
  • Market analysis provides meaningful insights
  • Corporate hierarchies visualize relationships correctly

This prompt ensures complete Django parity while providing comprehensive industry analysis capabilities that leverage modern data visualization and maintain ThrillWiki's screen-agnostic design principles.