// ─── API helpers ─────────────────────────────────────────────────────────── const api = { get: (u) => fetch(u).then(r => r.json()), post: (u, b) => fetch(u, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(b)}).then(r => r.status===204?null:r.json()), put: (u, b) => fetch(u, {method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(b)}).then(r => r.json()), del: (u) => fetch(u, {method:'DELETE'}), form: (u, fd, method='POST') => fetch(u, {method, body:fd}).then(r => r.status===204?null:r.json()), }; // ─── State ────────────────────────────────────────────────────────────────── let tools=[], clients=[], platforms=[], rentals=[]; let calYear = new Date().getFullYear(), calMonth = new Date().getMonth()+1; let rentalFilter = ''; // ─── Navigation ───────────────────────────────────────────────────────────── document.querySelectorAll('.nav-item').forEach(el => { el.addEventListener('click', () => { document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.querySelectorAll('.section').forEach(s => s.classList.add('hidden')); el.classList.add('active'); const sec = 'section-' + el.dataset.section; document.getElementById(sec).classList.remove('hidden'); if (el.dataset.section === 'dashboard') loadDashboard(); if (el.dataset.section === 'tools') loadTools(); if (el.dataset.section === 'clients') loadClients(); if (el.dataset.section === 'rentals') loadRentals(); if (el.dataset.section === 'calendar') loadCalendar(); if (el.dataset.section === 'listings') loadListings(); if (el.dataset.section === 'settings') loadPlatforms(); }); }); function closeModal(id) { document.getElementById(id).classList.add('hidden'); } document.querySelectorAll('.modal').forEach(m => m.addEventListener('click', e => { if(e.target===m) m.classList.add('hidden'); })); // ─── Dashboard ────────────────────────────────────────────────────────────── async function loadDashboard() { const d = await api.get('/api/dashboard'); const stats = document.getElementById('dash-stats'); stats.innerHTML = `
${d.active_rentals}
Location(s) en cours
${fmtEuro(d.revenue_month)}
Revenus ce mois
${fmtEuro(d.revenue_total)}
Revenus total
${d.tools_count}
Outils
${d.clients_count}
Clients
${d.pending_deposit_return}
Cautions à rendre
`; const ret = document.getElementById('dash-returning'); if (!d.returning_soon.length) { ret.innerHTML = '
Aucun retour prévu dans les 7 jours
'; return; } ret.innerHTML = d.returning_soon.map(r => `
🔨 ${r.tool_name} 👤 ${r.client_name} 📅 Retour le ${fmtDate(r.end_date)} ${statusLabel(r.status)}
`).join(''); } // ─── Tools ────────────────────────────────────────────────────────────────── async function loadTools() { tools = await api.get('/api/tools'); renderTools(); } function renderTools() { const q = document.getElementById('tools-search').value.toLowerCase(); const grid = document.getElementById('tools-grid'); const filtered = tools.filter(t => t.name.toLowerCase().includes(q) || (t.category||'').toLowerCase().includes(q)); if (!filtered.length) { grid.innerHTML = '
Aucun outil trouvé
'; return; } grid.innerHTML = filtered.map(t => { const img = t.main_image ? `` : `
🔧
`; return `
${img}
${esc(t.name)}
${esc(t.category||'')}
${fmtEuro(t.daily_price)}/j ${t.weekend_price ? `${fmtEuro(t.weekend_price)}/wk` : ''} ${t.deposit ? `🔒 ${fmtEuro(t.deposit)}` : ''}
`; }).join(''); } function openToolModal(tool=null) { document.getElementById('tool-id').value = tool?.id || ''; document.getElementById('modal-tool-title').textContent = tool ? 'Modifier l\'outil' : 'Ajouter un outil'; document.getElementById('tool-name').value = tool?.name || ''; document.getElementById('tool-category').value = tool?.category || ''; document.getElementById('tool-daily').value = tool?.daily_price ?? 0; document.getElementById('tool-weekend').value = tool?.weekend_price ?? 0; document.getElementById('tool-deposit').value = tool?.deposit ?? 0; document.getElementById('tool-desc').value = tool?.description || ''; document.getElementById('tool-notes').value = tool?.notes || ''; document.getElementById('tool-images').value = ''; const preview = document.getElementById('tool-images-preview'); preview.innerHTML = ''; if (tool?.images) { tool.images.forEach(img => { preview.innerHTML += `
${img.is_main ? '' : ``}
`; }); } document.getElementById('modal-tool').classList.remove('hidden'); } async function saveTool() { const id = document.getElementById('tool-id').value; const name = document.getElementById('tool-name').value.trim(); if (!name) return; if (id) { // update fields await api.put(`/api/tools/${id}`, { name, description: document.getElementById('tool-desc').value, category: document.getElementById('tool-category').value, daily_price: parseFloat(document.getElementById('tool-daily').value)||0, weekend_price: parseFloat(document.getElementById('tool-weekend').value)||0, deposit: parseFloat(document.getElementById('tool-deposit').value)||0, notes: document.getElementById('tool-notes').value }); // upload new images if any const files = document.getElementById('tool-images').files; for (const file of files) { const fd = new FormData(); fd.append('image', file); await api.form(`/api/tools/${id}/images`, fd); } } else { const fd = new FormData(); fd.append('name', name); fd.append('description', document.getElementById('tool-desc').value); fd.append('category', document.getElementById('tool-category').value); fd.append('daily_price', document.getElementById('tool-daily').value||0); fd.append('weekend_price', document.getElementById('tool-weekend').value||0); fd.append('deposit', document.getElementById('tool-deposit').value||0); fd.append('notes', document.getElementById('tool-notes').value); const files = document.getElementById('tool-images').files; for (const file of files) fd.append('images', file); await api.form('/api/tools', fd); } closeModal('modal-tool'); await loadTools(); } async function openToolDetail(id) { const t = await api.get(`/api/tools/${id}`); const rents = await api.get(`/api/rentals?tool_id=${id}`); const imgUrls = t.images.map(img => `/uploads/tools/${img.filename}`); const gallery = t.images.length ? t.images.map((img, idx) => `` ).join('') : 'Aucune photo'; document.getElementById('tool-detail-content').innerHTML = `

