2026-01-30 15:32:28 +01:00
|
|
|
|
|
|
|
|
export class FlowReserve extends HTMLAnchorElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.sidebarId = 'flow-reserve-sidebar';
|
|
|
|
|
this.storageKey = 'pl_list';
|
|
|
|
|
this.apiUrl = '/basket/json';
|
|
|
|
|
this.isOpen = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
|
this.addEventListener('click', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.toggleSidebar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Listen for updates to the cart from other components
|
|
|
|
|
window.addEventListener('cart:updated', () => this.updateBadge());
|
|
|
|
|
|
|
|
|
|
// Initial badge update
|
|
|
|
|
this.updateBadge();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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(localStorage.getItem(this.storageKey) || '[]');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeFromList(id) {
|
|
|
|
|
let list = this.getList();
|
|
|
|
|
list = list.filter(itemId => itemId.toString() !== id.toString());
|
|
|
|
|
localStorage.setItem(this.storageKey, JSON.stringify(list));
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensureSidebarExists() {
|
|
|
|
|
if (document.getElementById(this.sidebarId)) return;
|
|
|
|
|
|
|
|
|
|
const template = `
|
|
|
|
|
<div id="${this.sidebarId}" class="fixed inset-0 z-[100] flex justify-end pointer-events-none">
|
|
|
|
|
<!-- Backdrop -->
|
|
|
|
|
<div class="backdrop absolute inset-0 bg-slate-900/60 backdrop-blur-sm opacity-0 transition-opacity duration-300"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Panel -->
|
|
|
|
|
<div class="panel w-full max-w-md bg-white shadow-2xl translate-x-full transition-transform duration-300 flex flex-col relative z-10">
|
|
|
|
|
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="p-6 border-b border-gray-100 flex items-center justify-between bg-white">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="text-2xl font-black text-slate-900 uppercase italic tracking-tighter">Ma Super <br><span class="text-[#f39e36]">Future Réservation</span></h2>
|
|
|
|
|
</div>
|
|
|
|
|
<button id="flow-reserve-close" class="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
|
|
|
|
<svg class="w-6 h-6 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Content (Loader/List/Empty) -->
|
|
|
|
|
<div id="flow-reserve-content" class="flex-1 overflow-y-auto p-6 space-y-6 bg-gray-50/50 relative">
|
|
|
|
|
<!-- Content injected via JS -->
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Footer -->
|
|
|
|
|
<div id="flow-reserve-footer" class="p-6 border-t border-gray-100 bg-white">
|
|
|
|
|
<!-- Content injected via JS -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
document.body.insertAdjacentHTML('beforeend', template);
|
|
|
|
|
|
|
|
|
|
// Bind events
|
|
|
|
|
const sidebar = document.getElementById(this.sidebarId);
|
2026-01-30 18:27:30 +01:00
|
|
|
|
|
|
|
|
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);
|
2026-01-30 15:32:28 +01:00
|
|
|
}
|
|
|
|
|
async refreshContent() {
|
|
|
|
|
const container = document.getElementById('flow-reserve-content');
|
|
|
|
|
const footer = document.getElementById('flow-reserve-footer');
|
|
|
|
|
|
|
|
|
|
// Loader
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="absolute inset-0 flex items-center justify-center">
|
|
|
|
|
<div class="w-10 h-10 border-4 border-[#f39e36] border-t-transparent rounded-full animate-spin"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
footer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
const ids = this.getList();
|
2026-01-30 18:10:01 +01:00
|
|
|
|
|
|
|
|
// Retrieve dates from localStorage
|
|
|
|
|
let dates = { start: null, end: null };
|
|
|
|
|
try {
|
|
|
|
|
dates = JSON.parse(localStorage.getItem('reservation_dates') || '{}');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Invalid reservation dates in localStorage');
|
|
|
|
|
}
|
2026-01-30 15:32:28 +01:00
|
|
|
|
|
|
|
|
if (ids.length === 0) {
|
|
|
|
|
this.renderEmpty(container, footer);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(this.apiUrl, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
2026-01-30 18:10:01 +01:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
ids,
|
|
|
|
|
start: dates.start,
|
|
|
|
|
end: dates.end
|
|
|
|
|
})
|
2026-01-30 15:32:28 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error('Erreur réseau');
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
2026-01-30 18:10:01 +01:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
2026-01-30 15:32:28 +01:00
|
|
|
this.renderList(container, footer, data);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="text-center py-10">
|
|
|
|
|
<p class="text-red-500 font-bold">Une erreur est survenue lors du chargement de votre panier.</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderEmpty(container, footer) {
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="h-full flex flex-col items-center justify-center text-center opacity-60">
|
|
|
|
|
<svg class="w-16 h-16 text-slate-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/></svg>
|
|
|
|
|
<p class="text-lg font-bold text-slate-900 italic">Votre panier est vide</p>
|
|
|
|
|
<p class="text-sm text-slate-500">Ajoutez du bonheur à votre événement !</p>
|
|
|
|
|
<a href="/reservation/catalogue" id="flow-empty-catalog-link" class="mt-6 px-6 py-3 bg-[#f39e36] text-white rounded-xl font-bold text-sm uppercase tracking-widest hover:bg-slate-900 transition-colors">
|
|
|
|
|
Voir le catalogue
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
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 = `
|
|
|
|
|
<div class="bg-blue-50 border border-blue-100 rounded-2xl p-4 mb-4 text-center">
|
|
|
|
|
<p class="text-[10px] uppercase tracking-widest text-slate-500 font-bold mb-1">Votre période</p>
|
|
|
|
|
<div class="flex items-center justify-center gap-2 text-sm font-black text-slate-900 uppercase italic">
|
|
|
|
|
<span>${data.start_date}</span>
|
|
|
|
|
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
|
|
|
|
|
<span>${data.end_date}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- RENDER PRODUCTS ---
|
|
|
|
|
const productsHtml = data.products.map(product => `
|
|
|
|
|
<div class="flex gap-4 bg-white p-3 rounded-2xl shadow-sm border border-gray-100">
|
|
|
|
|
<div class="w-20 h-20 bg-gray-100 rounded-xl flex-shrink-0 overflow-hidden">
|
|
|
|
|
<img src="${product.image || '/provider/images/favicon.png'}" class="w-full h-full object-cover" alt="${product.name}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 flex flex-col justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<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>
|
|
|
|
|
${product.priceHTSupDay ? `<span class="text-slate-300">|</span><span>Sup: ${this.formatPrice(product.priceHTSupDay)} HT</span>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex justify-between items-end mt-2">
|
2026-01-30 18:18:49 +01:00
|
|
|
<span class="text-[#0782bc] font-black text-sm">${this.formatPrice(product.totalPriceTTC || product.totalPriceHT)} <span class="text-[9px] text-slate-400 font-bold">Total</span></span>
|
2026-01-30 18:10:01 +01:00
|
|
|
<button class="text-red-400 hover:text-red-600 p-1 remove-btn" data-remove-id="${product.id}">
|
2026-01-30 15:32:28 +01:00
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
container.innerHTML = `<div class="space-y-3">${datesHtml}${productsHtml}</div>`;
|
|
|
|
|
|
2026-01-30 18:18:49 +01:00
|
|
|
// Attach event listeners
|
2026-01-30 18:10:01 +01:00
|
|
|
container.querySelectorAll('.remove-btn').forEach(btn => {
|
|
|
|
|
btn.addEventListener('click', (e) => {
|
|
|
|
|
e.preventDefault();
|
2026-01-30 18:18:49 +01:00
|
|
|
e.stopPropagation();
|
2026-01-30 18:10:01 +01:00
|
|
|
this.removeFromList(btn.dataset.removeId);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-30 15:32:28 +01:00
|
|
|
// --- RENDER FOOTER (TOTALS) ---
|
|
|
|
|
const total = data.total || {};
|
|
|
|
|
const hasTva = total.totalTva > 0;
|
|
|
|
|
|
|
|
|
|
footer.innerHTML = `
|
|
|
|
|
<div class="space-y-2 mb-6">
|
|
|
|
|
<div class="flex justify-between text-xs text-slate-500 font-bold uppercase">
|
|
|
|
|
<span>Total HT</span>
|
|
|
|
|
<span>${this.formatPrice(total.totalHT)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
${hasTva ? `
|
|
|
|
|
<div class="flex justify-between text-xs text-slate-500 font-bold uppercase">
|
|
|
|
|
<span>TVA</span>
|
|
|
|
|
<span>${this.formatPrice(total.totalTva)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
<div class="flex justify-between text-lg text-slate-900 font-black uppercase italic border-t border-dashed border-gray-200 pt-2">
|
|
|
|
|
<span>Total ${hasTva ? 'TTC' : 'HT'}</span>
|
|
|
|
|
<span class="text-[#f39e36]">${this.formatPrice(total.totalTTC || total.totalHT)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<a href="/reservation/devis" class="block w-full py-4 bg-slate-900 text-white text-center rounded-2xl font-black uppercase italic tracking-widest hover:bg-[#fc0e50] transition-colors shadow-lg">
|
|
|
|
|
Valider ma demande
|
|
|
|
|
</a>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 18:10:01 +01:00
|
|
|
formatDate(dateString) {
|
|
|
|
|
if (!dateString) return '';
|
|
|
|
|
try {
|
|
|
|
|
const date = new Date(dateString);
|
|
|
|
|
return new Intl.DateTimeFormat('fr-FR').format(date);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return dateString;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 15:32:28 +01:00
|
|
|
formatPrice(amount) {
|
|
|
|
|
if (amount === undefined || amount === null) return '-';
|
|
|
|
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amount);
|
|
|
|
|
}
|
|
|
|
|
}
|