function formatDate(dateStr) { const d = new Date(dateStr); return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' }); } function showError(message) { document.getElementById('error').innerHTML = `
${escapeHtml(message)}
`; } function statTile(value, label) { return `
${value}
${escapeHtml(label)}
`; } function adminUserRowTemplate(u) { return `
${avatarHtml(u, 'avatar-sm')}
${escapeHtml(u.name)}${u.isAdmin ? ' admin' : ''}
Membre depuis ${formatDate(u.createdAt)} · Dernière connexion ${formatDate(u.lastSeenAt)}
🎵 ${u.songsAdded} 💡 ${u.suggestionsProposed} ✔ ${u.votesCast}
`; } // Shown for a registered feed instead of its full URL — an admin can already // see the real value via the "add calendar" flow when they set it, but the // list view doesn't need to keep re-displaying the plaintext on every load. function maskIcsUrl(url) { try { const u = new URL(url); return `${u.hostname}/••••`; } catch { return '••••'; } } function feedRowTemplate(feed) { return `
${feed.label ? `${escapeHtml(feed.label)} — ` : ''}${escapeHtml(maskIcsUrl(feed.icsUrl))}
`; } function calendarUserRowTemplate(user) { return `
${avatarHtml(user, 'avatar-sm')}
${escapeHtml(user.name)}${user.isAdmin ? ' admin' : ''}
${user.feeds.length ? user.feeds.map(feedRowTemplate).join('') : '

Aucun calendrier configuré.

'}
`; } async function loadCalendarUsers() { const users = await api.get('/api/calendar/people/admin'); const container = document.getElementById('calendar-people-list'); container.innerHTML = users.length ? users.map(calendarUserRowTemplate).join('') : '

Aucun utilisateur pour le moment.

'; container.querySelectorAll('.add-feed-form').forEach((form) => { form.addEventListener('submit', (e) => onAddFeed(e, parseInt(form.dataset.userId, 10))); }); container.querySelectorAll('.remove-feed-btn').forEach((btn) => { btn.addEventListener('click', () => onRemoveFeed(parseInt(btn.dataset.feedId, 10))); }); } async function onAddFeed(e, userId) { e.preventDefault(); const form = e.target; const label = form.label.value.trim(); const icsUrl = form.icsUrl.value.trim(); const submitBtn = form.querySelector('button[type="submit"]'); submitBtn.disabled = true; submitBtn.textContent = 'Vérification…'; try { await api.post(`/api/calendar/people/${userId}/feeds`, { label, icsUrl }); await loadCalendarUsers(); } catch (err) { showError(err.message); submitBtn.disabled = false; submitBtn.textContent = '+ Ajouter'; } } async function onRemoveFeed(feedId) { try { await api.del(`/api/calendar/feeds/${feedId}`); await loadCalendarUsers(); } catch (err) { showError(err.message); } } async function loadSlotSettingsForm() { const settings = await api.get('/api/calendar/settings'); const form = document.getElementById('slot-settings-form'); form.weekdayStart.value = settings.weekdayStart; form.weekdayEnd.value = settings.weekdayEnd; form.weekendStart.value = settings.weekendStart; form.weekendEnd.value = settings.weekendEnd; form.marginMinutes.value = settings.marginMinutes; } async function onSaveSlotSettings(e) { e.preventDefault(); const form = e.target; const statusEl = document.getElementById('slot-settings-status'); statusEl.textContent = ''; try { await api.patch('/api/calendar/settings', { weekdayStart: form.weekdayStart.value, weekdayEnd: form.weekdayEnd.value, weekendStart: form.weekendStart.value, weekendEnd: form.weekendEnd.value, marginMinutes: parseInt(form.marginMinutes.value, 10), }); statusEl.textContent = 'Horaires enregistrés.'; } catch (err) { showError(err.message); } } (async function init() { const me = await initNav('admin'); if (!me.isAdmin) { window.location.href = '/index.html'; return; } try { const stats = await api.get('/api/admin/stats'); const container = document.getElementById('content'); container.innerHTML = `

Administration

Vue d'ensemble de l'activité et des données de l'application.

Vue d'ensemble

${statTile(stats.userCount, 'Utilisateurs')} ${statTile(stats.songs.totalSongs, 'Morceaux au répertoire')} ${statTile(stats.songs.distinctArtists, 'Artistes différents')} ${statTile(stats.songs.tutorialsAdded, 'Tutoriels ajoutés')} ${statTile(stats.suggestions.pending, 'Suggestions en attente')} ${statTile(stats.suggestions.approved, 'Suggestions approuvées')} ${statTile(stats.suggestions.totalVotes, 'Votes exprimés')} ${statTile(stats.concerts.pastConcerts, 'Concerts passés')} ${statTile(stats.concerts.upcomingConcerts, 'Concerts à venir')} ${statTile(stats.concerts.avgSongsPerConcert, 'Morceaux / concert (moy.)')}

Activité des utilisateurs

La date de dernière connexion est approximative (dernière synchronisation du profil, pas un journal de connexion précis).

${stats.users.map(adminUserRowTemplate).join('')}

Créneaux de répétition

Horaires utilisés pour calculer les disponibilités sur /calendar.html.

La marge élargit uniquement la vérification de disponibilité (avant/après le créneau), pour tenir compte du temps de trajet entre deux évènements — le créneau affiché ne change pas.

Calendriers des membres

Chaque utilisateur de l'application peut avoir plusieurs calendriers (Google, Outlook, Apple...). Seuls les utilisateurs avec au moins un calendrier configuré apparaissent sur /calendar.html. L'application ne conserve jamais le contenu de ces calendriers — seul un statut disponible/occupé par créneau est déduit et enregistré. Comment trouver le lien ICS d'un utilisateur ?

Chargement…

`; document.getElementById('slot-settings-form').addEventListener('submit', onSaveSlotSettings); await loadSlotSettingsForm(); await loadCalendarUsers(); } catch (err) { showError(err.message); } })();