V0.9.49 UI updated to schedule
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user