Files
ludikevent_crm/templates/revervation/base.twig
Serreau Jovann 900b55c07b ```
 feat(ReserverController): Gère les options de produits au panier et en session.

Ajoute la gestion des options de produits lors de l'ajout au panier et dans la session de réservation. Inclut des corrections pour les options orphelines.
```
2026-02-04 11:58:07 +01:00

235 lines
13 KiB
Twig

<!DOCTYPE html>
<html lang="{{ app.request.locale }}" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{# --- SEO Fondamental --- #}
<title>
{% block title %}{{ 'base.seo.title'|trans }}{% endblock %}
</title>
{% if block('description') is defined %}
<meta name="description" content="{{ block('description')|replace({"\n": '', "\r": '', ' ': ''})|trim }}">
{% endif %}
{% block canonical %}
<link rel="canonical" href="{{ app.request.schemeAndHttpHost }}{{ app.request.pathinfo }}">
{% endblock %}
<meta name="keywords" content="{{ 'base.seo.keywords'|trans }}">
{# --- Open Graph --- #}
<meta property="og:type" content="website">
<meta property="og:url" content="{{ app.request.uri }}">
<meta property="og:title" content="{{ block('title') }}">
{% if block('description') is defined %}
<meta property="og:description" content="{{ block('description') }}">
{% endif %}
<meta property="og:image" content="{{ absolute_url(asset('provider/images/favicon.png')) }}">
{# --- Twitter Card --- #}
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{ block('title') }}">
<meta name="twitter:image" content="{{ absolute_url(asset('provider/images/favicon.png')) }}">
{% block extra_header %}{% endblock %}
{# ... scripts PWA / Analytics ... #}
{{ vite_asset('reserve.js',{}) }}
{% block stylesheets %}{% endblock %}
<link rel="alternate" type="application/rss+xml" title="LudikEvent - Tout" href="{{ url('app_rss_feed', {channel: 'all'}) }}">
<link rel="alternate" type="application/rss+xml" title="LudikEvent - Produits" href="{{ url('app_rss_feed', {channel: 'products'}) }}">
<link rel="alternate" type="application/rss+xml" title="LudikEvent - Formules" href="{{ url('app_rss_feed', {channel: 'formules'}) }}">
</head>
<body class="bg-gray-50 text-gray-900 font-sans antialiased min-h-screen flex flex-col">
{% if is_granted('ROLE_USER') %}
<utm-account id="{{ app.user.id }}" email="{{ app.user.email }}" name="{{ app.user.name }} {{ app.user.surname }}"></utm-account>
{% endif %}
{# --- MACROS --- #}
{% macro nav_link(route_name, label_key, is_external = false) %}
<a href="{{ is_external ? route_name : path(route_name) }}"
{% if is_external %}target="_blank"{% endif %}
class="text-gray-700 hover:text-[#f39e36] font-medium transition-colors">
{{ label_key|trans }}
</a>
{% endmacro %}
{% macro mobile_nav_link(route_name, label_key, is_external = false) %}
<a href="{{ is_external ? route_name : path(route_name) }}"
{% if is_external %}target="_blank"{% endif %}
class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl">
{{ label_key|trans }}
</a>
{% endmacro %}
{% import _self as macros %}
{# --- NAVIGATION --- #}
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-gray-100" role="navigation" aria-label="{{ 'nav.aria_label'|trans }}">
<div class="max-w-8xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-20">
{# Logo #}
<div class="flex-shrink-0 flex items-center">
<a href="{{ path('reservation') }}" class="flex items-center gap-2 group">
<img src="{{ asset('provider/images/favicon.png') | imagine_filter('logo') }}"
width="48" height="48" class="w-12 h-12 relative z-10 animate-pulse"
alt="Ludikevent">
<span class="text-2xl font-black tracking-tighter uppercase text-[#f39e36]">Ludik Event</span>
</a>
</div>
{# Menu Desktop #}
<div class="hidden md:flex items-center space-x-8">
{{ macros.nav_link('reservation', 'nav.home') }}
{{ macros.nav_link('reservation_catalogue', 'nav.catalogue') }}
{{ macros.nav_link('reservation_formules', 'nav.packages') }}
{{ macros.nav_link('/images/Catalogue.pdf', 'nav.pdf', true) }}
{{ macros.nav_link('reservation_workflow', 'nav.how_to_book') }}
{{ macros.nav_link('reservation_estimate_delivery', 'Estimer la livraison') }}
{{ macros.nav_link('reservation_contact', 'nav.contact') }}
<a is="flow-reserve" class="relative p-2 text-gray-600 hover:text-[#f39e36] transition-colors" aria-label="Panier">
<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="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>
<span class="absolute -top-1 -right-1 bg-red-500 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full hidden" data-count>0</span>
</a>
<a href="{{ path('reservation_search') }}" class="p-2 text-gray-600 hover:text-[#f39e36] transition-colors" aria-label="{{ 'nav.search_aria'|trans }}">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</a>
{% if app.user %}
<div class="flex items-center gap-4">
{% if is_granted('ROLE_ADMIN') %}
<a target="_blank" href="https://intranet.ludikevent.fr/crm" class="text-xs font-black uppercase tracking-widest text-amber-800 bg-amber-50 px-3 py-1 rounded-full border border-amber-200 hover:bg-amber-100 transition-colors">
{{ 'nav.admin'|trans }}
</a>
{% endif %}
<a href="{{ path('gestion_contrat') }}" class="text-[#f39e36] flex items-center gap-2 font-bold hover:opacity-70 transition-opacity">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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>
{{ 'nav.my_account'|trans }}
</a>
<a href="{{ path('reservation_logout') }}" class="text-gray-500 hover:text-red-600 transition-colors" title="{{ 'nav.logout'|trans }}" aria-label="{{ 'nav.logout'|trans }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
</a>
</div>
{% else %}
<a href="{{ path('reservation_login') }}" class="text-[#f39e36] font-bold transition-colors flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
{{ 'nav.login'|trans }}
</a>
{% endif %}
</div>
{# Bouton Menu Mobile #}
<div class="md:hidden flex items-center">
<button id="menu-button" type="button" class="text-gray-700 p-2 focus:outline-none" aria-expanded="false" aria-controls="mobile-menu" aria-label="{{ 'nav.mobile_open'|trans }}">
<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 6h16M4 12h16m-7 6h7" /></svg>
</button>
</div>
</div>
</div>
{# Menu Mobile #}
<div id="mobile-menu" class="hidden md:hidden bg-white border-t border-gray-100 shadow-xl">
<div class="px-4 pt-2 pb-6 space-y-2">
{{ macros.mobile_nav_link('reservation', 'Accueil') }}
{{ macros.mobile_nav_link('reservation_catalogue', 'Nos structures') }}
{{ macros.mobile_nav_link('reservation_formules', 'Nos Formules') }}
{{ macros.mobile_nav_link('/provider/Catalogue.pdf', 'Catalogue', true) }}
{{ macros.mobile_nav_link('reservation_workflow', 'Comment reserver') }}
{{ macros.mobile_nav_link('reservation_estimate_delivery', 'Estimer la livraison') }}
{{ macros.mobile_nav_link('reservation_search', 'Rechercher') }}
<a is="flow-reserve" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl flex items-center justify-between">
<span>Panier</span>
<span class="bg-red-500 text-white text-xs font-bold px-2 py-0.5 rounded-full hidden" data-count>0</span>
</a>
<div class="pt-4 border-t border-gray-50">
<a href="tel:0614172447" class="block px-3 py-3 text-center bg-blue-600 text-white rounded-xl font-bold">
{{ 'Appeler le'|trans }} 06 14 17 24 47
</a>
</div>
</div>
</div>
</nav>
{# --- MESSAGES FLASH --- #}
{% for label, messages in app.flashes %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4" role="alert">
{% for message in messages %}
{% set border_class = label == 'error' ? 'border-red-200 bg-red-50 text-red-800' : (label == 'success' ? 'border-green-200 bg-green-50 text-green-800' : 'border-blue-200 bg-blue-50 text-blue-800') %}
<div class="flex items-center justify-between p-4 rounded-2xl shadow-lg border {{ border_class }}">
<div class="flex items-center gap-3">
<p class="text-sm font-bold uppercase italic">{{ message|trans }}</p>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
<main class="flex-grow" id="main-content" role="main">
{# --- DATE SELECTION BLOCK --- #}
{% block date_selection %}
<div class="bg-[#f39e36] text-white py-4 px-4 sticky top-20 z-40 border-b border-white/10 shadow-xl">
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="p-2 bg-white/50 rounded-lg">
<svg class="w-5 h-5 text-[#f39e36]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 00-2 2z"></path></svg>
</div>
<div>
<p class="text-[10px] uppercase tracking-widest text-white font-bold">Période de location</p>
<p class="text-sm font-bold italic" id="header-date-display">
<span class="opacity-70">Aucune date sélectionnée</span>
</p>
</div>
</div>
<flow-datepicker>
<button class="w-full sm:w-auto px-6 py-2 bg-[#fc0e50] hover:bg-white hover:text-slate-900 text-white rounded-xl font-bold uppercase text-xs tracking-widest transition-all">
Choisir mes dates
</button>
</flow-datepicker>
</div>
</div>
{% endblock %}
{% block body %}{% endblock %}
</main>
{# --- PIED DE PAGE --- #}
<footer class="bg-white border-t border-gray-100 py-10 mt-auto" role="contentinfo">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center gap-8">
<div class="text-center md:text-left">
<p class="text-sm text-gray-700">
&copy; {{ "now"|date("Y") }} <span class="font-bold text-[#f39e36]">Ludikevent</span>.
{{ 'footer.rights'|trans }}
</p>
<p class="text-xs text-gray-600 mt-1">{{ 'footer.tagline'|trans }}</p>
</div>
<div class="flex flex-wrap justify-center gap-x-6 gap-y-2 text-xs text-gray-700 font-semibold">
<a href="{{ path('reservation_mentions-legal') }}" class="hover:text-blue-700 transition-colors">{{ 'footer.legal'|trans }}</a>
<a href="{{ path('reservation_cgv') }}" class="hover:text-blue-700 transition-colors">{{ 'footer.cgv'|trans }}</a>
<a href="{{ path('reservation_rgpd') }}" class="hover:text-blue-700 transition-colors">{{ 'footer.rgpd'|trans }}</a>
<a href="{{ path('reservation_cookies') }}" class="hover:text-blue-700 transition-colors">{{ 'footer.cookies'|trans }}</a>
</div>
</div>
</div>
</footer>
<cookie-banner></cookie-banner>
{% block javascripts %}{% endblock %}
</body>
</html>