V0.9.49 UI updated to schedule

This commit is contained in:
2026-03-17 12:09:03 -04:00
parent 417952af40
commit 7b89985a3d
8 changed files with 188 additions and 130 deletions

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import { api } from '../utils/api.js';
import { useToast } from '../contexts/ToastContext.jsx';
import { useAuth } from '../contexts/AuthContext.jsx';
import UserFooter from './UserFooter.jsx';
// ── Utilities ─────────────────────────────────────────────────────────────────
const DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
@@ -153,21 +154,33 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
const [localTypes,setLocalTypes]=useState(eventTypes);
const typeRef=useRef(null);
// Auto end time when type changes (only for new events)
// When event type changes: auto set duration and default group
useEffect(()=>{
if(!typeId||event) return;
if(!sd||!st) return;
const typ=localTypes.find(t=>t.id===Number(typeId));
if(!typ?.default_duration_hrs||!sd||!st) return;
const dur=typ?.default_duration_hrs||1;
const start=buildISO(sd,st);
setEd(toDateIn(addHours(start,typ.default_duration_hrs)));
setEt(toTimeIn(addHours(start,typ.default_duration_hrs)));
if(typ.default_user_group_id) setGrps(prev=>new Set([...prev,Number(typ.default_user_group_id)]));
if(start){
setEd(toDateIn(addHours(start,dur)));
setEt(toTimeIn(addHours(start,dur)));
}
if(typ?.default_user_group_id&&!event) setGrps(prev=>new Set([...prev,Number(typ.default_user_group_id)]));
},[typeId]);
// Auto-match end date to start date when start date changes
// When start date changes: auto-match end date
useEffect(()=>{ if(!event) setEd(sd); },[sd]);
// When start time changes: auto-update end time preserving duration
useEffect(()=>{
if(!event) setEd(sd);
},[sd]);
if(!sd||!st) return;
const typ=localTypes.find(t=>t.id===Number(typeId));
const dur=typ?.default_duration_hrs||1;
const start=buildISO(sd,st);
if(start){
setEd(toDateIn(addHours(start,dur)));
setEt(toTimeIn(addHours(start,dur)));
}
},[st]);
const toggleGrp=id=>setGrps(prev=>{const n=new Set(prev);n.has(id)?n.delete(id):n.add(id);return n;});
const groupsRequired=track; // when tracking, groups are required
@@ -198,7 +211,49 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
style={{fontSize:20,fontWeight:700,border:'none',borderBottom:'2px solid var(--border)',borderRadius:0,padding:'4px 0',background:'transparent',width:'100%'}}/>
</div>
{/* Availability (first — if enabled, groups become required) */}
{/* Event Type */}
<Row label="Event Type">
<div style={{display:'flex',gap:8,alignItems:'center',position:'relative'}} ref={typeRef}>
<select className="input" value={typeId} onChange={e=>setTypeId(e.target.value)} style={{flex:1}}>
<option value="">Default</option>
{localTypes.filter(t=>!t.is_default).map(t=><option key={t.id} value={t.id}>{t.name}</option>)}
</select>
{isToolManager&&<button className="btn btn-secondary btn-sm" style={{flexShrink:0}} onClick={()=>setShowTypeForm(v=>!v)}>{showTypeForm?'Cancel':'+ Type'}</button>}
{showTypeForm&&<EventTypePopup userGroups={userGroups} onSave={et=>{setLocalTypes(p=>[...p,et]);setShowTypeForm(false);}} onClose={()=>setShowTypeForm(false)}/>}
</div>
</Row>
{/* Date/Time */}
<Row label="Date & Time">
<div style={{display:'flex',flexDirection:'column',gap:8}}>
<div style={{display:'flex',alignItems:'center',gap:8,flexWrap:'nowrap'}}>
<input type="date" className="input" value={sd} onChange={e=>setSd(e.target.value)} style={{width:150,flexShrink:0}}/>
{!allDay&&(
<>
<select className="input" value={st} onChange={e=>setSt(e.target.value)} style={{width:120,flexShrink:0}}>
{TIME_SLOTS.map(s=><option key={s.value} value={s.value}>{s.label}</option>)}
</select>
<span style={{color:'var(--text-tertiary)',fontSize:13,flexShrink:0}}>to</span>
<select className="input" value={et} onChange={e=>setEt(e.target.value)} style={{width:120,flexShrink:0}}>
{TIME_SLOTS.map(s=><option key={s.value} value={s.value}>{s.label}</option>)}
</select>
<input type="date" className="input" value={ed} onChange={e=>setEd(e.target.value)} style={{width:150,flexShrink:0}}/>
</>
)}
</div>
<div style={{display:'flex',alignItems:'center',gap:16}}>
<label style={{display:'flex',alignItems:'center',gap:8,fontSize:13,cursor:'pointer'}}>
<input type="checkbox" checked={allDay} onChange={e=>setAllDay(e.target.checked)}/> All day
</label>
<label style={{display:'flex',alignItems:'center',gap:8,fontSize:13,cursor:'pointer',color:'var(--text-tertiary)'}}>
<input type="checkbox" disabled title="Recurring events coming soon"/> Recurring
<span style={{fontSize:11,background:'var(--surface-variant)',borderRadius:10,padding:'1px 6px'}}>Coming soon</span>
</label>
</div>
</div>
</Row>
{/* Availability */}
<Row label="Availability">
<label style={{display:'flex',alignItems:'center',gap:10,fontSize:13,cursor:'pointer',paddingTop:6}}>
<input type="checkbox" checked={track} onChange={e=>{setTrack(e.target.checked);if(!e.target.checked) setPub(true);}}/>
@@ -237,42 +292,6 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
</Row>
)}
{/* Date/Time */}
<Row label="Date & Time">
<div style={{display:'flex',flexDirection:'column',gap:8}}>
<div style={{display:'flex',alignItems:'center',gap:8,flexWrap:'nowrap'}}>
<input type="date" className="input" value={sd} onChange={e=>setSd(e.target.value)} style={{width:150,flexShrink:0}}/>
{!allDay&&(
<>
<select className="input" value={st} onChange={e=>setSt(e.target.value)} style={{width:120,flexShrink:0}}>
{TIME_SLOTS.map(s=><option key={s.value} value={s.value}>{s.label}</option>)}
</select>
<span style={{color:'var(--text-tertiary)',fontSize:13,flexShrink:0}}>to</span>
<select className="input" value={et} onChange={e=>setEt(e.target.value)} style={{width:120,flexShrink:0}}>
{TIME_SLOTS.map(s=><option key={s.value} value={s.value}>{s.label}</option>)}
</select>
<input type="date" className="input" value={ed} onChange={e=>setEd(e.target.value)} style={{width:150,flexShrink:0}}/>
</>
)}
</div>
<label style={{display:'flex',alignItems:'center',gap:8,fontSize:13,cursor:'pointer'}}>
<input type="checkbox" checked={allDay} onChange={e=>setAllDay(e.target.checked)}/> All day
</label>
</div>
</Row>
{/* Event Type */}
<Row label="Event Type">
<div style={{display:'flex',gap:8,alignItems:'center',position:'relative'}} ref={typeRef}>
<select className="input" value={typeId} onChange={e=>setTypeId(e.target.value)} style={{flex:1}}>
<option value="">Default</option>
{localTypes.filter(t=>!t.is_default).map(t=><option key={t.id} value={t.id}>{t.name}</option>)}
</select>
{isToolManager&&<button className="btn btn-secondary btn-sm" style={{flexShrink:0}} onClick={()=>setShowTypeForm(v=>!v)}>{showTypeForm?'Cancel':'+ Type'}</button>}
{showTypeForm&&<EventTypePopup userGroups={userGroups} onSave={et=>{setLocalTypes(p=>[...p,et]);setShowTypeForm(false);}} onClose={()=>setShowTypeForm(false)}/>}
</div>
</Row>
{/* Location */}
<Row label="Location">
<input className="input" placeholder="Add location" value={loc} onChange={e=>setLoc(e.target.value)}/>
@@ -298,11 +317,19 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
const toast=useToast();
const [myResp,setMyResp]=useState(event.my_response);
const [avail,setAvail]=useState(event.availability||[]);
// Sync when parent reloads event after availability change
useEffect(()=>{setMyResp(event.my_response);setAvail(event.availability||[]);},[event]);
const counts={going:0,maybe:0,not_going:0};
avail.forEach(r=>{if(counts[r.response]!==undefined)counts[r.response]++;});
const handleResp=async resp=>{
try{if(myResp===resp){await api.deleteAvailability(event.id);setMyResp(null);}else{await api.setAvailability(event.id,resp);setMyResp(resp);}onAvailabilityChange?.();}catch(e){toast(e.message,'error');}
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);}
onAvailabilityChange?.(next); // triggers parent re-fetch to update avail list
}catch(e){setMyResp(prev);toast(e.message,'error');} // rollback on error
};
return ReactDOM.createPortal(
@@ -521,7 +548,7 @@ export default function SchedulePage({ isToolManager, isMobile }) {
{/* Left panel — matches sidebar width */}
{!isMobile && (
<div style={{ width:SIDEBAR_W, flexShrink:0, borderRight:'1px solid var(--border)', display:'flex', flexDirection:'column', background:'var(--surface)', overflow:'hidden' }}>
<div style={{ padding:'16px 16px 8px', borderBottom:'1px solid var(--border)' }}>
<div style={{ padding:'16px 16px 0' }}>
<div style={{ fontSize:16, fontWeight:700, marginBottom:12, color:'var(--text-primary)' }}>Team Schedule</div>
{/* Create button — styled like new-chat-btn */}
@@ -550,9 +577,12 @@ export default function SchedulePage({ isToolManager, isMobile }) {
</div>
{/* Mini calendar */}
<div style={{ padding:16 }}>
<div style={{ padding:'8px 16px 16px' }}>
<div className="section-label" style={{ marginBottom:8 }}>Filter Events</div>
<MiniCalendar selected={selDate} onChange={d=>{setSelDate(d);setPanel('calendar');}} eventDates={eventDates}/>
</div>
<div style={{ flex:1 }}/>
<UserFooter />
</div>
)}
@@ -637,7 +667,11 @@ export default function SchedulePage({ isToolManager, isMobile }) {
isToolManager={isToolManager}
onClose={() => setDetailEvent(null)}
onEdit={() => { setEditingEvent(detailEvent); setPanel('eventForm'); setDetailEvent(null); }}
onAvailabilityChange={() => openDetail(detailEvent)}
onAvailabilityChange={(resp) => {
// Update the list so the "awaiting response" dot disappears immediately
setEvents(prev => prev.map(e => e.id === detailEvent.id ? {...e, my_response: resp} : e));
openDetail(detailEvent);
}}
/>
)}
</div>