Auto commit: 2026-04-06T07:35:25.615Z

This commit is contained in:
Flatlogic Bot 2026-04-06 07:35:25 +00:00
parent 429417d752
commit 6d57abd008
9 changed files with 274 additions and 169 deletions

View File

@ -9,7 +9,7 @@ use Filament\Widgets\ChartWidget;
class FunnelStageChart extends ChartWidget
{
protected static ?string $heading = 'Funnel ejecutivo';
protected static ?string $heading = 'Funnel';
protected int | string | array $columnSpan = 'full';
@ -32,7 +32,7 @@ class FunnelStageChart extends ChartWidget
'borderRadius' => 12,
'borderSkipped' => false,
]],
'labels' => ['Taxi', 'Views', 'Clicks', 'Bookings'],
'labels' => ['Taxi', 'Espera útil', 'Detalle', 'Reserva'],
];
}

View File

@ -21,28 +21,20 @@ class JourneyStoryboard extends Widget
$views = Event::where('event_type', 'recommendation_viewed')->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,
];
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Booking;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class LatestBookingsTable extends BaseWidget
{
protected static ?string $heading = 'Últimas reservas';
protected static ?int $sort = 4;
protected int | string | array $columnSpan = 1;
public function table(Table $table): Table
{
return $table
->query(Booking::query()->with(['offer', 'ride'])->latest()->limit(5))
->paginated(false)
->columns([
Tables\Columns\TextColumn::make('offer.title')
->label('Oferta')
->description(fn (Booking $record): string => $record->customer_name ?: ($record->ride?->pickup_label ?: 'Reserva directa'))
->weight('semibold'),
Tables\Columns\TextColumn::make('amount')
->label('Importe')
->money('EUR'),
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('created_at')
->label('Creada')
->since(),
]);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Ride;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class LatestRidesTable extends BaseWidget
{
protected static ?string $heading = 'Últimos trayectos';
protected static ?int $sort = 3;
protected int | string | array $columnSpan = 1;
public function table(Table $table): Table
{
return $table
->query(Ride::query()->latest()->limit(5))
->paginated(false)
->columns([
Tables\Columns\TextColumn::make('pickup_label')
->label('Trayecto')
->description(fn (Ride $record): string => '→ '.$record->destination_label)
->weight('semibold'),
Tables\Columns\TextColumn::make('status')
->label('Estado')
->badge()
->formatStateUsing(fn (?string $state): string => ucfirst($state ?: 'pendiente'))
->color(fn (?string $state): string => match ($state) {
'confirmed', 'completed' => 'success',
'pending' => 'warning',
'cancelled' => 'danger',
default => 'gray',
}),
Tables\Columns\TextColumn::make('eta_minutes')
->label('ETA')
->formatStateUsing(fn ($state): string => $state ? $state.' min' : '—'),
Tables\Columns\TextColumn::make('created_at')
->label('Creado')
->since(),
]);
}
}

View File

@ -4,6 +4,8 @@ namespace App\Providers\Filament;
use App\Filament\Widgets\FunnelStageChart;
use App\Filament\Widgets\JourneyStoryboard;
use App\Filament\Widgets\LatestBookingsTable;
use App\Filament\Widgets\LatestRidesTable;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
@ -41,6 +43,8 @@ class AdminPanelProvider extends PanelProvider
->widgets([
JourneyStoryboard::class,
FunnelStageChart::class,
LatestRidesTable::class,
LatestBookingsTable::class,
])
->middleware([
EncryptCookies::class,

View File

@ -8,33 +8,29 @@
<article class="card form-card">
<span class="eyebrow">Reserva confirmada</span>
<h1 style="font-size:clamp(2.15rem,4vw,3.7rem);max-width:12ch;">Reserva cerrada.</h1>
<p class="compact-lead">{{ $booking->customer_name }} ya tiene {{ $booking->offer->title }}.</p>
<p class="compact-lead">{{ $booking->offer->title }} ya está en marcha.</p>
<div class="story-rail" aria-label="Progreso del funnel">
<span class="story-chip is-done">1 · Taxi</span>
<span class="story-chip is-done">2 · Contexto</span>
<span class="story-chip is-done">2 · Confirmado</span>
<span class="story-chip is-done">3 · Oferta</span>
<span class="story-chip is-active">4 · Reserva</span>
</div>
<div class="route-card">
<small>Resultado</small>
<small>Estado actual</small>
<strong>{{ $booking->offer->title }}</strong>
<span>{{ $booking->amount ? '€'.number_format((float) $booking->amount, 2) : 'Importe pendiente' }} · {{ ucfirst($booking->status) }} · {{ $booking->ride?->context_zone ?: ($booking->offer->location_label ?: 'General') }}</span>
<span>{{ $booking->amount ? '€'.number_format((float) $booking->amount, 2) : 'Importe pendiente' }} · {{ ucfirst($booking->status) }}</span>
</div>
<div class="metric-strip metric-strip--3">
<div class="metric-strip metric-strip--2">
<div class="metric-cell">
<strong>{{ $booking->amount ? '€'.number_format((float) $booking->amount, 0) : '—' }}</strong>
<span>importe</span>
</div>
<div class="metric-cell">
<strong>{{ $booking->commission_amount ? '€'.number_format((float) $booking->commission_amount, 0) : '—' }}</strong>
<span>comisión</span>
</div>
<div class="metric-cell">
<strong>{{ $booking->recommendation?->position ? 'Top '.$booking->recommendation->position : 'Directa' }}</strong>
<span>atribución</span>
<strong>{{ ucfirst($booking->status) }}</strong>
<span>estado</span>
</div>
</div>
@ -43,37 +39,36 @@
@if($booking->ride)
<a class="btn btn-secondary" href="{{ route('rides.confirmed', $booking->ride) }}">Volver al taxi confirmado</a>
@endif
<button type="button" class="icon-button" data-modal-open="booking-business" data-tooltip="Detalle negocio">Ver negocio</button>
<button type="button" class="icon-button" data-modal-open="booking-business" data-tooltip="Más info"></button>
</div>
<details class="disclosure" style="margin-top:8px;">
<summary>
<span> Ver detalle de atribución</span>
<span> Más info</span>
<span>Abrir</span>
</summary>
<div class="disclosure-body">
<ul class="accordion-list">
<li>Booking UUID: <strong>{{ $booking->uuid }}</strong>.</li>
<li>Estado: <strong>{{ ucfirst($booking->status) }}</strong>.</li>
<li>Ride vinculado: <strong>{{ $booking->ride?->uuid ?? 'Reserva directa' }}</strong>.</li>
<li>Origen: <strong>{{ $booking->ride?->source_channel ? ucfirst($booking->ride->source_channel) : 'Directo' }}</strong>.</li>
<li>Zona: <strong>{{ $booking->ride?->context_zone ?: ($booking->offer->location_label ?: 'General') }}</strong>.</li>
</ul>
</div>
</details>
</article>
<aside class="card form-card">
<span class="eyebrow">Business snapshot</span>
<span class="eyebrow">Resumen</span>
<div class="decision-primary">
<h2>Valor visible</h2>
<h2>Todo listo.</h2>
<div class="screen-kpis">
<div class="screen-kpi">
<strong>{{ $booking->offer->location_label ?: 'Zona activa' }}</strong>
<span>zona</span>
</div>
<div class="screen-kpi">
<strong>{{ $booking->ride?->source_channel ? ucfirst($booking->ride->source_channel) : 'Directo' }}</strong>
<span>canal</span>
<strong>{{ $booking->party_size ?: 1 }}</strong>
<span>personas</span>
</div>
<div class="screen-kpi">
<strong>{{ $booking->customer_email ?: 'No informado' }}</strong>
@ -84,14 +79,14 @@
<details class="disclosure disclosure--soft" style="margin-top:16px;">
<summary>
<span> Ver datos operativos</span>
<span>Ver detalle</span>
<span>Abrir</span>
</summary>
<div class="disclosure-body">
<ul class="accordion-list">
<li>{{ $booking->offer->title }} queda como conversión cerrada.</li>
<li>La comisión demo ya es visible en dashboard.</li>
<li>El usuario ve un cierre limpio; negocio ve trazabilidad cuando la pide.</li>
<li>Si vienes desde taxi confirmado, la reserva mantiene la trazabilidad completa.</li>
<li>La lectura económica queda fuera de la vista principal.</li>
<li>Negocio la consulta solo cuando la necesita.</li>
</ul>
</div>
</details>
@ -102,15 +97,15 @@
<div class="app-modal-card">
<div class="app-modal-head">
<div>
<p class="eyebrow">Detalle negocio</p>
<h2 id="booking-business-title">Conversión atribuible</h2>
<p class="eyebrow">Más info</p>
<h2 id="booking-business-title">Detalle de atribución</h2>
</div>
<button type="button" class="app-modal-close" data-modal-close aria-label="Cerrar modal">Cerrar</button>
</div>
<div class="metric-strip metric-strip--3">
<div class="metric-cell">
<strong>{{ $booking->amount ? '€'.number_format((float) $booking->amount, 2) : 'Pendiente' }}</strong>
<span>GMV</span>
<span>importe</span>
</div>
<div class="metric-cell">
<strong>{{ $booking->commission_amount ? '€'.number_format((float) $booking->commission_amount, 2) : 'Pendiente' }}</strong>
@ -118,14 +113,14 @@
</div>
<div class="metric-cell">
<strong>{{ $booking->recommendation?->position ? 'Top '.$booking->recommendation->position : 'Directa' }}</strong>
<span>origen</span>
<span>atribución</span>
</div>
</div>
<ul class="accordion-list">
<li>Oferta: <strong>{{ $booking->offer->title }}</strong>.</li>
<li>Zona: <strong>{{ $booking->ride?->context_zone ?: ($booking->offer->location_label ?: 'General') }}</strong>.</li>
<li>Canal: <strong>{{ $booking->ride?->source_channel ? ucfirst($booking->ride->source_channel) : 'Directo' }}</strong>.</li>
<li>La UI principal sigue limpia; la explicación queda bajo demanda.</li>
<li>Ride vinculado: <strong>{{ $booking->ride?->uuid ?? 'Reserva directa' }}</strong>.</li>
<li>Origen: <strong>{{ $booking->ride?->source_channel ? ucfirst($booking->ride->source_channel) : 'Directo' }}</strong>.</li>
<li>La UI principal sigue limpia; el detalle de negocio aparece solo bajo demanda.</li>
</ul>
</div>
</dialog>

View File

@ -1,83 +1,79 @@
<x-filament-widgets::widget>
<x-filament::section>
<div class="rounded-3xl border border-primary-100 bg-gradient-to-br from-white via-primary-50/40 to-amber-50/60 p-6 shadow-sm">
<div class="rounded-3xl border border-white/80 bg-gradient-to-br from-white via-white to-primary-50/70 p-6 shadow-sm">
<div class="flex flex-col gap-6 xl:flex-row xl:items-end xl:justify-between">
<div class="space-y-3">
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-primary-600">Executive snapshot</p>
<div>
<h2 class="text-3xl font-semibold tracking-tight text-gray-950">Booking atribuible, sin ruido.</h2>
<p class="mt-2 max-w-2xl text-sm text-gray-600">Un KPI principal arriba. El funnel debajo. Lo demás, bajo demanda.</p>
<div class="flex items-center gap-3">
<div>
<h2 class="text-3xl font-semibold tracking-tight text-gray-950">Producto limpio. KPI claro.</h2>
<p class="mt-2 text-sm text-gray-600">Solo decisión arriba. Lectura profunda bajo demanda.</p>
</div>
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-gray-200 bg-white text-sm text-gray-500" title="La comisión atribuible es el KPI principal del dashboard."></span>
</div>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-stretch">
<div class="min-w-[220px] rounded-2xl border border-white/80 bg-white/90 p-5 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.22em] text-gray-500">KPI principal</p>
<p class="mt-2 text-4xl font-semibold tracking-tight text-gray-950">{{ number_format($commission, 0) }}</p>
<p class="mt-2 text-sm text-gray-600">Comisión atribuible</p>
<div class="min-w-[240px] rounded-3xl border border-gray-950/5 bg-gray-950 p-6 text-white shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.22em] text-white/60">KPI principal</p>
<p class="mt-3 text-4xl font-semibold tracking-tight">{{ number_format($commission, 0) }}</p>
<div class="mt-3 flex items-center gap-2 text-sm text-white/70">
<span>Comisión atribuible</span>
<span class="inline-flex h-6 w-6 items-center justify-center rounded-full border border-white/10 bg-white/5 text-[11px]" title="Suma de commission_amount en bookings generados por el funnel."></span>
</div>
</div>
<a href="{{ route('home') }}" class="inline-flex items-center justify-center rounded-2xl bg-primary-600 px-5 py-4 text-sm font-semibold text-white shadow-sm transition hover:bg-primary-500">
Ver front demo
</a>
</div>
</div>
<div class="mt-6 grid gap-3 md:grid-cols-4">
<article class="rounded-2xl border border-white/80 bg-white/85 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Taxi requests</p>
<p class="mt-2 text-2xl font-semibold tracking-tight text-gray-950">{{ number_format($rides) }}</p>
<div class="mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<article class="rounded-2xl border border-white/80 bg-white/90 p-4 shadow-sm">
<div class="flex items-center justify-between gap-3">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Taxi</p>
<span class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-gray-100 text-[11px] text-gray-500" title="Total de rides creados."></span>
</div>
<p class="mt-3 text-2xl font-semibold tracking-tight text-gray-950">{{ number_format($rides) }}</p>
</article>
<article class="rounded-2xl border border-white/80 bg-white/85 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">CTR view click</p>
<p class="mt-2 text-2xl font-semibold tracking-tight text-gray-950">{{ $clickThrough }}%</p>
<article class="rounded-2xl border border-white/80 bg-white/90 p-4 shadow-sm">
<div class="flex items-center justify-between gap-3">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Bookings</p>
<span class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-gray-100 text-[11px] text-gray-500" title="Reservas cerradas desde la demo."></span>
</div>
<p class="mt-3 text-2xl font-semibold tracking-tight text-gray-950">{{ number_format($bookingsCount) }}</p>
</article>
<article class="rounded-2xl border border-white/80 bg-white/85 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Bookings</p>
<p class="mt-2 text-2xl font-semibold tracking-tight text-gray-950">{{ number_format($bookingsCount) }}</p>
<article class="rounded-2xl border border-white/80 bg-white/90 p-4 shadow-sm">
<div class="flex items-center justify-between gap-3">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">CTR</p>
<span class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-gray-100 text-[11px] text-gray-500" title="Relación view → click sobre las recomendaciones visibles."></span>
</div>
<p class="mt-3 text-2xl font-semibold tracking-tight text-gray-950">{{ $clickThrough }}%</p>
</article>
<article class="rounded-2xl border border-white/80 bg-white/85 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">GMV demo</p>
<p class="mt-2 text-2xl font-semibold tracking-tight text-gray-950">{{ number_format($gmv, 0) }}</p>
<article class="rounded-2xl border border-white/80 bg-white/90 p-4 shadow-sm">
<div class="flex items-center justify-between gap-3">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">GMV</p>
<span class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-gray-100 text-[11px] text-gray-500" title="Volumen bruto de las reservas registradas."></span>
</div>
<p class="mt-3 text-2xl font-semibold tracking-tight text-gray-950">{{ number_format($gmv, 0) }}</p>
</article>
</div>
<div class="mt-6 grid gap-3 xl:grid-cols-4">
<article class="rounded-2xl border border-gray-200 bg-gray-950 p-4 text-white shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-white/60">1 · Taxi</p>
<p class="mt-2 text-2xl font-semibold tracking-tight">{{ number_format($rides) }}</p>
<p class="mt-2 text-sm text-white/70">entradas al funnel</p>
</article>
<article class="rounded-2xl border border-amber-200 bg-amber-50/80 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-amber-800">2 · Contexto</p>
<div class="mt-6 grid gap-3 md:grid-cols-3">
<article class="rounded-2xl border border-gray-200 bg-white/80 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Espera útil</p>
<p class="mt-2 text-2xl font-semibold tracking-tight text-gray-950">{{ number_format($views) }}</p>
<p class="mt-2 text-sm text-gray-600">views en espera útil</p>
</article>
<article class="rounded-2xl border border-teal-200 bg-teal-50/80 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-teal-800">3 · Oferta</p>
<article class="rounded-2xl border border-gray-200 bg-white/80 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Detalle oferta</p>
<p class="mt-2 text-2xl font-semibold tracking-tight text-gray-950">{{ number_format($clicks) }}</p>
<p class="mt-2 text-sm text-gray-600">clics a detalle</p>
</article>
<article class="rounded-2xl border border-emerald-200 bg-emerald-50/80 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-emerald-800">4 · Booking</p>
<article class="rounded-2xl border border-gray-200 bg-white/80 p-4 shadow-sm">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Ride booking</p>
<p class="mt-2 text-2xl font-semibold tracking-tight text-gray-950">{{ $rideToBooking }}%</p>
<p class="mt-2 text-sm text-gray-600">ride booking</p>
</article>
</div>
<details class="mt-6 rounded-2xl border border-gray-200 bg-white/80 p-4">
<summary class="cursor-pointer list-none text-sm font-semibold text-gray-900"> Ver contexto ejecutivo</summary>
<div class="mt-4 grid gap-3 md:grid-cols-3">
<div class="rounded-2xl bg-gray-50 p-4">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Zona con más señal</p>
<p class="mt-2 text-lg font-semibold text-gray-950">{{ $topZone }}</p>
</div>
<div class="rounded-2xl bg-gray-50 p-4 md:col-span-2">
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Última reserva</p>
<p class="mt-2 text-lg font-semibold text-gray-950">{{ $latestBooking?->offer?->title ?: 'Todavía no hay bookings' }}</p>
<p class="mt-2 text-sm text-gray-600">La explicación profunda queda oculta por defecto para mantener el dashboard limpio.</p>
</div>
</div>
</details>
</div>
</x-filament::section>
</x-filament-widgets::widget>

View File

@ -11,19 +11,21 @@
'activity' => 'Actividad',
'service' => 'Servicio',
];
$contextLine = \Illuminate\Support\Str::limit($offer->excerpt ?: 'Disponible para este trayecto ahora mismo.', 110);
@endphp
<section class="split">
<article class="card offer-card">
<div class="story-rail" aria-label="Progreso del funnel">
<span class="story-chip is-done">1 · Taxi</span>
<span class="story-chip is-done">2 · Contexto</span>
<span class="story-chip is-done">2 · Confirmado</span>
<span class="story-chip is-active">3 · Oferta</span>
<span class="story-chip">4 · Reserva</span>
</div>
<div class="phone-screen phone-screen--focused" style="padding:18px;">
<div class="phone-topbar">
<span>Propuesta contextual</span>
<span>Detalle oferta</span>
<span class="phone-dot-group" aria-hidden="true">
<span class="phone-dot"></span>
<span class="phone-dot"></span>
@ -34,53 +36,47 @@
<div class="offer-visual" aria-hidden="true"></div>
<div class="decision-primary">
<span class="screen-badge">Detalle oferta</span>
<span class="screen-badge">Oferta seleccionada</span>
<h1 style="font-size:clamp(2.15rem,4vw,3.6rem);max-width:13ch;margin-top:14px;">{{ $offer->title }}</h1>
<p class="screen-copy">{{ $contextLine }}</p>
<div class="offer-meta">
<span class="pill">{{ $categoryLabels[$offer->category] ?? ucfirst($offer->category) }}</span>
@if($offer->location_label)<span class="pill">{{ $offer->location_label }}</span>@endif
@if($offer->price_from)<span class="pill">Desde {{ number_format((float) $offer->price_from, 0) }}</span>@endif
@if($offer->price_from)<span class="pill">{{ number_format((float) $offer->price_from, 0) }}</span>@endif
@if($offer->duration_minutes)<span class="pill">{{ $offer->duration_minutes }} min</span>@endif
@if($offer->available_now)<span class="pill">Disponible hoy</span>@endif
<span class="pill">{{ $offer->available_now ? 'Disponible hoy' : 'Consultar disponibilidad' }}</span>
</div>
<div class="inline-actions">
<div class="inline-actions inline-actions--stack-mobile">
<a class="btn" href="#reserve">Reservar ahora</a>
@if($recommendation)
<button type="button" class="icon-button" data-modal-open="offer-signal" data-tooltip="Señal completa">Ver señal</button>
<button type="button" class="btn btn-secondary" data-modal-open="offer-signal"> Más info</button>
@endif
</div>
</div>
@if($offer->excerpt)
<p class="screen-copy">{{ $offer->excerpt }}</p>
@endif
@if($recommendation || $ride)
<details class="disclosure disclosure--soft">
<summary>
<span> ¿Por qué aparece ahora?</span>
<span>Abrir</span>
</summary>
<div class="disclosure-body">
<ul class="accordion-list">
@if($ride)
<li>Llega después de un trayecto hacia <strong>{{ $ride->destination_label }}</strong>.</li>
@endif
@if($offer->location_label)
<li>Está alineada con la zona <strong>{{ $offer->location_label }}</strong>.</li>
@endif
<li>{{ $recommendation?->reason ?: 'Se prioriza por proximidad, timing y facilidad de decisión.' }}</li>
</ul>
</div>
</details>
@endif
<details class="disclosure disclosure--soft">
<summary>
<span>Más detalles</span>
<span>Abrir</span>
</summary>
<div class="disclosure-body">
<ul class="accordion-list">
@if($offer->location_label)
<li>Zona: <strong>{{ $offer->location_label }}</strong>.</li>
@endif
<li>Disponibilidad: <strong>{{ $offer->available_now ? 'Activa ahora' : 'Bajo confirmación' }}</strong>.</li>
@if($offer->description)
<li>{{ \Illuminate\Support\Str::limit($offer->description, 180) }}</li>
@endif
</ul>
</div>
</details>
</div>
</article>
<aside class="card form-card" id="reserve">
<span class="eyebrow">Reserva</span>
<h2>Confirma y sigue.</h2>
<p class="compact-lead">Solo pedimos lo necesario para cerrar la decisión.</p>
<p class="compact-lead">Solo lo necesario para cerrar la decisión.</p>
@if ($errors->any())
<div class="errors">
@ -138,14 +134,14 @@
<details class="disclosure" style="margin-top:8px;">
<summary>
<span> Ver detalle de atribución</span>
<span> Más info</span>
<span>Abrir</span>
</summary>
<div class="disclosure-body">
<ul class="accordion-list">
<li>La reserva se vincula al trayecto si existe <code>ride_id</code>.</li>
<li>Si vienes desde una recomendación, también queda guardada su posición.</li>
<li>Eso permite enseñar conversión y comisión en admin sin ruido en la UI principal.</li>
<li>La reserva se vincula al trayecto solo si llegas desde taxi confirmado.</li>
<li>La atribución queda guardada sin ensuciar esta pantalla.</li>
<li>La lectura de negocio vive en admin, no en el flujo de decisión.</li>
</ul>
</div>
</details>
@ -157,8 +153,8 @@
<div class="app-modal-card">
<div class="app-modal-head">
<div>
<p class="eyebrow">Señal completa</p>
<h2 id="offer-signal-title">Lectura contextual</h2>
<p class="eyebrow">Más info</p>
<h2 id="offer-signal-title">Por qué aparece esta oferta</h2>
</div>
<button type="button" class="app-modal-close" data-modal-close aria-label="Cerrar modal">Cerrar</button>
</div>
@ -168,12 +164,12 @@
<span>posición</span>
</div>
<div class="metric-cell">
<strong>{{ $ride?->eta_minutes ?? '—' }}</strong>
<span>ETA del taxi</span>
<strong>{{ $offer->price_from ? '€'.number_format((float) $offer->price_from, 0) : '—' }}</strong>
<span>precio visible</span>
</div>
<div class="metric-cell">
<strong>{{ $ride?->context_zone ?: ($offer->location_label ?: 'General') }}</strong>
<span>zona activa</span>
<strong>{{ $offer->duration_minutes ? $offer->duration_minutes.' min' : 'Flexible' }}</strong>
<span>duración</span>
</div>
</div>
<ul class="accordion-list">
@ -181,7 +177,9 @@
@if($ride)
<li>Trayecto vinculado: <strong>{{ $ride->pickup_label }}</strong> <strong>{{ $ride->destination_label }}</strong>.</li>
@endif
<li>Oferta con ticket visible y reserva rápida.</li>
@if($offer->location_label)
<li>Zona relevante: <strong>{{ $offer->location_label }}</strong>.</li>
@endif
</ul>
</div>
</dialog>

View File

@ -5,22 +5,22 @@
@section('content')
@php
$primaryRecommendation = $recommendations->first();
$secondaryRecommendations = $recommendations->skip(1);
$recommendationOptions = $recommendations->take(3);
$primaryRecommendation = $recommendationOptions->first();
$secondaryRecommendations = $recommendationOptions->skip(1);
@endphp
<section class="hero">
<article class="card hero-copy hero-copy--compact">
<div>
<span class="eyebrow">Taxi confirmado</span>
<h1>Tu taxi llega en {{ $ride->eta_minutes ?? 6 }} min.</h1>
<p class="compact-lead">Decide una sola cosa más, si te interesa. El resto queda fuera.</p>
<p class="compact-lead">Elige si quieres una parada útil antes de llegar.</p>
</div>
<div class="meta-row">
<span class="meta-pill">{{ $ride->pickup_label }}</span>
<span class="meta-pill">{{ $ride->destination_label }}</span>
<span class="meta-pill">{{ ucfirst($ride->source_channel) }}</span>
<span class="meta-pill">{{ $ride->context_zone ?: 'Zona activa' }}</span>
<div class="route-card">
<small>Trayecto activo</small>
<strong>{{ $ride->pickup_label }} {{ $ride->destination_label }}</strong>
<span>{{ ucfirst($ride->status) }} · {{ $ride->eta_minutes ?? 6 }} min</span>
</div>
<div class="story-rail" aria-label="Progreso del funnel">
@ -33,36 +33,33 @@
@if ($primaryRecommendation)
<div class="decision-card">
<div class="decision-primary decision-primary--accent">
<span class="screen-badge">Siguiente mejor opción</span>
<span class="screen-badge">Recomendación principal</span>
<h2 class="decision-title">{{ $primaryRecommendation->offer->title }}</h2>
<p class="screen-copy">{{ $primaryRecommendation->offer->location_label ?: $ride->destination_label }} · Disponible ahora</p>
<div class="offer-meta">
@if($primaryRecommendation->offer->location_label)<span class="pill">{{ $primaryRecommendation->offer->location_label }}</span>@endif
@if($primaryRecommendation->offer->price_from)<span class="pill">{{ number_format((float) $primaryRecommendation->offer->price_from, 0) }}</span>@endif
@if($primaryRecommendation->offer->duration_minutes)<span class="pill">{{ $primaryRecommendation->offer->duration_minutes }} min</span>@endif
<span class="pill">Disponible ahora</span>
<span class="pill">Top {{ $primaryRecommendation->position }}</span>
</div>
<div class="inline-actions">
<div class="inline-actions inline-actions--stack-mobile">
<a class="btn" href="{{ route('offers.show', $primaryRecommendation->offer->slug) }}?ride={{ $ride->id }}&recommendation={{ $primaryRecommendation->id }}">Ver y reservar</a>
<button type="button" class="icon-button" data-modal-open="signal-primary" data-tooltip="Señal completa">Ver señal</button>
<button type="button" class="btn btn-secondary" data-modal-open="signal-primary"> Más info</button>
<button type="button" class="icon-button" data-modal-open="impact-primary" data-tooltip="Ver impacto">Impacto</button>
</div>
</div>
<details class="disclosure">
<summary>
<span> ¿Por qué esta opción?</span>
<span>¿Por qué esta opción?</span>
<span>Abrir</span>
</summary>
<div class="disclosure-body">
<ul class="accordion-list">
<li>Encaja con tu destino: <strong>{{ $ride->destination_label }}</strong>.</li>
@if($primaryRecommendation->offer->location_label)
<li>Está cerca de <strong>{{ $primaryRecommendation->offer->location_label }}</strong> y se puede decidir rápido.</li>
@endif
@if($primaryRecommendation->reason)
<li>{{ $primaryRecommendation->reason }}</li>
@else
<li>La priorizamos por cercanía, disponibilidad y facilidad de cierre.</li>
<li>Queda cerca de <strong>{{ $primaryRecommendation->offer->location_label }}</strong>.</li>
@endif
<li>{{ $primaryRecommendation->reason ?: 'La priorizamos por cercanía, disponibilidad y cierre rápido.' }}</li>
</ul>
</div>
</details>
@ -72,15 +69,15 @@
<div class="app-modal-card">
<div class="app-modal-head">
<div>
<p class="eyebrow">Señal completa</p>
<h2 id="signal-primary-title">Por qué {{ $primaryRecommendation->offer->title }} sube arriba</h2>
<p class="eyebrow">Más info</p>
<h2 id="signal-primary-title">{{ $primaryRecommendation->offer->title }}</h2>
</div>
<button type="button" class="app-modal-close" data-modal-close aria-label="Cerrar modal">Cerrar</button>
</div>
<div class="metric-strip metric-strip--2">
<div class="metric-cell">
<strong>Top {{ $primaryRecommendation->position }}</strong>
<span>posición actual</span>
<span>prioridad</span>
</div>
<div class="metric-cell">
<strong>{{ $ride->eta_minutes ?? 6 }} min</strong>
@ -88,13 +85,44 @@
</div>
</div>
<ul class="accordion-list">
<li>Trayecto activo: <strong>{{ $ride->pickup_label }}</strong> <strong>{{ $ride->destination_label }}</strong>.</li>
<li>Zona relevante: <strong>{{ $ride->context_zone ?: 'General' }}</strong>.</li>
<li>Canal de origen: <strong>{{ ucfirst($ride->source_channel) }}</strong>.</li>
<li>Trayecto: <strong>{{ $ride->pickup_label }}</strong> <strong>{{ $ride->destination_label }}</strong>.</li>
<li>Zona: <strong>{{ $ride->context_zone ?: 'General' }}</strong>.</li>
<li>Canal: <strong>{{ ucfirst($ride->source_channel) }}</strong>.</li>
<li>{{ $primaryRecommendation->reason ?: 'La propuesta destaca por contexto, proximidad y facilidad de reserva.' }}</li>
</ul>
</div>
</dialog>
<dialog class="app-modal" id="impact-primary" aria-labelledby="impact-primary-title">
<div class="app-modal-card">
<div class="app-modal-head">
<div>
<p class="eyebrow">Impacto</p>
<h2 id="impact-primary-title">Qué mueve esta propuesta</h2>
</div>
<button type="button" class="app-modal-close" data-modal-close aria-label="Cerrar modal">Cerrar</button>
</div>
<div class="metric-strip metric-strip--3">
<div class="metric-cell">
<strong>{{ $primaryRecommendation->offer->price_from ? '€'.number_format((float) $primaryRecommendation->offer->price_from, 0) : '—' }}</strong>
<span>ticket visible</span>
</div>
<div class="metric-cell">
<strong>{{ $primaryRecommendation->offer->duration_minutes ? $primaryRecommendation->offer->duration_minutes.' min' : 'Flexible' }}</strong>
<span>duración</span>
</div>
<div class="metric-cell">
<strong>{{ ucfirst($ride->source_channel) }}</strong>
<span>origen del trayecto</span>
</div>
</div>
<ul class="accordion-list">
<li>Se muestra cuando el trayecto ya está resuelto y la decisión es simple.</li>
<li>La atribución y la lectura de negocio quedan fuera de la pantalla principal.</li>
<li>Si el usuario reserva, el dashboard recoge el impacto sin añadir ruido aquí.</li>
</ul>
</div>
</dialog>
@else
<div class="notice">Todavía no hay una propuesta activa para este trayecto.</div>
@endif
@ -114,7 +142,7 @@
<div class="route-card">
<small>Trayecto activo</small>
<strong>{{ $ride->pickup_label }} {{ $ride->destination_label }}</strong>
<span>ETA {{ $ride->eta_minutes ?? 6 }} min · {{ ucfirst($ride->source_channel) }} · {{ $ride->context_zone ?: 'Zona activa' }}</span>
<span>ETA {{ $ride->eta_minutes ?? 6 }} min · {{ ucfirst($ride->status) }}</span>
</div>
@if ($primaryRecommendation)
@ -123,11 +151,11 @@
<strong class="phone-action-title">{{ $primaryRecommendation->offer->title }}</strong>
<span>Top {{ $primaryRecommendation->position }}</span>
</div>
<small>Disponible ahora</small>
<small>{{ $primaryRecommendation->offer->location_label ?: 'Disponible ahora' }}</small>
<div class="phone-action-meta">
@if($primaryRecommendation->offer->location_label)<span>{{ $primaryRecommendation->offer->location_label }}</span>@endif
@if($primaryRecommendation->offer->price_from)<span>{{ number_format((float) $primaryRecommendation->offer->price_from, 0) }}</span>@endif
@if($primaryRecommendation->offer->duration_minutes)<span>{{ $primaryRecommendation->offer->duration_minutes }} min</span>@endif
<span>Disponible</span>
</div>
<a class="btn" href="{{ route('offers.show', $primaryRecommendation->offer->slug) }}?ride={{ $ride->id }}&recommendation={{ $primaryRecommendation->id }}">Ver y reservar</a>
</article>
@ -142,9 +170,9 @@
<span>Top {{ $recommendation->position }}</span>
</div>
<div class="phone-action-meta">
@if($recommendation->offer->location_label)<span>{{ $recommendation->offer->location_label }}</span>@endif
@if($recommendation->offer->price_from)<span>{{ number_format((float) $recommendation->offer->price_from, 0) }}</span>@endif
@if($recommendation->offer->duration_minutes)<span>{{ $recommendation->offer->duration_minutes }} min</span>@endif
<span>{{ $recommendation->offer->location_label ?: 'Disponible ahora' }}</span>
</div>
</a>
@endforeach
@ -158,8 +186,8 @@
<section class="section">
<div class="section-head section-head--compact">
<div>
<span class="eyebrow">s opciones</span>
<h2>Solo si quieres comparar</h2>
<span class="eyebrow">Otras opciones</span>
<h2>Solo 2 más para comparar</h2>
</div>
</div>
@ -169,17 +197,18 @@
<div>
<div class="offer-meta">
<span class="pill">Top {{ $recommendation->position }}</span>
@if($recommendation->offer->location_label)<span class="pill">{{ $recommendation->offer->location_label }}</span>@endif
@if($recommendation->offer->price_from)<span class="pill">{{ number_format((float) $recommendation->offer->price_from, 0) }}</span>@endif
@if($recommendation->offer->duration_minutes)<span class="pill">{{ $recommendation->offer->duration_minutes }} min</span>@endif
</div>
<h3>{{ $recommendation->offer->title }}</h3>
<p class="screen-copy">{{ $recommendation->offer->location_label ?: 'Disponible ahora' }}</p>
</div>
<div class="inline-actions inline-actions--stack-mobile">
<a class="btn btn-secondary" href="{{ route('offers.show', $recommendation->offer->slug) }}?ride={{ $ride->id }}&recommendation={{ $recommendation->id }}">Ver detalle</a>
<a class="btn btn-secondary" href="{{ route('offers.show', $recommendation->offer->slug) }}?ride={{ $ride->id }}&recommendation={{ $recommendation->id }}">Ver y reservar</a>
</div>
<details class="disclosure disclosure--soft">
<summary>
<span> Ver motivo</span>
<span> Más info</span>
<span>Abrir</span>
</summary>
<div class="disclosure-body">