${esc(t.name)} ${t.category ? `${esc(t.category)}` : ''}

${fmtEuro(t.daily_price)}
${t.weekend_price ? fmtEuro(t.weekend_price) : '—'}
${t.deposit ? fmtEuro(t.deposit) : '—'}
${esc(t.description)||'—'}
${t.notes ? `
${esc(t.notes)}
` : ''}

Historique locations (${rents.length})

${rents.slice(0,5).map(r => `
👤 ${esc(r.client?.name||'')}
${fmtDate(r.start_date)} → ${fmtDate(r.end_date)}${fmtEuro(r.price)}${statusLabel(r.status)}
`).join('')} ${rents.length > 5 ? `
… et ${rents.length-5} autres
` : ''} `; document.getElementById('btn-edit-tool').onclick = () => { closeModal('modal-tool-detail'); openToolModal(t); }; document.getElementById('btn-delete-tool').onclick = async () => { if (!confirm('Supprimer cet outil ?')) return; await api.del(`/api/tools/${id}`); closeModal('modal-tool-detail'); loadTools(); }; document.getElementById('modal-tool-detail').classList.remove('hidden'); } async function deleteToolImage(toolId, imgId) { await api.del(`/api/tools/${toolId}/images/${imgId}`); const t = await api.get(`/api/tools/${toolId}`); openToolModal(t); } async function setMainImage(toolId, imgId) { await api.form(`/api/tools/${toolId}/images/${imgId}/main`, new FormData(), 'POST'); const t = await api.get(`/api/tools/${toolId}`); openToolModal(t); } // ─── Clients ──────────────────────────────────────────────────────────────── async function loadClients() { clients = await api.get('/api/clients'); renderClients(); } function renderClients() { const q = document.getElementById('clients-search').value.toLowerCase(); const list = document.getElementById('clients-list'); const filtered = clients.filter(c => c.name.toLowerCase().includes(q) || (c.phone||'').includes(q) || (c.email||'').toLowerCase().includes(q)); if (!filtered.length) { list.innerHTML = '
Aucun client trouvé
'; return; } list.innerHTML = filtered.map(c => `
${c.name[0].toUpperCase()}
${esc(c.name)}
${c.phone ? `📞 ${esc(c.phone)}` : ''} ${c.email ? `✉️ ${esc(c.email)}` : ''} 📋 ${c.rental_count} location(s)
`).join(''); } function openClientModal(client=null) { document.getElementById('client-id').value = client?.id || ''; document.getElementById('modal-client-title').textContent = client ? 'Modifier le client' : 'Ajouter un client'; document.getElementById('client-name').value = client?.name || ''; document.getElementById('client-phone').value = client?.phone || ''; document.getElementById('client-email').value = client?.email || ''; document.getElementById('client-address').value = client?.address || ''; document.getElementById('client-notes').value = client?.notes || ''; document.getElementById('modal-client').classList.remove('hidden'); } async function saveClient() { const id = document.getElementById('client-id').value; const name = document.getElementById('client-name').value.trim(); if (!name) return; const fd = new FormData(); fd.append('name', name); fd.append('phone', document.getElementById('client-phone').value); fd.append('email', document.getElementById('client-email').value); fd.append('address', document.getElementById('client-address').value); fd.append('notes', document.getElementById('client-notes').value); if (id) { await api.put(`/api/clients/${id}`, { name, phone: document.getElementById('client-phone').value, email: document.getElementById('client-email').value, address: document.getElementById('client-address').value, notes: document.getElementById('client-notes').value }); } else { await api.form('/api/clients', fd); } closeModal('modal-client'); loadClients(); } async function openClientDetail(id) { const c = await api.get(`/api/clients/${id}`); const docsHtml = c.documents.length ? c.documents.map(d => { const isImg = /\.(jpg|jpeg|png|gif|webp)$/i.test(d.filename); return `
${isImg ? `` : `
📄
`}
${esc(d.label)}
`; }).join('') : ''; document.getElementById('client-detail-content').innerHTML = `

