Vous n'avez pas encore ajouté de calendrier — ajoutez-en un pour apparaître dans les disponibilités.
+
+
@@ -63,6 +68,23 @@
+
+
+
+
Mes calendriers
+
+ Ajoutez le lien ICS (iCal) d'un ou plusieurs de vos calendriers pour apparaître dans les disponibilités.
+ L'application ne conserve jamais leur contenu — seul un statut disponible/occupé par créneau est déduit.
+
+
Chargement…
+
+
+
+
diff --git a/public/js/calendar.js b/public/js/calendar.js
index 2ec869f..d6b2e62 100644
--- a/public/js/calendar.js
+++ b/public/js/calendar.js
@@ -143,6 +143,7 @@ function renderCalendar() {
function buildFilters() {
const listEl = document.getElementById('people-list');
+ listEl.innerHTML = '';
for (const person of state.people) {
const row = document.createElement('label');
row.className = 'person-toggle';
@@ -172,24 +173,6 @@ function buildFilters() {
}
buildLegend();
-
- document.getElementById('btn-refresh').addEventListener('click', async () => {
- const btn = document.getElementById('btn-refresh');
- btn.disabled = true;
- btn.textContent = 'Actualisation…';
- showToast('Synchronisation des calendriers…');
- try {
- await api.post('/api/calendar/refresh', {});
- showToast('Synchronisation en cours — en attente des résultats…');
- pollWorkflowStatus(btn);
- } catch (err) {
- const message = err.message === 'no_feeds_configured'
- ? "Aucun calendrier n'est configuré — voir la page Administration."
- : err.message;
- showToast(message, true);
- resetRefreshButton(btn);
- }
- });
}
function resetRefreshButton(btn) {
@@ -278,6 +261,7 @@ function closeFilters() {
function buildLegend() {
const el = document.getElementById('legend');
+ el.innerHTML = '';
for (const person of state.people) {
const item = document.createElement('div');
item.className = 'legend-item';
@@ -307,6 +291,87 @@ function showError(message) {
document.getElementById('error').innerHTML = `
${escapeHtml(message)}
`;
}
+// ---------- Mes calendriers (self-service ICS feeds) ----------
+
+let myFeeds = [];
+
+// Shown for a registered feed instead of its full URL — consistent with the
+// admin/profile calendar-management panels.
+function maskIcsUrl(url) {
+ try {
+ const u = new URL(url);
+ return `${u.hostname}/••••`;
+ } catch {
+ return '••••';
+ }
+}
+
+function myFeedRowTemplate(feed) {
+ return `
+
diff --git a/public/js/profile.js b/public/js/profile.js
index d9cbf59..c2c592b 100644
--- a/public/js/profile.js
+++ b/public/js/profile.js
@@ -16,6 +16,62 @@ function statTile(value, label) {
`;
}
+// Shown for a registered feed instead of its full URL, consistent with the
+// admin calendar-management panel — the URL itself is a secret (whoever has
+// it can read that person's calendar), so the list view doesn't re-display
+// the plaintext on every load.
+function maskIcsUrl(url) {
+ try {
+ const u = new URL(url);
+ return `${u.hostname}/••••`;
+ } catch {
+ return '••••';
+ }
+}
+
+function myFeedRowTemplate(feed) {
+ return `
+
+ Ajoutez le lien ICS (iCal) d'un ou plusieurs de vos calendriers (Google, Outlook, Apple...) pour
+ apparaître dans les disponibilités du groupe sur la page Disponibilités.
+ L'application ne conserve jamais le contenu de vos calendriers — seul un statut disponible/occupé
+ par créneau est déduit et enregistré.
+
+
+
Chargement…
+
+
+
Identité gérée par Authentik — pour changer votre nom, email ou mot de passe,
@@ -55,6 +127,8 @@ function statTile(value, label) {
`;
+ document.getElementById('add-my-feed-form').addEventListener('submit', onAddMyFeed);
+ await loadMyFeeds();
} catch (err) {
showError(err.message);
}
diff --git a/src/repositories/calendarRepo.js b/src/repositories/calendarRepo.js
index 10e8e5a..d50b179 100644
--- a/src/repositories/calendarRepo.js
+++ b/src/repositories/calendarRepo.js
@@ -166,6 +166,18 @@ async function removeFeed(feedId) {
await pool.query('DELETE FROM calendar_feeds WHERE id = $1', [feedId]);
}
+// Scoped delete for the self-service "my calendars" endpoints — a user must
+// only ever be able to delete their own feeds, never guess another user's
+// feed id. Returns whether a row actually matched (both wrong id and
+// someone-else's feed look identical from the caller's side: nothing deleted).
+async function removeFeedForUser(feedId, userId) {
+ const { rowCount } = await pool.query('DELETE FROM calendar_feeds WHERE id = $1 AND user_id = $2', [
+ feedId,
+ userId,
+ ]);
+ return rowCount > 0;
+}
+
// Every app user (not just ones already on the calendar) with their
// registered feeds attached — lets an admin give someone their first feed,
// not just manage existing entries. The public getPeople() above deliberately
@@ -227,6 +239,7 @@ module.exports = {
findFeedsForUser,
addFeed,
removeFeed,
+ removeFeedForUser,
getUsersWithFeeds,
getSlotSettings,
updateSlotSettings,
diff --git a/src/routes/calendar.js b/src/routes/calendar.js
index a57a88d..ae41bd2 100644
--- a/src/routes/calendar.js
+++ b/src/routes/calendar.js
@@ -75,6 +75,43 @@ router.post(
})
);
+// Self-service: any authenticated user manages their own calendar feeds here
+// (no requireAdmin) — distinct from the /people/:id/feeds admin routes below,
+// which let an admin manage anyone's feeds on their behalf.
+router.get(
+ '/my-feeds',
+ asyncHandler(async (req, res) => {
+ const feeds = await calendarRepo.findFeedsForUser(req.user.id);
+ res.json(feeds.map((f) => ({ id: f.id, label: f.label, icsUrl: f.ics_url })));
+ })
+);
+
+router.post(
+ '/my-feeds',
+ asyncHandler(async (req, res) => {
+ const { label, icsUrl } = req.body || {};
+ if (!icsUrl || !icsUrl.trim()) {
+ return res.status(400).json({ error: 'ics_url_required' });
+ }
+ const feed = await calendarRepo.addFeed(req.user.id, {
+ label: label ? label.trim() : null,
+ icsUrl: icsUrl.trim(),
+ });
+ res.status(201).json({ id: feed.id, label: feed.label, icsUrl: feed.ics_url });
+ })
+);
+
+router.delete(
+ '/my-feeds/:feedId',
+ asyncHandler(async (req, res) => {
+ const deleted = await calendarRepo.removeFeedForUser(parseInt(req.params.feedId, 10), req.user.id);
+ if (!deleted) {
+ return res.status(404).json({ error: 'feed_not_found' });
+ }
+ res.status(204).end();
+ })
+);
+
router.get(
'/people/admin',
requireAdmin,