update: add who refused and who did not give answer yet in ics feed description
Some checks failed
CI/CD / test (push) Has been cancelled
CI/CD / build-and-push (push) Has been cancelled

This commit is contained in:
Nathan FONTEYNE 2026-07-31 12:19:13 +02:00
parent 3a56ef44c3
commit b056af78ac
3 changed files with 24 additions and 5 deletions

View file

@ -40,15 +40,22 @@ function foldLine(line) {
return chunks.map((chunk, i) => (i === 0 ? chunk : ` ${chunk}`)).join('\r\n'); return chunks.map((chunk, i) => (i === 0 ? chunk : ` ${chunk}`)).join('\r\n');
} }
function rehearsalToEvent(rehearsal, { threshold, baseUrl }) { function rehearsalToEvent(rehearsal, { threshold, baseUrl, allUsers }) {
const status = computeRehearsalStatus(rehearsal.votes, threshold); const status = computeRehearsalStatus(rehearsal.votes, threshold);
const icsStatus = status === 'confirmed' ? 'CONFIRMED' : 'TENTATIVE'; const icsStatus = status === 'confirmed' ? 'CONFIRMED' : 'TENTATIVE';
const summary = status === 'confirmed' ? 'Répétition' : 'Répétition (proposition)'; const summary = status === 'confirmed' ? 'Répétition' : 'Répétition (proposition)';
const voteUrl = `${baseUrl}/calendar.html?rehearsalId=${rehearsal.id}`; const voteUrl = `${baseUrl}/calendar.html?rehearsalId=${rehearsal.id}`;
const accepted = rehearsal.votes.filter((v) => v.vote === 'accept').map((v) => v.name); const accepted = rehearsal.votes.filter((v) => v.vote === 'accept').map((v) => v.name);
const refused = rehearsal.votes.filter((v) => v.vote === 'reject').map((v) => v.name);
const votedUserIds = new Set(rehearsal.votes.map((v) => v.userId));
const pending = allUsers.filter((u) => !votedUserIds.has(u.id)).map((u) => u.name);
const description = [ const description = [
`Votez ici : ${voteUrl}`, `Votez ici : ${voteUrl}`,
accepted.length ? `Ont accepté : ${accepted.join(', ')}` : "Personne n'a encore accepté.", accepted.length ? `Ont accepté : ${accepted.join(', ')}` : "Personne n'a encore accepté.",
refused.length ? `Ont refusé : ${refused.join(', ')}` : "Personne n'a refusé.",
pending.length ? `En attente : ${pending.join(', ')}` : "Tout le monde a voté.",
].join('\n'); ].join('\n');
const lines = [ const lines = [
@ -67,7 +74,7 @@ function rehearsalToEvent(rehearsal, { threshold, baseUrl }) {
return lines; return lines;
} }
function buildRehearsalsFeed(rehearsals, { threshold, baseUrl }) { function buildRehearsalsFeed(rehearsals, { threshold, baseUrl, allUsers }) {
const lines = [ const lines = [
'BEGIN:VCALENDAR', 'BEGIN:VCALENDAR',
'VERSION:2.0', 'VERSION:2.0',
@ -75,7 +82,7 @@ function buildRehearsalsFeed(rehearsals, { threshold, baseUrl }) {
'CALSCALE:GREGORIAN', 'CALSCALE:GREGORIAN',
'METHOD:PUBLISH', 'METHOD:PUBLISH',
'X-WR-CALNAME:Répétitions Octane', 'X-WR-CALNAME:Répétitions Octane',
...rehearsals.flatMap((r) => rehearsalToEvent(r, { threshold, baseUrl })), ...rehearsals.flatMap((r) => rehearsalToEvent(r, { threshold, baseUrl, allUsers })),
'END:VCALENDAR', 'END:VCALENDAR',
]; ];
return lines.map(foldLine).join('\r\n'); return lines.map(foldLine).join('\r\n');

View file

@ -92,6 +92,13 @@ async function findByIcsToken(token) {
return rows[0] || null; return rows[0] || null;
} }
// Minimal id/name list — used to work out who *hasn't* voted on a rehearsal
// yet (the ICS feed's "en attente" list), so no need for the full profile.
async function findAllNames() {
const { rows } = await pool.query('SELECT id, name FROM users ORDER BY name');
return rows;
}
module.exports = { module.exports = {
findById, findById,
upsertFromClaims, upsertFromClaims,
@ -100,4 +107,5 @@ module.exports = {
findAllWithActivity, findAllWithActivity,
ensureIcsToken, ensureIcsToken,
findByIcsToken, findByIcsToken,
findAllNames,
}; };

View file

@ -17,12 +17,16 @@ router.get(
const user = await usersRepo.findByIcsToken(req.params.token); const user = await usersRepo.findByIcsToken(req.params.token);
if (!user) return res.status(404).send('Not found'); if (!user) return res.status(404).send('Not found');
const [rehearsals, settings] = await Promise.all([rehearsalsRepo.findUpcoming(), calendarRepo.getSlotSettings()]); const [rehearsals, settings, allUsers] = await Promise.all([
rehearsalsRepo.findUpcoming(),
calendarRepo.getSlotSettings(),
usersRepo.findAllNames(),
]);
// req.protocol honors X-Forwarded-Proto here — see app.set('trust proxy', 1) in app.js. // req.protocol honors X-Forwarded-Proto here — see app.set('trust proxy', 1) in app.js.
const baseUrl = `${req.protocol}://${req.get('host')}`; const baseUrl = `${req.protocol}://${req.get('host')}`;
res res
.type('text/calendar; charset=utf-8') .type('text/calendar; charset=utf-8')
.send(buildRehearsalsFeed(rehearsals, { threshold: settings.rehearsalConfirmThreshold, baseUrl })); .send(buildRehearsalsFeed(rehearsals, { threshold: settings.rehearsalConfirmThreshold, baseUrl, allUsers }));
}) })
); );