✨ feat(reservation/flow): Améliore le flux de réservation et ajoute des options.
Cette commit améliore le flux de réservation, ajoute une estimation des
frais de livraison et gère les options de produit et les paiements.
```
344 lines
28 KiB
Twig
344 lines
28 KiB
Twig
{% extends 'revervation/base.twig' %}
|
|
|
|
{% block title %}Finalisation de votre demande{% endblock %}
|
|
|
|
{% block body %}
|
|
<div class="max-w-7xl mx-auto px-4 py-12">
|
|
<div class="bg-white rounded-3xl shadow-xl p-8 border border-gray-100">
|
|
<h1 class="text-3xl font-black text-slate-900 uppercase italic mb-8 text-center">Récapitulatif de votre demande</h1>
|
|
|
|
{% if session.customer is null %}
|
|
{# --- LOGIN FORM --- #}
|
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
|
<div class="text-center mb-6">
|
|
<h2 class="text-xl font-bold text-gray-800">Connectez-vous pour continuer</h2>
|
|
<p class="mt-2 text-sm text-gray-600">
|
|
Ou <a href="{{ path('reservation_register') }}" class="font-medium text-blue-600 hover:text-blue-500">créez un compte</a>.
|
|
</p>
|
|
</div>
|
|
|
|
{% if error is defined and error %}
|
|
<div class="mb-4 p-4 rounded-2xl bg-red-50 border border-red-100 text-red-700 text-sm">
|
|
{{ error.messageKey|trans(error.messageData, 'security') }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form action="{{ path('reservation_flow', {sessionId: session.uuid}) }}" method="post" class="space-y-6">
|
|
<div>
|
|
<label for="username" class="block text-sm font-semibold text-gray-700">Adresse Email</label>
|
|
<div class="mt-1">
|
|
<input type="email" id="username" name="_username" value="{{ last_username|default('') }}" required autofocus
|
|
class="appearance-none block w-full px-4 py-3 border border-gray-200 rounded-2xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="password" class="block text-sm font-semibold text-gray-700">Mot de passe</label>
|
|
<div class="mt-1">
|
|
<input type="password" id="password" name="_password" required
|
|
class="appearance-none block w-full px-4 py-3 border border-gray-200 rounded-2xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all">
|
|
</div>
|
|
</div>
|
|
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate_flow') }}">
|
|
|
|
<div>
|
|
<button type="submit"
|
|
class="w-full flex justify-center py-4 px-4 border border-transparent rounded-2xl shadow-lg text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all">
|
|
Se connecter
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
{# --- LOGGED IN STATE --- #}
|
|
<div class="max-w-3xl mx-auto space-y-8">
|
|
{# --- CART DETAILS --- #}
|
|
{% if cart is defined %}
|
|
<div class="bg-slate-50 rounded-2xl p-6 border border-slate-100">
|
|
<div class="flex items-center gap-4 mb-6">
|
|
<div class="bg-indigo-100 p-3 rounded-full">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-bold text-slate-900">Détails de la réservation</h3>
|
|
{% if cart.startDate and cart.endDate %}
|
|
<p class="text-sm text-slate-600">Du <span class="font-medium text-slate-900">{{ cart.startDate|date('d/m/Y') }}</span> au <span class="font-medium text-slate-900">{{ cart.endDate|date('d/m/Y') }}</span> ({{ cart.duration }} jours)</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
{% for item in cart.items %}
|
|
<div class="flex items-center bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
|
{% if item.image %}
|
|
<img src="{{ item.image }}" alt="{{ item.product.name }}" class="h-16 w-16 object-cover rounded-lg mr-4 bg-slate-100">
|
|
{% else %}
|
|
<div class="h-16 w-16 bg-slate-100 rounded-lg mr-4 flex items-center justify-center text-slate-400">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
{% endif %}
|
|
<div class="flex-1">
|
|
<h4 class="font-bold text-slate-800">{{ item.product.name }}</h4>
|
|
|
|
<div class="text-xs text-slate-600 bg-slate-50 p-2 rounded-lg border border-slate-100 inline-block mt-2">
|
|
<div class="flex flex-wrap gap-x-3 gap-y-1">
|
|
<span>1er jour : <strong class="text-slate-800">{{ item.price1Day|number_format(2, ',', ' ') }} €</strong></span>
|
|
{% if cart.duration > 1 %}
|
|
<span class="text-slate-300">|</span>
|
|
<span>Jours supp. : <strong class="text-slate-800">{{ item.priceSup|number_format(2, ',', ' ') }} €</strong> <span class="text-slate-400">x {{ cart.duration - 1 }}</span></span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if item.options is defined and item.options|length > 0 %}
|
|
<div class="mt-2 text-xs">
|
|
<p class="font-semibold text-slate-500 uppercase tracking-wider mb-1">Options incluses :</p>
|
|
<ul class="list-disc list-inside text-slate-600 space-y-0.5">
|
|
{% for option in item.options %}
|
|
<li>
|
|
{{ option.name }}
|
|
<span class="font-medium text-slate-800">({{ option.price|number_format(2, ',', ' ') }} €)</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="font-bold text-slate-900">{{ item.totalPriceHT|number_format(2, ',', ' ') }} € HT</p>
|
|
{% if cart.tvaEnabled %}
|
|
<p class="text-xs text-slate-500">{{ item.totalPriceTTC|number_format(2, ',', ' ') }} € TTC</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-center text-slate-500 py-4">Aucun produit sélectionné.</p>
|
|
{% endfor %}
|
|
|
|
{% if cart.options is defined and cart.options|length > 0 %}
|
|
<div class="mt-4 border-t border-slate-100 pt-4">
|
|
<h4 class="text-sm font-bold text-slate-900 mb-3">Options supplémentaires</h4>
|
|
{% for option in cart.options %}
|
|
<div class="flex items-center justify-between bg-white p-3 rounded-xl border border-slate-200 shadow-sm mb-2">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-indigo-50 p-2 rounded-lg text-indigo-600">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
</div>
|
|
<span class="font-medium text-slate-800 text-sm">{{ option.name }}</span>
|
|
</div>
|
|
<span class="font-bold text-slate-900 text-sm">{{ option.price|number_format(2, ',', ' ') }} € HT</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="mt-6 border-t border-slate-200 pt-4 space-y-2">
|
|
<div class="flex justify-between text-sm text-slate-600">
|
|
<span>Total HT</span>
|
|
<span class="font-medium">{{ cart.totalHT|number_format(2, ',', ' ') }} €</span>
|
|
</div>
|
|
{% if cart.tvaEnabled %}
|
|
<div class="flex justify-between text-sm text-slate-600">
|
|
<span>TVA (20%)</span>
|
|
<span class="font-medium">{{ cart.totalTva|number_format(2, ',', ' ') }} €</span>
|
|
</div>
|
|
<div class="flex justify-between text-lg font-black text-slate-900 pt-2 border-t border-slate-200 mt-2">
|
|
<span>Total TTC</span>
|
|
<span>{{ cart.totalTTC|number_format(2, ',', ' ') }} €</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="bg-slate-50 rounded-2xl p-6 border border-slate-100 flex flex-col md:flex-row items-center md:items-start gap-6">
|
|
<div class="bg-blue-100 p-4 rounded-full flex-shrink-0">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1 w-full text-center md:text-left">
|
|
<h3 class="text-lg font-bold text-slate-900 mb-4">Informations Client</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm">
|
|
<span class="block text-xs text-slate-500 uppercase font-semibold tracking-wider">Nom complet</span>
|
|
<span class="block text-slate-800 font-medium mt-1">{{ session.customer.name }} {{ session.customer.surname }}</span>
|
|
</div>
|
|
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm">
|
|
<span class="block text-xs text-slate-500 uppercase font-semibold tracking-wider">Téléphone</span>
|
|
<span class="block text-slate-800 font-medium mt-1">{{ session.customer.phone }}</span>
|
|
</div>
|
|
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm md:col-span-2">
|
|
<span class="block text-xs text-slate-500 uppercase font-semibold tracking-wider">Email</span>
|
|
<span class="block text-slate-800 font-medium mt-1">{{ session.customer.email }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# --- EVENT & BILLING FORM --- #}
|
|
<div class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
|
|
<h3 class="text-xl font-bold text-slate-900 mb-6 flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
</svg>
|
|
Informations de l'événement
|
|
</h3>
|
|
|
|
<form action="{{ path('reservation_flow_update', {sessionId: session.uuid}) }}" method="post" class="space-y-10">
|
|
|
|
{# Billing Address #}
|
|
<div>
|
|
<div class="flex items-center gap-3 mb-6 pb-2 border-b border-slate-100">
|
|
<span class="bg-blue-100 text-blue-600 p-2 rounded-lg">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /></svg>
|
|
</span>
|
|
<h4 class="font-bold text-lg text-slate-900">Adresse de facturation</h4>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="md:col-span-2">
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Adresse complète</label>
|
|
<input type="text" name="billingAddress" value="{{ session.billingAddress }}" required
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
|
|
placeholder="123 Rue de la Paix">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Code Postal</label>
|
|
<input type="text" name="billingZipCode" value="{{ session.billingZipCode }}" required
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
|
|
placeholder="75000">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Ville</label>
|
|
<select name="billingTown" id="billingTownSelect" required
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all">
|
|
{% if session.billingTown %}
|
|
<option value="{{ session.billingTown }}" selected>{{ session.billingTown }}</option>
|
|
{% else %}
|
|
<option value="">Sélectionnez une ville</option>
|
|
{% endif %}
|
|
</select>
|
|
</div> </div>
|
|
</div>
|
|
|
|
{# Event Address #}
|
|
<div>
|
|
<div class="flex items-center gap-3 mb-6 pb-2 border-b border-slate-100">
|
|
<span class="bg-blue-100 text-blue-600 p-2 rounded-lg">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
|
</span>
|
|
<h4 class="font-bold text-lg text-slate-900">Lieu de l'événement</h4>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="md:col-span-2">
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Adresse de l'événement</label>
|
|
<input type="text" name="adressEvent" value="{{ session.adressEvent }}" required
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400">
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Complément d'adresse <span class="text-slate-300 font-normal normal-case">(Optionnel)</span></label>
|
|
<input type="text" name="adress2Event" value="{{ session.adress2Event }}"
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Code Postal</label>
|
|
<input type="text" name="zipCodeEvent" value="{{ session.zipCodeEvent }}" required
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Ville</label>
|
|
<select name="townEvent" id="townEventSelect" required
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all">
|
|
{% if session.townEvent %}
|
|
<option value="{{ session.townEvent }}" selected>{{ session.townEvent }}</option>
|
|
{% else %}
|
|
<option value="">Sélectionnez une ville</option>
|
|
{% endif %}
|
|
</select>
|
|
</div> </div>
|
|
</div>
|
|
|
|
{# Event Details #}
|
|
<div>
|
|
<div class="flex items-center gap-3 mb-6 pb-2 border-b border-slate-100">
|
|
<span class="bg-blue-100 text-blue-600 p-2 rounded-lg">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
|
</span>
|
|
<h4 class="font-bold text-lg text-slate-900">Détails techniques</h4>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="md:col-span-2">
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Type d'événement</label>
|
|
<select name="type" required
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all">
|
|
<option value="">Sélectionnez le type...</option>
|
|
{% for type in ['Anniversaire enfant', 'Kermesse scolaire', 'Fête communale', 'Événement d\'entreprise', 'Mariage', 'Autre'] %}
|
|
<option value="{{ type }}" {% if session.type == type %}selected{% endif %}>{{ type }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Détails supplémentaires</label>
|
|
<textarea name="details" rows="3"
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
|
|
placeholder="Précisions utiles pour l'installation...">{{ session.details }}</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Type de sol</label>
|
|
<input type="text" name="typeSol" value="{{ session.typeSol }}"
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
|
|
placeholder="Ex: Herbe, Bitume...">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Pente</label>
|
|
<input type="text" name="pente" value="{{ session.pente }}"
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
|
|
placeholder="Ex: Terrain plat">
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Accès (largeur portail, escaliers...)</label>
|
|
<textarea name="access" rows="2"
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
|
|
placeholder="Informations d'accès pour le véhicule...">{{ session.access }}</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Distance prise électrique (m)</label>
|
|
<input type="number" step="0.1" name="distancePower" value="{{ session.distancePower }}"
|
|
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
|
|
placeholder="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-6 border-t border-slate-100">
|
|
<button type="submit" class="w-full md:w-auto px-8 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-bold rounded-2xl shadow-lg shadow-blue-200 hover:shadow-xl hover:scale-[1.02] transition-all flex items-center justify-center gap-2 text-lg mx-auto md:mx-0">
|
|
Valider ma demande
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block javascripts %}
|
|
{{ vite_asset('flow_reservation.js',{}) }}
|
|
{% endblock %}
|