add the option for the user to add a note to their availability
This commit is contained in:
@@ -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&¬eChanged&&(
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user