Files
ludikevent_crm/templates/revervation/produit.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

370 lines
26 KiB
Twig

{% extends 'revervation/base.twig' %}
{# --- SEO DYNAMIQUE & META-DONNÉES --- #}
{% block title %}Location {{ product.name }} à {{ product.priceDay }}€ | Ludik Event Aisne{% endblock %}
{% block description %}
Louez {{ product.name }} chez Ludikevent. Structure gonflable parfaite pour {{ product.category }}.
Tarif : {{ product.priceDay }}€/jour. Installation et livraison dans l'Aisne (02).
{{ product.description|striptags|slice(0, 130) }}... Réservez votre date !
{% endblock %}
{% block jsonld %}
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "{{ product.name }}",
"image": ["{% if product.imageName %}{{ absolute_url(vich_uploader_asset(product, 'imageFile')) }}{% else %}{{ absolute_url(asset('provider/images/favicon.png')) }}{% endif %}"],
"description": "{{ product.description|striptags|slice(0, 160) }}",
"sku": "{{ product.ref }}",
"brand": { "@type": "Brand", "name": "Ludikevent" },
"offers": {
"@type": "Offer",
"url": "{{ app.request.uri }}",
"priceCurrency": "EUR",
"price": "{{ product.priceDay }}",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/UsedCondition",
"priceValidUntil": "{{ "now"|date_modify("+1 year")|date("Y-m-d") }}"
}
}
</script>
{% endblock %}
{% block breadcrumb_json %}
,{
"@type": "ListItem",
"position": 1,
"name": "Catalogue",
"item": "{{ absolute_url(path('reservation_catalogue')) }}"
},{
"@type": "ListItem",
"position": 2,
"name": "{{ product.name }}",
"item": "{{ absolute_url(path('reservation_product_show',{id:product.id})) }}"
}
{% endblock %}
{% block body %}
<utm-event event="view_product" data="{{ product.json }}"></utm-event>
<div class="min-h-screen bg-white font-sans antialiased">
{# --- NAVIGATION / BREADCRUMB --- #}
<div class="max-w-7xl mx-auto pt-16 pb-8 px-4 text-center">
<nav class="flex justify-center space-x-4 text-[10px] mb-8 uppercase tracking-[0.3em] font-black italic">
<a href="{{ url('reservation') }}" class="text-slate-400 hover:text-[#fc0e50] transition">ACCUEIL</a>
<span class="text-slate-300">/</span>
<a href="{{ url('reservation_catalogue') }}" class="text-slate-400 hover:text-[#fc0e50] transition">Catalogue</a>
<span class="text-slate-300">/</span>
<span class="text-amber-500 underline decoration-2 underline-offset-4">{{ product.name }}</span>
</nav>
</div>
{# --- BOUTON RETOUR --- #}
<div class="max-w-7xl mx-auto px-4 pt-4">
<a href="{{ path('reservation_catalogue') }}" class="group inline-flex items-center gap-3 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 hover:text-blue-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transform group-hover:-translate-x-2 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Retour au catalogue
</a>
</div>
<main class="max-w-7xl mx-auto px-4 py-12 md:py-20">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 md:gap-24 items-start">
{# --- COLONNE GAUCHE : VISUEL --- #}
<div class="top-24">
<div class="relative overflow-hidden rounded-[3rem] md:rounded-[4rem] bg-slate-50 aspect-[4/5] shadow-inner">
{% if product.imageName %}
<img src="{{ vich_uploader_asset(product,'imageFile') | imagine_filter('webp') }}"
alt="{{ product.name }}"
class="w-full h-full object-contain p-4 md:p-8">
{% else %}
<div class="h-full flex flex-col items-center justify-center p-12 text-center opacity-50">
<img src="{{ asset('provider/images/favicon.png') }}" alt="Ludik Event" class="w-48">
</div>
{% endif %}
<div class="absolute top-6 left-6 md:top-8 md:left-8">
<span class="bg-[#f39e36] text-white px-4 py-2 md:px-6 md:py-2 rounded-2xl text-[9px] md:text-[10px] font-black uppercase tracking-widest shadow-xl">
{{ product.category }}
</span>
</div>
</div>
</div>
{# --- COLONNE DROITE : CONTENU --- #}
<div class="flex flex-col h-full py-4">
<div class="mb-10 md:mb-12">
<span class="text-[11px] md:text-[12px] font-black text-slate-300 uppercase tracking-[0.4em] mb-4 block italic text-center md:text-left">
Référence : {{ product.ref }}
</span>
<h1 class="text-5xl md:text-8xl font-black text-slate-900 uppercase italic tracking-tighter leading-[0.9] mb-10 text-center md:text-left">
{{ product.name }}
</h1>
<div class="flex flex-col items-center md:items-start space-y-6">
{# Prix principal #}
<div class="flex flex-col md:flex-row items-center md:items-baseline gap-2 md:gap-4 text-center md:text-left">
{% if tvaEnabled %}
<span class="text-4xl md:text-3xl font-black text-[#f39e36] leading-none">{{ (product.priceDay*1.20)|format_currency('EUR') }} TTC</span>
{% else %}
<span class="text-4xl md:text-3xl font-black text-[#f39e36] leading-none">{{ product.priceDay|format_currency('EUR') }}</span>
{% endif %}
{% if product.category == "barnums" %}
<span class="text-[10px] md:text-sm font-bold text-slate-400 uppercase tracking-widest italic">Week-End</span>
{% else %}
<span class="text-[10px] md:text-sm font-bold text-slate-400 uppercase tracking-widest italic">La première journée</span>
{% endif %}
</div>
{# Grille de badges Tarifs/Caution #}
<div class="flex flex-wrap justify-center md:justify-start gap-3 w-full">
{% if product.category != "barnums" %}
<div class="flex items-center gap-3 bg-slate-50 px-5 py-3 rounded-2xl border border-slate-100 shadow-sm">
{% if tvaEnabled %}
<span class="text-md md:text-2xl font-black text-slate-900 italic">+ {{ (product.priceSup*1.20)|format_currency('EUR') }} TTC</span>
{% else %}
<span class="text-md md:text-2xl font-black text-slate-900 italic">+ {{ product.priceSup|format_currency('EUR') }}</span>
{% endif %}
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest italic">Jour supplémentaire</span>
</div>
{% endif %}
<div class="flex items-center gap-3 bg-slate-50 px-5 py-3 rounded-2xl border border-slate-100 shadow-sm">
<span class="text-xl md:text-2xl font-black text-slate-900 italic">{{ product.caution|format_currency('EUR') }}</span>
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest italic">LA CAUTION</span>
</div>
</div>
{# Options disponibles #}
{% if product.options|length > 0 %}
<div class="w-full mt-6">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-4 block italic text-center md:text-left">Options disponibles</span>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for option in product.options %}
{% if option.isPublish %}
<label class="relative flex items-start gap-4 bg-white border-2 border-transparent p-4 rounded-2xl shadow-sm hover:shadow-md transition-all cursor-pointer group has-[:checked]:border-[#f39e36] has-[:checked]:bg-amber-50/50">
<input type="checkbox" value="{{ option.id }}" data-option-id="{{ option.id }}" class="product-option-checkbox sr-only peer">
<div class="w-16 h-16 bg-slate-50 rounded-xl overflow-hidden flex-shrink-0">
{% if option.imageName %}
<img src="{{ vich_uploader_asset(option, 'imageFile') }}" alt="{{ option.name }}" class="w-full h-full object-cover">
{% else %}
<div class="w-full h-full flex items-center justify-center text-slate-300">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" /></svg>
</div>
{% endif %}
</div>
<div class="pr-2">
<h4 class="text-sm font-black text-slate-900 uppercase italic mb-1 group-hover:text-[#f39e36] peer-checked:text-[#f39e36] transition-colors">{{ option.name }}</h4>
{% if tvaEnabled %}
<span class="text-xs font-bold text-slate-500 peer-checked:text-slate-700">+ {{ (option.priceHt*1.20)|format_currency('EUR') }} TTC</span>
{% else %}
<span class="text-xs font-bold text-slate-500 peer-checked:text-slate-700">+ {{ option.priceHt|format_currency('EUR') }}</span>
{% endif %}
</div>
{# Checkmark Icon visible when checked #}
<div class="absolute top-4 right-4 text-[#f39e36] opacity-0 peer-checked:opacity-100 transition-opacity">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" /></svg>
</div>
</label>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{# --- DESCRIPTION --- #}
<div class="prose prose-slate prose-lg max-w-none mb-12 text-slate-600 leading-relaxed text-center md:text-left">
{% set desc = product.description %}
{# On vérifie si la chaîne contient l'un des marqueurs HTML de ton éditeur #}
{% if '<p' in desc or '<div' in desc or '<span' in desc or '<br' in desc %}
{# C'est du HTML (WYSIWYG) #}
{{ desc|raw }}
{% else %}
{# C'est du texte brut (Ancien système) #}
{{ desc|nl2br }}
{% endif %}
</div>
{# --- DIMENSIONS (Version Responsive) --- #}
{% if product.dimP is not empty and product.dimP != "" %}
<div class="mb-12">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-4 md:mb-6 block italic text-center md:text-left">Dimensions de la structure</span>
<div class="bg-slate-50 rounded-[2.5rem] md:rounded-[3rem] p-6 md:p-8 border border-slate-100 relative overflow-hidden">
<div class="flex flex-col md:grid md:grid-cols-2 gap-8 md:gap-12 items-center">
{# Visuel Schématique #}
<div class="relative h-32 md:h-48 flex items-center justify-center w-full">
<div class="relative w-28 h-16 md:w-32 md:h-20 bg-blue-600/10 border-2 border-blue-500/20 rounded-lg transform -rotate-12 skew-x-12 flex items-center justify-center">
<div class="absolute -right-4 -top-6 md:-top-8 h-full border-r-2 border-dashed border-blue-400/40 flex items-center">
<span class="ml-2 text-[9px] md:text-[10px] font-black text-blue-500 italic">{{ product.dimH }}m</span>
</div>
<div class="absolute -bottom-5 md:-bottom-6 left-0 w-full border-b-2 border-dashed border-blue-400/40 text-center">
<span class="block mt-1 text-[9px] md:text-[10px] font-black text-blue-500 italic">{{ product.dimW }}m</span>
</div>
<div class="absolute -bottom-3 -right-6 md:-right-8 w-16 md:w-20 border-b-2 border-dashed border-blue-400/40 transform rotate-[45deg] text-center">
<span class="block mt-1 text-[9px] md:text-[10px] font-black text-blue-500 italic">{{ product.dimP }}m</span>
</div>
<svg class="w-8 h-8 md:w-12 md:h-12 text-blue-500/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
</div>
</div>
{# Chiffres #}
<div class="grid grid-cols-1 gap-3 md:gap-4 w-full">
<div class="flex items-center justify-between border-b border-slate-200/60 pb-3">
<span class="text-[9px] md:text-[10px] font-black text-slate-400 uppercase italic">Longueur </span>
<span class="text-lg md:text-xl font-black text-slate-900 italic">{{ product.dimP }} m</span>
</div>
<div class="flex items-center justify-between border-b border-slate-200/60 pb-3">
<span class="text-[9px] md:text-[10px] font-black text-slate-400 uppercase italic">Largeur </span>
<span class="text-lg md:text-xl font-black text-slate-900 italic">{{ product.dimW }} m</span>
</div>
<div class="flex items-center justify-between">
<span class="text-[9px] md:text-[10px] font-black text-slate-400 uppercase italic">Hauteur</span>
<span class="text-lg md:text-xl font-black text-slate-900 italic">{{ product.dimH }} m</span>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if product.category != "barnums" %}
<div class="border-t border-slate-100 pt-10 mb-12">
<div class="grid grid-cols-1 gap-8">
<div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 flex items-center justify-between shadow-sm">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Âge conseillé</span>
<span class="text-lg font-black text-slate-900 uppercase italic">{{ product.category }}</span>
</div>
</div>
</div>
{% endif %}
{# --- ACTIONS FINALES --- #}
<div class="mt-auto space-y-4">
{% if product.category == "2-7 ans" %}
<div class="p-6 border-2 border-slate-100 rounded-[2.5rem] text-center">
<p class="text-[11px] font-bold text-slate-800 leading-relaxed uppercase tracking-wide">
Structure soumise à des conditions spéciales.
</p>
</div>
<a href="{{ path('reservation_contact', {id: product.id}) }}"
class="flex items-center justify-center w-full py-6 md:py-8 bg-slate-900 text-white rounded-[2.5rem] font-black uppercase text-[11px] md:text-[12px] tracking-[0.3em] hover:bg-[#f39e36] transition-all shadow-2xl hover:scale-[1.02] active:scale-95">
Nous contacter
</a>
{% else %}
<flow-add-to-cart product-id="{{ product.id }}">
<button class="flex items-center justify-center w-full py-6 md:py-8 bg-slate-900 text-white rounded-[2.5rem] font-black uppercase text-[11px] md:text-[12px] tracking-[0.3em] hover:bg-[#f39e36] transition-all shadow-2xl hover:scale-[1.02] active:scale-95">
Vérifier la disponibilité
</button>
</flow-add-to-cart>
{% endif %}
<p class="text-center text-[9px] font-black text-slate-300 uppercase tracking-widest italic">
Devis gratuit • Ludikevent • Qualité Pro
</p>
</div>
</div>
</div>
{# --- GALERIE PHOTOS --- #}
{% if product.productPhotos|length > 0 %}
<div class="mt-20">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8 block italic text-center md:text-left">Galerie Photos</span>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6">
{% for photo in product.productPhotos %}
<div class="relative overflow-hidden rounded-[2rem] bg-slate-50 aspect-square shadow-sm hover:shadow-xl transition-all duration-500 group">
<img src="{{ vich_uploader_asset(photo, 'imageFile') }}"
alt="Photo {{ product.name }}"
class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-700"
loading="lazy">
</div>
{% endfor %}
</div>
</div>
{% endif %}
{# --- VIDEOS --- #}
{% if product.productVideos|length > 0 %}
<div class="mt-20">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8 block italic text-center md:text-left">Vidéos de présentation</span>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
{% for video in product.productVideos %}
<div class="relative overflow-hidden rounded-[2.5rem] bg-black shadow-lg aspect-video group">
<video controls class="w-full h-full object-cover" preload="metadata">
<source src="{{ vich_uploader_asset(video, 'imageFile') }}" type="video/mp4">
Votre navigateur ne supporte pas la lecture de vidéos.
</video>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{# --- DOCUMENTS PUBLICS --- #}
{% set publicDocs = product.productDocs|filter(doc => doc.isPublic) %}
{% if publicDocs|length > 0 %}
<div class="mt-20">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-6 block italic text-center md:text-left">Ressources techniques</span>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for doc in publicDocs %}
<a href="{{ vich_uploader_asset(doc, 'docProduct') }}"
download="{{ doc.name }}.pdf"
class="flex items-center justify-between p-5 bg-white border border-slate-100 rounded-3xl hover:border-blue-500 hover:shadow-lg transition-all group">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-red-50 text-red-500 rounded-xl flex items-center justify-center group-hover:bg-red-500 group-hover:text-white transition-colors">
<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="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/></svg>
</div>
<p class="text-[11px] font-black text-slate-900 uppercase italic">{{ doc.name }}</p>
</div>
<svg class="w-5 h-5 text-slate-300 group-hover:text-blue-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
</a>
{% endfor %}
</div>
</div>
{% endif %}
</main>
{# --- SUGGESTIONS --- #}
<section class="max-w-7xl mx-auto px-4 py-24 border-t border-slate-100 mt-12">
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-16 text-center md:text-left">
<div>
<span class="text-[10px] font-black text-[#f39e36] uppercase tracking-[0.3em] mb-3 block italic">Vous pourriez aussi aimer</span>
<h2 class="text-4xl md:text-6xl font-black text-slate-900 uppercase italic tracking-tighter leading-none">D'autres <span class="text-[#f39e36]">idées ?</span></h2>
</div>
<a href="{{ path('reservation_catalogue') }}" class="inline-flex items-center justify-center gap-2 text-[10px] font-black uppercase tracking-widest border-b-2 border-slate-900 pb-1 hover:text-blue-600 hover:border-blue-600 transition-all">
Tout le catalogue
</a>
</div>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
{% for other in otherProducts %}
<a href="{{ path('reservation_product_show', {id: (other.slug)}) }}" class="group block">
<div class="relative overflow-hidden rounded-[2rem] md:rounded-[2.5rem] bg-slate-50 aspect-square mb-6 shadow-sm group-hover:shadow-2xl transition-all duration-700">
{% if other.imageName %}
<img src="{{ vich_uploader_asset(other,'imageFile') | imagine_filter('webp') }}" alt="{{ other.name }}" class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-1000">
{% else %}
<div class="w-full h-full flex items-center justify-center opacity-10"><img src="{{ asset('provider/images/favicon.png') }}" class="w-16"></div>
{% endif %}
<div class="absolute top-4 right-4 bg-white/95 backdrop-blur-md px-3 py-1.5 rounded-xl shadow-sm">
<p class="text-slate-900 font-black text-[11px] md:text-[12px] italic leading-none">{{ other.priceDay }}€</p>
</div>
</div>
<div class="px-2 text-center md:text-left">
<span class="text-[8px] font-black text-slate-300 uppercase tracking-widest mb-1 block italic">{{ other.category }}</span>
<h3 class="text-md md:text-lg font-black text-slate-900 uppercase italic tracking-tighter leading-tight group-hover:text-[#f39e36] transition-colors line-clamp-1">{{ other.name }}</h3>
</div>
</a>
{% endfor %}
</div>
</section>
</div>
{% endblock %}