add the option for the user to add a note to their availability

This commit is contained in:
2026-03-28 19:54:01 -04:00
parent a43d067e61
commit 1ed9d9d95e
4 changed files with 95 additions and 24 deletions

View File

@@ -768,23 +768,45 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isToolManager, userId }) {
const toast=useToast();
const [myResp,setMyResp]=useState(event.my_response);
const [myNote,setMyNote]=useState(event.my_note||'');
const [noteInput,setNoteInput]=useState(event.my_note||'');
const [noteSaving,setNoteSaving]=useState(false);
const [avail,setAvail]=useState(event.availability||[]);
const [expandedNotes,setExpandedNotes]=useState(new Set());
// Sync when parent reloads event after availability change
useEffect(()=>{setMyResp(event.my_response);setAvail(event.availability||[]);},[event]);
useEffect(()=>{
setMyResp(event.my_response);
setAvail(event.availability||[]);
setMyNote(event.my_note||'');
setNoteInput(event.my_note||'');
},[event]);
const counts={going:0,maybe:0,not_going:0};
avail.forEach(r=>{if(counts[r.response]!==undefined)counts[r.response]++;});
const isPast = !event.all_day && event.end_at && new Date(event.end_at) < new Date();
const noteChanged = noteInput.trim() !== myNote.trim();
const handleResp=async resp=>{
const prev=myResp;
const next=myResp===resp?null:resp;
setMyResp(next); // optimistic update
try{
if(prev===resp){await api.deleteAvailability(event.id);}else{await api.setAvailability(event.id,resp);}
if(prev===resp){await api.deleteAvailability(event.id);}else{await api.setAvailability(event.id,resp,noteInput.trim()||null);}
onAvailabilityChange?.(next); // triggers parent re-fetch to update avail list
}catch(e){setMyResp(prev);toast(e.message,'error');} // rollback on error
};
const handleNoteSave=async()=>{
if(!myResp) return; // no response row to attach note to
setNoteSaving(true);
try{
await api.setAvailabilityNote(event.id,noteInput.trim()||null);
setMyNote(noteInput.trim());
onAvailabilityChange?.(myResp); // re-fetch to update responses list
}catch(e){toast(e.message,'error');}finally{setNoteSaving(false);}
};
const toggleNote=id=>setExpandedNotes(prev=>{const s=new Set(prev);s.has(id)?s.delete(id):s.add(id);return s;});
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={e=>e.target===e.currentTarget&&onClose()}>
<div className="modal" style={{maxWidth:520,maxHeight:'88vh',overflowY:'auto'}}>
@@ -827,13 +849,31 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
{isPast ? (
<p style={{fontSize:13,color:'var(--text-tertiary)',marginBottom:16}}>Past event availability is read-only.</p>
) : (
<div style={{display:'flex',gap:8,marginBottom:16}}>
{Object.entries(RESP_LABEL).map(([key,label])=>(
<button key={key} onClick={()=>handleResp(key)} style={{flex:1,padding:'9px 4px',borderRadius:'var(--radius)',border:`2px solid ${RESP_COLOR[key]}`,background:myResp===key?RESP_COLOR[key]:'transparent',color:myResp===key?'white':RESP_COLOR[key],fontSize:13,fontWeight:600,cursor:'pointer',transition:'all 0.15s'}}>
{myResp===key?'✓ ':''}{label}
</button>
))}
</div>
<>
<div style={{display:'flex',gap:8,marginBottom:12}}>
{Object.entries(RESP_LABEL).map(([key,label])=>(
<button key={key} onClick={()=>handleResp(key)} style={{flex:1,padding:'9px 4px',borderRadius:'var(--radius)',border:`2px solid ${RESP_COLOR[key]}`,background:myResp===key?RESP_COLOR[key]:'transparent',color:myResp===key?'white':RESP_COLOR[key],fontSize:13,fontWeight:600,cursor:'pointer',transition:'all 0.15s'}}>
{myResp===key?'✓ ':''}{label}
</button>
))}
</div>
<div style={{display:'flex',gap:8,alignItems:'center',marginBottom:16}}>
<input
type="text"
value={noteInput}
onChange={e=>setNoteInput(e.target.value.slice(0,20))}
placeholder="Add a note (optional)"
maxLength={20}
style={{flex:1,padding:'7px 10px',borderRadius:'var(--radius)',border:'1px solid var(--border)',background:'var(--surface)',color:'var(--text-primary)',fontSize:13,outline:'none'}}
/>
<span style={{fontSize:11,color:'var(--text-tertiary)',flexShrink:0,minWidth:32,textAlign:'right'}}>{noteInput.length}/20</span>
{myResp&&noteChanged&&(
<button onClick={handleNoteSave} disabled={noteSaving} className="btn btn-primary btn-sm" style={{flexShrink:0}}>
{noteSaving?'…':'Save'}
</button>
)}
</div>
</>
)}
{(isToolManager||avail.length>0)&&(
<>
@@ -844,13 +884,30 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
</div>
{avail.length>0&&(
<div style={{border:'1px solid var(--border)',borderRadius:'var(--radius)',overflow:'hidden'}}>
{avail.map(r=>(
<div key={r.user_id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 12px',borderBottom:'1px solid var(--border)',fontSize:13}}>
<span style={{width:9,height:9,borderRadius:'50%',background:RESP_COLOR[r.response],flexShrink:0,display:'inline-block'}}/>
<span style={{flex:1}}>{r.display_name||r.name}</span>
<span style={{color:RESP_COLOR[r.response],fontSize:12,fontWeight:600}}>{RESP_LABEL[r.response]}</span>
</div>
))}
{avail.map(r=>{
const hasNote=!!(r.note&&r.note.trim());
const expanded=expandedNotes.has(r.user_id);
return(
<div key={r.user_id} style={{borderBottom:'1px solid var(--border)'}}>
<div
style={{display:'flex',alignItems:'center',gap:10,padding:'8px 12px',fontSize:13,cursor:hasNote?'pointer':'default'}}
onClick={hasNote?()=>toggleNote(r.user_id):undefined}
>
<span style={{width:9,height:9,borderRadius:'50%',background:RESP_COLOR[r.response],flexShrink:0,display:'inline-block'}}/>
<span style={{flex:1}}>{r.display_name||r.name}</span>
{hasNote&&(
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2.5" style={{flexShrink:0,transition:'transform 0.15s',transform:expanded?'rotate(180deg)':'rotate(0deg)'}}><polyline points="6 9 12 15 18 9"/></svg>
)}
<span style={{color:RESP_COLOR[r.response],fontSize:12,fontWeight:600}}>{RESP_LABEL[r.response]}</span>
</div>
{hasNote&&expanded&&(
<div style={{padding:'0 12px 10px 31px',fontSize:12,color:'var(--text-secondary)',fontStyle:'italic'}}>
{r.note}
</div>
)}
</div>
);
})}
</div>
)}
</>

View File

@@ -119,7 +119,8 @@ export const api = {
createEvent: (body) => req('POST', '/schedule', body), // body may include recurrenceRule: {freq,interval,byDay,ends,endDate,endCount}
updateEvent: (id, body) => req('PATCH', `/schedule/${id}`, body),
deleteEvent: (id, scope = 'this') => req('DELETE', `/schedule/${id}`, { recurringScope: scope }),
setAvailability: (id, response) => req('PUT', `/schedule/${id}/availability`, { response }),
setAvailability: (id, response, note) => req('PUT', `/schedule/${id}/availability`, { response, note }),
setAvailabilityNote: (id, note) => req('PATCH', `/schedule/${id}/availability/note`, { note }),
deleteAvailability: (id) => req('DELETE', `/schedule/${id}/availability`),
getPendingAvailability: () => req('GET', '/schedule/me/pending'),
bulkAvailability: (responses) => req('POST', '/schedule/me/bulk-availability', { responses }),