266 lines
15 KiB
PHP
266 lines
15 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
require_once __DIR__ . '/app.php';
|
||
|
||
ensure_schema();
|
||
|
||
$errors = [];
|
||
$input = [
|
||
'passenger_name' => '',
|
||
'pickup_point' => '',
|
||
'destination_area' => 'marina',
|
||
'pickup_time' => (new DateTimeImmutable('+20 minutes'))->format('Y-m-d\TH:i'),
|
||
'source_channel' => 'hotel',
|
||
'party_size' => '2',
|
||
'notes' => '',
|
||
];
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$input['passenger_name'] = trim((string) ($_POST['passenger_name'] ?? ''));
|
||
$input['pickup_point'] = trim((string) ($_POST['pickup_point'] ?? ''));
|
||
$input['destination_area'] = trim((string) ($_POST['destination_area'] ?? ''));
|
||
$input['pickup_time'] = trim((string) ($_POST['pickup_time'] ?? ''));
|
||
$input['source_channel'] = trim((string) ($_POST['source_channel'] ?? ''));
|
||
$input['party_size'] = trim((string) ($_POST['party_size'] ?? '1'));
|
||
$input['notes'] = trim((string) ($_POST['notes'] ?? ''));
|
||
|
||
if ($input['passenger_name'] === '') {
|
||
$errors['passenger_name'] = 'Add a passenger or booking reference.';
|
||
}
|
||
if ($input['pickup_point'] === '') {
|
||
$errors['pickup_point'] = 'Pickup point is required.';
|
||
}
|
||
if (!array_key_exists($input['destination_area'], destination_options())) {
|
||
$errors['destination_area'] = 'Choose a valid destination area.';
|
||
}
|
||
|
||
$dateTime = DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $input['pickup_time']);
|
||
if (!$dateTime) {
|
||
$errors['pickup_time'] = 'Choose a valid pickup time.';
|
||
} else {
|
||
$input['pickup_time'] = $dateTime->format('Y-m-d H:i:s');
|
||
}
|
||
|
||
if (!array_key_exists($input['source_channel'], source_channel_options())) {
|
||
$errors['source_channel'] = 'Choose a valid source channel.';
|
||
}
|
||
|
||
$partySize = filter_var($input['party_size'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 8]]);
|
||
if ($partySize === false) {
|
||
$errors['party_size'] = 'Party size must be between 1 and 8.';
|
||
}
|
||
$input['party_size'] = (string) ($partySize ?: 1);
|
||
|
||
if ($errors === []) {
|
||
$requestId = create_taxi_request([
|
||
'passenger_name' => $input['passenger_name'],
|
||
'pickup_point' => $input['pickup_point'],
|
||
'destination_area' => $input['destination_area'],
|
||
'pickup_time' => $input['pickup_time'],
|
||
'source_channel' => $input['source_channel'],
|
||
'party_size' => (int) $input['party_size'],
|
||
'notes' => $input['notes'],
|
||
]);
|
||
|
||
header('Location: /request_success.php?id=' . $requestId . '&created=1');
|
||
exit;
|
||
}
|
||
}
|
||
|
||
$stats = fetch_dashboard_stats();
|
||
$recentRequests = fetch_requests(6);
|
||
$heroDescription = 'Confirm a taxi request and immediately surface the top 2–3 contextual offers with simple booking attribution.';
|
||
|
||
render_head(app_name() . ' | Taxi request workflow', $heroDescription);
|
||
render_header('home');
|
||
?>
|
||
<section class="section-block section-hero">
|
||
<div class="container">
|
||
<div class="row g-4 align-items-start">
|
||
<div class="col-lg-7">
|
||
<div class="hero-panel">
|
||
<div class="eyebrow">Initial MVP slice · tourist flow</div>
|
||
<h1 class="display-title">Taxi confirmation with immediate upsell recommendations.</h1>
|
||
<p class="lead-copy"><?= h($heroDescription) ?></p>
|
||
<div class="d-flex flex-wrap gap-2 mt-4">
|
||
<span class="chip"><i class="bi bi-check2-circle"></i> Taxi request</span>
|
||
<span class="chip"><i class="bi bi-stars"></i> Top-3 recommendations</span>
|
||
<span class="chip"><i class="bi bi-journal-check"></i> Booking attribution</span>
|
||
</div>
|
||
<div class="metric-grid mt-4">
|
||
<article class="metric-card">
|
||
<span class="metric-label">Requests</span>
|
||
<strong><?= (int) $stats['total_requests'] ?></strong>
|
||
<small>captured end-to-end</small>
|
||
</article>
|
||
<article class="metric-card">
|
||
<span class="metric-label">CTR</span>
|
||
<strong><?= h(number_format((float) $stats['ctr'], 1)) ?>%</strong>
|
||
<small>offer click-through</small>
|
||
</article>
|
||
<article class="metric-card">
|
||
<span class="metric-label">Bookings</span>
|
||
<strong><?= (int) $stats['bookings'] ?></strong>
|
||
<small>from taxi flow</small>
|
||
</article>
|
||
<article class="metric-card">
|
||
<span class="metric-label">Avg. delay</span>
|
||
<strong><?= $stats['avg_delay'] !== null ? (int) $stats['avg_delay'] . 'm' : '—' ?></strong>
|
||
<small>request to booking</small>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-5">
|
||
<div class="card-panel shadow-sm">
|
||
<div class="card-head">
|
||
<div>
|
||
<h2 class="h5 mb-1">Create taxi request</h2>
|
||
<p class="text-secondary mb-0">Save the ride context and instantly generate contextual offers.</p>
|
||
</div>
|
||
<span class="status-pill">Live MVP</span>
|
||
</div>
|
||
<?php if ($errors !== []): ?>
|
||
<div class="alert alert-danger mt-3 mb-0" role="alert">Please review the highlighted fields and try again.</div>
|
||
<?php endif; ?>
|
||
<form method="post" class="row g-3 mt-1" novalidate>
|
||
<div class="col-12">
|
||
<label for="passenger_name" class="form-label">Passenger / booking reference</label>
|
||
<input type="text" class="form-control <?= isset($errors['passenger_name']) ? 'is-invalid' : '' ?>" id="passenger_name" name="passenger_name" value="<?= h($input['passenger_name']) ?>" placeholder="e.g. Room 204 · Sofia M.">
|
||
<?php if (isset($errors['passenger_name'])): ?><div class="invalid-feedback"><?= h($errors['passenger_name']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-12">
|
||
<label for="pickup_point" class="form-label">Pickup point</label>
|
||
<input type="text" class="form-control <?= isset($errors['pickup_point']) ? 'is-invalid' : '' ?>" id="pickup_point" name="pickup_point" value="<?= h($input['pickup_point']) ?>" placeholder="Hotel lobby, gate, cruise terminal...">
|
||
<?php if (isset($errors['pickup_point'])): ?><div class="invalid-feedback"><?= h($errors['pickup_point']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label for="destination_area" class="form-label">Destination area</label>
|
||
<select class="form-select <?= isset($errors['destination_area']) ? 'is-invalid' : '' ?>" id="destination_area" name="destination_area">
|
||
<?php foreach (destination_options() as $value => $label): ?>
|
||
<option value="<?= h($value) ?>" <?= $input['destination_area'] === $value ? 'selected' : '' ?>><?= h($label) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php if (isset($errors['destination_area'])): ?><div class="invalid-feedback"><?= h($errors['destination_area']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label for="pickup_time" class="form-label">Pickup time</label>
|
||
<input type="datetime-local" class="form-control <?= isset($errors['pickup_time']) ? 'is-invalid' : '' ?>" id="pickup_time" name="pickup_time" value="<?= h(str_replace(' ', 'T', substr($input['pickup_time'], 0, 16))) ?>">
|
||
<?php if (isset($errors['pickup_time'])): ?><div class="invalid-feedback"><?= h($errors['pickup_time']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label for="source_channel" class="form-label">Source channel</label>
|
||
<select class="form-select <?= isset($errors['source_channel']) ? 'is-invalid' : '' ?>" id="source_channel" name="source_channel">
|
||
<?php foreach (source_channel_options() as $value => $label): ?>
|
||
<option value="<?= h($value) ?>" <?= $input['source_channel'] === $value ? 'selected' : '' ?>><?= h($label) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php if (isset($errors['source_channel'])): ?><div class="invalid-feedback"><?= h($errors['source_channel']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label for="party_size" class="form-label">Party size</label>
|
||
<input type="number" min="1" max="8" class="form-control <?= isset($errors['party_size']) ? 'is-invalid' : '' ?>" id="party_size" name="party_size" value="<?= h($input['party_size']) ?>">
|
||
<?php if (isset($errors['party_size'])): ?><div class="invalid-feedback"><?= h($errors['party_size']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-12">
|
||
<label for="notes" class="form-label">Context notes <span class="text-secondary">(optional)</span></label>
|
||
<textarea class="form-control" id="notes" name="notes" rows="3" placeholder="Family, mobility needs, preferred cuisine, celebration, arrival context..."><?= h($input['notes']) ?></textarea>
|
||
</div>
|
||
<div class="col-12 d-grid">
|
||
<button type="submit" class="btn btn-dark btn-lg">Confirm taxi and generate offers</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-block border-top bg-white">
|
||
<div class="container">
|
||
<div class="section-heading">
|
||
<div>
|
||
<div class="eyebrow">Workflow</div>
|
||
<h2 class="section-title">The first thin slice follows the real tourist journey.</h2>
|
||
</div>
|
||
<a href="/requests.php" class="btn btn-outline-dark">Open operations board</a>
|
||
</div>
|
||
<div class="row g-3 mt-1">
|
||
<div class="col-md-4">
|
||
<article class="step-card h-100">
|
||
<span class="step-index">01</span>
|
||
<h3>Capture the taxi request</h3>
|
||
<p>Store pickup, destination, time, party size, and channel using a focused intake form.</p>
|
||
</article>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<article class="step-card h-100">
|
||
<span class="step-index">02</span>
|
||
<h3>Generate contextual offers</h3>
|
||
<p>Score nearby restaurants and experiences by geography, timing, quality, and strategic value.</p>
|
||
</article>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<article class="step-card h-100">
|
||
<span class="step-index">03</span>
|
||
<h3>Book and attribute</h3>
|
||
<p>Confirm the best offer in one click and keep a simple log for CTR and taxi-to-booking conversion.</p>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-block border-top">
|
||
<div class="container">
|
||
<div class="section-heading">
|
||
<div>
|
||
<div class="eyebrow">Recent activity</div>
|
||
<h2 class="section-title">Live requests and current booking outcomes.</h2>
|
||
</div>
|
||
</div>
|
||
<div class="card-panel p-0 overflow-hidden">
|
||
<?php if ($recentRequests === []): ?>
|
||
<div class="empty-state text-center">
|
||
<div class="empty-icon"><i class="bi bi-car-front"></i></div>
|
||
<h3>No requests yet</h3>
|
||
<p>Submit the first taxi flow above to populate the operations board and recommendation funnel.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table table-slim align-middle mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Passenger</th>
|
||
<th>Destination</th>
|
||
<th>Channel</th>
|
||
<th>Status</th>
|
||
<th>Created</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($recentRequests as $request): ?>
|
||
<tr>
|
||
<td>
|
||
<div class="fw-semibold"><?= h($request['passenger_name']) ?></div>
|
||
<div class="text-secondary small"><?= h($request['pickup_point']) ?></div>
|
||
</td>
|
||
<td><?= h(destination_label((string) $request['destination_area'])) ?></td>
|
||
<td><?= h(source_channel_label((string) $request['source_channel'])) ?></td>
|
||
<td><span class="badge rounded-pill <?= h(status_badge_class((string) $request['status'])) ?>"><?= h(str_replace('_', ' ', (string) $request['status'])) ?></span></td>
|
||
<td><?= h(date('d M H:i', strtotime((string) $request['created_at']))) ?></td>
|
||
<td class="text-end"><a href="/request_detail.php?id=<?= (int) $request['id'] ?>" class="btn btn-sm btn-outline-dark">Detail</a></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<?php render_footer(); ?>
|