From e8e941c436df1d02e4b7553bd6ecdb43eeb0b6a3 Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Mon, 30 Mar 2026 08:39:55 -0400 Subject: [PATCH] v0.12.42 new availibilty list download --- backend/package.json | 2 +- backend/src/routes/schedule.js | 17 +++--- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/SchedulePage.jsx | 66 +++++++++++++++++++++++- 5 files changed, 79 insertions(+), 10 deletions(-) diff --git a/backend/package.json b/backend/package.json index 11b8104..a8df2e7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.41", + "version": "0.12.42", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/routes/schedule.js b/backend/src/routes/schedule.js index b9fc3cb..88c4f93 100644 --- a/backend/src/routes/schedule.js +++ b/backend/src/routes/schedule.js @@ -242,16 +242,21 @@ router.get('/:id', authMiddleware, async (req, res) => { `, [event.id, req.user.id])); if (event.track_availability && (itm || isMember)) { event.availability = await query(req.schema, ` - SELECT ea.response, ea.note, ea.updated_at, u.id AS user_id, u.name, u.display_name, u.avatar + SELECT ea.response, ea.note, ea.updated_at, u.id AS user_id, u.name, u.first_name, u.last_name, u.display_name, u.avatar FROM event_availability ea JOIN users u ON u.id=ea.user_id WHERE ea.event_id=$1 `, [req.params.id]); if (itm) { - const assignedIds = (await query(req.schema, ` - SELECT DISTINCT ugm.user_id FROM event_user_groups eug - JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id WHERE eug.event_id=$1 - `, [req.params.id])).map(r => r.user_id); + const assignedRows = await query(req.schema, ` + SELECT DISTINCT u.id AS user_id, u.name, u.first_name, u.last_name, u.display_name + FROM event_user_groups eug + JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id + JOIN users u ON u.id=ugm.user_id + WHERE eug.event_id=$1 + `, [req.params.id]); const respondedIds = new Set(event.availability.map(r => r.user_id)); - event.no_response_count = assignedIds.filter(id => !respondedIds.has(id)).length; + const noResponseRows = assignedRows.filter(r => !respondedIds.has(r.user_id)); + event.no_response_count = noResponseRows.length; + event.no_response_users = noResponseRows; } } const mine = await queryOne(req.schema, 'SELECT response, note FROM event_availability WHERE event_id=$1 AND user_id=$2', [req.params.id, req.user.id]); diff --git a/build.sh b/build.sh index 76eaa2b..11d8706 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.41}" +VERSION="${1:-0.12.42}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index e00ebb4..a517550 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.41", + "version": "0.12.42", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/SchedulePage.jsx b/frontend/src/components/SchedulePage.jsx index a10ec94..5a9dcc8 100644 --- a/frontend/src/components/SchedulePage.jsx +++ b/frontend/src/components/SchedulePage.jsx @@ -823,6 +823,57 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool const toggleNote=id=>setExpandedNotes(prev=>{const s=new Set(prev);s.has(id)?s.delete(id):s.add(id);return s;}); + const handleDownloadAvailability = () => { + // Format as "Lastname, Firstname" using first_name/last_name fields when available + const fmtName = u => { + const last = (u.last_name || '').trim(); + const first = (u.first_name || '').trim(); + if (last && first) return `${last}, ${first}`; + // Fall back to splitting the combined name field + const parts = (u.name || u.display_name || 'Unknown').trim().split(/\s+/); + if (parts.length >= 2) return `${parts[parts.length - 1]}, ${parts.slice(0, -1).join(' ')}`; + return parts[0] || 'Unknown'; + }; + const sortByLastName = arr => [...arr].sort((a, b) => fmtName(a).localeCompare(fmtName(b))); + const fmtEntry = u => { + const note = (u.note || '').trim(); + return note ? `${fmtName(u)} - Note: ${note}` : fmtName(u); + }; + + const going = sortByLastName(avail.filter(r => r.response === 'going')); + const maybe = sortByLastName(avail.filter(r => r.response === 'maybe')); + const notGoing = sortByLastName(avail.filter(r => r.response === 'not_going')); + const noResp = sortByLastName(event.no_response_users || []); + + const sections = [ + { heading: 'Going', rows: going }, + { heading: 'Maybe', rows: maybe }, + { heading: 'Not Going', rows: notGoing }, + { heading: 'No Response', rows: noResp }, + ]; + + const eventDate = event.start_at ? fmtDate(new Date(event.start_at)) : ''; + const lines = [`${event.title}${eventDate ? ' — ' + eventDate : ''}`, '']; + for (const sec of sections) { + lines.push(`#### ${sec.heading}`); + if (sec.rows.length === 0) { + lines.push('(none)'); + } else { + sec.rows.forEach(r => lines.push(fmtEntry(r))); + } + lines.push(''); + } + + const blob = new Blob([lines.join('\n')], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const safeName = (event.title || 'event').replace(/[^a-z0-9]+/gi, '_').toLowerCase(); + a.href = url; + a.download = `availability_${safeName}.txt`; + a.click(); + URL.revokeObjectURL(url); + }; + return ReactDOM.createPortal(
e.target===e.currentTarget&&onClose()}>
@@ -861,7 +912,20 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool {!!event.track_availability&&(
-
Your Availability
+
+
Your Availability
+ {isToolManager&&( + + )} +
{isPast ? (

Past event — availability is read-only.

) : (