v0.12.42 new availibilty list download
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rosterchirp-backend",
|
||||
"version": "0.12.41",
|
||||
"version": "0.12.42",
|
||||
"description": "RosterChirp backend server",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
2
build.sh
2
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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rosterchirp-frontend",
|
||||
"version": "0.12.41",
|
||||
"version": "0.12.42",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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(
|
||||
<div className="modal-overlay" onClick={e=>e.target===e.currentTarget&&onClose()}>
|
||||
<div className="modal" style={{maxWidth:520,maxHeight:'88vh',overflowY:'auto'}}>
|
||||
@@ -861,7 +912,20 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
|
||||
|
||||
{!!event.track_availability&&(
|
||||
<div style={{borderTop:'1px solid var(--border)',paddingTop:16,marginTop:4}}>
|
||||
<div style={{fontSize:12,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.6px',marginBottom:10}}>Your Availability</div>
|
||||
<div style={{display:'flex',alignItems:'center',marginBottom:10}}>
|
||||
<div style={{fontSize:12,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.6px',flex:1}}>Your Availability</div>
|
||||
{isToolManager&&(
|
||||
<button
|
||||
onClick={handleDownloadAvailability}
|
||||
title="Download Availability List"
|
||||
style={{background:'none',border:'none',padding:2,cursor:'pointer',color:'var(--text-secondary)',display:'flex',alignItems:'center',borderRadius:4}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" width="18" height="18">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isPast ? (
|
||||
<p style={{fontSize:13,color:'var(--text-tertiary)',marginBottom:16}}>Past event — availability is read-only.</p>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user