export class FlowReserve extends HTMLAnchorElement {
constructor() {
super();
this.sidebarId = 'flow-reserve-sidebar';
this.storageKey = 'pl_list';
this.apiUrl = '/basket/json';
this.checkAvailabilityUrl = '/produit/check/basket';
this.isOpen = false;
this.productsAvailable = true;
}
connectedCallback() {
this.addEventListener('click', (e) => {
e.preventDefault();
this.toggleSidebar();
});
// Listen for updates to the cart from other components
window.addEventListener('cart:updated', () => {
this.updateBadge();
this._checkProductAvailability(); // Re-check on cart update
});
// Initial badge update
this.updateBadge();
this._checkProductAvailability();
}
/**
* Updates the notification badge on the icon
*/
updateBadge() {
const list = this.getList();
const badge = this.querySelector('[data-count]');
if (badge) {
badge.innerText = list.length;
if (list.length > 0) {
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
}
getList() {
try {
return JSON.parse(sessionStorage.getItem(this.storageKey) || '[]');
} catch (e) {
return [];
}
}
getOptions() {
try {
return JSON.parse(sessionStorage.getItem('pl_options') || '{}');
} catch (e) {
return {};
}
}
removeFromList(id) {
let list = this.getList();
list = list.filter(itemId => itemId.toString() !== id.toString());
sessionStorage.setItem(this.storageKey, JSON.stringify(list));
// Remove options for this product
const options = this.getOptions();
if (options[id]) {
delete options[id];
sessionStorage.setItem('pl_options', JSON.stringify(options));
}
window.dispatchEvent(new CustomEvent('cart:updated'));
this.refreshContent(); // Re-fetch and render
}
toggleSidebar() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open() {
this.ensureSidebarExists();
const sidebar = document.getElementById(this.sidebarId);
const backdrop = sidebar.querySelector('.backdrop');
const panel = sidebar.querySelector('.panel');
sidebar.classList.remove('pointer-events-none');
backdrop.classList.remove('opacity-0');
panel.classList.remove('translate-x-full');
this.isOpen = true;
this.refreshContent();
}
close() {
const sidebar = document.getElementById(this.sidebarId);
if (!sidebar) return;
const backdrop = sidebar.querySelector('.backdrop');
const panel = sidebar.querySelector('.panel');
backdrop.classList.add('opacity-0');
panel.classList.add('translate-x-full');
setTimeout(() => {
sidebar.classList.add('pointer-events-none');
}, 300); // Match transition duration
this.isOpen = false;
}
async _checkProductAvailability() {
const ids = this.getList();
if (ids.length === 0) {
this.productsAvailable = true;
return;
}
let dates = { start: null, end: null };
try {
dates = JSON.parse(sessionStorage.getItem('reservation_dates') || '{}');
} catch (e) {
console.warn('Invalid reservation dates in sessionStorage for availability check');
}
try {
const response = await fetch(this.checkAvailabilityUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids,
start: dates.start,
end: dates.end
})
});
if (!response.ok) throw new Error('Erreur réseau lors de la vérification de disponibilité');
const data = await response.json();
this.productsAvailable = data.available;
if (data.unavailable_products_ids && data.unavailable_products_ids.length > 0) {
let currentList = this.getList();
const initialLength = currentList.length;
// Filter out unavailable items
currentList = currentList.filter(id => !data.unavailable_products_ids.includes(parseInt(id)) && !data.unavailable_products_ids.includes(String(id)));
if (currentList.length !== initialLength) {
sessionStorage.setItem(this.storageKey, JSON.stringify(currentList));
window.dispatchEvent(new CustomEvent('cart:updated'));
console.warn('Produits indisponibles retirés du panier:', data.unavailable_products_ids);
// Force refresh to update UI immediately
this.refreshContent();
}
}
} catch (error) {
console.error('Erreur lors de la vérification de disponibilité:', error);
this.productsAvailable = false;
}
}
ensureSidebarExists() {
if (document.getElementById(this.sidebarId)) return;
const template = `
`;
document.body.insertAdjacentHTML('beforeend', template);
// Bind events
const sidebar = document.getElementById(this.sidebarId);
const closeHandler = (e) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
this.close();
};
sidebar.querySelector('.backdrop').addEventListener('click', closeHandler);
sidebar.querySelector('#flow-reserve-close').addEventListener('click', closeHandler);
}
async refreshContent() {
const container = document.getElementById('flow-reserve-content');
const footer = document.getElementById('flow-reserve-footer');
// Loader
container.innerHTML = `
`;
footer.innerHTML = '';
const ids = this.getList();
const options = this.getOptions();
// Retrieve dates from sessionStorage
let dates = { start: null, end: null };
try {
dates = JSON.parse(sessionStorage.getItem('reservation_dates') || '{}');
} catch (e) {
console.warn('Invalid reservation dates in sessionStorage');
}
if (ids.length === 0) {
this.renderEmpty(container, footer);
this.productsAvailable = true;
return;
}
// Display warning if products are not available
if (!this.productsAvailable) {
container.innerHTML = `
Attention :
Certains produits de votre panier ne sont plus disponibles. Veuillez vérifier votre sélection.
`;
footer.innerHTML = `
Valider ma demande (Produits indisponibles)
`;
return;
}
try {
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids,
options,
start: dates.start,
end: dates.end,
formule: sessionStorage.getItem('active_formule')
})
});
if (!response.ok) throw new Error('Erreur réseau');
const data = await response.json();
// Handle removed products (deleted from DB)
if (data.unavailable_products_ids && data.unavailable_products_ids.length > 0) {
let currentList = this.getList();
const initialLength = currentList.length;
currentList = currentList.filter(id => !data.unavailable_products_ids.includes(parseInt(id)) && !data.unavailable_products_ids.includes(String(id))); // Handle string/int types
if (currentList.length !== initialLength) {
sessionStorage.setItem(this.storageKey, JSON.stringify(currentList));
window.dispatchEvent(new CustomEvent('cart:updated'));
console.warn('Certains produits ont été retirés car ils n\'existent plus:', data.unavailable_products_ids);
}
}
// Merge client-side dates if server didn't return them (or to prioritize client choice)
if (!data.start_date && dates.start) data.start_date = this.formatDate(dates.start);
if (!data.end_date && dates.end) data.end_date = this.formatDate(dates.end);
// Fallback: if server returns dates in a format we can use directly, fine.
// If we just want to display what is in local storage:
if (dates.start) data.start_date = this.formatDate(dates.start);
if (dates.end) data.end_date = this.formatDate(dates.end);
this.renderList(container, footer, data);
} catch (error) {
console.error(error);
container.innerHTML = `
Une erreur est survenue lors du chargement de votre panier.
`;
}
}
renderEmpty(container, footer) {
container.innerHTML = `
Votre panier est vide
Ajoutez du bonheur à votre événement !
Voir le catalogue
`;
footer.innerHTML = '';
container.querySelector('#flow-empty-catalog-link').addEventListener('click', () => this.close());
}
renderList(container, footer, data) {
// --- HEADER DATES ---
let datesHtml = '';
if (data.start_date && data.end_date) {
datesHtml = `
Votre période
${data.start_date}
${data.end_date}
`;
}
const productsHtml = data.products.map(product => {
let optionsHtml = '';
if (product.options && product.options.length > 0) {
optionsHtml = '';
product.options.forEach(opt => {
optionsHtml += `
+ ${opt.name}
${this.formatPrice(opt.price)}
`;
});
optionsHtml += '
';
}
return `
${product.in_formule ? `
Inclus ` : ''}
${product.name}
1J: ${this.formatPrice(product.priceHt1Day)} HT
${product.priceHTSupDay ? `| Sup: ${this.formatPrice(product.priceHTSupDay)} HT ` : ''}
${optionsHtml}
${this.formatPrice(product.totalPriceTTC || product.totalPriceHT)} Total
`;
}).join('');
container.innerHTML = `${datesHtml}${productsHtml}
`;
// Attach event listeners
container.querySelectorAll('.remove-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.removeFromList(btn.dataset.removeId);
});
});
// --- RENDER FOOTER (TOTALS) ---
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 += `
Formule
${formuleName}
`;
}
if (promotion && total.discount > 0) {
originalTotalHT = total.totalHT + total.discount;
promoHtml += `
${promotion.name} (-${promotion.percentage}%)
-${this.formatPrice(total.discount)}
`;
}
footer.innerHTML = `
Total HT
${this.formatPrice(originalTotalHT)}
${promoHtml}
${hasTva ? `
TVA
${this.formatPrice(total.totalTva)}
` : ''}
Total ${hasTva ? 'TTC' : 'HT'}
${this.formatPrice(total.totalTTC || total.totalHT)}
Valider ma demande
`;
const validateBtn = footer.querySelector('#flow-validate-btn');
if (validateBtn) {
validateBtn.addEventListener('click', (e) => {
this.validateBasket(e);
});
}
}
async validateBasket(e) {
e.preventDefault();
const ids = this.getList();
const options = this.getOptions();
let dates = { start: null, end: null };
try {
dates = JSON.parse(sessionStorage.getItem('reservation_dates') || '{}');
} catch (error) {
console.warn('Invalid reservation dates in sessionStorage');
}
try {
const response = await fetch('/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids,
options,
start: dates.start,
end: dates.end,
formule: sessionStorage.getItem('active_formule')
})
});
if (!response.ok) throw new Error('Erreur réseau');
const data = await response.json();
if (data.flowUrl) {
window.location.href = data.flowUrl;
}
} catch (error) {
console.error('Erreur lors de la validation du panier', error);
}
}
formatDate(dateString) {
if (!dateString) return '';
try {
const date = new Date(dateString);
return new Intl.DateTimeFormat('fr-FR').format(date);
} catch (e) {
return dateString;
}
}
formatPrice(amount) {
if (amount === undefined || amount === null) return '-';
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amount);
}
}