From ca311e99b4fa4c631d1156db9853b91dbb863f34 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 6 Apr 2026 07:22:53 +0000 Subject: [PATCH] Auto commit: 2026-04-06T07:22:53.709Z --- app/Filament/Resources/BookingResource.php | 145 ++-- app/Filament/Resources/EventResource.php | 103 +-- app/Filament/Resources/OfferResource.php | 151 ++-- app/Filament/Resources/RideResource.php | 155 ++-- app/Filament/Widgets/FunnelStageChart.php | 43 +- app/Filament/Widgets/JourneyStoryboard.php | 48 ++ app/Providers/Filament/AdminPanelProvider.php | 10 +- resources/views/bookings/success.blade.php | 147 ++-- .../widgets/journey-storyboard.blade.php | 83 ++ resources/views/home.blade.php | 153 +++- resources/views/layouts/app.blade.php | 745 +++++++++++++++++- resources/views/offers/show.blade.php | 163 ++-- resources/views/rides/confirmed.blade.php | 233 ++++-- 13 files changed, 1664 insertions(+), 515 deletions(-) create mode 100644 app/Filament/Widgets/JourneyStoryboard.php create mode 100644 resources/views/filament/widgets/journey-storyboard.blade.php diff --git a/app/Filament/Resources/BookingResource.php b/app/Filament/Resources/BookingResource.php index 4fe2097..459eb99 100644 --- a/app/Filament/Resources/BookingResource.php +++ b/app/Filament/Resources/BookingResource.php @@ -3,117 +3,102 @@ namespace App\Filament\Resources; use App\Filament\Resources\BookingResource\Pages; -use App\Filament\Resources\BookingResource\RelationManagers; use App\Models\Booking; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class BookingResource extends Resource { protected static ?string $model = Booking::class; protected static ?string $navigationIcon = 'heroicon-o-ticket'; - protected static ?string $navigationLabel = 'Bookings'; - protected static ?string $navigationGroup = 'Demo TAXILANZ'; + protected static ?string $navigationLabel = 'Reservas'; + protected static ?string $navigationGroup = 'Funnel TAXILANZ'; + protected static ?string $modelLabel = 'Reserva'; + protected static ?string $pluralModelLabel = 'Reservas'; protected static ?int $navigationSort = 3; + public static function getNavigationBadge(): ?string + { + return (string) static::getModel()::count(); + } + + public static function getNavigationBadgeColor(): ?string + { + return 'success'; + } + public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\TextInput::make('uuid') - ->label('UUID') - ->required() - ->maxLength(36), - Forms\Components\Select::make('ride_id') - ->relationship('ride', 'id') - ->default(null), - Forms\Components\Select::make('offer_id') - ->relationship('offer', 'title') - ->required(), - Forms\Components\TextInput::make('ride_recommendation_id') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('customer_name') - ->maxLength(255) - ->default(null), - Forms\Components\TextInput::make('customer_email') - ->email() - ->maxLength(255) - ->default(null), - Forms\Components\TextInput::make('customer_phone') - ->tel() - ->maxLength(255) - ->default(null), - Forms\Components\TextInput::make('party_size') - ->required() - ->numeric() - ->default(1), - Forms\Components\DateTimePicker::make('booking_for'), - Forms\Components\TextInput::make('status') - ->required(), - Forms\Components\TextInput::make('amount') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('commission_amount') - ->numeric() - ->default(null), - Forms\Components\Textarea::make('notes') - ->columnSpanFull(), + Forms\Components\TextInput::make('uuid')->label('UUID')->required()->maxLength(36), + Forms\Components\Select::make('ride_id')->label('Trayecto')->relationship('ride', 'id')->default(null), + Forms\Components\Select::make('offer_id')->label('Oferta')->relationship('offer', 'title')->required(), + Forms\Components\TextInput::make('ride_recommendation_id')->label('Recomendación')->numeric()->default(null), + Forms\Components\TextInput::make('customer_name')->label('Cliente')->maxLength(255)->default(null), + Forms\Components\TextInput::make('customer_email')->email()->maxLength(255)->default(null), + Forms\Components\TextInput::make('customer_phone')->tel()->maxLength(255)->default(null), + Forms\Components\TextInput::make('party_size')->label('Personas')->required()->numeric()->default(1), + Forms\Components\DateTimePicker::make('booking_for')->label('Fecha / hora'), + Forms\Components\TextInput::make('status')->label('Estado')->required(), + Forms\Components\TextInput::make('amount')->label('Importe')->numeric()->default(null), + Forms\Components\TextInput::make('commission_amount')->label('Comisión')->numeric()->default(null), + Forms\Components\Textarea::make('notes')->label('Nota')->columnSpanFull(), ]); } public static function table(Table $table): Table { return $table + ->defaultSort('created_at', 'desc') ->columns([ - Tables\Columns\TextColumn::make('uuid') - ->label('UUID') - ->searchable(), - Tables\Columns\TextColumn::make('ride.id') - ->numeric() - ->sortable(), Tables\Columns\TextColumn::make('offer.title') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('ride_recommendation_id') - ->numeric() - ->sortable(), + ->label('Oferta') + ->searchable() + ->weight('semibold'), Tables\Columns\TextColumn::make('customer_name') - ->searchable(), - Tables\Columns\TextColumn::make('customer_email') - ->searchable(), - Tables\Columns\TextColumn::make('customer_phone') - ->searchable(), - Tables\Columns\TextColumn::make('party_size') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('booking_for') - ->dateTime() - ->sortable(), - Tables\Columns\TextColumn::make('status'), + ->label('Cliente') + ->searchable() + ->description(fn (Booking $record): string => $record->ride?->pickup_label ? 'Ride: '.$record->ride->pickup_label : 'Reserva directa'), + Tables\Columns\TextColumn::make('status') + ->label('Estado') + ->badge() + ->color(fn (?string $state): string => match ($state) { + 'confirmed', 'completed' => 'success', + 'pending' => 'warning', + 'cancelled' => 'danger', + default => 'gray', + }), Tables\Columns\TextColumn::make('amount') - ->numeric() + ->label('Importe') + ->money('EUR') ->sortable(), Tables\Columns\TextColumn::make('commission_amount') - ->numeric() + ->label('Comisión') + ->money('EUR') ->sortable(), + Tables\Columns\TextColumn::make('ride_recommendation_id') + ->label('Top') + ->formatStateUsing(fn ($state): string => $state ? 'Sí' : 'Directa') + ->badge() + ->color(fn ($state): string => $state ? 'primary' : 'gray'), + Tables\Columns\TextColumn::make('booking_for') + ->label('Fecha') + ->dateTime('d/m/Y H:i') + ->sortable() + ->toggleable(), Tables\Columns\TextColumn::make('created_at') - ->dateTime() - ->sortable() + ->label('Creada') + ->since() + ->sortable(), + Tables\Columns\TextColumn::make('uuid') + ->label('UUID') + ->copyable() ->toggleable(isToggledHiddenByDefault: true), - Tables\Columns\TextColumn::make('updated_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - ]) - ->filters([ - // ]) ->actions([ Tables\Actions\EditAction::make(), @@ -127,9 +112,7 @@ class BookingResource extends Resource public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array diff --git a/app/Filament/Resources/EventResource.php b/app/Filament/Resources/EventResource.php index 16892ff..0d66c3f 100644 --- a/app/Filament/Resources/EventResource.php +++ b/app/Filament/Resources/EventResource.php @@ -3,77 +3,92 @@ namespace App\Filament\Resources; use App\Filament\Resources\EventResource\Pages; -use App\Filament\Resources\EventResource\RelationManagers; use App\Models\Event; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class EventResource extends Resource { protected static ?string $model = Event::class; protected static ?string $navigationIcon = 'heroicon-o-bolt'; - protected static ?string $navigationLabel = 'Events'; - protected static ?string $navigationGroup = 'Demo TAXILANZ'; + protected static ?string $navigationLabel = 'Eventos'; + protected static ?string $navigationGroup = 'Funnel TAXILANZ'; + protected static ?string $modelLabel = 'Evento'; + protected static ?string $pluralModelLabel = 'Eventos'; protected static ?int $navigationSort = 4; + public static function getNavigationBadge(): ?string + { + return (string) static::getModel()::count(); + } + + public static function getNavigationBadgeColor(): ?string + { + return 'gray'; + } + public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\TextInput::make('event_type') - ->required(), - Forms\Components\Select::make('ride_id') - ->relationship('ride', 'id') - ->default(null), - Forms\Components\Select::make('offer_id') - ->relationship('offer', 'title') - ->default(null), - Forms\Components\Select::make('booking_id') - ->relationship('booking', 'id') - ->default(null), - Forms\Components\TextInput::make('ride_recommendation_id') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('session_id') - ->maxLength(255) - ->default(null), - Forms\Components\Textarea::make('meta') - ->columnSpanFull(), + Forms\Components\TextInput::make('event_type')->label('Tipo')->required(), + Forms\Components\Select::make('ride_id')->label('Trayecto')->relationship('ride', 'id')->default(null), + Forms\Components\Select::make('offer_id')->label('Oferta')->relationship('offer', 'title')->default(null), + Forms\Components\Select::make('booking_id')->label('Reserva')->relationship('booking', 'id')->default(null), + Forms\Components\TextInput::make('ride_recommendation_id')->label('Recomendación')->numeric()->default(null), + Forms\Components\TextInput::make('session_id')->label('Sesión')->maxLength(255)->default(null), + Forms\Components\Textarea::make('meta')->label('Meta')->columnSpanFull(), ]); } public static function table(Table $table): Table { return $table + ->defaultSort('created_at', 'desc') ->columns([ - Tables\Columns\TextColumn::make('event_type'), - Tables\Columns\TextColumn::make('ride.id') - ->numeric() - ->sortable(), + Tables\Columns\TextColumn::make('event_type') + ->label('Evento') + ->badge() + ->formatStateUsing(fn (?string $state): string => str($state ?: 'unknown')->replace('_', ' ')->title()) + ->color(fn (?string $state): string => match ($state) { + 'request_created' => 'info', + 'recommendation_viewed' => 'warning', + 'recommendation_clicked' => 'primary', + 'booking_started' => 'gray', + 'booking_completed' => 'success', + default => 'gray', + }), + Tables\Columns\TextColumn::make('ride_id') + ->label('Ride') + ->badge() + ->color('info'), Tables\Columns\TextColumn::make('offer.title') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('booking.id') - ->numeric() - ->sortable(), + ->label('Oferta') + ->searchable() + ->toggleable(), + Tables\Columns\TextColumn::make('booking_id') + ->label('Booking') + ->badge() + ->color('success') + ->toggleable(), Tables\Columns\TextColumn::make('ride_recommendation_id') - ->numeric() - ->sortable(), + ->label('Top') + ->badge() + ->color('primary') + ->toggleable(), Tables\Columns\TextColumn::make('session_id') - ->searchable(), - Tables\Columns\TextColumn::make('created_at') - ->dateTime() - ->sortable() + ->label('Sesión') + ->limit(18) + ->searchable() ->toggleable(isToggledHiddenByDefault: true), - ]) - ->filters([ - // + Tables\Columns\TextColumn::make('created_at') + ->label('Creado') + ->dateTime('d/m/Y H:i') + ->sortable(), ]) ->actions([ Tables\Actions\EditAction::make(), @@ -87,9 +102,7 @@ class EventResource extends Resource public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array diff --git a/app/Filament/Resources/OfferResource.php b/app/Filament/Resources/OfferResource.php index 6a418ed..82f9e73 100644 --- a/app/Filament/Resources/OfferResource.php +++ b/app/Filament/Resources/OfferResource.php @@ -3,124 +3,107 @@ namespace App\Filament\Resources; use App\Filament\Resources\OfferResource\Pages; -use App\Filament\Resources\OfferResource\RelationManagers; use App\Models\Offer; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class OfferResource extends Resource { protected static ?string $model = Offer::class; protected static ?string $navigationIcon = 'heroicon-o-sparkles'; - protected static ?string $navigationLabel = 'Offers'; - protected static ?string $navigationGroup = 'Demo TAXILANZ'; + protected static ?string $navigationLabel = 'Ofertas'; + protected static ?string $navigationGroup = 'Funnel TAXILANZ'; + protected static ?string $modelLabel = 'Oferta'; + protected static ?string $pluralModelLabel = 'Ofertas'; protected static ?int $navigationSort = 1; + public static function getNavigationBadge(): ?string + { + return (string) static::getModel()::count(); + } + + public static function getNavigationBadgeColor(): ?string + { + return 'warning'; + } + public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\TextInput::make('uuid') - ->label('UUID') - ->required() - ->maxLength(36), - Forms\Components\TextInput::make('title') - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('slug') - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('category') - ->required(), - Forms\Components\TextInput::make('excerpt') - ->maxLength(255) - ->default(null), - Forms\Components\Textarea::make('description') - ->columnSpanFull(), - Forms\Components\TextInput::make('location_label') - ->maxLength(255) - ->default(null), - Forms\Components\TextInput::make('lat') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('lng') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('price_from') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('duration_minutes') - ->numeric() - ->default(null), - Forms\Components\FileUpload::make('image_url') - ->image(), - Forms\Components\TextInput::make('status') - ->required(), - Forms\Components\Toggle::make('is_featured') - ->required(), - Forms\Components\TextInput::make('priority_score') - ->required() - ->numeric() - ->default(0), - Forms\Components\Toggle::make('available_now') - ->required(), + Forms\Components\TextInput::make('uuid')->label('UUID')->required()->maxLength(36), + Forms\Components\TextInput::make('title')->label('Título')->required()->maxLength(255), + Forms\Components\TextInput::make('slug')->required()->maxLength(255), + Forms\Components\TextInput::make('category')->label('Categoría')->required(), + Forms\Components\TextInput::make('excerpt')->label('Resumen')->maxLength(255)->default(null), + Forms\Components\Textarea::make('description')->label('Descripción')->columnSpanFull(), + Forms\Components\TextInput::make('location_label')->label('Ubicación')->maxLength(255)->default(null), + Forms\Components\TextInput::make('lat')->numeric()->default(null), + Forms\Components\TextInput::make('lng')->numeric()->default(null), + Forms\Components\TextInput::make('price_from')->label('Precio desde')->numeric()->default(null), + Forms\Components\TextInput::make('duration_minutes')->label('Duración')->numeric()->default(null), + Forms\Components\FileUpload::make('image_url')->label('Imagen')->image(), + Forms\Components\TextInput::make('status')->label('Estado')->required(), + Forms\Components\Toggle::make('is_featured')->label('Destacada')->required(), + Forms\Components\TextInput::make('priority_score')->label('Prioridad')->required()->numeric()->default(0), + Forms\Components\Toggle::make('available_now')->label('Disponible ahora')->required(), ]); } public static function table(Table $table): Table { return $table + ->defaultSort('priority_score', 'desc') ->columns([ - Tables\Columns\TextColumn::make('uuid') - ->label('UUID') - ->searchable(), Tables\Columns\TextColumn::make('title') - ->searchable(), - Tables\Columns\TextColumn::make('slug') - ->searchable(), - Tables\Columns\TextColumn::make('category'), - Tables\Columns\TextColumn::make('excerpt') - ->searchable(), + ->label('Oferta') + ->searchable() + ->weight('semibold') + ->description(fn (Offer $record): string => $record->excerpt ?: ($record->location_label ?: 'Sin resumen')), + Tables\Columns\TextColumn::make('category') + ->label('Categoría') + ->badge() + ->formatStateUsing(fn (?string $state): string => ucfirst($state ?: 'general')) + ->color('warning'), Tables\Columns\TextColumn::make('location_label') - ->searchable(), - Tables\Columns\TextColumn::make('lat') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('lng') - ->numeric() - ->sortable(), + ->label('Ubicación') + ->badge() + ->toggleable(), Tables\Columns\TextColumn::make('price_from') - ->numeric() + ->label('Desde') + ->money('EUR') ->sortable(), Tables\Columns\TextColumn::make('duration_minutes') - ->numeric() + ->label('Duración') + ->suffix(' min') ->sortable(), - Tables\Columns\ImageColumn::make('image_url'), - Tables\Columns\TextColumn::make('status'), + Tables\Columns\TextColumn::make('status') + ->label('Estado') + ->badge() + ->color(fn (?string $state): string => match ($state) { + 'active' => 'success', + 'draft' => 'warning', + 'archived' => 'gray', + default => 'primary', + }), + Tables\Columns\IconColumn::make('available_now') + ->label('Ahora') + ->boolean(), Tables\Columns\IconColumn::make('is_featured') + ->label('Destacada') ->boolean(), Tables\Columns\TextColumn::make('priority_score') - ->numeric() + ->label('Prioridad') + ->badge() ->sortable(), - Tables\Columns\IconColumn::make('available_now') - ->boolean(), Tables\Columns\TextColumn::make('created_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - Tables\Columns\TextColumn::make('updated_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - ]) - ->filters([ - // + ->label('Creada') + ->since() + ->sortable(), ]) ->actions([ Tables\Actions\EditAction::make(), @@ -134,9 +117,7 @@ class OfferResource extends Resource public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array diff --git a/app/Filament/Resources/RideResource.php b/app/Filament/Resources/RideResource.php index 6360d67..6c7d900 100644 --- a/app/Filament/Resources/RideResource.php +++ b/app/Filament/Resources/RideResource.php @@ -3,114 +3,107 @@ namespace App\Filament\Resources; use App\Filament\Resources\RideResource\Pages; -use App\Filament\Resources\RideResource\RelationManagers; use App\Models\Ride; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class RideResource extends Resource { protected static ?string $model = Ride::class; protected static ?string $navigationIcon = 'heroicon-o-map'; - protected static ?string $navigationLabel = 'Rides'; - protected static ?string $navigationGroup = 'Demo TAXILANZ'; + protected static ?string $navigationLabel = 'Trayectos'; + protected static ?string $navigationGroup = 'Funnel TAXILANZ'; + protected static ?string $modelLabel = 'Trayecto'; + protected static ?string $pluralModelLabel = 'Trayectos'; protected static ?int $navigationSort = 2; + public static function getNavigationBadge(): ?string + { + return (string) static::getModel()::count(); + } + + public static function getNavigationBadgeColor(): ?string + { + return 'primary'; + } + public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\TextInput::make('uuid') - ->label('UUID') - ->required() - ->maxLength(36), - Forms\Components\TextInput::make('pickup_label') - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('pickup_lat') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('pickup_lng') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('destination_label') - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('destination_lat') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('destination_lng') - ->numeric() - ->default(null), - Forms\Components\DateTimePicker::make('scheduled_for'), - Forms\Components\TextInput::make('status') - ->required(), - Forms\Components\TextInput::make('eta_minutes') - ->numeric() - ->default(null), - Forms\Components\TextInput::make('source_channel') - ->required(), - Forms\Components\TextInput::make('context_zone') - ->maxLength(255) - ->default(null), - Forms\Components\TextInput::make('locale') - ->maxLength(10) - ->default(null), + Forms\Components\TextInput::make('uuid')->label('UUID')->required()->maxLength(36), + Forms\Components\TextInput::make('pickup_label')->label('Recogida')->required()->maxLength(255), + Forms\Components\TextInput::make('pickup_lat')->numeric()->default(null), + Forms\Components\TextInput::make('pickup_lng')->numeric()->default(null), + Forms\Components\TextInput::make('destination_label')->label('Destino')->required()->maxLength(255), + Forms\Components\TextInput::make('destination_lat')->numeric()->default(null), + Forms\Components\TextInput::make('destination_lng')->numeric()->default(null), + Forms\Components\DateTimePicker::make('scheduled_for')->label('Programado para'), + Forms\Components\TextInput::make('status')->label('Estado')->required(), + Forms\Components\TextInput::make('eta_minutes')->label('ETA (min)')->numeric()->default(null), + Forms\Components\TextInput::make('source_channel')->label('Canal')->required(), + Forms\Components\TextInput::make('context_zone')->label('Zona')->maxLength(255)->default(null), + Forms\Components\TextInput::make('locale')->maxLength(10)->default(null), ]); } public static function table(Table $table): Table { return $table + ->defaultSort('created_at', 'desc') ->columns([ - Tables\Columns\TextColumn::make('uuid') - ->label('UUID') - ->searchable(), Tables\Columns\TextColumn::make('pickup_label') - ->searchable(), - Tables\Columns\TextColumn::make('pickup_lat') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('pickup_lng') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('destination_label') - ->searchable(), - Tables\Columns\TextColumn::make('destination_lat') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('destination_lng') - ->numeric() + ->label('Trayecto') + ->searchable() + ->description(fn (Ride $record): string => '→ '.$record->destination_label) + ->weight('semibold'), + Tables\Columns\TextColumn::make('source_channel') + ->label('Canal') + ->badge() + ->formatStateUsing(fn (?string $state): string => ucfirst($state ?: 'directo')) + ->color(fn (?string $state): string => match ($state) { + 'hotel' => 'warning', + 'app' => 'success', + 'web' => 'info', + 'reception' => 'gray', + default => 'primary', + }), + Tables\Columns\TextColumn::make('context_zone') + ->label('Zona') + ->badge() + ->searchable() + ->toggleable(), + Tables\Columns\TextColumn::make('status') + ->label('Estado') + ->badge() + ->color(fn (?string $state): string => match ($state) { + 'confirmed' => 'success', + 'pending' => 'warning', + 'cancelled' => 'danger', + default => 'gray', + }), + Tables\Columns\TextColumn::make('eta_minutes') + ->label('ETA') + ->suffix(' min') ->sortable(), Tables\Columns\TextColumn::make('scheduled_for') - ->dateTime() - ->sortable(), - Tables\Columns\TextColumn::make('status'), - Tables\Columns\TextColumn::make('eta_minutes') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('source_channel'), - Tables\Columns\TextColumn::make('context_zone') - ->searchable(), - Tables\Columns\TextColumn::make('locale') - ->searchable(), + ->label('Programado') + ->dateTime('d/m/Y H:i') + ->sortable() + ->toggleable(), + Tables\Columns\TextColumn::make('uuid') + ->label('UUID') + ->searchable() + ->copyable() + ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('created_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - Tables\Columns\TextColumn::make('updated_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - ]) - ->filters([ - // + ->label('Creado') + ->since() + ->sortable(), ]) ->actions([ Tables\Actions\EditAction::make(), @@ -124,9 +117,7 @@ class RideResource extends Resource public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array diff --git a/app/Filament/Widgets/FunnelStageChart.php b/app/Filament/Widgets/FunnelStageChart.php index a35830f..595fcfe 100644 --- a/app/Filament/Widgets/FunnelStageChart.php +++ b/app/Filament/Widgets/FunnelStageChart.php @@ -9,7 +9,13 @@ use Filament\Widgets\ChartWidget; class FunnelStageChart extends ChartWidget { - protected static ?string $heading = 'Embudo TAXILANZ'; + protected static ?string $heading = 'Funnel ejecutivo'; + + protected int | string | array $columnSpan = 'full'; + + protected static ?int $sort = 2; + + protected static ?string $maxHeight = '260px'; protected function getData(): array { @@ -20,13 +26,40 @@ class FunnelStageChart extends ChartWidget return [ 'datasets' => [[ - 'label' => 'Eventos del funnel', + 'label' => 'Funnel', 'data' => [$rides, $views, $clicks, $bookings], - 'backgroundColor' => ['#0f766e', '#14b8a6', '#f59e0b', '#16a34a'], - 'borderRadius' => 10, + 'backgroundColor' => ['#0f172a', '#f59e0b', '#0f766e', '#16a34a'], + 'borderRadius' => 12, 'borderSkipped' => false, ]], - 'labels' => ['Taxi requests', 'Recommendation views', 'Clicks', 'Bookings'], + 'labels' => ['Taxi', 'Views', 'Clicks', 'Bookings'], + ]; + } + + protected function getOptions(): array + { + return [ + 'plugins' => [ + 'legend' => [ + 'display' => false, + ], + ], + 'scales' => [ + 'x' => [ + 'grid' => [ + 'display' => false, + ], + ], + 'y' => [ + 'beginAtZero' => true, + 'grid' => [ + 'color' => 'rgba(148, 163, 184, 0.16)', + ], + 'ticks' => [ + 'precision' => 0, + ], + ], + ], ]; } diff --git a/app/Filament/Widgets/JourneyStoryboard.php b/app/Filament/Widgets/JourneyStoryboard.php new file mode 100644 index 0000000..33e5f62 --- /dev/null +++ b/app/Filament/Widgets/JourneyStoryboard.php @@ -0,0 +1,48 @@ +count(); + $clicks = Event::where('event_type', 'recommendation_clicked')->count(); + $bookingsCount = Booking::count(); + $latestBooking = Booking::with(['offer', 'ride', 'recommendation'])->latest()->first(); + $commission = (float) Booking::sum('commission_amount'); + $gmv = (float) Booking::sum('amount'); + $rideToBooking = $rides > 0 ? round(($bookingsCount / $rides) * 100, 1) : 0; + $clickThrough = $views > 0 ? round(($clicks / $views) * 100, 1) : 0; + $topZone = Ride::query() + ->selectRaw("COALESCE(context_zone, 'General') as zone, COUNT(*) as aggregate") + ->groupBy('zone') + ->orderByDesc('aggregate') + ->value('zone') ?: 'General'; + + return [ + 'rides' => $rides, + 'views' => $views, + 'clicks' => $clicks, + 'bookingsCount' => $bookingsCount, + 'latestBooking' => $latestBooking, + 'commission' => $commission, + 'gmv' => $gmv, + 'rideToBooking' => $rideToBooking, + 'clickThrough' => $clickThrough, + 'topZone' => $topZone, + ]; + } +} diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 08a0742..9b88523 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,10 +2,8 @@ namespace App\Providers\Filament; -use App\Filament\Widgets\FunnelOverview; use App\Filament\Widgets\FunnelStageChart; -use App\Filament\Widgets\SourceChannelChart; - +use App\Filament\Widgets\JourneyStoryboard; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -14,7 +12,6 @@ use Filament\Pages; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; -use Filament\Widgets; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; @@ -31,6 +28,7 @@ class AdminPanelProvider extends PanelProvider ->id('admin') ->path('admin') ->login() + ->brandName('TAXILANZ Admin') ->colors([ 'primary' => Color::Teal, ]) @@ -41,10 +39,8 @@ class AdminPanelProvider extends PanelProvider ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ - Widgets\AccountWidget::class, - FunnelOverview::class, + JourneyStoryboard::class, FunnelStageChart::class, - SourceChannelChart::class, ]) ->middleware([ EncryptCookies::class, diff --git a/resources/views/bookings/success.blade.php b/resources/views/bookings/success.blade.php index 60498d2..15834bb 100644 --- a/resources/views/bookings/success.blade.php +++ b/resources/views/bookings/success.blade.php @@ -5,73 +5,128 @@ @section('content')
-
- 6 · Reserva confirmada -

Reserva cerrada.

-

- {{ $booking->customer_name }} ya quedó registrado para {{ $booking->offer->title }}. - Aquí se ve el valor del MVP: un trayecto real terminó en una intención comercial medible y atribuible. -

+
+ Reserva confirmada +

Reserva cerrada.

+

{{ $booking->customer_name }} ya tiene {{ $booking->offer->title }}.

+ +
+ 1 · Taxi + 2 · Contexto + 3 · Oferta + 4 · Reserva +
- Resultado visible + Resultado {{ $booking->offer->title }} {{ $booking->amount ? '€'.number_format((float) $booking->amount, 2) : 'Importe pendiente' }} · {{ ucfirst($booking->status) }} · {{ $booking->ride?->context_zone ?: ($booking->offer->location_label ?: 'General') }}
-
-
+
+
{{ $booking->amount ? '€'.number_format((float) $booking->amount, 0) : '—' }} importe
-
+
{{ $booking->commission_amount ? '€'.number_format((float) $booking->commission_amount, 0) : '—' }} comisión
-
- {{ $booking->recommendation?->id ? 'Top '.$booking->recommendation->position : 'Directa' }} +
+ {{ $booking->recommendation?->position ? 'Top '.$booking->recommendation->position : 'Directa' }} atribución
-
-
Booking UUID: {{ $booking->uuid }}
-
Estado: {{ ucfirst($booking->status) }}
-
Importe: {{ $booking->amount ? '€'.number_format((float) $booking->amount, 2) : 'Pendiente' }}
-
Comisión estimada: {{ $booking->commission_amount ? '€'.number_format((float) $booking->commission_amount, 2) : 'Pendiente' }}
-
- -
- Qué demuestra este cierre: - el taxi no fue solo transporte. Funcionó como disparador de una reserva adicional con valor económico visible. -
-
- -
+ +
+ + +
+
+
+

Detalle negocio

+

Conversión atribuible

+
+ +
+
+
+ {{ $booking->amount ? '€'.number_format((float) $booking->amount, 2) : 'Pendiente' }} + GMV +
+
+ {{ $booking->commission_amount ? '€'.number_format((float) $booking->commission_amount, 2) : 'Pendiente' }} + comisión +
+
+ {{ $booking->recommendation?->position ? 'Top '.$booking->recommendation->position : 'Directa' }} + origen +
+
+
    +
  • Oferta: {{ $booking->offer->title }}.
  • +
  • Zona: {{ $booking->ride?->context_zone ?: ($booking->offer->location_label ?: 'General') }}.
  • +
  • Canal: {{ $booking->ride?->source_channel ? ucfirst($booking->ride->source_channel) : 'Directo' }}.
  • +
  • La UI principal sigue limpia; la explicación queda bajo demanda.
  • +
+
+
@endsection diff --git a/resources/views/filament/widgets/journey-storyboard.blade.php b/resources/views/filament/widgets/journey-storyboard.blade.php new file mode 100644 index 0000000..245cd78 --- /dev/null +++ b/resources/views/filament/widgets/journey-storyboard.blade.php @@ -0,0 +1,83 @@ + + +
+
+
+

Executive snapshot

+
+

Booking atribuible, sin ruido.

+

Un KPI principal arriba. El funnel debajo. Lo demás, bajo demanda.

+
+
+ +
+
+

KPI principal

+

€{{ number_format($commission, 0) }}

+

Comisión atribuible

+
+ + Ver front demo + +
+
+ +
+
+

Taxi requests

+

{{ number_format($rides) }}

+
+
+

CTR view → click

+

{{ $clickThrough }}%

+
+
+

Bookings

+

{{ number_format($bookingsCount) }}

+
+
+

GMV demo

+

€{{ number_format($gmv, 0) }}

+
+
+ +
+
+

1 · Taxi

+

{{ number_format($rides) }}

+

entradas al funnel

+
+
+

2 · Contexto

+

{{ number_format($views) }}

+

views en espera útil

+
+
+

3 · Oferta

+

{{ number_format($clicks) }}

+

clics a detalle

+
+
+

4 · Booking

+

{{ $rideToBooking }}%

+

ride → booking

+
+
+ +
+ ℹ️ Ver contexto ejecutivo +
+
+

Zona con más señal

+

{{ $topZone }}

+
+
+

Última reserva

+

{{ $latestBooking?->offer?->title ?: 'Todavía no hay bookings' }}

+

La explicación profunda queda oculta por defecto para mantener el dashboard limpio.

+
+
+
+
+
+
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 5877c77..e2bd6ba 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -4,6 +4,10 @@ @section('meta_description', 'Demo TAXILANZ en Laravel: el taxi funciona como punto de entrada para activar 2–3 propuestas relevantes y convertirlas en reservas medibles.') @section('content') +@php + $primaryOffer = $featuredOffers->first(); + $secondaryOffer = $featuredOffers->skip(1)->first(); +@endphp
@@ -34,32 +38,37 @@
- Hoy en Lanzarote -

Pedir taxi

-

La app entra por movilidad y enseguida abre una siguiente decisión útil, simple y medible.

+ Storyboard activo +

4 pantallas, 1 historia

+

La demo ahora se entiende como producto móvil real: pedir taxi, esperar, decidir una propuesta y cerrar la reserva.

- Trayecto sugerido + Punto de entrada Aeropuerto César Manrique → Puerto del Carmen - Contexto suficiente para activar sugerencias relevantes sin parecer marketplace. + Primero movilidad. Después contexto. Y solo al final, conversión atribuible.
-
{{ $metrics['rides'] }}rides
-
{{ $metrics['clicks'] }}clics
-
{{ $metrics['bookings'] }}bookings
+
01taxi
+
02contexto
+
04booking
- @foreach ($featuredOffers->take(2) as $offer) -
-
- {{ $offer->title }} - {{ $loop->first ? 'Top now' : 'Next' }} -
- {{ $offer->location_label ?: 'Zona activa' }} +
+
+ Pedir taxi + start
- @endforeach + Necesidad inmediata +
+
+
+ {{ $primaryOffer?->title ?: 'Oferta contextual' }} + next +
+ {{ $primaryOffer?->location_label ?: 'Zona activa' }} +
Activar demo @@ -67,6 +76,96 @@
+
+
+
+ 4-screen mobile storyboard +

La demo se lee en 20 segundos

+
+

Este bloque baja la abstracción: enseña exactamente qué ve el turista y por qué eso termina en ingreso atribuible.

+
+ +
+
+ Pantalla 1 +
+ Taxi first +

Pedir taxi

+

Acción principal clarísima, pocos campos y cero fricción para entrar en el funnel.

+
+
+ Aeropuerto → hotel + Origen y destino bastan para empezar. +
+
+ Canal hotel / app + También deja señal de origen para ventas. +
+
+
+
+ +
+ Pantalla 2 +
+ Momento exacto +

Taxi confirmado

+

Con ETA visible aparece la ventana de atención correcta: no catálogo, solo 2–3 decisiones útiles.

+
+
+ {{ $metrics['views'] }} vistas + La pantalla ya activa interés medible. +
+
+ {{ $metrics['clicks'] }} clics + Intención real mientras el taxi llega. +
+
+
+
+ +
+ Pantalla 3 +
+ Oferta contextual +

Propuesta relevante

+

La mejor oferta se presenta como continuidad natural del trayecto, no como un escaparate infinito.

+
+
+ {{ $primaryOffer?->title ?: 'Oferta destacada' }} + {{ $primaryOffer?->location_label ?: 'Zona activa' }} · {{ $primaryOffer?->price_from ? 'Desde €'.number_format((float) $primaryOffer->price_from, 0) : 'Ticket consultable' }} +
+ @if($secondaryOffer) +
+ {{ $secondaryOffer->title }} + {{ $secondaryOffer->duration_minutes ? $secondaryOffer->duration_minutes.' min' : 'Plan rápido' }} · alternativa secundaria +
+ @endif +
+
+
+ +
+ Pantalla 4 +
+ Conversión +

Reserva y atribución

+

La historia termina con dinero visible: booking, GMV demo y comisión estimada en el panel.

+
+
+ {{ $metrics['bookings'] }} reservas + {{ $metrics['ride_to_booking_rate'] }}% ride → booking +
+
+ €{{ number_format($metrics['commission'], 0) }} + Comisión estimada ya presentable en admin. +
+
+
+
+
+
+
@@ -148,11 +247,23 @@
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 6c4bd2e..59cde94 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -282,6 +282,122 @@ min-height: 100%; } + .storyboard-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + } + + .storyboard-card { + display: grid; + gap: 14px; + align-content: start; + } + + .storyboard-phone { + display: grid; + gap: 12px; + min-height: 100%; + padding: 16px; + border-radius: 24px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: + radial-gradient(circle at top right, rgba(245, 158, 11, 0.18), transparent 26%), + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 245, 0.95)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); + } + + .storyboard-step { + display: inline-flex; + align-items: center; + gap: 8px; + width: fit-content; + padding: 7px 11px; + border-radius: 999px; + background: rgba(15, 118, 110, 0.10); + color: var(--accent-strong); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; + } + + .storyboard-step.is-warm { + background: rgba(245, 158, 11, 0.14); + color: #9a6700; + } + + .storyboard-step.is-success { + background: rgba(34, 197, 94, 0.12); + color: #166534; + } + + .storyboard-caption { + margin: 0; + color: var(--muted); + font-size: 14px; + line-height: 1.65; + } + + .storyboard-mini-list { + display: grid; + gap: 10px; + } + + .storyboard-mini-item { + display: grid; + gap: 4px; + padding: 12px 13px; + border-radius: 16px; + border: 1px solid rgba(15, 23, 42, 0.07); + background: rgba(255, 255, 255, 0.86); + } + + .storyboard-mini-item strong { + font-size: 14px; + } + + .storyboard-mini-item span { + color: var(--muted); + font-size: 12px; + } + + .storyboard-progress { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + margin-top: 22px; + } + + .storyboard-progress-step { + display: grid; + gap: 4px; + padding: 13px 14px; + border-radius: 18px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.72); + } + + .storyboard-progress-step strong { + font-size: 13px; + } + + .storyboard-progress-step span { + color: var(--muted); + font-size: 12px; + line-height: 1.5; + } + + .storyboard-progress-step.is-done { + border-color: rgba(15, 118, 110, 0.16); + background: rgba(15, 118, 110, 0.08); + } + + .storyboard-progress-step.is-active { + border-color: rgba(245, 158, 11, 0.22); + background: rgba(245, 158, 11, 0.10); + } + .phone-shell { position: relative; @@ -726,6 +842,290 @@ color: var(--accent-strong); } + + .hero-copy--compact { + gap: 18px; + } + + .compact-lead { + max-width: 34ch; + color: var(--text); + font-size: 1rem; + line-height: 1.55; + } + + .meta-row { + display: flex; + flex-wrap: wrap; + gap: 10px; + } + + .meta-pill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-radius: 999px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.78); + color: var(--text); + font-size: 13px; + font-weight: 700; + } + + .story-rail { + display: flex; + flex-wrap: wrap; + gap: 10px; + } + + .story-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.06); + color: var(--muted); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.02em; + } + + .story-chip.is-done { + background: rgba(15, 118, 110, 0.10); + color: var(--accent-strong); + } + + .story-chip.is-active { + background: rgba(245, 158, 11, 0.14); + color: #9a6700; + } + + .decision-card, + .decision-primary { + display: grid; + gap: 14px; + } + + .decision-primary { + padding: 18px; + border-radius: 24px; + border: 1px solid rgba(15, 23, 42, 0.07); + background: rgba(255, 255, 255, 0.9); + } + + .decision-primary--accent { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(240, 253, 250, 0.92)); + border-color: rgba(15, 118, 110, 0.14); + } + + .decision-title { + margin: 0; + font-size: clamp(1.8rem, 4vw, 2.7rem); + line-height: 0.98; + letter-spacing: -0.05em; + } + + .inline-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + } + + .inline-actions .btn { + min-width: 190px; + } + + .icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 48px; + padding: 12px 15px; + border: 1px solid rgba(15, 23, 42, 0.10); + border-radius: 16px; + background: rgba(255, 255, 255, 0.86); + color: var(--text); + cursor: pointer; + font: inherit; + font-weight: 700; + position: relative; + } + + .icon-button:hover { + background: #ffffff; + transform: translateY(-1px); + } + + .icon-button[data-tooltip]:hover::after, + .icon-button[data-tooltip]:focus-visible::after { + content: attr(data-tooltip); + position: absolute; + left: 50%; + bottom: calc(100% + 8px); + transform: translateX(-50%); + padding: 8px 10px; + border-radius: 12px; + background: rgba(15, 23, 42, 0.94); + color: #fff; + white-space: nowrap; + font-size: 12px; + font-weight: 700; + box-shadow: 0 14px 28px rgba(15, 23, 42, 0.18); + } + + .disclosure { + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 20px; + background: rgba(255, 255, 255, 0.76); + overflow: hidden; + } + + .disclosure--soft { + background: rgba(15, 23, 42, 0.03); + } + + .disclosure summary { + list-style: none; + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 14px 16px; + cursor: pointer; + color: var(--text); + font-size: 14px; + font-weight: 800; + } + + .disclosure summary::-webkit-details-marker { + display: none; + } + + .disclosure[open] summary { + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + } + + .disclosure-body { + display: grid; + gap: 10px; + padding: 14px 16px 16px; + } + + .accordion-list { + margin: 0; + padding-left: 18px; + color: var(--muted); + display: grid; + gap: 8px; + } + + .accordion-list strong { + color: var(--text); + } + + .phone-screen--focused { + gap: 14px; + } + + .recommendation-focus { + display: grid; + gap: 12px; + padding: 16px; + border-radius: 24px; + background: rgba(255, 255, 255, 0.98); + border: 1px solid rgba(15, 118, 110, 0.14); + box-shadow: 0 18px 40px rgba(15, 118, 110, 0.10); + } + + .recommendation-focus .btn { + width: 100%; + } + + .metric-strip { + display: grid; + gap: 12px; + } + + .metric-strip--2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .metric-strip--3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .metric-cell { + display: grid; + gap: 4px; + padding: 14px; + border-radius: 18px; + background: rgba(15, 23, 42, 0.04); + border: 1px solid rgba(15, 23, 42, 0.06); + } + + .metric-cell strong { + font-size: 1.2rem; + letter-spacing: -0.04em; + } + + .metric-cell span { + color: var(--muted); + font-size: 12px; + } + + .section-head--compact { + margin-bottom: 14px; + } + + .cards--compact { + gap: 16px; + } + + .offer-card--compact { + gap: 14px; + } + + .app-modal { + width: min(560px, calc(100% - 24px)); + border: 0; + border-radius: 26px; + padding: 0; + background: var(--surface-strong); + box-shadow: 0 30px 80px rgba(15, 23, 42, 0.24); + } + + .app-modal::backdrop { + background: rgba(15, 23, 42, 0.42); + backdrop-filter: blur(4px); + } + + .app-modal-card { + display: grid; + gap: 16px; + padding: 24px; + } + + .app-modal-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + } + + .app-modal-close { + border: 0; + border-radius: 14px; + background: rgba(15, 23, 42, 0.08); + color: var(--text); + padding: 10px 12px; + font: inherit; + font-weight: 700; + cursor: pointer; + } + .footer-note { margin-top: 24px; padding-top: 20px; @@ -742,21 +1142,309 @@ .hero, .split, .cards, - .proof-grid { + .proof-grid, + .storyboard-grid { grid-template-columns: 1fr; } } @media (max-width: 720px) { .topbar-inner, - .footer-note { + + .hero-copy--compact { + gap: 18px; + } + + .compact-lead { + max-width: 34ch; + color: var(--text); + font-size: 1rem; + line-height: 1.55; + } + + .meta-row { + display: flex; + flex-wrap: wrap; + gap: 10px; + } + + .meta-pill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-radius: 999px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.78); + color: var(--text); + font-size: 13px; + font-weight: 700; + } + + .story-rail { + display: flex; + flex-wrap: wrap; + gap: 10px; + } + + .story-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.06); + color: var(--muted); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.02em; + } + + .story-chip.is-done { + background: rgba(15, 118, 110, 0.10); + color: var(--accent-strong); + } + + .story-chip.is-active { + background: rgba(245, 158, 11, 0.14); + color: #9a6700; + } + + .decision-card, + .decision-primary { + display: grid; + gap: 14px; + } + + .decision-primary { + padding: 18px; + border-radius: 24px; + border: 1px solid rgba(15, 23, 42, 0.07); + background: rgba(255, 255, 255, 0.9); + } + + .decision-primary--accent { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(240, 253, 250, 0.92)); + border-color: rgba(15, 118, 110, 0.14); + } + + .decision-title { + margin: 0; + font-size: clamp(1.8rem, 4vw, 2.7rem); + line-height: 0.98; + letter-spacing: -0.05em; + } + + .inline-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + } + + .inline-actions .btn { + min-width: 190px; + } + + .icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 48px; + padding: 12px 15px; + border: 1px solid rgba(15, 23, 42, 0.10); + border-radius: 16px; + background: rgba(255, 255, 255, 0.86); + color: var(--text); + cursor: pointer; + font: inherit; + font-weight: 700; + position: relative; + } + + .icon-button:hover { + background: #ffffff; + transform: translateY(-1px); + } + + .icon-button[data-tooltip]:hover::after, + .icon-button[data-tooltip]:focus-visible::after { + content: attr(data-tooltip); + position: absolute; + left: 50%; + bottom: calc(100% + 8px); + transform: translateX(-50%); + padding: 8px 10px; + border-radius: 12px; + background: rgba(15, 23, 42, 0.94); + color: #fff; + white-space: nowrap; + font-size: 12px; + font-weight: 700; + box-shadow: 0 14px 28px rgba(15, 23, 42, 0.18); + } + + .disclosure { + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 20px; + background: rgba(255, 255, 255, 0.76); + overflow: hidden; + } + + .disclosure--soft { + background: rgba(15, 23, 42, 0.03); + } + + .disclosure summary { + list-style: none; + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 14px 16px; + cursor: pointer; + color: var(--text); + font-size: 14px; + font-weight: 800; + } + + .disclosure summary::-webkit-details-marker { + display: none; + } + + .disclosure[open] summary { + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + } + + .disclosure-body { + display: grid; + gap: 10px; + padding: 14px 16px 16px; + } + + .accordion-list { + margin: 0; + padding-left: 18px; + color: var(--muted); + display: grid; + gap: 8px; + } + + .accordion-list strong { + color: var(--text); + } + + .phone-screen--focused { + gap: 14px; + } + + .recommendation-focus { + display: grid; + gap: 12px; + padding: 16px; + border-radius: 24px; + background: rgba(255, 255, 255, 0.98); + border: 1px solid rgba(15, 118, 110, 0.14); + box-shadow: 0 18px 40px rgba(15, 118, 110, 0.10); + } + + .recommendation-focus .btn { + width: 100%; + } + + .metric-strip { + display: grid; + gap: 12px; + } + + .metric-strip--2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .metric-strip--3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .metric-cell { + display: grid; + gap: 4px; + padding: 14px; + border-radius: 18px; + background: rgba(15, 23, 42, 0.04); + border: 1px solid rgba(15, 23, 42, 0.06); + } + + .metric-cell strong { + font-size: 1.2rem; + letter-spacing: -0.04em; + } + + .metric-cell span { + color: var(--muted); + font-size: 12px; + } + + .section-head--compact { + margin-bottom: 14px; + } + + .cards--compact { + gap: 16px; + } + + .offer-card--compact { + gap: 14px; + } + + .app-modal { + width: min(560px, calc(100% - 24px)); + border: 0; + border-radius: 26px; + padding: 0; + background: var(--surface-strong); + box-shadow: 0 30px 80px rgba(15, 23, 42, 0.24); + } + + .app-modal::backdrop { + background: rgba(15, 23, 42, 0.42); + backdrop-filter: blur(4px); + } + + .app-modal-card { + display: grid; + gap: 16px; + padding: 24px; + } + + .app-modal-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + } + + .app-modal-close { + border: 0; + border-radius: 14px; + background: rgba(15, 23, 42, 0.08); + color: var(--text); + padding: 10px 12px; + font: inherit; + font-weight: 700; + cursor: pointer; + } + + .footer-note { flex-direction: column; align-items: flex-start; } .stats, .grid-2, - .screen-kpis { + .screen-kpis, + .storyboard-progress, + .metric-strip--2, + .metric-strip--3 { grid-template-columns: 1fr; } @@ -768,11 +1456,22 @@ .phone-topbar, .phone-list-row, .phone-action-top, - .section-head { + .section-head, + .app-modal-head { align-items: flex-start; flex-direction: column; } + .inline-actions, + .inline-actions--stack-mobile { + flex-direction: column; + } + + .inline-actions .btn, + .icon-button { + width: 100%; + } + main { padding-top: 28px; } .card { padding: 22px; } .nav-links { gap: 12px; } @@ -809,5 +1508,43 @@ + + + diff --git a/resources/views/offers/show.blade.php b/resources/views/offers/show.blade.php index 6a77aec..8886eb2 100644 --- a/resources/views/offers/show.blade.php +++ b/resources/views/offers/show.blade.php @@ -14,7 +14,14 @@ @endphp
-
+
+ 1 · Taxi + 2 · Contexto + 3 · Oferta + 4 · Reserva +
+ +
Propuesta contextual
-
+ +@if($recommendation) + +
+
+
+

Señal completa

+

Lectura contextual

+
+ +
+
+
+ Top {{ $recommendation->position }} + posición +
+
+ {{ $ride?->eta_minutes ?? '—' }} + ETA del taxi +
+
+ {{ $ride?->context_zone ?: ($offer->location_label ?: 'General') }} + zona activa +
+
+
    +
  • {{ $recommendation->reason ?: 'La propuesta aparece por contexto, proximidad y capacidad de cierre.' }}
  • + @if($ride) +
  • Trayecto vinculado: {{ $ride->pickup_label }}{{ $ride->destination_label }}.
  • + @endif +
  • Oferta con ticket visible y reserva rápida.
  • +
+
+
+@endif @endsection diff --git a/resources/views/rides/confirmed.blade.php b/resources/views/rides/confirmed.blade.php index 2234d99..b65fdee 100644 --- a/resources/views/rides/confirmed.blade.php +++ b/resources/views/rides/confirmed.blade.php @@ -1,44 +1,107 @@ @extends('layouts.app') @section('title', 'Taxi confirmado | TAXILANZ Demo') -@section('meta_description', 'Pantalla de taxi confirmado con recomendaciones contextuales y tracking de visualización.') +@section('meta_description', 'Pantalla de taxi confirmado con recomendaciones contextuales y decisiones rápidas en TAXILANZ Demo.') @section('content') +@php + $primaryRecommendation = $recommendations->first(); + $secondaryRecommendations = $recommendations->skip(1); +@endphp
-
+
- 2 · Taxi confirmado + Taxi confirmado

Tu taxi llega en {{ $ride->eta_minutes ?? 6 }} min.

-

- Perfecto. Mientras llega, TAXILANZ no te enseña un catálogo infinito: te propone solo opciones que - encajan con este trayecto, cerca de tu destino y fáciles de decidir ahora mismo. -

+

Decide una sola cosa más, si te interesa. El resto queda fuera.

-
-
- {{ $ride->eta_minutes ?? 6 }} min - ventana de atención antes de subir -
-
- {{ $recommendations->count() }} - opciones priorizadas, no ruido -
-
- 1 toque - para pasar del trayecto a la reserva -
+
+ {{ $ride->pickup_label }} + {{ $ride->destination_label }} + {{ ucfirst($ride->source_channel) }} + {{ $ride->context_zone ?: 'Zona activa' }}
-
-
Recogida: {{ $ride->pickup_label }}
-
Destino: {{ $ride->destination_label }}
-
Canal: {{ ucfirst($ride->source_channel) }} · Zona: {{ $ride->context_zone ?: 'General' }}
+
+ 1 · Taxi + 2 · Confirmado + 3 · Oferta + 4 · Reserva
+ + @if ($primaryRecommendation) +
+
+ Siguiente mejor opción +

{{ $primaryRecommendation->offer->title }}

+
+ @if($primaryRecommendation->offer->location_label){{ $primaryRecommendation->offer->location_label }}@endif + @if($primaryRecommendation->offer->price_from)€{{ number_format((float) $primaryRecommendation->offer->price_from, 0) }}@endif + @if($primaryRecommendation->offer->duration_minutes){{ $primaryRecommendation->offer->duration_minutes }} min@endif + Disponible ahora +
+
+ Ver y reservar + +
+
+ +
+ + ℹ️ ¿Por qué esta opción? + Abrir + +
+
    +
  • Encaja con tu destino: {{ $ride->destination_label }}.
  • + @if($primaryRecommendation->offer->location_label) +
  • Está cerca de {{ $primaryRecommendation->offer->location_label }} y se puede decidir rápido.
  • + @endif + @if($primaryRecommendation->reason) +
  • {{ $primaryRecommendation->reason }}
  • + @else +
  • La priorizamos por cercanía, disponibilidad y facilidad de cierre.
  • + @endif +
+
+
+
+ + +
+
+
+

Señal completa

+

Por qué {{ $primaryRecommendation->offer->title }} sube arriba

+
+ +
+
+
+ Top {{ $primaryRecommendation->position }} + posición actual +
+
+ {{ $ride->eta_minutes ?? 6 }} min + momento de atención +
+
+
    +
  • Trayecto activo: {{ $ride->pickup_label }}{{ $ride->destination_label }}.
  • +
  • Zona relevante: {{ $ride->context_zone ?: 'General' }}.
  • +
  • Canal de origen: {{ ucfirst($ride->source_channel) }}.
  • +
  • {{ $primaryRecommendation->reason ?: 'La propuesta destaca por contexto, proximidad y facilidad de reserva.' }}
  • +
+
+
+ @else +
Todavía no hay una propuesta activa para este trayecto.
+ @endif
-
-
-
- 3 · Recomendaciones -

Encajan con tu ruta ahora

+@if ($secondaryRecommendations->isNotEmpty()) +
+
+
+ Más opciones +

Solo si quieres comparar

+
-

Primero relevancia. Después variedad. Siempre con contexto.

-
- @if ($recommendations->isEmpty()) -
- No hay recomendaciones activas para este trayecto todavía. La solicitud sí quedó registrada y lista para atribución. -
- @else -
- @foreach ($recommendations as $recommendation) -
- +
+ @foreach ($secondaryRecommendations as $recommendation) +
Top {{ $recommendation->position }} - {{ $recommendation->position === 1 ? 'Parada rápida' : 'Plan al llegar' }} - @if($recommendation->offer->available_now) - Disponible hoy - @endif - @if($recommendation->offer->location_label) - {{ $recommendation->offer->location_label }} - @endif -
-

{{ $recommendation->offer->title }}

-

{{ $recommendation->offer->excerpt }}

- -
- @if($recommendation->offer->duration_minutes) -
Duración: {{ $recommendation->offer->duration_minutes }} min · pensada para decidir sin fricción
- @endif - @if($recommendation->offer->price_from) -
Desde: €{{ number_format((float) $recommendation->offer->price_from, 0) }} · ticket claro para mostrar impacto comercial
- @endif -
- -
- Por qué encaja: {{ $recommendation->reason }} + @if($recommendation->offer->location_label){{ $recommendation->offer->location_label }}@endif + @if($recommendation->offer->price_from)€{{ number_format((float) $recommendation->offer->price_from, 0) }}@endif
+

{{ $recommendation->offer->title }}

- Ver detalle y reservar + +
+ + ℹ️ Ver motivo + Abrir + +
+

{{ $recommendation->reason ?: 'Opción contextual cercana al destino y fácil de reservar.' }}

+
+
@endforeach
- @endif -
+ +@endif @endsection