Auto commit: 2026-04-06T06:39:36.955Z

This commit is contained in:
Flatlogic Bot 2026-04-06 06:39:36 +00:00
parent d844b2d94f
commit 11ba79824c
19 changed files with 836 additions and 241 deletions

View File

@ -17,7 +17,10 @@ class BookingResource extends Resource
{
protected static ?string $model = Booking::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationIcon = 'heroicon-o-ticket';
protected static ?string $navigationLabel = 'Bookings';
protected static ?string $navigationGroup = 'Demo TAXILANZ';
protected static ?int $navigationSort = 3;
public static function form(Form $form): Form
{

View File

@ -17,7 +17,10 @@ class EventResource extends Resource
{
protected static ?string $model = Event::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationIcon = 'heroicon-o-bolt';
protected static ?string $navigationLabel = 'Events';
protected static ?string $navigationGroup = 'Demo TAXILANZ';
protected static ?int $navigationSort = 4;
public static function form(Form $form): Form
{

View File

@ -17,7 +17,10 @@ class OfferResource extends Resource
{
protected static ?string $model = Offer::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationIcon = 'heroicon-o-sparkles';
protected static ?string $navigationLabel = 'Offers';
protected static ?string $navigationGroup = 'Demo TAXILANZ';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{

View File

@ -17,7 +17,10 @@ class RideResource extends Resource
{
protected static ?string $model = Ride::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationIcon = 'heroicon-o-map';
protected static ?string $navigationLabel = 'Rides';
protected static ?string $navigationGroup = 'Demo TAXILANZ';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{

View File

@ -0,0 +1,43 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Booking;
use App\Models\Event;
use App\Models\Ride;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class FunnelOverview extends StatsOverviewWidget
{
protected function getStats(): array
{
$rides = Ride::count();
$views = Event::where('event_type', 'recommendation_viewed')->count();
$clicks = Event::where('event_type', 'recommendation_clicked')->count();
$bookings = Booking::count();
$gmv = (float) Booking::sum('amount');
$commission = (float) Booking::sum('commission_amount');
$viewToClick = $views > 0 ? round(($clicks / $views) * 100, 1) : 0;
$rideToBooking = $rides > 0 ? round(($bookings / $rides) * 100, 1) : 0;
return [
Stat::make('Taxi requests', number_format($rides))
->description('Entradas del funnel')
->color('primary'),
Stat::make('Recommendation views', number_format($views))
->description('Usuarios expuestos a propuestas')
->color('info'),
Stat::make('Clicks', number_format($clicks))
->description('CTR vista → clic: '.$viewToClick.'%')
->color('warning'),
Stat::make('Bookings', number_format($bookings))
->description('Conversión ride → booking: '.$rideToBooking.'% · GMV €'.number_format($gmv, 0))
->color('success'),
Stat::make('Comisión estimada', '€'.number_format($commission, 0))
->description('Valor económico demo ya atribuible')
->color('primary'),
];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Booking;
use App\Models\Event;
use App\Models\Ride;
use Filament\Widgets\ChartWidget;
class FunnelStageChart extends ChartWidget
{
protected static ?string $heading = 'Embudo TAXILANZ';
protected function getData(): array
{
$rides = Ride::count();
$views = Event::where('event_type', 'recommendation_viewed')->count();
$clicks = Event::where('event_type', 'recommendation_clicked')->count();
$bookings = Booking::count();
return [
'datasets' => [[
'label' => 'Eventos del funnel',
'data' => [$rides, $views, $clicks, $bookings],
'backgroundColor' => ['#0f766e', '#14b8a6', '#f59e0b', '#16a34a'],
'borderRadius' => 10,
'borderSkipped' => false,
]],
'labels' => ['Taxi requests', 'Recommendation views', 'Clicks', 'Bookings'],
];
}
protected function getType(): string
{
return 'bar';
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Ride;
use Filament\Widgets\ChartWidget;
class SourceChannelChart extends ChartWidget
{
protected static ?string $heading = 'Taxi requests por canal';
protected function getData(): array
{
$channels = Ride::query()
->selectRaw('source_channel, COUNT(*) as aggregate')
->groupBy('source_channel')
->orderByDesc('aggregate')
->pluck('aggregate', 'source_channel');
return [
'datasets' => [[
'label' => 'Solicitudes',
'data' => $channels->values()->all(),
'backgroundColor' => ['#0f766e', '#14b8a6', '#f59e0b', '#0f172a'],
'borderWidth' => 0,
]],
'labels' => $channels->keys()->map(fn (string $channel) => ucfirst($channel))->all(),
];
}
protected function getType(): string
{
return 'bar';
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Models\Booking;
use App\Models\Event;
use App\Models\Offer;
use App\Models\Ride;
@ -10,12 +11,23 @@ class HomeController extends Controller
{
public function __invoke()
{
$rides = Ride::count();
$views = Event::where('event_type', 'recommendation_viewed')->count();
$clicks = Event::where('event_type', 'recommendation_clicked')->count();
$bookings = Booking::count();
$gmv = (float) Booking::sum('amount');
$commission = (float) Booking::sum('commission_amount');
return view('home', [
'featuredOffers' => Offer::query()->where('status', 'published')->orderByDesc('is_featured')->orderByDesc('priority_score')->take(3)->get(),
'metrics' => [
'rides' => Ride::count(),
'views' => Event::where('event_type', 'recommendation_viewed')->count(),
'bookings' => Event::where('event_type', 'booking_completed')->count(),
'rides' => $rides,
'views' => $views,
'clicks' => $clicks,
'bookings' => $bookings,
'gmv' => $gmv,
'commission' => $commission,
'ride_to_booking_rate' => $rides > 0 ? round(($bookings / $rides) * 100, 1) : 0,
],
]);
}

View File

@ -104,10 +104,14 @@ class RideController extends Controller
$normalized = mb_strtolower($destinationLabel);
return match (true) {
str_contains($normalized, 'beach') => 'beach',
str_contains($normalized, 'old town') => 'old town',
str_contains($normalized, 'puerto del carmen') => 'puerto del carmen',
str_contains($normalized, 'playa blanca') => 'playa blanca',
str_contains($normalized, 'costa teguise') => 'costa teguise',
str_contains($normalized, 'arrecife') => 'arrecife',
str_contains($normalized, 'marina') => 'marina',
str_contains($normalized, 'center'), str_contains($normalized, 'centre') => 'city center',
str_contains($normalized, 'playa'), str_contains($normalized, 'beach') => 'playa',
str_contains($normalized, 'old town'), str_contains($normalized, 'casco antiguo') => 'casco antiguo',
str_contains($normalized, 'centro'), str_contains($normalized, 'center'), str_contains($normalized, 'centre') => 'centro',
default => trim($destinationLabel),
};
}
@ -115,6 +119,7 @@ class RideController extends Controller
protected function estimateEta(string $pickup, string $destination): int
{
$seed = strlen($pickup) + strlen($destination);
return max(4, min(16, ($seed % 13) + 4));
}
}

View File

@ -2,6 +2,10 @@
namespace App\Providers\Filament;
use App\Filament\Widgets\FunnelOverview;
use App\Filament\Widgets\FunnelStageChart;
use App\Filament\Widgets\SourceChannelChart;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
@ -28,7 +32,7 @@ class AdminPanelProvider extends PanelProvider
->path('admin')
->login()
->colors([
'primary' => Color::Amber,
'primary' => Color::Teal,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
@ -38,7 +42,9 @@ class AdminPanelProvider extends PanelProvider
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
FunnelOverview::class,
FunnelStageChart::class,
SourceChannelChart::class,
])
->middleware([
EncryptCookies::class,

View File

@ -25,44 +25,45 @@ class RecommendationEngine
if ($offer->available_now) {
$score += 18;
$reasons[] = 'Available right now';
$reasons[] = 'Disponible ahora';
}
if ($offer->is_featured) {
$score += 14;
$reasons[] = 'Featured partner';
$reasons[] = 'Partner destacado';
}
foreach (array_filter([$zone, $destination]) as $needle) {
if ($needle !== '' && Str::contains($haystack, $needle)) {
$score += 22;
$reasons[] = 'Matches destination context';
$reasons[] = 'Encaja con el destino';
break;
}
}
if ($hour >= 18 && $offer->category === 'restaurant') {
$score += 12;
$reasons[] = 'Good fit for the evening';
$reasons[] = 'Muy buena opción para esta tarde/noche';
}
if ($hour >= 10 && $hour <= 18 && in_array($offer->category, ['experience', 'activity'], true)) {
$score += 12;
$reasons[] = 'Works well while waiting or after arrival';
$reasons[] = 'Funciona bien durante la espera o al llegar';
}
if (($offer->duration_minutes ?? 0) > 0 && $offer->duration_minutes <= 120) {
$score += 8;
$reasons[] = 'Easy to fit into today';
$reasons[] = 'Fácil de encajar hoy';
}
if (($offer->price_from ?? 0) > 0 && $offer->price_from <= 50) {
$score += 6;
$reasons[] = 'Easy price point';
$reasons[] = 'Precio fácil de aceptar';
}
$offer->demo_score = $score;
$offer->demo_reason = collect($reasons)->unique()->take(2)->implode(' · ') ?: 'Strong local fit';
$offer->demo_reason = collect($reasons)->unique()->take(2)->implode(' · ') ?: 'Buen encaje local';
return $offer;
})
->sortByDesc(fn (Offer $offer) => [$offer->demo_score, $offer->is_featured, $offer->priority_score])

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -11,11 +11,11 @@ class DemoDataSeeder extends Seeder
{
$offers = [
[
'title' => 'Sunset Marina Table',
'title' => 'Mesa atardecer en Marina Lanzarote',
'slug' => 'sunset-marina-table',
'category' => 'restaurant',
'excerpt' => 'A polished waterfront dinner spot for couples or small groups.',
'description' => 'Priority seating, fast confirmation, and a memorable marina view that works well right after a taxi drop-off.',
'excerpt' => 'Cena frente al puerto con confirmación rápida para parejas o grupos pequeños.',
'description' => 'Una propuesta premium pero fácil de cerrar justo después del traslado, con vista a la marina y entrada sin fricción.',
'location_label' => 'Marina',
'price_from' => 42,
'duration_minutes' => 90,
@ -25,12 +25,12 @@ class DemoDataSeeder extends Seeder
'available_now' => true,
],
[
'title' => 'Old Town Tapas Walk',
'title' => 'Ruta de tapas por el casco antiguo',
'slug' => 'old-town-tapas-walk',
'category' => 'experience',
'excerpt' => 'A short guided route through the most photogenic local food spots.',
'description' => 'Ideal for first-time visitors who want a curated plan without committing half a day.',
'location_label' => 'Old Town',
'excerpt' => 'Recorrido corto por paradas fotogénicas y muy fáciles de disfrutar el mismo día.',
'description' => 'Pensado para primeros visitantes que quieren una experiencia local curada sin bloquear media jornada.',
'location_label' => 'Casco antiguo',
'price_from' => 34,
'duration_minutes' => 75,
'status' => 'published',
@ -39,12 +39,12 @@ class DemoDataSeeder extends Seeder
'available_now' => true,
],
[
'title' => 'Beach Club Day Pass',
'title' => 'Beach club con acceso rápido',
'slug' => 'beach-club-day-pass',
'category' => 'activity',
'excerpt' => 'Fast-entry pass with towel, lounger, and drink credit included.',
'description' => 'An easy same-day upgrade for guests heading toward the waterfront zone.',
'location_label' => 'Beach',
'excerpt' => 'Acceso directo con tumbona, toalla y crédito de bebida incluido.',
'description' => 'Ideal para turistas que se dirigen a zona de playa y quieren una mejora simple, inmediata y medible.',
'location_label' => 'Playa',
'price_from' => 29,
'duration_minutes' => 120,
'status' => 'published',
@ -53,12 +53,12 @@ class DemoDataSeeder extends Seeder
'available_now' => true,
],
[
'title' => 'City Center Brunch Slot',
'title' => 'Brunch asegurado en Arrecife centro',
'slug' => 'city-center-brunch-slot',
'category' => 'restaurant',
'excerpt' => 'Reliable reservation at a high-demand brunch concept.',
'description' => 'Convenient for arrivals into the city core with a predictable handoff to the host team.',
'location_label' => 'City Center',
'excerpt' => 'Reserva fiable en un local demandado del centro.',
'description' => 'Muy útil para llegadas al núcleo urbano con necesidad de una decisión simple y rápida.',
'location_label' => 'Arrecife',
'price_from' => 26,
'duration_minutes' => 60,
'status' => 'published',
@ -67,12 +67,12 @@ class DemoDataSeeder extends Seeder
'available_now' => true,
],
[
'title' => 'Private Sailing Intro',
'title' => 'Salida premium desde Puerto del Carmen',
'slug' => 'private-sailing-intro',
'category' => 'experience',
'excerpt' => 'Short premium sailing experience from the marina.',
'description' => 'A premium upsell for guests already moving toward the port area.',
'location_label' => 'Marina',
'excerpt' => 'Experiencia breve en barco para elevar una tarde sin complicaciones.',
'description' => 'Upsell premium para huéspedes que ya van hacia marina o zona costera y están listos para decidir.',
'location_label' => 'Puerto del Carmen',
'price_from' => 79,
'duration_minutes' => 105,
'status' => 'published',
@ -81,12 +81,12 @@ class DemoDataSeeder extends Seeder
'available_now' => true,
],
[
'title' => 'Hotel Spa Recovery',
'title' => 'Ritual spa recovery de llegada',
'slug' => 'hotel-spa-recovery',
'category' => 'service',
'excerpt' => 'Fast-access recovery ritual for same-day arrivals.',
'description' => 'Especially useful after airport or station pickups with short waiting windows.',
'location_label' => 'Hotel District',
'excerpt' => 'Acceso rápido a un ritual de recuperación para el mismo día.',
'description' => 'Útil tras recogidas en aeropuerto u hotel cuando hay una ventana corta antes del siguiente plan.',
'location_label' => 'Zona hotelera',
'price_from' => 55,
'duration_minutes' => 60,
'status' => 'published',

View File

@ -1,32 +1,36 @@
@extends('layouts.app')
@section('title', 'Booking confirmed | ArrivalFlow')
@section('meta_description', 'Booking success screen for the taxi-to-offer demo flow.')
@section('title', 'Reserva confirmada | TAXILANZ Demo')
@section('meta_description', 'Pantalla de éxito de la reserva para el funnel taxi → recomendación → booking.')
@section('content')
<section class="split">
<article class="card success">
<span class="eyebrow">6 · Booking confirmed</span>
<h1 style="font-size:clamp(2.1rem,4vw,3.5rem);">Reservation confirmed.</h1>
<p>{{ $booking->customer_name }} is booked for <strong>{{ $booking->offer->title }}</strong>. This step closes the demo funnel with a real <code>booking_completed</code> event.</p>
<div class="list" style="margin-top:18px;">
<span class="eyebrow">6 · Reserva confirmada</span>
<h1 style="font-size:clamp(2.15rem,4vw,3.7rem);max-width:14ch;">Reserva cerrada.</h1>
<p>
{{ $booking->customer_name }} ya quedó registrado para <strong>{{ $booking->offer->title }}</strong>.
Este paso cierra el funnel demo con un evento real de <code>booking_completed</code>.
</p>
<div class="list">
<div class="list-item">Booking UUID: {{ $booking->uuid }}</div>
<div class="list-item">Status: {{ ucfirst($booking->status) }}</div>
<div class="list-item">Amount: {{ $booking->amount ? '€'.number_format((float) $booking->amount, 2) : 'TBD' }}</div>
<div class="list-item">Commission: {{ $booking->commission_amount ? '€'.number_format((float) $booking->commission_amount, 2) : 'TBD' }}</div>
<div class="list-item">Estado: {{ ucfirst($booking->status) }}</div>
<div class="list-item">Importe: {{ $booking->amount ? '€'.number_format((float) $booking->amount, 2) : 'Pendiente' }}</div>
<div class="list-item">Comisión estimada: {{ $booking->commission_amount ? '€'.number_format((float) $booking->commission_amount, 2) : 'Pendiente' }}</div>
</div>
</article>
<aside class="card">
<span class="eyebrow">Attribution summary</span>
<span class="eyebrow">Resumen de atribución</span>
<div class="list">
<div class="list-item">Ride linked: {{ $booking->ride?->uuid ?? 'Direct booking' }}</div>
<div class="list-item">Recommendation linked: {{ $booking->recommendation?->id ? '#'.$booking->recommendation->position : 'No' }}</div>
<div class="list-item">Customer email: {{ $booking->customer_email ?: 'Not provided' }}</div>
<div class="list-item">Ride vinculado: {{ $booking->ride?->uuid ?? 'Reserva directa' }}</div>
<div class="list-item">Recomendación vinculada: {{ $booking->recommendation?->id ? '#'.$booking->recommendation->position : 'No' }}</div>
<div class="list-item">Email cliente: {{ $booking->customer_email ?: 'No informado' }}</div>
</div>
<div style="margin-top:18px; display:flex; gap:12px; flex-wrap:wrap;">
<a class="btn" href="{{ route('home') }}">Start another ride</a>
<div class="cta-row" style="margin-top:18px;">
<a class="btn" href="{{ route('home') }}">Iniciar otro trayecto</a>
@if($booking->ride)
<a class="btn btn-secondary" href="{{ route('rides.confirmed', $booking->ride) }}">Back to ride</a>
<a class="btn btn-secondary" href="{{ route('rides.confirmed', $booking->ride) }}">Volver al taxi confirmado</a>
@endif
</div>
</aside>

View File

@ -1,52 +1,84 @@
@extends('layouts.app')
@section('title', 'Taxi arrival demo | ArrivalFlow')
@section('meta_description', 'Request a taxi, confirm the ride, and convert the arrival into a contextual local booking.')
@section('title', 'TAXILANZ Demo | Taxi, recomendaciones contextuales y reserva simple')
@section('meta_description', 'Demo TAXILANZ en Laravel: el taxi funciona como punto de entrada para activar 23 propuestas relevantes y convertirlas en reservas medibles.')
@section('content')
<section class="hero">
<article class="card hero-copy">
<div>
<span class="eyebrow">Thin slice · Laravel-first demo</span>
<h1>Turn a taxi request into a booking in under two minutes.</h1>
<p>This demo keeps the story brutally simple: request a taxi, confirm the arrival, surface 23 relevant offers, and close a booking with measurable tracking.</p>
<span class="eyebrow">Laravel-first demo · TAXILANZ</span>
<h1>El taxi se convierte en el inicio de una reserva útil.</h1>
<p>
Esta demo cuenta una historia muy concreta: un turista pide taxi, el trayecto se confirma,
aparecen 23 propuestas relevantes y una de ellas se convierte en reserva con tracking real.
Sin marketplace. Sin catálogo infinito. Solo activación en el momento exacto.
</p>
</div>
<div class="stats">
<div class="stat"><strong>{{ $metrics['rides'] }}</strong><span>Taxi requests</span></div>
<div class="stat"><strong>{{ $metrics['views'] }}</strong><span>Recommendation views</span></div>
<div class="stat"><strong>{{ $metrics['bookings'] }}</strong><span>Bookings completed</span></div>
<div class="stat"><strong>{{ $metrics['rides'] }}</strong><span>Solicitudes de taxi</span></div>
<div class="stat"><strong>{{ $metrics['views'] }}</strong><span>Vistas de recomendación</span></div>
<div class="stat"><strong>{{ $metrics['bookings'] }}</strong><span>Reservas cerradas</span></div>
</div>
</article>
<aside class="card orb-wrap">
<div class="stack">
<div class="mini-card">
<small>Story hook</small>
<h3>Taxi confirmed</h3>
<p>Now show the guest something useful while they wait or right after arrival.</p>
<small>Momento exacto</small>
<h3>Durante la espera o el trayecto</h3>
<p>El usuario ya está en movimiento y receptivo. Ahí es donde la recomendación tiene sentido.</p>
</div>
<div class="mini-card">
<small>Signal</small>
<h3>Real tracking</h3>
<p>We log request creation, recommendation views, clicks, and booking completion.</p>
<small>Tracking real</small>
<h3>No es humo de pitch</h3>
<p>Se registran creación del ride, vistas, clics y reserva completada para demostrar impacto.</p>
</div>
<div class="mini-card success">
<small>Admin angle</small>
<h3>Demo-ready funnel</h3>
<p>Enough structure to seed content, show traction, and extend into a fuller platform later.</p>
<small>Ángulo B2B</small>
<h3>Fácil para partners después</h3>
<p>La experiencia es simple para el turista y luego escalable para hotel, recepción o partner local.</p>
</div>
</div>
</aside>
</section>
<section class="section">
<div class="proof-grid">
<article class="card proof-card">
<span class="eyebrow">Impacto partner</span>
<strong class="proof-stat">{{ $metrics['ride_to_booking_rate'] }}%</strong>
<h2>Conversión ride booking</h2>
<p>La demo enseña que un trayecto confirmado puede convertirse en ingreso atribuible, no solo en transporte resuelto.</p>
</article>
<article class="card proof-card">
<span class="eyebrow">Señal comercial</span>
<strong class="proof-stat">{{ number_format($metrics['commission'], 0) }}</strong>
<h2>Comisión estimada trazable</h2>
<p>Desde el panel puedes enseñar GMV demo, comisión y etapas del funnel sin depender de discurso abstracto.</p>
</article>
<article class="card proof-card">
<span class="eyebrow">Ángulo ventas</span>
<strong class="proof-stat">{{ $metrics['clicks'] }}</strong>
<h2>Clics con intención real</h2>
<p>Menos catálogo, más contexto. Ese es el argumento que entienden hotel, recepción y operador local.</p>
<a class="inline-link" href="/admin/login">Abrir panel demo</a>
</article>
</div>
</section>
<section id="request" class="split">
<article class="card">
<span class="eyebrow">1 · Request taxi</span>
<h2>Request a ride</h2>
<p class="muted">Use realistic destination context so the recommendation engine has something to work with.</p>
<span class="eyebrow">1 · Solicitar taxi</span>
<h2>Activa un trayecto demo</h2>
<p class="muted">Usa un origen y destino realistas de Lanzarote para que el motor pueda puntuar mejor las propuestas.</p>
@if ($errors->any())
<div class="errors" style="margin-top:16px;">
<strong>Please fix the form:</strong>
<div class="errors">
<strong>Revisa el formulario:</strong>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@ -59,53 +91,54 @@
@csrf
<div class="grid-2">
<label>
Pickup label
<input type="text" name="pickup_label" value="{{ old('pickup_label', 'Airport Terminal 1') }}" placeholder="Airport Terminal 1" required>
Punto de recogida
<input type="text" name="pickup_label" value="{{ old('pickup_label', 'Aeropuerto César Manrique') }}" placeholder="Aeropuerto César Manrique" required>
</label>
<label>
Destination label
<input type="text" name="destination_label" value="{{ old('destination_label', 'Old Town') }}" placeholder="Old Town, Marina, Beach..." required>
Destino
<input type="text" name="destination_label" value="{{ old('destination_label', 'Puerto del Carmen') }}" placeholder="Puerto del Carmen, Marina, Playa Blanca..." required>
</label>
</div>
<div class="grid-2">
<label>
Scheduled for
Programado para
<input type="datetime-local" name="scheduled_for" value="{{ old('scheduled_for') }}">
</label>
<label>
Source channel
Canal de origen
<select name="source_channel" required>
@foreach (['web' => 'Web', 'hotel' => 'Hotel', 'reception' => 'Reception', 'app' => 'App'] as $value => $label)
<option value="{{ $value }}" @selected(old('source_channel', 'web') === $value)>{{ $label }}</option>
@foreach (['web' => 'Web', 'hotel' => 'Hotel', 'reception' => 'Recepción', 'app' => 'App'] as $value => $label)
<option value="{{ $value }}" @selected(old('source_channel', 'hotel') === $value)>{{ $label }}</option>
@endforeach
</select>
</label>
</div>
<input type="hidden" name="locale" value="en">
<button class="btn" type="submit">Confirm taxi and show offers</button>
<input type="hidden" name="locale" value="es">
<button class="btn" type="submit">Confirmar taxi y activar sugerencias</button>
</form>
</article>
<aside class="card" id="how">
<span class="eyebrow">Demo logic</span>
<h2>What happens next</h2>
<div class="list" style="margin-top:18px;">
<div class="list-item"><strong>1.</strong> Ride is stored with context zone, ETA, and source channel.</div>
<div class="list-item"><strong>2.</strong> A simple rules engine scores live offers for that destination.</div>
<div class="list-item"><strong>3.</strong> The confirmed ride screen logs recommendation views and invites a click.</div>
<div class="list-item"><strong>4.</strong> The booking form confirms demand and logs the conversion path.</div>
<aside id="how" class="card">
<span class="eyebrow">Qué demuestra esta demo</span>
<h2>Flujo MVP real</h2>
<div class="list">
<div class="list-item"><strong>1.</strong> Se guarda el ride con contexto, ETA y canal. <p>Eso ya deja una señal útil para operación y atribución.</p></div>
<div class="list-item"><strong>2.</strong> Un motor simple puntúa ofertas publicadas. <p>No busca mostrarlo todo; solo lo que mejor encaja ahora.</p></div>
<div class="list-item"><strong>3.</strong> La pantalla de taxi confirmado genera vistas y clics medibles. <p>Ahí empieza el funnel de impacto.</p></div>
<div class="list-item"><strong>4.</strong> La reserva cierra el circuito con conversión trazable. <p>Es la historia mínima que necesita un demo killer.</p></div>
</div>
</aside>
</section>
<section class="section">
<div style="display:flex;justify-content:space-between;align-items:end;gap:16px;margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;align-items:end;gap:16px;flex-wrap:wrap;margin-bottom:16px;">
<div>
<span class="eyebrow">Demo inventory</span>
<h2>Seeded offers</h2>
<span class="eyebrow">Inventario demo</span>
<h2>Ofertas listas para sugerir</h2>
</div>
<p>These are the kinds of offers the ride confirmation step can surface.</p>
<p>Contenido corto, accionable y relevante para un turista que ya ha pedido transporte.</p>
</div>
<div class="cards">
@foreach ($featuredOffers as $offer)
<article class="card offer-card">
@ -113,11 +146,11 @@
<div>
<div class="offer-meta">
<span class="pill">{{ ucfirst($offer->category) }}</span>
@if($offer->price_from)
<span class="pill">From {{ number_format((float) $offer->price_from, 0) }}</span>
@if($offer->location_label)
<span class="pill">{{ $offer->location_label }}</span>
@endif
@if($offer->duration_minutes)
<span class="pill">{{ $offer->duration_minutes }} min</span>
@if($offer->price_from)
<span class="pill">Desde {{ number_format((float) $offer->price_from, 0) }}</span>
@endif
</div>
<h3 style="margin-top:14px;">{{ $offer->title }}</h3>
@ -127,4 +160,15 @@
@endforeach
</div>
</section>
<section class="section">
<article class="card" style="display:flex;justify-content:space-between;align-items:center;gap:18px;flex-wrap:wrap;">
<div>
<span class="eyebrow">Cierre demo</span>
<h2>Front simple para turista. Historia clara para negocio.</h2>
<p class="muted">Cuando termines el recorrido, entra en el panel para enseñar funnel, canales y reservas sin salir de la narrativa TAXILANZ.</p>
</div>
<a class="btn btn-secondary" href="/admin/login">Ir al dashboard Filament</a>
</article>
</section>
@endsection

View File

@ -1,150 +1,510 @@
<!DOCTYPE html>
<html lang="en">
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'Taxi to booking demo')</title>
<meta name="description" content="@yield('meta_description', 'Taxi request demo that turns arrivals into contextual recommendations and simple bookings.')">
<meta name="author" content="Flatlogic Laravel Demo">
<title>@yield('title', 'TAXILANZ Demo | Taxi + recomendaciones + reserva')</title>
<meta name="description" content="@yield('meta_description', 'Demo TAXILANZ: el taxi como punto de entrada para activar recomendaciones útiles y reservas medibles en el momento exacto.')">
<meta name="author" content="TAXILANZ Demo">
<style>
:root {
--bg: #f7f7f2;
--surface: rgba(255,255,255,.88);
--bg: #f7f7f1;
--surface: rgba(255, 255, 255, 0.84);
--surface-strong: #ffffff;
--text: #111827;
--muted: #667085;
--line: rgba(17,24,39,.08);
--text: #0f172a;
--muted: #64748b;
--line: rgba(15, 23, 42, 0.08);
--accent: #0f766e;
--accent-2: #f59e0b;
--accent-strong: #115e59;
--accent-soft: rgba(15, 118, 110, 0.12);
--warm: #f59e0b;
--warm-soft: rgba(245, 158, 11, 0.12);
--success: #dcfce7;
--shadow: 0 24px 80px rgba(15, 23, 42, .10);
--radius: 24px;
--danger: #fef2f2;
--danger-text: #b91c1c;
--shadow: 0 24px 70px rgba(15, 23, 42, 0.10);
--radius: 26px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(circle at top left, rgba(245,158,11,.12), transparent 24%),
radial-gradient(circle at top right, rgba(15,118,110,.15), transparent 26%),
linear-gradient(180deg, #fcfcf8 0%, #f2f4ef 100%);
color: var(--text);
background:
radial-gradient(circle at top left, rgba(245, 158, 11, 0.16), transparent 22%),
radial-gradient(circle at top right, rgba(20, 184, 166, 0.18), transparent 24%),
linear-gradient(180deg, #fcfcf8 0%, #f2f4ef 100%);
}
a { color: inherit; text-decoration: none; }
.shell { width: min(1180px, calc(100% - 32px)); margin: 0 auto; }
img { max-width: 100%; display: block; }
code {
padding: 0.12rem 0.4rem;
border-radius: 8px;
background: rgba(15, 23, 42, 0.06);
font-size: 0.92em;
}
.shell {
width: min(1180px, calc(100% - 32px));
margin: 0 auto;
}
.topbar {
position: sticky; top: 0; z-index: 40;
backdrop-filter: blur(20px);
background: rgba(247,247,242,.78);
position: sticky;
top: 0;
z-index: 50;
backdrop-filter: blur(18px);
background: rgba(247, 247, 241, 0.82);
border-bottom: 1px solid var(--line);
}
.topbar-inner {
display: flex; align-items: center; justify-content: space-between;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 0;
}
.brand { display:flex; align-items:center; gap:12px; font-weight:700; letter-spacing:-.02em; }
.brand-mark {
width: 38px; height: 38px; border-radius: 14px;
background: linear-gradient(135deg, var(--accent), #14b8a6);
box-shadow: inset 0 1px 0 rgba(255,255,255,.4);
.brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 800;
letter-spacing: -0.03em;
}
.nav-links { display:flex; gap:18px; color: var(--muted); font-size: 14px; }
main { padding: 44px 0 80px; }
.brand-mark {
width: 40px;
height: 40px;
border-radius: 14px;
background: linear-gradient(135deg, var(--accent), #14b8a6);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.42), 0 16px 32px rgba(15, 118, 110, 0.18);
}
.brand-copy small {
display: block;
color: var(--muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
}
.nav-links {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 18px;
color: var(--muted);
font-size: 14px;
}
.nav-links a:hover { color: var(--text); }
main { padding: 42px 0 80px; }
.hero {
display:grid; grid-template-columns: 1.1fr .9fr; gap: 24px; align-items: stretch;
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
gap: 24px;
margin-bottom: 28px;
}
.card {
background: var(--surface);
border: 1px solid rgba(255,255,255,.6);
box-shadow: var(--shadow);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: var(--radius);
padding: 28px;
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.eyebrow { display:inline-flex; align-items:center; gap:8px; padding:8px 12px; border-radius:999px; font-size:12px; font-weight:700; background: rgba(15,118,110,.10); color: var(--accent); }
h1, h2, h3 { margin: 0 0 12px; letter-spacing: -.03em; }
h1 { font-size: clamp(2.4rem, 5vw, 4.5rem); line-height: .95; }
h2 { font-size: clamp(1.5rem, 3vw, 2.3rem); }
h3 { font-size: 1.1rem; }
p { margin: 0; color: var(--muted); line-height: 1.65; }
.hero-copy { display:flex; flex-direction:column; justify-content:space-between; gap:20px; }
.stats { display:grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 18px; }
.stat { padding: 18px; border-radius: 20px; background: rgba(255,255,255,.66); border: 1px solid var(--line); }
.stat strong { display:block; font-size: 1.75rem; margin-bottom: 6px; }
.stat span { color: var(--muted); font-size: 14px; }
.orb-wrap { position:relative; overflow:hidden; min-height: 100%; }
.orb-wrap::before, .orb-wrap::after {
content:''; position:absolute; border-radius:999px; filter: blur(2px);
.hero-copy {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 20px;
}
.orb-wrap::before { inset: auto auto 24px 18px; width: 120px; height: 120px; background: rgba(15,118,110,.18); }
.orb-wrap::after { inset: 18px 18px auto auto; width: 160px; height: 160px; background: rgba(245,158,11,.18); }
.stack { display:grid; gap:16px; position:relative; z-index:1; }
.mini-card { background: rgba(255,255,255,.8); border: 1px solid rgba(17,24,39,.06); border-radius: 22px; padding: 18px; }
.mini-card small { color: var(--muted); text-transform: uppercase; letter-spacing: .12em; }
form { display:grid; gap:14px; }
label { display:grid; gap:8px; font-size: 14px; font-weight: 600; }
input, select, textarea {
width:100%; border:1px solid rgba(17,24,39,.10); border-radius:16px; padding:14px 16px;
background:#fff; color: var(--text); font: inherit;
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.03em;
background: var(--accent-soft);
color: var(--accent);
}
textarea { min-height: 110px; resize: vertical; }
.grid-2 { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap:14px; }
.btn {
display:inline-flex; align-items:center; justify-content:center; gap:10px;
padding: 15px 20px; border-radius: 16px; border:0; cursor:pointer; font: inherit; font-weight: 700;
background: linear-gradient(135deg, var(--accent), #14b8a6); color: #fff; box-shadow: 0 16px 36px rgba(15,118,110,.26);
h1, h2, h3 {
margin: 0 0 12px;
letter-spacing: -0.04em;
}
.btn-secondary { background: rgba(17,24,39,.06); color: var(--text); box-shadow:none; }
.section { margin-top: 28px; }
.cards { display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap:16px; }
.offer-card { display:grid; gap:16px; }
.offer-visual {
aspect-ratio: 16 / 10; border-radius: 20px;
background: linear-gradient(135deg, rgba(15,118,110,.9), rgba(245,158,11,.85));
position: relative; overflow:hidden;
h1 {
font-size: clamp(2.5rem, 5vw, 4.9rem);
line-height: 0.93;
max-width: 12ch;
}
.offer-visual::before, .offer-visual::after { content:''; position:absolute; border-radius:999px; background: rgba(255,255,255,.14); }
.offer-visual::before { width: 140px; height: 140px; top: -30px; right: -20px; }
.offer-visual::after { width: 92px; height: 92px; left: 16px; bottom: -24px; }
.offer-meta { display:flex; flex-wrap:wrap; gap:8px; font-size:12px; color: var(--muted); }
.pill { display:inline-flex; align-items:center; gap:8px; padding:8px 11px; border-radius:999px; background:#fff; border:1px solid var(--line); }
.split { display:grid; grid-template-columns: 1.08fr .92fr; gap: 24px; }
.list { display:grid; gap: 14px; }
.list-item { padding: 18px; border-radius: 20px; border: 1px solid var(--line); background: rgba(255,255,255,.66); }
.success { background: linear-gradient(135deg, rgba(220,252,231,.95), rgba(187,247,208,.88)); }
h2 { font-size: clamp(1.55rem, 3vw, 2.4rem); }
h3 { font-size: 1.12rem; }
p {
margin: 0;
color: var(--muted);
line-height: 1.68;
}
.muted { color: var(--muted); }
.errors { padding: 14px 16px; border-radius: 18px; background: rgba(239,68,68,.08); color: #991b1b; }
.footer { padding: 36px 0 16px; color: var(--muted); font-size: 14px; }
.kpis { display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 14px; }
.stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.stat {
padding: 18px;
border-radius: 20px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.68);
}
.stat strong {
display: block;
margin-bottom: 6px;
font-size: 1.8rem;
letter-spacing: -0.04em;
}
.stat span {
color: var(--muted);
font-size: 14px;
}
.orb-wrap {
position: relative;
overflow: hidden;
min-height: 100%;
}
.orb-wrap::before,
.orb-wrap::after {
content: "";
position: absolute;
border-radius: 999px;
filter: blur(2px);
}
.orb-wrap::before {
width: 130px;
height: 130px;
inset: auto auto 24px 16px;
background: rgba(15, 118, 110, 0.18);
}
.orb-wrap::after {
width: 170px;
height: 170px;
inset: 16px 18px auto auto;
background: rgba(245, 158, 11, 0.16);
}
.stack {
position: relative;
z-index: 1;
display: grid;
gap: 16px;
}
.mini-card {
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 22px;
padding: 18px;
}
.mini-card small {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.14em;
}
.success {
background: linear-gradient(180deg, rgba(220, 252, 231, 0.92), rgba(255, 255, 255, 0.86));
}
.split,
.cards,
.list,
.offer-meta,
.cta-row {
display: grid;
gap: 16px;
}
.split { grid-template-columns: minmax(0, 1.05fr) minmax(300px, 0.95fr); }
.cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.list { margin-top: 18px; }
.list-item {
padding: 16px 18px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.7);
color: var(--text);
}
.list-item p { margin-top: 6px; }
.section { margin-top: 28px; }
.proof-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.proof-card {
display: grid;
gap: 10px;
align-content: start;
min-height: 100%;
}
.proof-stat {
font-size: clamp(2rem, 5vw, 2.8rem);
letter-spacing: -0.05em;
line-height: 1;
}
.inline-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--accent-strong);
font-weight: 600;
}
.btn-secondary {
background: rgba(15, 23, 42, 0.92);
color: white;
}
.btn-secondary:hover {
transform: translateY(-1px);
box-shadow: 0 14px 28px rgba(15, 23, 42, 0.18);
}
.offer-card {
display: grid;
gap: 16px;
align-content: start;
}
.offer-visual {
aspect-ratio: 16 / 10;
border-radius: 22px;
position: relative;
overflow: hidden;
background: linear-gradient(135deg, rgba(15, 118, 110, 0.95), rgba(245, 158, 11, 0.85));
}
.offer-visual::before,
.offer-visual::after {
content: "";
position: absolute;
border-radius: 999px;
}
.offer-visual::before {
width: 150px;
height: 150px;
background: rgba(255, 255, 255, 0.16);
top: -30px;
right: -10px;
}
.offer-visual::after {
width: 94px;
height: 94px;
background: rgba(15, 23, 42, 0.14);
left: 18px;
bottom: 18px;
}
.offer-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.06);
color: var(--text);
font-size: 12px;
font-weight: 700;
}
form { display: grid; gap: 14px; }
label {
display: grid;
gap: 8px;
font-size: 14px;
font-weight: 700;
}
input,
select,
textarea {
width: 100%;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(15, 23, 42, 0.12);
background: #ffffff;
color: var(--text);
font: inherit;
}
textarea {
min-height: 110px;
resize: vertical;
}
.grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 15px 20px;
border: 0;
border-radius: 16px;
cursor: pointer;
font: inherit;
font-weight: 800;
color: #fff;
background: linear-gradient(135deg, var(--accent), #14b8a6);
box-shadow: 0 16px 36px rgba(15, 118, 110, 0.24);
}
.btn-secondary {
color: var(--text);
background: rgba(15, 23, 42, 0.06);
box-shadow: none;
}
.btn:hover { transform: translateY(-1px); }
.errors,
.notice {
padding: 16px 18px;
border-radius: 18px;
margin-top: 12px;
}
.errors {
background: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.16);
color: var(--danger-text);
}
.notice {
background: rgba(15, 118, 110, 0.08);
border: 1px solid rgba(15, 118, 110, 0.16);
color: var(--accent-strong);
}
.footer-note {
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 980px) {
.hero, .split, .cards, .kpis { grid-template-columns: 1fr; }
.grid-2, .stats { grid-template-columns: 1fr; }
.nav-links { display:none; }
.hero,
.split,
.cards,
.proof-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.topbar-inner,
.footer-note {
flex-direction: column;
align-items: flex-start;
}
.stats,
.grid-2 {
grid-template-columns: 1fr;
}
main { padding-top: 28px; }
.card { padding: 22px; }
.nav-links { gap: 12px; }
}
</style>
@yield('head')
@stack('head')
</head>
<body>
<header class="topbar">
<div class="shell topbar-inner">
<a href="{{ route('home') }}" class="brand">
<span class="brand-mark"></span>
<span>ArrivalFlow Demo</span>
<a href="{{ route('home') }}" class="brand" aria-label="Ir al inicio de TAXILANZ Demo">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-copy">
<strong>TAXILANZ Demo</strong>
<small>Taxi sugerencia reserva</small>
</span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="{{ route('home') }}#request">Request taxi</a>
<a href="{{ route('home') }}#how">How it works</a>
<nav class="nav-links" aria-label="Navegación principal">
<a href="{{ route('home') }}#request">Pedir taxi</a>
<a href="{{ route('home') }}#how">Cómo funciona</a>
<a href="/admin/login">Panel demo</a>
<a href="{{ route('healthz') }}">Health</a>
</nav>
</div>
</header>
<main>
<div class="shell">@yield('content')</div>
<div class="shell">
@yield('content')
<footer class="footer-note">
<span>No es un marketplace infinito: activa la mejor propuesta en el momento exacto.</span>
<span>{{ now()->format('Y') }} · Laravel + Filament demo slice</span>
</footer>
</div>
</main>
<footer class="shell footer">
Demo slice in Laravel: taxi request contextual offer booking confirmation.
</footer>
</body>
</html>

View File

@ -1,37 +1,40 @@
@extends('layouts.app')
@section('title', $offer->title . ' | ArrivalFlow')
@section('meta_description', $offer->excerpt ?: 'Contextual offer detail with a simple booking form.')
@section('title', $offer->title . ' | TAXILANZ Demo')
@section('meta_description', $offer->excerpt ?: 'Detalle de oferta contextual con reserva simple en TAXILANZ Demo.')
@section('content')
<section class="split">
<article class="card offer-card">
<div class="offer-visual" aria-hidden="true"></div>
<div>
<span class="eyebrow">4 · Offer detail</span>
<h1 style="font-size:clamp(2.1rem,4vw,3.4rem);">{{ $offer->title }}</h1>
<span class="eyebrow">4 · Detalle de oferta</span>
<h1 style="font-size:clamp(2.15rem,4vw,3.6rem);max-width:14ch;">{{ $offer->title }}</h1>
<p>{{ $offer->description ?: $offer->excerpt }}</p>
<div class="offer-meta" style="margin-top:16px;">
<span class="pill">{{ ucfirst($offer->category) }}</span>
@if($offer->location_label)<span class="pill">{{ $offer->location_label }}</span>@endif
@if($offer->price_from)<span class="pill">From {{ number_format((float) $offer->price_from, 0) }}</span>@endif
@if($offer->price_from)<span class="pill">Desde {{ number_format((float) $offer->price_from, 0) }}</span>@endif
@if($offer->duration_minutes)<span class="pill">{{ $offer->duration_minutes }} min</span>@endif
</div>
@if($recommendation)
<div class="list-item" style="margin-top:18px;">
<strong>Recommendation context:</strong> This click is tied to ride #{{ $ride?->id }} and recommendation #{{ $recommendation->position }}.
<div class="notice" style="margin-top:18px;">
<strong>Contexto de recomendación:</strong>
esta visita viene del trayecto #{{ $ride?->id }} y de la sugerencia en posición {{ $recommendation->position }}.
El clic ya cuenta como <code>recommendation_clicked</code>.
</div>
@endif
</div>
</article>
<aside class="card">
<span class="eyebrow">5 · Reserve</span>
<h2>Simple booking form</h2>
<p>This POST creates a confirmed booking and logs both <code>booking_started</code> and <code>booking_completed</code>.</p>
<span class="eyebrow">5 · Reserva simple</span>
<h2>Confirma en menos de un minuto</h2>
<p>Este formulario crea la reserva y registra <code>booking_started</code> y <code>booking_completed</code>.</p>
@if ($errors->any())
<div class="errors" style="margin-top:16px;">
<div class="errors">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@ -45,7 +48,7 @@
<input type="hidden" name="ride_id" value="{{ $ride?->id }}">
<input type="hidden" name="ride_recommendation_id" value="{{ $recommendation?->id }}">
<label>
Customer name
Nombre
<input type="text" name="customer_name" value="{{ old('customer_name', 'Alex Morgan') }}" required>
</label>
<div class="grid-2">
@ -54,25 +57,25 @@
<input type="email" name="customer_email" value="{{ old('customer_email', 'alex@example.com') }}">
</label>
<label>
Phone
<input type="text" name="customer_phone" value="{{ old('customer_phone', '+1 555 010 224') }}">
Teléfono
<input type="text" name="customer_phone" value="{{ old('customer_phone', '+34 600 123 456') }}">
</label>
</div>
<div class="grid-2">
<label>
Party size
Personas
<input type="number" name="party_size" value="{{ old('party_size', 2) }}" min="1" max="12" required>
</label>
<label>
Booking for
Fecha / hora
<input type="datetime-local" name="booking_for" value="{{ old('booking_for') }}">
</label>
</div>
<label>
Notes
<textarea name="notes">{{ old('notes', 'Window table if available.') }}</textarea>
Nota
<textarea name="notes">{{ old('notes', 'Mesa tranquila si está disponible.') }}</textarea>
</label>
<button class="btn" type="submit">Confirm booking</button>
<button class="btn" type="submit">Confirmar reserva</button>
</form>
</aside>
</section>

View File

@ -1,43 +1,76 @@
@extends('layouts.app')
@section('title', 'Taxi confirmed | ArrivalFlow')
@section('meta_description', 'Ride confirmed. Surface the best contextual offers while the guest waits or arrives.')
@section('title', 'Taxi confirmado | TAXILANZ Demo')
@section('meta_description', 'Pantalla de taxi confirmado con recomendaciones contextuales y tracking de visualización.')
@section('content')
<section class="split">
<article class="card success">
<span class="eyebrow">2 · Taxi confirmed</span>
<h1 style="font-size:clamp(2.1rem,4vw,3.5rem);">Your taxi is on the way.</h1>
<p>Pickup: <strong>{{ $ride->pickup_label }}</strong><br>Destination: <strong>{{ $ride->destination_label }}</strong><br>ETA: <strong>{{ $ride->eta_minutes }} min</strong></p>
<div class="list" style="margin-top:18px;">
<div class="list-item">Ride UUID: {{ $ride->uuid }}</div>
<div class="list-item">Source channel: {{ ucfirst($ride->source_channel) }}</div>
<div class="list-item">Context zone: {{ $ride->context_zone ?: 'General' }}</div>
<section class="hero">
<article class="card hero-copy">
<div>
<span class="eyebrow">2 · Taxi confirmado</span>
<h1>Tu taxi llega en {{ $ride->eta_minutes ?? 6 }} min.</h1>
<p>
Mientras esperas, mostramos propuestas que encajan con tu destino y tu momento.
Aquí es donde TAXILANZ deja claro que no vende “más catálogo”, sino mejores decisiones justo a tiempo.
</p>
</div>
<div class="list" style="margin-top:0;">
<div class="list-item"><strong>Recogida:</strong> {{ $ride->pickup_label }}</div>
<div class="list-item"><strong>Destino:</strong> {{ $ride->destination_label }}</div>
<div class="list-item"><strong>Canal:</strong> {{ ucfirst($ride->source_channel) }} · <strong>Zona:</strong> {{ $ride->context_zone ?: 'General' }}</div>
</div>
</article>
<aside class="card">
<span class="eyebrow">3 · While you wait</span>
<h2>Best next offers</h2>
<p>These recommendations were stored against this ride and logged as viewed for demo attribution.</p>
<aside class="card orb-wrap">
<div class="stack">
<div class="mini-card">
<small>Señal capturada</small>
<h3>Ride creado</h3>
<p>La solicitud ya quedó registrada con contexto para atribución y operación.</p>
</div>
<div class="mini-card">
<small>Motor simple</small>
<h3>Solo 23 opciones</h3>
<p>La lógica prioriza relevancia, disponibilidad y facilidad de cierre, no volumen.</p>
</div>
<div class="mini-card success">
<small>Objetivo demo</small>
<h3>Provocar clic útil</h3>
<p>Si el usuario entra al detalle y reserva, el panel ya puede contar una historia de impacto.</p>
</div>
</div>
</aside>
</section>
<section class="section">
<div style="display:flex;justify-content:space-between;align-items:end;gap:16px;flex-wrap:wrap;margin-bottom:16px;">
<div>
<span class="eyebrow">3 · Recomendaciones</span>
<h2>Encajan con tu ruta ahora</h2>
</div>
<p>Estas vistas ya están generando eventos <code>recommendation_viewed</code>.</p>
</div>
<div class="cards">
@foreach ($recommendations as $recommendation)
<article class="card offer-card">
<div class="offer-visual" aria-hidden="true"></div>
<div>
<div class="offer-meta">
<span class="pill">#{{ $recommendation->position }}</span>
<span class="pill">Score {{ number_format((float) $recommendation->score, 0) }}</span>
<span class="pill">{{ ucfirst($recommendation->offer->category) }}</span>
<span class="pill">Top {{ $recommendation->position }}</span>
<span class="pill">Score {{ (int) $recommendation->score }}</span>
@if($recommendation->offer->location_label)
<span class="pill">{{ $recommendation->offer->location_label }}</span>
@endif
</div>
<h3 style="margin-top:14px;">{{ $recommendation->offer->title }}</h3>
<p>{{ $recommendation->offer->excerpt }}</p>
<p style="margin-top:12px;"><strong>Why it fits:</strong> {{ $recommendation->reason }}</p>
<div class="notice" style="margin-top:14px;">
<strong>Por qué encaja:</strong> {{ $recommendation->reason }}
</div>
</div>
<a class="btn" href="{{ route('offers.show', $recommendation->offer->slug) }}?ride={{ $ride->id }}&recommendation={{ $recommendation->id }}">View offer</a>
<a class="btn" href="{{ route('offers.show', $recommendation->offer->slug) }}?ride={{ $ride->id }}&recommendation={{ $recommendation->id }}">Ver detalle y reservar</a>
</article>
@endforeach
</div>