feat(formule): Intègre la gestion des formules et packs libres dans le parcours de réservation et les documents.

This commit is contained in:
Serreau Jovann
2026-02-09 14:06:26 +01:00
parent 1f393fcf24
commit 822f187dfb
23 changed files with 734 additions and 92 deletions

View File

@@ -130,7 +130,9 @@ export class SearchOptionsFormule extends HTMLButtonElement {
const row = this.closest('.form-repeater__row');
if (row) {
const nameInput = row.querySelector('input[name*="[product]"]');
const nameId = row.querySelector('input[name*="[id]"]');
if(nameInput) nameInput.value = option.name;
if(nameId) nameId.value = option.id;
const fieldset = row.querySelector('fieldset');
if (fieldset) {
@@ -275,8 +277,10 @@ export class SearchProductFormule extends HTMLButtonElement {
fillFormLine(product) {
const row = this.closest('.form-repeater__row');
if (row) {
const nameId = row.querySelector('input[name*="[id]"]');
const nameInput = row.querySelector('input[name*="[product]"]');
if(nameInput) nameInput.value = product.name;
if(nameId) nameId.value = product.id;
const fieldset = row.querySelector('fieldset');
if (fieldset) {

View File

@@ -4,6 +4,8 @@ import { CookieBanner } from "./tools/CookieBanner.js";
import { FlowReserve } from "./tools/FlowReserve.js";
import { FlowDatePicker } from "./tools/FlowDatePicker.js";
import { FlowAddToCart } from "./tools/FlowAddToCart.js";
import { FlowFormuleConfigurator } from "./tools/FlowFormuleConfigurator.js";
import { SubmitClearStorage } from "./tools/SubmitClearStorage.js";
import { LeafletMap } from "./tools/LeafletMap.js";
import { LocalStorageClear } from "./tools/LocalStorageClear.js";
import * as Turbo from "@hotwired/turbo";
@@ -265,6 +267,12 @@ const registerComponents = () => {
if(!customElements.get('flow-add-to-cart'))
customElements.define('flow-add-to-cart',FlowAddToCart)
if(!customElements.get('flow-formule-configurator'))
customElements.define('flow-formule-configurator',FlowFormuleConfigurator)
if(!customElements.get('submit-clear-storage'))
customElements.define('submit-clear-storage',SubmitClearStorage, { extends: 'form' })
};
document.addEventListener('DOMContentLoaded', () => {

View File

@@ -0,0 +1,190 @@
export class FlowFormuleConfigurator extends HTMLElement {
constructor() {
super();
this.counts = { structure: 0, alimentaire: 0, barnum: 0 };
this.selection = [];
this.blocked = false;
this.mode = 'free';
}
connectedCallback() {
try {
this.limits = JSON.parse(this.getAttribute('data-limits') || '{}');
this.prices = JSON.parse(this.getAttribute('data-prices') || '{}');
} catch (e) {
console.error('Invalid limits/prices JSON', e);
return;
}
this.formuleId = this.getAttribute('data-formule-id');
this.mode = this.getAttribute('data-mode') || 'free';
this.validateBtn = this.querySelector('#btn-validate-pack');
if (this.mode === 'pack') {
try {
this.selection = JSON.parse(this.getAttribute('data-preselected-ids') || '[]');
} catch (e) { console.error('Invalid preselected-ids JSON', e); }
} else {
this.querySelectorAll('.product-card').forEach(card => {
card.addEventListener('click', (e) => {
e.preventDefault();
this.toggleItem(card);
});
});
}
if (this.validateBtn) {
this.validateBtn.addEventListener('click', (e) => {
e.preventDefault();
this.validate();
});
}
this.checkDuration();
this.updateUI();
}
checkDuration() {
const datesStr = sessionStorage.getItem('reservation_dates');
if (!datesStr) return;
try {
const dates = JSON.parse(datesStr);
if (!dates.start || !dates.end) return;
const start = new Date(dates.start);
const end = new Date(dates.end);
const diffTime = Math.abs(end - start);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
let message = null;
let block = false;
if (diffDays > 5) {
message = `Attention : La durée de votre réservation (${diffDays} jours) dépasse la limite autorisée de 5 jours pour cette formule.`;
block = true;
} else if (diffDays >= 3 && diffDays <= 5) {
message = `Information : Pour une durée de ${diffDays} jours, le tarif "5 jours" sera appliqué automatiquement.`;
}
if (message) {
let msgContainer = this.querySelector('.formule-warning');
if (!msgContainer) {
msgContainer = document.createElement('div');
this.prepend(msgContainer);
}
msgContainer.className = `formule-warning p-4 mb-6 rounded-2xl text-sm font-bold text-center border-2 ${block ? 'bg-red-50 border-red-100 text-red-600' : 'bg-blue-50 border-blue-100 text-blue-600'}`;
msgContainer.innerHTML = message;
}
this.blocked = block;
this.updateUI();
} catch (e) { console.error(e); }
}
toggleItem(el) {
if (this.blocked || this.mode === 'pack') return;
const category = el.getAttribute('data-category');
const id = el.getAttribute('data-id');
const isSelected = el.getAttribute('data-selected') === 'true';
if (isSelected) {
this.deselect(el, category, id);
} else {
if (this.counts[category] < this.limits[category]) {
this.select(el, category, id);
} else {
this.shake(el);
}
}
this.updateUI();
}
select(el, category, id) {
el.setAttribute('data-selected', 'true');
el.classList.add('ring-4', 'ring-[#f39e36]', 'scale-95');
const indicator = el.querySelector('.selection-indicator');
if (indicator) {
indicator.classList.remove('opacity-0', 'group-hover:opacity-100');
indicator.classList.add('opacity-100');
indicator.querySelector('div')?.classList.remove('hidden');
}
this.counts[category]++;
this.selection.push(id);
}
deselect(el, category, id) {
el.setAttribute('data-selected', 'false');
el.classList.remove('ring-4', 'ring-[#f39e36]', 'scale-95');
const indicator = el.querySelector('.selection-indicator');
if (indicator) {
indicator.classList.remove('opacity-100');
indicator.classList.add('opacity-0', 'group-hover:opacity-100');
indicator.querySelector('div')?.classList.add('hidden');
}
this.counts[category]--;
this.selection = this.selection.filter(item => item !== id);
}
shake(el) {
el.classList.add('animate-pulse');
setTimeout(() => el.classList.remove('animate-pulse'), 500);
alert("Limite atteinte pour cette catégorie.");
}
updateUI() {
if (this.mode === 'free') {
['structure', 'alimentaire', 'barnum'].forEach(cat => {
const span = this.querySelector(`#count-${cat}`);
if (span) span.innerText = this.counts[cat];
});
}
const total = this.selection.length;
if (this.validateBtn) {
if (this.blocked) {
this.validateBtn.innerText = 'Durée non autorisée (> 5j)';
this.validateBtn.classList.add('opacity-50', 'pointer-events-none', 'translate-y-4', 'bg-red-600');
this.validateBtn.classList.remove('bg-slate-900', 'hover:bg-[#fc0e50]');
} else {
this.validateBtn.innerText = this.mode === 'pack' ? 'Réserver ce pack' : `Valider mon pack (${total})`;
this.validateBtn.classList.remove('bg-red-600');
this.validateBtn.classList.add('bg-slate-900', 'hover:bg-[#fc0e50]');
if (total > 0) {
this.validateBtn.classList.remove('opacity-50', 'pointer-events-none', 'translate-y-4');
} else {
this.validateBtn.classList.add('opacity-50', 'pointer-events-none', 'translate-y-4');
}
}
}
}
validate() {
if (this.blocked || this.selection.length === 0) return;
sessionStorage.setItem('active_formule', this.formuleId);
let list = [];
try {
list = JSON.parse(sessionStorage.getItem('pl_list') || '[]');
} catch(e) {}
const newItems = this.selection.filter(id => !list.includes(id));
if (newItems.length > 0) {
list = [...list, ...newItems];
sessionStorage.setItem('pl_list', JSON.stringify(list));
}
window.dispatchEvent(new CustomEvent('cart:updated'));
const cart = document.querySelector('[is="flow-reserve"]');
if (cart) cart.open();
}
}

View File

@@ -267,7 +267,8 @@ export class FlowReserve extends HTMLAnchorElement {
ids,
options,
start: dates.start,
end: dates.end
end: dates.end,
formule: sessionStorage.getItem('active_formule')
})
});
@@ -362,6 +363,7 @@ export class FlowReserve extends HTMLAnchorElement {
</div>
<div class="flex-1 flex flex-col justify-between">
<div>
${product.in_formule ? `<span class="inline-block px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-widest bg-blue-100 text-blue-600 mb-1">Inclus</span>` : ''}
<h4 class="font-black text-slate-900 leading-tight uppercase italic text-sm line-clamp-2">${product.name}</h4>
<div class="mt-1 flex flex-wrap gap-2 text-[10px] text-slate-500 font-bold uppercase">
<span>1J: ${this.formatPrice(product.priceHt1Day)} HT</span>
@@ -395,13 +397,23 @@ export class FlowReserve extends HTMLAnchorElement {
const total = data.total || {};
const hasTva = total.totalTva > 0;
const promotion = data.promotion;
const formuleName = total.formule;
let promoHtml = '';
let originalTotalHT = total.totalHT;
if (formuleName) {
promoHtml += `
<div class="flex justify-between text-xs text-blue-600 font-bold uppercase mb-2">
<span>Formule</span>
<span>${formuleName}</span>
</div>
`;
}
if (promotion && total.discount > 0) {
originalTotalHT = total.totalHT + total.discount;
promoHtml = `
promoHtml += `
<div class="flex justify-between text-xs text-[#f39e36] font-bold uppercase">
<span>${promotion.name} (-${promotion.percentage}%)</span>
<span>-${this.formatPrice(total.discount)}</span>
@@ -460,7 +472,8 @@ export class FlowReserve extends HTMLAnchorElement {
ids,
options,
start: dates.start,
end: dates.end
end: dates.end,
formule: sessionStorage.getItem('active_formule')
})
});

View File

@@ -0,0 +1,12 @@
export class SubmitClearStorage extends HTMLFormElement {
constructor() {
super();
}
connectedCallback() {
this.addEventListener('submit', () => {
localStorage.clear();
sessionStorage.clear();
});
}
}

View File

@@ -38,7 +38,7 @@ export class UtmAccount extends HTMLElement {
// 4. Envoi du sessionId à ton backend Symfony
if (sessionId) {
await fetch('/reservation/umami', {
await fetch('/umami', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ umami_session: sessionId })