From 6de6cfacf27825738dd55fc8d8d1e328d4b63781 Mon Sep 17 00:00:00 2001 From: Nathan FONTEYNE Date: Thu, 9 Jul 2026 18:15:34 +0200 Subject: [PATCH] fix: delete n8n old calendar to keep only cal one --- README.md | 2 +- public/css/style.css | 76 +++++++++++---- public/js/admin.js | 41 ++++---- public/js/calendar.js | 8 +- public/js/nav.js | 34 +++++-- src/db/migrations/011_calendar_users.sql | 33 +++++++ src/repositories/calendarRepo.js | 116 +++++++++-------------- src/routes/calendar.js | 21 +++- src/services/calendarSync.js | 20 ++-- 9 files changed, 220 insertions(+), 131 deletions(-) create mode 100644 src/db/migrations/011_calendar_users.sql diff --git a/README.md b/README.md index b2cb782..4009341 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ La garantie de confidentialité ne repose donc pas sur le fournisseur, mais sur ### Personnes suivies -La liste des personnes (et leur couleur) est amorcée en base par la migration `006_calendar_seed_people.sql` (Nathan, Raphaël, Yann, Jules, AK). De nouvelles personnes peuvent être ajoutées directement en base ; leurs flux de calendrier se gèrent ensuite depuis `/admin.html`. +Les personnes affichées sur le calendrier ne sont pas une liste séparée à maintenir : ce sont directement les utilisateurs de l'application (comptes Authentik) ayant au moins un calendrier enregistré. Depuis `/admin.html`, section "Calendriers des membres", un admin voit tous les utilisateurs et peut attacher un ou plusieurs flux ICS à n'importe lequel d'entre eux — dès qu'un utilisateur a au moins un flux, il apparaît automatiquement sur `/calendar.html` (avec une couleur assignée automatiquement, sans configuration). Un utilisateur sans flux configuré n'apparaît pas. ## Prérequis diff --git a/public/css/style.css b/public/css/style.css index 8807b5c..0735a76 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -180,21 +180,55 @@ nav#main-nav { margin-left: auto; } -.nav-user > a { - color: var(--accent); - text-decoration: none; - font-weight: 500; +.nav-profile { + position: relative; } -.nav-profile-link { +.nav-profile-trigger { display: flex; align-items: center; gap: 0.5rem; - color: var(--text) !important; + background: transparent; + border: none; + padding: 0.3rem 0.4rem; + border-radius: var(--radius-sm); + color: var(--text); font-weight: 500; + font-size: 0.88rem; + cursor: pointer; } -.nav-profile-link span { color: var(--text); } +.nav-profile-trigger:hover { background: var(--surface-alt); } + +.nav-profile-trigger span { color: var(--text); } + +.nav-profile-dropdown { + display: none; + position: absolute; + top: calc(100% + 0.4rem); + right: 0; + min-width: 180px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + box-shadow: var(--shadow); + padding: 0.35rem; + flex-direction: column; + z-index: 30; +} + +.nav-profile-dropdown.open { display: flex; } + +.nav-profile-dropdown a { + color: var(--text); + text-decoration: none; + font-weight: 500; + font-size: 0.9rem; + padding: 0.5rem 0.6rem; + border-radius: var(--radius-sm); +} + +.nav-profile-dropdown a:hover { background: var(--surface-alt); } #theme-toggle { border-radius: 999px; @@ -1068,16 +1102,31 @@ a.back-link:hover { color: var(--accent); } .nav-user { flex-direction: column; - align-items: flex-start; + align-items: stretch; gap: 0; margin-left: 0; } - .nav-user > a, - .nav-user > .nav-profile-link { + .nav-profile-trigger { width: 100%; padding: 0.5rem 0; border-bottom: 1px solid var(--border); + border-radius: 0; + } + + .nav-profile-dropdown { + position: static; + box-shadow: none; + border: none; + padding: 0; + min-width: 0; + } + + .nav-profile-dropdown a { + width: 100%; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); + border-radius: 0; } main { padding: 1.25rem 0.9rem 3rem; } @@ -1330,13 +1379,6 @@ a.back-link:hover { color: var(--accent); } gap: 0.6rem; } -.calendar-color-dot { - width: 10px; - height: 10px; - border-radius: 50%; - flex: 0 0 auto; -} - .calendar-feeds { display: flex; flex-direction: column; diff --git a/public/js/admin.js b/public/js/admin.js index e538443..0d3a1b4 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -56,16 +56,16 @@ function feedRowTemplate(feed) { `; } -function calendarPersonRowTemplate(person) { +function calendarUserRowTemplate(user) { return ` -
+
- -
${escapeHtml(person.name)}
+ ${avatarHtml(user, 'avatar-sm')} +
${escapeHtml(user.name)}${user.isAdmin ? ' admin' : ''}
- ${person.feeds.length ? person.feeds.map(feedRowTemplate).join('') : '

Aucun calendrier configuré.

'} -
+ ${user.feeds.length ? user.feeds.map(feedRowTemplate).join('') : '

Aucun calendrier configuré.

'} + @@ -75,29 +75,29 @@ function calendarPersonRowTemplate(person) { `; } -async function loadCalendarPeople() { - const people = await api.get('/api/calendar/people/admin'); +async function loadCalendarUsers() { + const users = await api.get('/api/calendar/people/admin'); const container = document.getElementById('calendar-people-list'); - container.innerHTML = people.length - ? people.map(calendarPersonRowTemplate).join('') - : '

Aucune personne suivie pour le moment.

'; + 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.personId, 10))); + 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, personId) { +async function onAddFeed(e, userId) { e.preventDefault(); const form = e.target; const label = form.label.value.trim(); const icsUrl = form.icsUrl.value.trim(); try { - await api.post(`/api/calendar/people/${personId}/feeds`, { label, icsUrl }); - await loadCalendarPeople(); + await api.post(`/api/calendar/people/${userId}/feeds`, { label, icsUrl }); + await loadCalendarUsers(); } catch (err) { showError(err.message); } @@ -106,7 +106,7 @@ async function onAddFeed(e, personId) { async function onRemoveFeed(feedId) { try { await api.del(`/api/calendar/feeds/${feedId}`); - await loadCalendarPeople(); + await loadCalendarUsers(); } catch (err) { showError(err.message); } @@ -197,9 +197,10 @@ async function onSaveSlotSettings(e) {

Calendriers des membres

- Chaque personne peut avoir plusieurs calendriers (Google, Outlook, Apple...). L'application ne - conserve jamais le contenu de ces calendriers — seul un statut disponible/occupé par créneau est - déduit et enregistré. + 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é.

Chargement…

@@ -207,7 +208,7 @@ async function onSaveSlotSettings(e) { `; document.getElementById('slot-settings-form').addEventListener('submit', onSaveSlotSettings); await loadSlotSettingsForm(); - await loadCalendarPeople(); + await loadCalendarUsers(); } catch (err) { showError(err.message); } diff --git a/public/js/calendar.js b/public/js/calendar.js index 6f301fb..2ec869f 100644 --- a/public/js/calendar.js +++ b/public/js/calendar.js @@ -177,14 +177,14 @@ function buildFilters() { const btn = document.getElementById('btn-refresh'); btn.disabled = true; btn.textContent = 'Actualisation…'; - showToast('Déclenchement du workflow n8n…'); + showToast('Synchronisation des calendriers…'); try { await api.post('/api/calendar/refresh', {}); - showToast('Workflow en cours — en attente des résultats…'); + showToast('Synchronisation en cours — en attente des résultats…'); pollWorkflowStatus(btn); } catch (err) { - const message = err.message === 'n8n_not_configured' - ? "L'actualisation automatique n'est pas configurée (n8n)." + const message = err.message === 'no_feeds_configured' + ? "Aucun calendrier n'est configuré — voir la page Administration." : err.message; showToast(message, true); resetRefreshButton(btn); diff --git a/public/js/nav.js b/public/js/nav.js index c1d224f..dfa4e9a 100644 --- a/public/js/nav.js +++ b/public/js/nav.js @@ -34,12 +34,16 @@ async function initNav(activePage) { ${me.isAdmin ? `Administration` : ''}