${esc(c.name)}

${c.phone ? `
${esc(c.phone)}
` : ''} ${c.email ? `
${esc(c.email)}
` : ''} ${c.address ? `
${esc(c.address)}
` : ''} ${c.notes ? `
${esc(c.notes)}
` : ''}

Documents (${c.documents.length})

${docsHtml}
📎 Ajouter un document

Historique locations (${c.rentals.length})

${c.rentals.slice(0,5).map(r => `
🔨 ${esc(r.tool_name)}
${fmtDate(r.start_date)} → ${fmtDate(r.end_date)}${fmtEuro(r.price)}${statusLabel(r.status)}
`).join('')} `; document.getElementById('btn-edit-client').onclick = () => { closeModal('modal-client-detail'); openClientModal(c); }; document.getElementById('btn-delete-client').onclick = async () => { if (!confirm('Supprimer ce client ?')) return; await api.del(`/api/clients/${id}`); closeModal('modal-client-detail'); loadClients(); }; document.getElementById('modal-client-detail').classList.remove('hidden'); } async function uploadDoc(clientId, input) { const file = input.files[0]; if (!file) return; const label = prompt('Libellé du document ?', 'Pièce d\'identité') || 'Document'; const fd = new FormData(); fd.append('file', file); fd.append('label', label); await api.form(`/api/clients/${clientId}/documents`, fd); openClientDetail(clientId); } async function deleteDoc(clientId, docId) { if (!confirm('Supprimer ce document ?')) return; await api.del(`/api/clients/${clientId}/documents/${docId}`); openClientDetail(clientId); } // ─── Rentals ──────────────────────────────────────────────────────────────── async function loadRentals() { [tools, clients, platforms] = await Promise.all([api.get('/api/tools'), api.get('/api/clients'), api.get('/api/platforms')]); const url = rentalFilter ? `/api/rentals?status=${rentalFilter}` : '/api/rentals'; rentals = await api.get(url); renderRentals(); } document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); rentalFilter = btn.dataset.status; loadRentals(); }); }); function renderRentals() { const list = document.getElementById('rentals-list'); if (!rentals.length) { list.innerHTML = '
Aucune location trouvée
'; return; } list.innerHTML = rentals.map(r => `
🔨 ${esc(r.tool?.name||'')} — 👤 ${esc(r.client?.name||'')}
📅 ${fmtDate(r.start_date)} → ${fmtDate(r.end_date)} 💶 ${fmtEuro(r.price)} ${r.platform ? `🔗 ${esc(r.platform.name)}` : ''} ${statusLabel(r.status)} ${r.deposit_collected && !r.deposit_returned ? '🔒 Caution à rendre' : ''}
`).join(''); } async function openRentalModal(rental=null) { if (!tools.length) [tools, clients, platforms] = await Promise.all([api.get('/api/tools'), api.get('/api/clients'), api.get('/api/platforms')]); document.getElementById('rental-id').value = rental?.id || ''; document.getElementById('modal-rental-title').textContent = rental ? 'Modifier la location' : 'Nouvelle location'; const tSel = document.getElementById('rental-tool'); const cSel = document.getElementById('rental-client'); const pSel = document.getElementById('rental-platform'); tSel.innerHTML = tools.map(t => ``).join(''); cSel.innerHTML = clients.map(c => ``).join(''); pSel.innerHTML = `` + platforms.map(p => ``).join(''); document.getElementById('rental-start').value = rental?.start_date || ''; document.getElementById('rental-end').value = rental?.end_date || ''; document.getElementById('rental-price').value = rental?.price ?? ''; document.getElementById('rental-deposit-collected').checked = !!rental?.deposit_collected; document.getElementById('rental-deposit-returned').checked = !!rental?.deposit_returned; document.getElementById('rental-notes').value = rental?.return_notes || ''; document.getElementById('rental-status').value = rental?.status || 'confirmed'; document.getElementById('rental-price-hint').textContent = ''; document.getElementById('modal-rental').classList.remove('hidden'); } function calcRentalPrice() { const start = document.getElementById('rental-start').value; const end = document.getElementById('rental-end').value; const toolId = parseInt(document.getElementById('rental-tool').value); if (!start || !end || !toolId) return; const t = tools.find(t => t.id === toolId); if (!t) return; const days = Math.max(1, Math.ceil((new Date(end) - new Date(start)) / 86400000) + 1); const isWeekend = days <= 3 && [6,0].includes(new Date(start).getDay()); const price = isWeekend && t.weekend_price ? t.weekend_price : t.daily_price * days; document.getElementById('rental-price').value = price.toFixed(2); document.getElementById('rental-price-hint').textContent = `${days} jour(s) × ${fmtEuro(t.daily_price)} = ${fmtEuro(price)} (estimé)`; } async function saveRental() { const id = document.getElementById('rental-id').value; const data = { tool_id: parseInt(document.getElementById('rental-tool').value), client_id: parseInt(document.getElementById('rental-client').value), platform_id: parseInt(document.getElementById('rental-platform').value)||null, start_date: document.getElementById('rental-start').value, end_date: document.getElementById('rental-end').value, price: parseFloat(document.getElementById('rental-price').value)||0, deposit_collected: document.getElementById('rental-deposit-collected').checked, deposit_returned: document.getElementById('rental-deposit-returned').checked, status: document.getElementById('rental-status').value, return_notes: document.getElementById('rental-notes').value }; if (!data.start_date || !data.end_date) return; if (id) await api.put(`/api/rentals/${id}`, data); else await api.post('/api/rentals', data); closeModal('modal-rental'); loadRentals(); } async function openRentalDetail(id) { const r = await api.get(`/api/rentals/${id}`); const days = Math.max(1, Math.ceil((new Date(r.end_date) - new Date(r.start_date)) / 86400000) + 1); // reuse rental modal for edit await openRentalModal(r); document.getElementById('modal-rental-title').textContent = 'Modifier la location'; // add delete button const actions = document.querySelector('#modal-rental .modal-actions'); if (!document.getElementById('btn-del-rental')) { const del = document.createElement('button'); del.className = 'btn btn-danger'; del.id = 'btn-del-rental'; del.textContent = 'Supprimer'; del.onclick = async () => { if (!confirm('Supprimer cette location ?')) return; await api.del(`/api/rentals/${id}`); closeModal('modal-rental'); loadRentals(); }; actions.prepend(del); } else { document.getElementById('btn-del-rental').onclick = async () => { if (!confirm('Supprimer cette location ?')) return; await api.del(`/api/rentals/${id}`); closeModal('modal-rental'); loadRentals(); }; } } // ─── Calendar ──────────────────────────────────────────────────────────────── async function loadCalendar() { const data = await api.get(`/api/rentals/calendar?year=${calYear}&month=${calMonth}`); renderCalendar(data); } function calPrev() { calMonth--; if(calMonth<1){calMonth=12;calYear--;} loadCalendar(); } function calNext() { calMonth++; if(calMonth>12){calMonth=1;calYear++;} loadCalendar(); } // Palette de couleurs par outil (stable par ID) const CAL_COLORS = [ '#6366f1','#22c55e','#f59e0b','#38bdf8','#f472b6', '#a78bfa','#34d399','#fb923c','#60a5fa','#e879f9','#4ade80' ]; function toolColor(toolId) { return CAL_COLORS[(toolId - 1) % CAL_COLORS.length]; } function renderCalendar(rentals) { document.getElementById('cal-title').textContent = new Date(calYear, calMonth-1, 1).toLocaleDateString('fr-FR', {month:'long', year:'numeric'}); const today = new Date().toISOString().slice(0,10); const firstDay = new Date(calYear, calMonth-1, 1).getDay(); const daysInMonth = new Date(calYear, calMonth, 0).getDate(); const startOffset = (firstDay + 6) % 7; // Légende des outils présents ce mois const toolsInMonth = [...new Map(rentals.map(r => [r.tool_id, r])).values()]; const legend = toolsInMonth.map(r => `${esc(r.tool_name)}` ).join(''); let html = `
${legend}
`; ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'].forEach((d, i) => { html += `
${d}
`; }); for (let i = 0; i < startOffset; i++) html += `
`; for (let day = 1; day <= daysInMonth; day++) { const dateStr = `${calYear}-${String(calMonth).padStart(2,'0')}-${String(day).padStart(2,'0')}`; const dayRentals = rentals.filter(r => r.start_date <= dateStr && r.end_date >= dateStr); const isToday = dateStr === today; const dow = (startOffset + day - 1) % 7; const isWeekend = dow >= 5; const events = dayRentals.map(r => { const color = toolColor(r.tool_id); const isStart = r.start_date === dateStr; const isEnd = r.end_date === dateStr; const label = isStart ? `🔨 ${esc(r.tool_name)}` : esc(r.tool_name); return `
${label}
`; }).join(''); html += `
${day}
${events}
`; } // Remplir la dernière ligne const total = startOffset + daysInMonth; const remaining = total % 7 === 0 ? 0 : 7 - (total % 7); for (let i = 0; i < remaining; i++) html += `
`; html += `
`; document.getElementById('calendar-wrap').innerHTML = html; } // ─── Platforms ─────────────────────────────────────────────────────────────── async function loadPlatforms() { platforms = await api.get('/api/platforms'); const list = document.getElementById('platforms-list'); list.innerHTML = platforms.length ? platforms.map(p => `
${esc(p.name)}
`).join('') : '
Aucune plateforme
'; } async function addPlatform() { const name = document.getElementById('new-platform').value.trim(); if (!name) return; await api.post('/api/platforms', {name}); document.getElementById('new-platform').value = ''; loadPlatforms(); } document.getElementById('new-platform').addEventListener('keydown', e => { if(e.key==='Enter') addPlatform(); }); async function deletePlatform(id) { if (!confirm('Supprimer cette plateforme ?')) return; await api.del(`/api/platforms/${id}`); loadPlatforms(); } // ─── Utils ────────────────────────────────────────────────────────────────── function fmtEuro(v) { return Number(v).toLocaleString('fr-FR', {style:'currency', currency:'EUR'}); } function fmtDate(d) { if(!d) return '—'; return new Date(d+'T00:00:00').toLocaleDateString('fr-FR'); } function esc(s) { return (s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function statusLabel(s) { return {confirmed:'Confirmée', ongoing:'En cours', returned:'Retournée', cancelled:'Annulée'}[s] || s; } // ─── Listings ─────────────────────────────────────────────────────────────── let _matrix = null; async function loadListings() { _matrix = await api.get('/api/listings/matrix'); renderListingsMatrix(); } function renderListingsMatrix() { const { tools, platforms, listings } = _matrix; if (!tools.length) { document.getElementById('listings-matrix').innerHTML = '
Aucun outil
'; return; } if (!platforms.length) { document.getElementById('listings-matrix').innerHTML = '
Aucune plateforme configurée (allez dans ⚙️)
'; return; } let html = `
`; platforms.forEach(p => { html += ``; }); html += ``; tools.forEach(t => { html += ``; platforms.forEach(p => { const key = `${t.id},${p.id}`; const l = listings[key]; let cls = 'empty', icon = '+', title = 'Créer une annonce'; if (l) { if (l.is_active) { cls = 'active'; icon = '✓'; title = 'Annonce active — cliquer pour modifier'; } else { cls = 'inactive'; icon = '✗'; title = 'Annonce inactive — cliquer pour modifier'; } } html += ``; }); html += ``; }); html += `
Outil${esc(p.name)}
${esc(t.name)}
${fmtEuro(t.daily_price)}/j
`; document.getElementById('listings-matrix').innerHTML = html; } async function openListingModal(toolId, platformId) { const { tools, platforms, listings } = _matrix; const tool = tools.find(t => t.id === toolId); const platform = platforms.find(p => p.id === platformId); const key = `${toolId},${platformId}`; const l = listings[key] || null; document.getElementById('listing-id').value = l?.id || ''; document.getElementById('listing-tool-id').value = toolId; document.getElementById('listing-platform-id').value = platformId; document.getElementById('modal-listing-title').textContent = `Annonce — ${tool?.name}`; document.getElementById('listing-meta').textContent = `Plateforme : ${platform?.name}`; document.getElementById('listing-title').value = l?.title || (tool ? `${tool.name} à louer — ${tool.daily_price}€/jour` : ''); document.getElementById('listing-desc').value = l?.description || ''; document.getElementById('listing-price').value = l?.price ?? tool?.daily_price ?? ''; document.getElementById('listing-url').value = l?.url || ''; document.getElementById('listing-notes').value = l?.notes || ''; document.getElementById('listing-active').checked = l ? !!l.is_active : true; const delBtn = document.getElementById('btn-listing-delete'); const copyBtn = document.getElementById('btn-listing-copy'); if (l) { delBtn.style.display = ''; delBtn.onclick = () => deleteListing(l.id); copyBtn.style.display = ''; } else { delBtn.style.display = 'none'; copyBtn.style.display = 'none'; } document.getElementById('modal-listing').classList.remove('hidden'); } async function saveListing() { const data = { tool_id: parseInt(document.getElementById('listing-tool-id').value), platform_id: parseInt(document.getElementById('listing-platform-id').value), is_active: document.getElementById('listing-active').checked, title: document.getElementById('listing-title').value.trim(), description: document.getElementById('listing-desc').value.trim(), price: parseFloat(document.getElementById('listing-price').value) || null, url: document.getElementById('listing-url').value.trim(), notes: document.getElementById('listing-notes').value.trim() }; await api.post('/api/listings', data); closeModal('modal-listing'); _matrix = await api.get('/api/listings/matrix'); renderListingsMatrix(); } async function deleteListing(id) { if (!confirm('Supprimer cette annonce ?')) return; await api.del(`/api/listings/${id}`); closeModal('modal-listing'); _matrix = await api.get('/api/listings/matrix'); renderListingsMatrix(); } function copyListingText() { const title = document.getElementById('listing-title').value; const desc = document.getElementById('listing-desc').value; const price = document.getElementById('listing-price').value; const text = [title, price ? `Prix : ${price}€/jour` : '', '', desc].filter(Boolean).join('\n'); navigator.clipboard.writeText(text).then(() => { const btn = document.getElementById('btn-listing-copy'); btn.textContent = '✅ Copié !'; setTimeout(() => btn.textContent = '📋 Copier', 2000); }); } // ─── Lightbox ─────────────────────────────────────────────────────────────── let _lbImages = [], _lbIdx = 0; function openLightbox(images, idx) { _lbImages = images; _lbIdx = idx; _lbRender(); document.getElementById('lightbox').classList.remove('hidden'); document.addEventListener('keydown', _lbKey); } function closeLightbox() { document.getElementById('lightbox').classList.add('hidden'); document.removeEventListener('keydown', _lbKey); } function lightboxNav(dir) { _lbIdx = (_lbIdx + dir + _lbImages.length) % _lbImages.length; _lbRender(); } function _lbRender() { const url = _lbImages[_lbIdx]; const img = document.getElementById('lightbox-img'); img.src = url; document.getElementById('lightbox-counter').textContent = `${_lbIdx + 1} / ${_lbImages.length}`; const dl = document.getElementById('lightbox-download'); dl.href = url; dl.download = url.split('/').pop(); document.querySelector('.lightbox-prev').style.display = _lbImages.length > 1 ? '' : 'none'; document.querySelector('.lightbox-next').style.display = _lbImages.length > 1 ? '' : 'none'; } function _lbKey(e) { if (e.key === 'ArrowLeft') lightboxNav(-1); else if (e.key === 'ArrowRight') lightboxNav(1); else if (e.key === 'Escape') closeLightbox(); } // ─── Init ─────────────────────────────────────────────────────────────────── loadDashboard();