Compare commits

..

No commits in common. "d0e9dfb62ea42ab0a9fcbea9ad8560beef74aac5" and "b056af78acff3aceb8cb1c3918afc9c35561b4bd" have entirely different histories.

10 changed files with 30 additions and 145 deletions

View file

@ -15,11 +15,6 @@
--radius: 14px; --radius: 14px;
--radius-sm: 8px; --radius-sm: 8px;
--shadow: 0 1px 2px rgba(32, 31, 43, 0.04), 0 4px 16px rgba(32, 31, 43, 0.06); --shadow: 0 1px 2px rgba(32, 31, 43, 0.04), 0 4px 16px rgba(32, 31, 43, 0.06);
/* Higher than the dark-mode value below pale accent tints on a white
surface read as barely-there in light mode, so light needs a stronger mix
to hit the same perceived contrast dark mode gets "for free" against a
dark surface. */
--cal-marker-mix: 38%;
font-family: "Segoe UI", system-ui, -apple-system, Roboto, sans-serif; font-family: "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
} }
@ -39,7 +34,6 @@
--success: #4fd190; --success: #4fd190;
--success-bg: #163828; --success-bg: #163828;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 8px 24px rgba(0, 0, 0, 0.35); --shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 8px 24px rgba(0, 0, 0, 0.35);
--cal-marker-mix: 24%;
} }
} }
@ -57,7 +51,6 @@
--danger-bg: #3a201f; --danger-bg: #3a201f;
--success: #4fd190; --success: #4fd190;
--success-bg: #163828; --success-bg: #163828;
--cal-marker-mix: 24%;
} }
:root[data-theme="light"] { :root[data-theme="light"] {
@ -74,7 +67,6 @@
--danger-bg: #fdecea; --danger-bg: #fdecea;
--success: #1e8e5a; --success: #1e8e5a;
--success-bg: #e8f7ef; --success-bg: #e8f7ef;
--cal-marker-mix: 38%;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
@ -1562,8 +1554,6 @@ button.calendar-filters-close { display: none; }
.modal-section .rehearsal-actions { margin-top: 0.5rem; } .modal-section .rehearsal-actions { margin-top: 0.5rem; }
.rehearsal-accepted-by .avatar-sm { width: 1.3rem; height: 1.3rem; font-size: 0.65rem; } .rehearsal-accepted-by .avatar-sm { width: 1.3rem; height: 1.3rem; font-size: 0.65rem; }
.rehearsal-accepted-by .note { margin: 0; }
.rehearsal-refused-by { opacity: 0.75; }
.vote-badge { .vote-badge {
font-size: 0.85rem; font-size: 0.85rem;
@ -1575,10 +1565,6 @@ button.calendar-filters-close { display: none; }
.vote-badge.accept { border-left: 3px solid var(--success); } .vote-badge.accept { border-left: 3px solid var(--success); }
.vote-badge.reject { border-left: 3px solid var(--danger); } .vote-badge.reject { border-left: 3px solid var(--danger); }
.vote-badge.rehearsal-status-confirmed { border-left: 3px solid var(--accent); }
.vote-badge.rehearsal-status-suggested { border-left: 3px solid var(--success); }
.vote-badge.rehearsal-status-rejected { border-left: 3px solid var(--danger); }
.rehearsal-voted-section { margin-top: 0.75rem; } .rehearsal-voted-section { margin-top: 0.75rem; }
.rehearsal-voted-section > summary { .rehearsal-voted-section > summary {
@ -1607,22 +1593,11 @@ button.calendar-filters-close { display: none; }
/* Concert/rehearsal markers take priority over the availability heat colors /* Concert/rehearsal markers take priority over the availability heat colors
above (same specificity, later in the cascade) a scheduled date matters above (same specificity, later in the cascade) a scheduled date matters
more than who's free that day. more than who's free that day. */
Colors: confirmed rehearsals are purple (accent) the "settled, book it" .cal-cell.has-rehearsal { background: color-mix(in srgb, var(--accent) 20%, var(--surface)); border-color: var(--accent); }
state; still-pending suggestions are green an invitation to vote, not yet .cal-cell.has-concert { background: color-mix(in srgb, var(--accent-2) 20%, var(--surface)); border-color: var(--accent-2); }
settled; a rejected rehearsal (host said no, or too many refusals) is red. */ .cal-cell.has-rehearsal.has-concert {
.cal-cell.rehearsal-confirmed { background: color-mix(in srgb, var(--accent) var(--cal-marker-mix), var(--surface)); border-color: var(--accent); } background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 20%, var(--surface)), color-mix(in srgb, var(--accent-2) 20%, var(--surface)));
.cal-cell.rehearsal-suggested { background: color-mix(in srgb, var(--success) var(--cal-marker-mix), var(--surface)); border-color: var(--success); }
.cal-cell.rehearsal-rejected { background: color-mix(in srgb, var(--danger) var(--cal-marker-mix), var(--surface)); border-color: var(--danger); }
.cal-cell.has-concert { background: color-mix(in srgb, var(--accent-2) var(--cal-marker-mix), var(--surface)); border-color: var(--accent-2); }
.cal-cell.rehearsal-confirmed.has-concert {
background: linear-gradient(135deg, color-mix(in srgb, var(--accent) var(--cal-marker-mix), var(--surface)), color-mix(in srgb, var(--accent-2) var(--cal-marker-mix), var(--surface)));
}
.cal-cell.rehearsal-suggested.has-concert {
background: linear-gradient(135deg, color-mix(in srgb, var(--success) var(--cal-marker-mix), var(--surface)), color-mix(in srgb, var(--accent-2) var(--cal-marker-mix), var(--surface)));
}
.cal-cell.rehearsal-rejected.has-concert {
background: linear-gradient(135deg, color-mix(in srgb, var(--danger) var(--cal-marker-mix), var(--surface)), color-mix(in srgb, var(--accent-2) var(--cal-marker-mix), var(--surface)));
} }
/* ---------- Concerts: upcoming list + detail actions ---------- */ /* ---------- Concerts: upcoming list + detail actions ---------- */

View file

@ -179,14 +179,7 @@ async function onRemoveFeed(feedId) {
} }
} }
function hostOptionsHtml(users, selectedId) { async function loadSlotSettingsForm() {
const options = users.map(
(u) => `<option value="${u.id}"${u.id === selectedId ? ' selected' : ''}>${escapeHtml(u.name)}</option>`
);
return `<option value=""${selectedId ? '' : ' selected'}>(aucun)</option>${options.join('')}`;
}
async function loadSlotSettingsForm(users) {
const settings = await api.get('/api/calendar/settings'); const settings = await api.get('/api/calendar/settings');
const form = document.getElementById('slot-settings-form'); const form = document.getElementById('slot-settings-form');
form.weekdayStart.value = settings.weekdayStart; form.weekdayStart.value = settings.weekdayStart;
@ -197,7 +190,6 @@ async function loadSlotSettingsForm(users) {
form.concertStart.value = settings.concertStart; form.concertStart.value = settings.concertStart;
form.concertEnd.value = settings.concertEnd; form.concertEnd.value = settings.concertEnd;
form.rehearsalConfirmThreshold.value = settings.rehearsalConfirmThreshold; form.rehearsalConfirmThreshold.value = settings.rehearsalConfirmThreshold;
form.rehearsalHostUserId.innerHTML = hostOptionsHtml(users, settings.rehearsalHostUserId);
} }
async function onSaveSlotSettings(e) { async function onSaveSlotSettings(e) {
@ -215,7 +207,6 @@ async function onSaveSlotSettings(e) {
concertStart: form.concertStart.value, concertStart: form.concertStart.value,
concertEnd: form.concertEnd.value, concertEnd: form.concertEnd.value,
rehearsalConfirmThreshold: parseInt(form.rehearsalConfirmThreshold.value, 10), rehearsalConfirmThreshold: parseInt(form.rehearsalConfirmThreshold.value, 10),
rehearsalHostUserId: form.rehearsalHostUserId.value || null,
}); });
statusEl.textContent = 'Horaires enregistrés.'; statusEl.textContent = 'Horaires enregistrés.';
} catch (err) { } catch (err) {
@ -286,14 +277,6 @@ async function onSaveSlotSettings(e) {
n'est pas atteint ; elle passe ensuite en répétition confirmée (dans l'application et dans le n'est pas atteint ; elle passe ensuite en répétition confirmée (dans l'application et dans le
flux ICS). flux ICS).
</p> </p>
<label>Hôte des répétitions ()
<select name="rehearsalHostUserId"></select>
</label>
<p class="note">
Si un hôte est désigné, une répétition ne peut être confirmée que si cette personne a elle-même
accepté le créneau sans elle, pas de lieu pour répéter. Choisissez « (aucun) » pour désactiver
cette contrainte.
</p>
<p class="note" id="slot-settings-status"></p> <p class="note" id="slot-settings-status"></p>
<button type="submit">Enregistrer</button> <button type="submit">Enregistrer</button>
</form> </form>
@ -327,7 +310,7 @@ async function onSaveSlotSettings(e) {
`; `;
document.getElementById('slot-settings-form').addEventListener('submit', onSaveSlotSettings); document.getElementById('slot-settings-form').addEventListener('submit', onSaveSlotSettings);
document.getElementById('add-discord-webhook-form').addEventListener('submit', onAddDiscordWebhook); document.getElementById('add-discord-webhook-form').addEventListener('submit', onAddDiscordWebhook);
await loadSlotSettingsForm(stats.users); await loadSlotSettingsForm();
await loadCalendarUsers(); await loadCalendarUsers();
await loadDiscordWebhooks(); await loadDiscordWebhooks();
} catch (err) { } catch (err) {

View file

@ -106,7 +106,6 @@ function concertLinksHtml(concert) {
function rehearsalStatusLabel(r) { function rehearsalStatusLabel(r) {
if (r.status === 'confirmed') return 'Répétition confirmée'; if (r.status === 'confirmed') return 'Répétition confirmée';
if (r.status === 'rejected') return 'Répétition refusée';
const accepted = r.votes.filter((v) => v.vote === 'accept').length; const accepted = r.votes.filter((v) => v.vote === 'accept').length;
return `Suggestion de répétition (${accepted}/${r.confirmThreshold} votes)`; return `Suggestion de répétition (${accepted}/${r.confirmThreshold} votes)`;
} }
@ -217,14 +216,9 @@ function renderCalendar() {
const rehearsal = rehearsalsByDate.get(isoDate(date)); const rehearsal = rehearsalsByDate.get(isoDate(date));
if (rehearsal) { if (rehearsal) {
const statusTitles = { cell.classList.add('has-rehearsal');
confirmed: 'Répétition confirmée',
rejected: 'Répétition refusée',
suggested: 'Répétition proposée (en attente de votes)',
};
cell.classList.add('has-rehearsal', `rehearsal-${rehearsal.status}`);
const badge = document.createElement('span'); const badge = document.createElement('span');
badge.title = statusTitles[rehearsal.status] || statusTitles.suggested; badge.title = 'Répétition proposée';
badge.textContent = '🎸'; badge.textContent = '🎸';
badges.appendChild(badge); badges.appendChild(badge);
} }
@ -494,29 +488,13 @@ function rehearsalVoteActionsHtml(r) {
`; `;
} }
function rehearsalVoterHtml(v, r) {
const isHost = r.hostUserId && v.userId === r.hostUserId;
const name = isHost ? `${v.name}` : v.name;
return `${avatarHtml({ name: v.name, avatarUrl: v.avatarUrl }, 'avatar-sm')}<span>${escapeHtml(name)}</span>`;
}
function rehearsalAcceptedByHtml(r) { function rehearsalAcceptedByHtml(r) {
const accepted = r.votes.filter((v) => v.vote === 'accept'); const accepted = r.votes.filter((v) => v.vote === 'accept');
const refused = r.votes.filter((v) => v.vote === 'reject'); if (!accepted.length) return '';
if (!accepted.length && !refused.length) return '';
return ` return `
${accepted.length ? `
<div class="rehearsal-accepted-by"> <div class="rehearsal-accepted-by">
<span class="note">Ont accepté :</span> ${accepted.map((v) => `${avatarHtml({ name: v.name, avatarUrl: v.avatarUrl }, 'avatar-sm')}<span>${escapeHtml(v.name)}</span>`).join('')}
${accepted.map((v) => rehearsalVoterHtml(v, r)).join('')}
</div> </div>
` : ''}
${refused.length ? `
<div class="rehearsal-accepted-by rehearsal-refused-by">
<span class="note">Ont refusé :</span>
${refused.map((v) => rehearsalVoterHtml(v, r)).join('')}
</div>
` : ''}
`; `;
} }
@ -527,7 +505,7 @@ function rehearsalRowTemplate(r) {
return ` return `
<div class="rehearsal-row" data-rehearsal-id="${r.id}"> <div class="rehearsal-row" data-rehearsal-id="${r.id}">
<div> <div>
<span class="vote-badge rehearsal-status-${r.status}">${escapeHtml(rehearsalStatusLabel(r))}</span> <span class="vote-badge ${r.status === 'confirmed' ? 'accept' : ''}">${escapeHtml(rehearsalStatusLabel(r))}</span>
<div class="card-title">${formatDatetime(r.startsAt)} ${formatTime(r.endsAt)}</div> <div class="card-title">${formatDatetime(r.startsAt)} ${formatTime(r.endsAt)}</div>
<div class="card-subtitle">${r.location ? `${escapeHtml(r.location)} · ` : ''}Proposée par ${escapeHtml(r.proposedByName)}</div> <div class="card-subtitle">${r.location ? `${escapeHtml(r.location)} · ` : ''}Proposée par ${escapeHtml(r.proposedByName)}</div>
${rehearsalAcceptedByHtml(r)} ${rehearsalAcceptedByHtml(r)}

View file

@ -1,2 +0,0 @@
ALTER TABLE calendar_settings
ADD COLUMN IF NOT EXISTS rehearsal_host_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;

View file

@ -40,11 +40,8 @@ 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');
} }
// Returns null for a rejected rehearsal — those are dropped entirely from the function rehearsalToEvent(rehearsal, { threshold, baseUrl, allUsers }) {
// shared feed (see buildRehearsalsFeed) rather than shown as tentative/cancelled. const status = computeRehearsalStatus(rehearsal.votes, threshold);
function rehearsalToEvent(rehearsal, { threshold, hostUserId, baseUrl, allUsers }) {
const status = computeRehearsalStatus(rehearsal.votes, threshold, hostUserId);
if (status === 'rejected') return null;
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}`;
@ -77,7 +74,7 @@ function rehearsalToEvent(rehearsal, { threshold, hostUserId, baseUrl, allUsers
return lines; return lines;
} }
function buildRehearsalsFeed(rehearsals, { threshold, hostUserId, baseUrl, allUsers }) { function buildRehearsalsFeed(rehearsals, { threshold, baseUrl, allUsers }) {
const lines = [ const lines = [
'BEGIN:VCALENDAR', 'BEGIN:VCALENDAR',
'VERSION:2.0', 'VERSION:2.0',
@ -85,10 +82,7 @@ function buildRehearsalsFeed(rehearsals, { threshold, hostUserId, baseUrl, allUs
'CALSCALE:GREGORIAN', 'CALSCALE:GREGORIAN',
'METHOD:PUBLISH', 'METHOD:PUBLISH',
'X-WR-CALNAME:Répétitions Octane', 'X-WR-CALNAME:Répétitions Octane',
...rehearsals ...rehearsals.flatMap((r) => rehearsalToEvent(r, { threshold, baseUrl, allUsers })),
.map((r) => rehearsalToEvent(r, { threshold, hostUserId, baseUrl, allUsers }))
.filter(Boolean)
.flat(),
'END:VCALENDAR', 'END:VCALENDAR',
]; ];
return lines.map(foldLine).join('\r\n'); return lines.map(foldLine).join('\r\n');

View file

@ -1,22 +1,8 @@
// Shared between the API (rehearsals list) and the ICS feed so both agree on // Shared between the API (rehearsals list) and the ICS feed so both agree on
// when a proposed rehearsal counts as confirmed, rejected, or still a // when a proposed rehearsal counts as confirmed.
// suggestion. When a host is configured, their acceptance is mandatory: function computeRehearsalStatus(votes, threshold) {
// without the host there is nowhere to play, so an explicit host refusal (or
// too many "reject" votes) kills the rehearsal outright rather than leaving
// it as a pending suggestion.
function computeRehearsalStatus(votes, threshold, hostUserId) {
const acceptCount = votes.filter((v) => v.vote === 'accept').length; const acceptCount = votes.filter((v) => v.vote === 'accept').length;
const rejectCount = votes.filter((v) => v.vote === 'reject').length; return acceptCount >= threshold ? 'confirmed' : 'suggested';
const hostVote = hostUserId ? votes.find((v) => v.userId === hostUserId) : null;
if ((hostVote && hostVote.vote === 'reject') || rejectCount >= threshold) {
return 'rejected';
}
if (acceptCount < threshold) return 'suggested';
if (hostUserId && !(hostVote && hostVote.vote === 'accept')) {
return 'suggested';
}
return 'confirmed';
} }
module.exports = { computeRehearsalStatus }; module.exports = { computeRehearsalStatus };

View file

@ -205,12 +205,7 @@ function parseTime(hhmmss) {
async function getSlotSettings() { async function getSlotSettings() {
const { rows } = await pool.query( const { rows } = await pool.query(
`SELECT s.weekday_start, s.weekday_end, s.weekend_start, s.weekend_end, s.margin_minutes, 'SELECT weekday_start, weekday_end, weekend_start, weekend_end, margin_minutes, concert_start, concert_end, rehearsal_confirm_threshold FROM calendar_settings WHERE id = 1'
s.concert_start, s.concert_end, s.rehearsal_confirm_threshold, s.rehearsal_host_user_id,
h.name AS rehearsal_host_name
FROM calendar_settings s
LEFT JOIN users h ON h.id = s.rehearsal_host_user_id
WHERE s.id = 1`
); );
const row = rows[0]; const row = rows[0];
const weekdayStart = parseTime(row.weekday_start); const weekdayStart = parseTime(row.weekday_start);
@ -225,19 +220,17 @@ async function getSlotSettings() {
marginMinutes: row.margin_minutes, marginMinutes: row.margin_minutes,
concert: { startHour: concertStart.hour, startMinute: concertStart.minute, endHour: concertEnd.hour, endMinute: concertEnd.minute }, concert: { startHour: concertStart.hour, startMinute: concertStart.minute, endHour: concertEnd.hour, endMinute: concertEnd.minute },
rehearsalConfirmThreshold: row.rehearsal_confirm_threshold, rehearsalConfirmThreshold: row.rehearsal_confirm_threshold,
rehearsalHostUserId: row.rehearsal_host_user_id,
rehearsalHostName: row.rehearsal_host_name,
}; };
} }
async function updateSlotSettings({ weekdayStart, weekdayEnd, weekendStart, weekendEnd, marginMinutes, concertStart, concertEnd, rehearsalConfirmThreshold, rehearsalHostUserId }) { async function updateSlotSettings({ weekdayStart, weekdayEnd, weekendStart, weekendEnd, marginMinutes, concertStart, concertEnd, rehearsalConfirmThreshold }) {
const { rows } = await pool.query( const { rows } = await pool.query(
`UPDATE calendar_settings `UPDATE calendar_settings
SET weekday_start = $1, weekday_end = $2, weekend_start = $3, weekend_end = $4, margin_minutes = $5, SET weekday_start = $1, weekday_end = $2, weekend_start = $3, weekend_end = $4, margin_minutes = $5,
concert_start = $6, concert_end = $7, rehearsal_confirm_threshold = $8, rehearsal_host_user_id = $9 concert_start = $6, concert_end = $7, rehearsal_confirm_threshold = $8
WHERE id = 1 WHERE id = 1
RETURNING weekday_start, weekday_end, weekend_start, weekend_end, margin_minutes, concert_start, concert_end, rehearsal_confirm_threshold, rehearsal_host_user_id`, RETURNING weekday_start, weekday_end, weekend_start, weekend_end, margin_minutes, concert_start, concert_end, rehearsal_confirm_threshold`,
[weekdayStart, weekdayEnd, weekendStart, weekendEnd, marginMinutes, concertStart, concertEnd, rehearsalConfirmThreshold, rehearsalHostUserId || null] [weekdayStart, weekdayEnd, weekendStart, weekendEnd, marginMinutes, concertStart, concertEnd, rehearsalConfirmThreshold]
); );
return rows[0]; return rows[0];
} }

View file

@ -188,8 +188,6 @@ router.get(
concertStart: formatTime(settings.concert.startHour, settings.concert.startMinute), concertStart: formatTime(settings.concert.startHour, settings.concert.startMinute),
concertEnd: formatTime(settings.concert.endHour, settings.concert.endMinute), concertEnd: formatTime(settings.concert.endHour, settings.concert.endMinute),
rehearsalConfirmThreshold: settings.rehearsalConfirmThreshold, rehearsalConfirmThreshold: settings.rehearsalConfirmThreshold,
rehearsalHostUserId: settings.rehearsalHostUserId,
rehearsalHostName: settings.rehearsalHostName,
}); });
}) })
); );
@ -216,7 +214,7 @@ router.patch(
'/settings', '/settings',
requireAdmin, requireAdmin,
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const { weekdayStart, weekdayEnd, weekendStart, weekendEnd, marginMinutes, concertStart, concertEnd, rehearsalConfirmThreshold, rehearsalHostUserId } = req.body || {}; const { weekdayStart, weekdayEnd, weekendStart, weekendEnd, marginMinutes, concertStart, concertEnd, rehearsalConfirmThreshold } = req.body || {};
const times = { weekdayStart, weekdayEnd, weekendStart, weekendEnd, concertStart, concertEnd }; const times = { weekdayStart, weekdayEnd, weekendStart, weekendEnd, concertStart, concertEnd };
for (const [key, value] of Object.entries(times)) { for (const [key, value] of Object.entries(times)) {
if (!TIME_RE.test(value || '')) { if (!TIME_RE.test(value || '')) {
@ -241,19 +239,7 @@ router.patch(
return res.status(400).json({ error: 'invalid_rehearsal_confirm_threshold' }); return res.status(400).json({ error: 'invalid_rehearsal_confirm_threshold' });
} }
let hostUserId = null; const values = { ...times, marginMinutes: margin, rehearsalConfirmThreshold: threshold };
if (rehearsalHostUserId !== null && rehearsalHostUserId !== undefined && rehearsalHostUserId !== '') {
hostUserId = Number(rehearsalHostUserId);
if (!Number.isInteger(hostUserId)) {
return res.status(400).json({ error: 'invalid_rehearsal_host_user_id' });
}
const host = await usersRepo.findById(hostUserId);
if (!host) {
return res.status(400).json({ error: 'rehearsal_host_user_not_found' });
}
}
const values = { ...times, marginMinutes: margin, rehearsalConfirmThreshold: threshold, rehearsalHostUserId: hostUserId };
await calendarRepo.updateSlotSettings(values); await calendarRepo.updateSlotSettings(values);
res.json(values); res.json(values);
}) })

View file

@ -26,12 +26,7 @@ router.get(
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, { .send(buildRehearsalsFeed(rehearsals, { threshold: settings.rehearsalConfirmThreshold, baseUrl, allUsers }));
threshold: settings.rehearsalConfirmThreshold,
hostUserId: settings.rehearsalHostUserId,
baseUrl,
allUsers,
}));
}) })
); );

View file

@ -12,7 +12,6 @@ router.get(
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const [rehearsals, settings] = await Promise.all([rehearsalsRepo.findUpcoming(), calendarRepo.getSlotSettings()]); const [rehearsals, settings] = await Promise.all([rehearsalsRepo.findUpcoming(), calendarRepo.getSlotSettings()]);
const threshold = settings.rehearsalConfirmThreshold; const threshold = settings.rehearsalConfirmThreshold;
const hostUserId = settings.rehearsalHostUserId;
res.json( res.json(
rehearsals.map((r) => ({ rehearsals.map((r) => ({
id: r.id, id: r.id,
@ -22,10 +21,8 @@ router.get(
proposedBy: r.proposed_by, proposedBy: r.proposed_by,
proposedByName: r.proposed_by_name, proposedByName: r.proposed_by_name,
votes: r.votes, votes: r.votes,
status: computeRehearsalStatus(r.votes, threshold, hostUserId), status: computeRehearsalStatus(r.votes, threshold),
confirmThreshold: threshold, confirmThreshold: threshold,
hostUserId,
hostName: settings.rehearsalHostName,
})) }))
); );
}) })