Resorts

ResortsIndex.php
<?php

namespace App\Livewire;

use App\Models\Resort;
use Illuminate\Contracts\View\View;
use LaraGrid\Actions\Action;
use LaraGrid\Aggregate;
use LaraGrid\Columns\ComputedColumn;
use LaraGrid\Columns\DateColumn;
use LaraGrid\Columns\IntegerColumn;
use LaraGrid\Columns\SerialColumn;
use LaraGrid\Columns\TextColumn;
use LaraGrid\Columns\YesNoColumn;
use LaraGrid\Filters\SelectFilter;
use LaraGrid\Grid;
use LaraGrid\Livewire\WithLaraGrid;
use LaraGrid\Support\CellHtml;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;

/**
 * All resorts — server-side readonly LaraGrid over the resorts table.
 *
 * Showcases: whitelisted sort/search, toolbar filters (type, visibility), footer sum,
 * a badge via an ->html() computed column, a row call-action (visibility toggle) and a
 * bulk hide with confirm — all declared here, zero blade wiring.
 */
#[Title('Resorts')]
#[Layout('components.layouts.app')]
class ResortsIndex extends Component
{
    use WithLaraGrid;

    /**
     * @return array<string, Grid>
     */
    protected function grids(): array
    {
        return [
            'resorts' => Grid::make('resorts')
                // is_visible: a derived 1/0 the YesNoColumn below paints as Y/N — typed value
                // columns read a real row value, so derivations belong in the query (unlike
                // ComputedColumn, whose ->state() bakes values row-by-row after the query).
                ->query(fn() => Resort::query()
                    ->selectRaw("resorts.*, (COALESCE(visibility, 'show') = 'show') as is_visible"))
                // Demo app has no auth/policies — permit openly. Gate with a policy in real apps.
                ->authorize(fn(): bool => true)
                ->paginate(10, [10, 25, 50, 100])
                ->savedViews()
                ->defaultSort('name')
                // name + shortcode are declared columns; slug/address are searched as raw DB
                // columns via the dot-qualified form (undeclared bare names are rejected —
                // the whitelist that keeps the search surface injection-closed).
                ->searchable(['name', 'resorts.slug'])
                ->filters([
                    SelectFilter::make('type')->label('Type')
                        ->options(fn() => Resort::query()
                            ->whereNotNull('type')
                            ->distinct()->orderBy('type')
                            ->pluck('type', 'type')),
                    SelectFilter::make('visibility')->label('Visibility')
                        ->options(['show' => 'Show', 'hide' => 'Hide']),
                ])
                ->columns([
                    SerialColumn::make(),
                    IntegerColumn::make('id')->label('ID')->sortable()->width(70),
                    TextColumn::make('name')->label('Resort')->sortable()->searchable()->grow(),
                    TextColumn::make('type')->label('Type')->sortable()->width(120),
                    YesNoColumn::make('is_visible')->label('Status Y/N')->width(90),
                    IntegerColumn::make('comparison_tariff')->label('Tariff')->sortable()->width(100),
                    IntegerColumn::make('hits')->label('Hits')->sortable()->width(90),
                    ComputedColumn::make('status')->label('Status')->html()->width(90)
                        ->state(fn(array $row): string => ($row['visibility'] ?? 'show') === 'show'
                            ? CellHtml::badge('green', 'Show')
                            : CellHtml::badge('zinc', 'Hide')),
                    DateColumn::make('created_at')->label('Added')->sortable()->width(110),
                ])
                ->exportable()
                ->footer([
                    Aggregate::sum('hits')->format('number'),
                ])
                ->actions([
                    Action::make('toggle')->label('Show / Hide')->icon('👁')
                        ->call(function (array $row): void {
                            Resort::whereKey($row['id'])->update([
                                'visibility' => ($row['visibility'] ?? 'show') === 'show' ? 'hide' : 'show',
                            ]);
                        }),
                ])
                ->bulkActions([
                    Action::make('hide-selected')->label('Hide')->icon('🙈')
                        ->confirm('Hide all selected resorts?')
                        ->call(fn(array $keys) => Resort::whereKey($keys)->update(['visibility' => 'hide'])),
                ])
                ->stickyHeader()
                ->striped()
                ->maxHeight('70vh')
                ->emptyState('No resorts found.'),
        ];
    }

    public function render(): View
    {
        return view('livewire.resorts-index');
    }
}
app/Livewire/ResortsIndex.php