✨ 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:
@@ -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) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
190
assets/tools/FlowFormuleConfigurator.js
Normal file
190
assets/tools/FlowFormuleConfigurator.js
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
12
assets/tools/SubmitClearStorage.js
Normal file
12
assets/tools/SubmitClearStorage.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export class SubmitClearStorage extends HTMLFormElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('submit', () => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user