Files
rosterchirp-dev/frontend/src/components/SchedulePage.jsx

646 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
import { api } from '../utils/api.js';
import { useToast } from '../contexts/ToastContext.jsx';
import { useAuth } from '../contexts/AuthContext.jsx';
// ── Utilities ─────────────────────────────────────────────────────────────────
const DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const SHORT_MONTHS= ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function fmtDate(d) { return `${d.getDate()} ${SHORT_MONTHS[d.getMonth()]} ${d.getFullYear()}`; }
function fmtTime(iso) { if(!iso) return ''; const d=new Date(iso); return d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); }
function fmtRange(s,e) { return `${fmtTime(s)} ${fmtTime(e)}`; }
function toDateIn(iso) { return iso?iso.slice(0,10):''; }
function toTimeIn(iso) { if(!iso) return ''; const d=new Date(iso); const h=String(d.getHours()).padStart(2,'0'), m=d.getMinutes()<30?'00':'30'; return `${h}:${m}`; }
function buildISO(d,t) { return d&&t?`${d}T${t}:00`:''; }
function addHours(iso,h){ const d=new Date(iso); d.setMinutes(d.getMinutes()+h*60); return d.toISOString().slice(0,19); }
function sameDay(a,b) { return a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth()&&a.getDate()===b.getDate(); }
function weekStart(d) { const r=new Date(d); r.setDate(d.getDate()-d.getDay()); r.setHours(0,0,0,0); return r; }
function daysInMonth(y,m){ return new Date(y,m+1,0).getDate(); }
const RESP_LABEL = { going:'Going', maybe:'Maybe', not_going:'Not Going' };
const RESP_COLOR = { going:'#22c55e', maybe:'#f59e0b', not_going:'#ef4444' };
// 30-minute time slots
const TIME_SLOTS = (() => {
const s=[];
for(let h=0;h<24;h++) for(let m of [0,30]) {
const hh=String(h).padStart(2,'0'), mm=String(m).padStart(2,'0');
const disp=`${h===0?12:h>12?h-12:h}:${mm} ${h<12?'AM':'PM'}`;
s.push({value:`${hh}:${mm}`,label:disp});
}
return s;
})();
// ── Mini Calendar (desktop) ───────────────────────────────────────────────────
function MiniCalendar({ selected, onChange, eventDates=new Set() }) {
const [cur, setCur] = useState(()=>{ const d=new Date(selected||Date.now()); d.setDate(1); return d; });
const y=cur.getFullYear(), m=cur.getMonth(), first=new Date(y,m,1).getDay(), total=daysInMonth(y,m), today=new Date();
const cells=[]; for(let i=0;i<first;i++) cells.push(null); for(let d=1;d<=total;d++) cells.push(d);
return (
<div style={{userSelect:'none'}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8,fontSize:13,fontWeight:600}}>
<button style={{background:'none',border:'none',cursor:'pointer',padding:'2px 8px',color:'var(--text-secondary)',fontSize:16}} onClick={()=>{const n=new Date(cur);n.setMonth(m-1);setCur(n);}}></button>
<span>{MONTHS[m]} {y}</span>
<button style={{background:'none',border:'none',cursor:'pointer',padding:'2px 8px',color:'var(--text-secondary)',fontSize:16}} onClick={()=>{const n=new Date(cur);n.setMonth(m+1);setCur(n);}}></button>
</div>
<div style={{display:'grid',gridTemplateColumns:'repeat(7,1fr)',gap:1,fontSize:11}}>
{DAYS.map(d=><div key={d} style={{textAlign:'center',fontWeight:600,color:'var(--text-tertiary)',padding:'2px 0'}}>{d[0]}</div>)}
{cells.map((d,i)=>{
if(!d) return <div key={i}/>;
const date=new Date(y,m,d), isSel=selected&&sameDay(date,new Date(selected)), isToday=sameDay(date,today);
const key=`${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
return (
<div key={i} onClick={()=>onChange(date)} style={{textAlign:'center',padding:'3px 2px',borderRadius:4,cursor:'pointer',background:isSel?'var(--primary)':'transparent',color:isSel?'white':isToday?'var(--primary)':'var(--text-primary)',fontWeight:isToday?700:400,position:'relative'}}>
{d}
{eventDates.has(key)&&!isSel&&<span style={{position:'absolute',bottom:1,left:'50%',transform:'translateX(-50%)',width:4,height:4,borderRadius:'50%',background:'var(--primary)',display:'block'}}/>}
</div>
);
})}
</div>
</div>
);
}
// ── Mobile Date Picker (accordion month view) ─────────────────────────────────
function MobileDatePicker({ selected, onChange, eventDates=new Set() }) {
const [open, setOpen] = useState(false);
const [cur, setCur] = useState(()=>{ const d=new Date(selected||Date.now()); d.setDate(1); return d; });
const y=cur.getFullYear(), m=cur.getMonth(), first=new Date(y,m,1).getDay(), total=daysInMonth(y,m);
const cells=[]; for(let i=0;i<first;i++) cells.push(null); for(let d=1;d<=total;d++) cells.push(d);
const today=new Date();
return (
<div style={{borderBottom:'1px solid var(--border)',background:'var(--surface)'}}>
<button onClick={()=>setOpen(v=>!v)} style={{display:'flex',alignItems:'center',justifyContent:'space-between',width:'100%',padding:'10px 16px',background:'none',border:'none',cursor:'pointer',fontSize:14,fontWeight:600,color:'var(--text-primary)'}}>
<span>{MONTHS[m]} {y}</span>
<span style={{fontSize:10,transform:open?'rotate(180deg)':'none',display:'inline-block',transition:'transform 0.2s'}}></span>
</button>
{open && (
<div style={{padding:'8px 12px 12px',userSelect:'none'}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}>
<button style={{background:'none',border:'none',cursor:'pointer',padding:'4px 10px',fontSize:16,color:'var(--text-secondary)'}} onClick={()=>{const n=new Date(cur);n.setMonth(m-1);setCur(n);}}></button>
<button style={{background:'none',border:'none',cursor:'pointer',padding:'4px 10px',fontSize:16,color:'var(--text-secondary)'}} onClick={()=>{const n=new Date(cur);n.setMonth(m+1);setCur(n);}}></button>
</div>
<div style={{display:'grid',gridTemplateColumns:'repeat(7,1fr)',gap:2,fontSize:12}}>
{DAYS.map(d=><div key={d} style={{textAlign:'center',fontWeight:600,color:'var(--text-tertiary)',padding:'2px 0'}}>{d[0]}</div>)}
{cells.map((d,i)=>{
if(!d) return <div key={i}/>;
const date=new Date(y,m,d), isSel=selected&&sameDay(date,new Date(selected)), isToday=sameDay(date,today);
const key=`${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
return (
<div key={i} onClick={()=>{onChange(date);setOpen(false);}} style={{textAlign:'center',padding:'5px 2px',borderRadius:4,cursor:'pointer',background:isSel?'var(--primary)':'transparent',color:isSel?'white':isToday?'var(--primary)':'var(--text-primary)',fontWeight:isToday?700:400,position:'relative'}}>
{d}
{eventDates.has(key)&&!isSel&&<span style={{position:'absolute',bottom:2,left:'50%',transform:'translateX(-50%)',width:4,height:4,borderRadius:'50%',background:'var(--primary)',display:'block'}}/>}
</div>
);
})}
</div>
</div>
)}
</div>
);
}
// ── Event Type Popup ──────────────────────────────────────────────────────────
function EventTypePopup({ userGroups, onSave, onClose, editing=null }) {
const toast=useToast();
const DUR=[1,1.5,2,2.5,3,3.5,4,4.5,5];
const [name,setName]=useState(editing?.name||'');
const [colour,setColour]=useState(editing?.colour||'#6366f1');
const [groupId,setGroupId]=useState(editing?.default_user_group_id||'');
const [dur,setDur]=useState(editing?.default_duration_hrs||1);
const [useDur,setUseDur]=useState(!!(editing?.default_duration_hrs));
const [saving,setSaving]=useState(false);
const handle=async()=>{
if(!name.trim()) return toast('Name required','error');
setSaving(true);
try{const body={name:name.trim(),colour,defaultUserGroupId:groupId||null,defaultDurationHrs:useDur?dur:null};const r=editing?await api.updateEventType(editing.id,body):await api.createEventType(body);onSave(r.eventType);onClose();}catch(e){toast(e.message,'error');}finally{setSaving(false);}
};
return (
<div style={{position:'absolute',top:'100%',left:0,zIndex:300,background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'var(--radius)',padding:16,width:270,boxShadow:'0 4px 20px rgba(0,0,0,0.2)'}}>
<div style={{marginBottom:8}}><label className="settings-section-label">Name</label><input className="input" value={name} onChange={e=>setName(e.target.value)} style={{marginTop:4}} autoFocus/></div>
<div style={{marginBottom:8}}><label className="settings-section-label">Colour</label><input type="color" value={colour} onChange={e=>setColour(e.target.value)} style={{marginTop:4,width:'100%',height:32,padding:2,borderRadius:4,border:'1px solid var(--border)'}}/></div>
<div style={{marginBottom:8}}><label className="settings-section-label">Default Group</label><select className="input" value={groupId} onChange={e=>setGroupId(e.target.value)} style={{marginTop:4}}><option value="">None</option>{userGroups.map(g=><option key={g.id} value={g.id}>{g.name}</option>)}</select></div>
<div style={{marginBottom:12}}>
<label style={{display:'flex',alignItems:'center',gap:8,fontSize:13,cursor:'pointer'}}><input type="checkbox" checked={useDur} onChange={e=>setUseDur(e.target.checked)}/> Set default duration</label>
{useDur&&<select className="input" value={dur} onChange={e=>setDur(Number(e.target.value))} style={{marginTop:6}}>{DUR.map(d=><option key={d} value={d}>{d}hr{d!==1?'s':''}</option>)}</select>}
</div>
<div style={{display:'flex',gap:8}}><button className="btn btn-primary btn-sm" onClick={handle} disabled={saving}>{saving?'…':'Save'}</button><button className="btn btn-secondary btn-sm" onClick={onClose}>Cancel</button></div>
</div>
);
}
// ── Event Form ────────────────────────────────────────────────────────────────
function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCancel, onDelete, isToolManager }) {
const toast=useToast();
const def=selectedDate?selectedDate.toISOString().slice(0,10):new Date().toISOString().slice(0,10);
const [title,setTitle]=useState(event?.title||'');
const [typeId,setTypeId]=useState(event?.event_type_id||'');
const [sd,setSd]=useState(event?toDateIn(event.start_at):def);
const [st,setSt]=useState(event?toTimeIn(event.start_at):'09:00');
const [ed,setEd]=useState(event?toDateIn(event.end_at):def);
const [et,setEt]=useState(event?toTimeIn(event.end_at):'10:00');
const [allDay,setAllDay]=useState(!!event?.all_day);
const [loc,setLoc]=useState(event?.location||'');
const [desc,setDesc]=useState(event?.description||'');
const [pub,setPub]=useState(event?!!event.is_public:true);
const [track,setTrack]=useState(!!event?.track_availability);
const [grps,setGrps]=useState(new Set((event?.user_groups||[]).map(g=>g.id)));
const [saving,setSaving]=useState(false);
const [showTypeForm,setShowTypeForm]=useState(false);
const [localTypes,setLocalTypes]=useState(eventTypes);
const typeRef=useRef(null);
// Auto end time when type changes (only for new events)
useEffect(()=>{
if(!typeId||event) return;
const typ=localTypes.find(t=>t.id===Number(typeId));
if(!typ?.default_duration_hrs||!sd||!st) return;
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)]));
},[typeId]);
// Auto-match end date to start date when start date changes
useEffect(()=>{
if(!event) setEd(sd);
},[sd]);
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
const handle=async()=>{
if(!title.trim()) return toast('Title required','error');
if(!allDay&&(!sd||!st||!ed||!et)) return toast('Start and end required','error');
if(groupsRequired&&grps.size===0) return toast('Select at least one group for availability tracking','error');
setSaving(true);
try{const body={title:title.trim(),eventTypeId:typeId||null,startAt:allDay?`${sd}T00:00:00`:buildISO(sd,st),endAt:allDay?`${ed}T23:59:59`:buildISO(ed,et),allDay,location:loc,description:desc,isPublic:pub,trackAvailability:track,userGroupIds:[...grps]};const r=event?await api.updateEvent(event.id,body):await api.createEvent(body);onSave(r.event);}catch(e){toast(e.message,'error');}finally{setSaving(false);}
};
const Row=({label,children,required})=>(
<div style={{display:'flex',alignItems:'flex-start',gap:0,marginBottom:16}}>
<div style={{width:120,flexShrink:0,fontSize:13,color:'var(--text-tertiary)',paddingTop:9,paddingRight:16,textAlign:'right',whiteSpace:'nowrap'}}>
{label}{required&&<span style={{color:'var(--error)'}}> *</span>}
</div>
<div style={{flex:1,minWidth:0}}>{children}</div>
</div>
);
return (
<div style={{width:'100%',maxWidth:1024,overflowX:'auto'}}>
<div style={{minWidth:500}}>
{/* Title */}
<div style={{marginBottom:20}}>
<input className="input" placeholder="Add title" value={title} onChange={e=>setTitle(e.target.value)}
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) */}
<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);}}/>
Track availability for assigned groups
</label>
</Row>
{/* Groups — required when tracking */}
<Row label="Groups" required={groupsRequired}>
<div>
<div style={{border:`1px solid ${groupsRequired&&grps.size===0?'var(--error)':'var(--border)'}`,borderRadius:'var(--radius)',overflow:'hidden',maxHeight:160,overflowY:'auto'}}>
{userGroups.length===0
?<div style={{padding:'10px 14px',fontSize:13,color:'var(--text-tertiary)'}}>No user groups yet</div>
:userGroups.map(g=>(
<label key={g.id} style={{display:'flex',alignItems:'center',gap:10,padding:'7px 12px',borderBottom:'1px solid var(--border)',cursor:'pointer',fontSize:13}}>
<input type="checkbox" checked={grps.has(g.id)} onChange={()=>toggleGrp(g.id)} style={{accentColor:'var(--primary)'}}/>
{g.name}
</label>
))}
</div>
<p style={{fontSize:11,color:groupsRequired&&grps.size===0?'var(--error)':'var(--text-tertiary)',marginTop:4}}>
{grps.size===0
? (groupsRequired?'At least one group required for availability tracking':'No groups — event visible to all (if public)')
: `${grps.size} group${grps.size!==1?'s':''} selected`}
</p>
</div>
</Row>
{/* Visibility — only shown if groups selected OR tracking */}
{(grps.size>0||track) && (
<Row label="Visibility">
<label style={{display:'flex',alignItems:'center',gap:10,fontSize:13,cursor:'pointer',paddingTop:6}}>
<input type="checkbox" checked={!pub} onChange={e=>setPub(!e.target.checked)}/>
Viewable by selected groups only (private)
</label>
</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)}/>
</Row>
{/* Description */}
<Row label="Description">
<textarea className="input" placeholder="Add description" value={desc} onChange={e=>setDesc(e.target.value)} rows={3} style={{resize:'vertical'}}/>
</Row>
<div style={{display:'flex',gap:8,marginTop:8}}>
<button className="btn btn-primary btn-sm" onClick={handle} disabled={saving}>{saving?'Saving…':event?'Save Changes':'Create Event'}</button>
<button className="btn btn-secondary btn-sm" onClick={onCancel}>Cancel</button>
{event&&isToolManager&&<button className="btn btn-sm" style={{marginLeft:'auto',background:'var(--error)',color:'white'}} onClick={()=>onDelete(event)}>Delete</button>}
</div>
</div>
</div>
);
}
// ── Event Detail Modal ────────────────────────────────────────────────────────
function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isToolManager }) {
const toast=useToast();
const [myResp,setMyResp]=useState(event.my_response);
const [avail,setAvail]=useState(event.availability||[]);
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');}
};
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={e=>e.target===e.currentTarget&&onClose()}>
<div className="modal" style={{maxWidth:520,maxHeight:'88vh',overflowY:'auto'}}>
<div style={{display:'flex',alignItems:'flex-start',justifyContent:'space-between',marginBottom:16}}>
<div style={{flex:1,paddingRight:12}}>
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:4}}>
{event.event_type&&<span style={{width:13,height:13,borderRadius:'50%',background:event.event_type.colour,flexShrink:0,display:'inline-block'}}/>}
<h2 style={{fontSize:20,fontWeight:700,margin:0}}>{event.title}</h2>
</div>
<div style={{fontSize:13,color:'var(--text-secondary)',display:'flex',alignItems:'center',gap:8}}>
{event.event_type?.name&&<span>{event.event_type.name}</span>}
{!event.is_public&&<span style={{background:'var(--surface-variant)',borderRadius:10,padding:'1px 8px',fontSize:11}}>Private</span>}
</div>
</div>
<div style={{display:'flex',gap:6,flexShrink:0}}>
{isToolManager&&<button className="btn btn-secondary btn-sm" onClick={()=>{onClose();onEdit();}}>Edit</button>}
<button className="btn-icon" onClick={onClose}><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div>
</div>
<div style={{display:'flex',gap:10,alignItems:'center',marginBottom:12,fontSize:14}}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<span>{fmtDate(new Date(event.start_at))}{!event.all_day&&` · ${fmtRange(event.start_at,event.end_at)}`}</span>
</div>
{event.location&&<div style={{display:'flex',gap:10,alignItems:'center',marginBottom:12,fontSize:14}}><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>{event.location}</div>}
{event.description&&<div style={{display:'flex',gap:10,marginBottom:12,fontSize:14}}><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{flexShrink:0,marginTop:2}}><line x1="21" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="3" y2="18"/></svg><span style={{whiteSpace:'pre-wrap'}}>{event.description}</span></div>}
{(event.user_groups||[]).length>0&&<div style={{display:'flex',gap:10,marginBottom:16,fontSize:13,color:'var(--text-secondary)'}}><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{flexShrink:0,marginTop:2}}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/></svg>{event.user_groups.map(g=>g.name).join(', ')}</div>}
{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',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>
{isToolManager&&(
<>
<div style={{fontSize:12,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.6px',marginBottom:8}}>Responses</div>
<div style={{display:'flex',gap:20,marginBottom:10,fontSize:13}}>
{Object.entries(counts).map(([k,n])=><span key={k}><span style={{color:RESP_COLOR[k],fontWeight:700}}>{n}</span> {RESP_LABEL[k]}</span>)}
<span><span style={{fontWeight:700}}>{event.no_response_count||0}</span> No response</span>
</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>
))}
</div>
)}
</>
)}
</div>
)}
</div>
</div>,
document.body
);
}
// ── Event Types Panel ─────────────────────────────────────────────────────────
function EventTypesPanel({ eventTypes, userGroups, onUpdated }) {
const toast=useToast();
const [editingType,setEditingType]=useState(null);
const [showForm,setShowForm]=useState(false);
const handleDel=async et=>{
if(!confirm(`Delete "${et.name}"?`)) return;
try{await api.deleteEventType(et.id);toast('Deleted','success');onUpdated();}catch(e){toast(e.message,'error');}
};
return (
<div style={{maxWidth:560}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:16}}>
<div className="settings-section-label" style={{margin:0}}>Event Types</div>
<div style={{position:'relative'}}><button className="btn btn-primary btn-sm" onClick={()=>{setShowForm(v=>!v);setEditingType(null);}}>+ New Type</button>{showForm&&!editingType&&<EventTypePopup userGroups={userGroups} onSave={()=>onUpdated()} onClose={()=>setShowForm(false)}/>}</div>
</div>
<div style={{display:'flex',flexDirection:'column',gap:6}}>
{eventTypes.map(et=>(
<div key={et.id} style={{display:'flex',alignItems:'center',gap:10,padding:'9px 14px',border:'1px solid var(--border)',borderRadius:'var(--radius)'}}>
<span style={{width:16,height:16,borderRadius:'50%',background:et.colour,flexShrink:0}}/>
<span style={{flex:1,fontSize:14,fontWeight:500}}>{et.name}</span>
{et.default_duration_hrs&&<span style={{fontSize:12,color:'var(--text-tertiary)'}}>{et.default_duration_hrs}hr default</span>}
{!et.is_protected?(
<div style={{display:'flex',gap:6,position:'relative'}}>
<button className="btn btn-secondary btn-sm" onClick={()=>{setEditingType(et);setShowForm(true);}}>Edit</button>
{showForm&&editingType?.id===et.id&&<EventTypePopup editing={et} userGroups={userGroups} onSave={()=>{onUpdated();setShowForm(false);setEditingType(null);}} onClose={()=>{setShowForm(false);setEditingType(null);}}/>}
<button className="btn btn-sm" style={{background:'var(--error)',color:'white'}} onClick={()=>handleDel(et)}>Delete</button>
</div>
):<span style={{fontSize:11,color:'var(--text-tertiary)'}}>{et.is_default?'Default':'Protected'}</span>}
</div>
))}
</div>
</div>
);
}
// ── Bulk Import Panel ─────────────────────────────────────────────────────────
function BulkImportPanel({ onImported, onCancel }) {
const toast=useToast();
const [rows,setRows]=useState(null);
const [skipped,setSkipped]=useState(new Set());
const [saving,setSaving]=useState(false);
const handleFile=async e=>{const file=e.target.files[0];if(!file)return;try{const r=await api.importPreview(file);if(r.error)return toast(r.error,'error');setRows(r.rows);setSkipped(new Set(r.rows.filter(r=>r.duplicate||r.error).map(r=>r.row)));}catch{toast('Upload failed','error');}};
const handleImport=async()=>{setSaving(true);try{const toImport=rows.filter(r=>!skipped.has(r.row)&&!r.error);const{imported}=await api.importConfirm(toImport);toast(`${imported} event${imported!==1?'s':''} imported`,'success');onImported();}catch(e){toast(e.message,'error');}finally{setSaving(false);}};
return (
<div style={{maxWidth:800}}>
<div className="settings-section-label">Bulk Event Import</div>
<p style={{fontSize:12,color:'var(--text-tertiary)',marginBottom:12}}>CSV: <code>Event Title, start_date (YYYY-MM-DD), start_time (HH:MM), event_location, event_type, default_duration</code></p>
<input type="file" accept=".csv" onChange={handleFile} style={{marginBottom:16}}/>
{rows&&(<><div style={{overflowX:'auto',marginBottom:12}}><table style={{width:'100%',borderCollapse:'collapse',fontSize:12}}><thead><tr style={{borderBottom:'2px solid var(--border)'}}>{['','Row','Title','Start','End','Type','Dur','Status'].map(h=><th key={h} style={{padding:'4px 8px',textAlign:'left',color:'var(--text-tertiary)',whiteSpace:'nowrap'}}>{h}</th>)}</tr></thead><tbody>{rows.map(r=>(<tr key={r.row} style={{borderBottom:'1px solid var(--border)',opacity:skipped.has(r.row)?0.45:1}}><td style={{padding:'4px 8px'}}><input type="checkbox" checked={!skipped.has(r.row)} disabled={!!r.error} onChange={()=>setSkipped(p=>{const n=new Set(p);n.has(r.row)?n.delete(r.row):n.add(r.row);return n;})}/></td><td style={{padding:'4px 8px'}}>{r.row}</td><td style={{padding:'4px 8px',fontWeight:600}}>{r.title}</td><td style={{padding:'4px 8px'}}>{r.startAt?.slice(0,16).replace('T',' ')}</td><td style={{padding:'4px 8px'}}>{r.endAt?.slice(0,16).replace('T',' ')}</td><td style={{padding:'4px 8px'}}>{r.typeName}</td><td style={{padding:'4px 8px'}}>{r.durHrs}hr</td><td style={{padding:'4px 8px'}}>{r.error?<span style={{color:'var(--error)'}}>{r.error}</span>:r.duplicate?<span style={{color:'#f59e0b'}}> Duplicate</span>:<span style={{color:'var(--success)'}}> Ready</span>}</td></tr>))}</tbody></table></div><div style={{display:'flex',gap:8}}><button className="btn btn-primary btn-sm" onClick={handleImport} disabled={saving}>{saving?'Importing':`Import ${rows.filter(r=>!skipped.has(r.row)&&!r.error).length} events`}</button><button className="btn btn-secondary btn-sm" onClick={onCancel}>Cancel</button></div></>)}
</div>
);
}
// ── Calendar Views ────────────────────────────────────────────────────────────
function ScheduleView({ events, selectedDate, onSelect }) {
const filtered=events.filter(e=>new Date(e.end_at)>=(selectedDate||new Date(0)));
if(!filtered.length) return <div style={{textAlign:'center',padding:'60px 20px',color:'var(--text-tertiary)',fontSize:14}}>No upcoming events from selected date</div>;
return <>{filtered.map(e=>{const s=new Date(e.start_at);const col=e.event_type?.colour||'#9ca3af';return(<div key={e.id} onClick={()=>onSelect(e)} style={{display:'flex',alignItems:'center',gap:20,padding:'14px 20px',borderBottom:'1px solid var(--border)',cursor:'pointer'}} onMouseEnter={el=>el.currentTarget.style.background='var(--background)'} onMouseLeave={el=>el.currentTarget.style.background=''}><div style={{width:44,textAlign:'center',flexShrink:0}}><div style={{fontSize:22,fontWeight:700,lineHeight:1}}>{s.getDate()}</div><div style={{fontSize:11,color:'var(--text-tertiary)',textTransform:'uppercase'}}>{SHORT_MONTHS[s.getMonth()]}, {DAYS[s.getDay()]}</div></div><div style={{width:100,flexShrink:0,display:'flex',alignItems:'center',gap:8,fontSize:13,color:'var(--text-secondary)'}}><span style={{width:10,height:10,borderRadius:'50%',background:col,flexShrink:0}}/>{e.all_day?'All day':fmtRange(e.start_at,e.end_at)}</div><div style={{flex:1,minWidth:0}}><div style={{fontSize:14,fontWeight:600,display:'flex',alignItems:'center',gap:8}}>{e.event_type?.name&&<span style={{fontSize:11,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.5px',fontWeight:600}}>{e.event_type.name}:</span>}{e.title}{e.track_availability&&!e.my_response&&<span style={{width:8,height:8,borderRadius:'50%',background:'#ef4444',flexShrink:0}} title="Awaiting your response"/>}</div>{e.location&&<div style={{fontSize:12,color:'var(--text-tertiary)',marginTop:2}}>{e.location}</div>}</div></div>);})}</>;
}
function DayView({ events, selectedDate, onSelect }) {
const hours=Array.from({length:16},(_,i)=>i+7);
const day=events.filter(e=>sameDay(new Date(e.start_at),selectedDate));
return(<div><div style={{display:'flex',borderBottom:'1px solid var(--border)',padding:'8px 0 8px 60px',fontSize:13,fontWeight:600,color:'var(--primary)'}}><div style={{textAlign:'center'}}><div>{DAYS[selectedDate.getDay()]}</div><div style={{fontSize:28,fontWeight:700}}>{selectedDate.getDate()}</div></div></div>{hours.map(h=>(<div key={h} style={{display:'flex',borderBottom:'1px solid var(--border)',minHeight:52}}><div style={{width:60,flexShrink:0,fontSize:11,color:'var(--text-tertiary)',padding:'3px 10px 0',textAlign:'right'}}>{h>12?`${h-12} PM`:h===12?'12 PM':`${h} AM`}</div><div style={{flex:1,padding:'2px 4px'}}>{day.filter(e=>new Date(e.start_at).getHours()===h).map(e=>(<div key={e.id} onClick={()=>onSelect(e)} style={{margin:'2px 0',padding:'5px 10px',borderRadius:5,background:e.event_type?.colour||'#6366f1',color:'white',fontSize:12,cursor:'pointer',fontWeight:600}}>{e.title} · {fmtRange(e.start_at,e.end_at)}</div>))}</div></div>))}</div>);
}
function WeekView({ events, selectedDate, onSelect }) {
const ws=weekStart(selectedDate), days=Array.from({length:7},(_,i)=>{const d=new Date(ws);d.setDate(d.getDate()+i);return d;});
const hours=Array.from({length:16},(_,i)=>i+7), today=new Date();
return(<div><div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',borderBottom:'1px solid var(--border)'}}><div/>{days.map((d,i)=><div key={i} style={{textAlign:'center',padding:'6px 4px',fontSize:12,fontWeight:600,color:sameDay(d,today)?'var(--primary)':'var(--text-secondary)'}}>{DAYS[d.getDay()]} {d.getDate()}</div>)}</div>{hours.map(h=>(<div key={h} style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',borderBottom:'1px solid var(--border)',minHeight:46}}><div style={{fontSize:11,color:'var(--text-tertiary)',padding:'3px 10px 0',textAlign:'right'}}>{h>12?`${h-12} PM`:h===12?'12 PM':`${h} AM`}</div>{days.map((d,i)=>(<div key={i} style={{borderLeft:'1px solid var(--border)',padding:'1px 2px'}}>{events.filter(e=>sameDay(new Date(e.start_at),d)&&new Date(e.start_at).getHours()===h).map(e=>(<div key={e.id} onClick={()=>onSelect(e)} style={{background:e.event_type?.colour||'#6366f1',color:'white',borderRadius:3,padding:'2px 5px',fontSize:11,cursor:'pointer',marginBottom:1,fontWeight:600,overflow:'hidden',whiteSpace:'nowrap',textOverflow:'ellipsis'}}>{e.title}</div>))}</div>))}</div>))}</div>);
}
function MonthView({ events, selectedDate, onSelect, onSelectDay }) {
const y=selectedDate.getFullYear(), m=selectedDate.getMonth(), first=new Date(y,m,1).getDay(), total=daysInMonth(y,m), today=new Date();
const cells=[]; for(let i=0;i<first;i++) cells.push(null); for(let d=1;d<=total;d++) cells.push(d);
while(cells.length%7!==0) cells.push(null);
const weeks=[]; for(let i=0;i<cells.length;i+=7) weeks.push(cells.slice(i,i+7));
return(<div><div style={{display:'grid',gridTemplateColumns:'repeat(7,1fr)',borderBottom:'1px solid var(--border)'}}>{DAYS.map(d=><div key={d} style={{textAlign:'center',padding:'8px',fontSize:12,fontWeight:600,color:'var(--text-tertiary)'}}>{d}</div>)}</div>{weeks.map((week,wi)=>(<div key={wi} style={{display:'grid',gridTemplateColumns:'repeat(7,1fr)'}}>{week.map((d,di)=>{if(!d)return<div key={di} style={{borderRight:'1px solid var(--border)',borderBottom:'1px solid var(--border)',minHeight:90,background:'var(--surface-variant)'}}/>;const date=new Date(y,m,d),dayEvs=events.filter(e=>sameDay(new Date(e.start_at),date)),isToday=sameDay(date,today);return(<div key={di} onClick={()=>onSelectDay(date)} style={{borderRight:'1px solid var(--border)',borderBottom:'1px solid var(--border)',padding:'4px',minHeight:90,cursor:'pointer'}} onMouseEnter={el=>el.currentTarget.style.background='var(--background)'} onMouseLeave={el=>el.currentTarget.style.background=''}><div style={{width:26,height:26,borderRadius:'50%',display:'flex',alignItems:'center',justifyContent:'center',marginBottom:3,fontSize:13,fontWeight:isToday?700:400,background:isToday?'var(--primary)':'transparent',color:isToday?'white':'var(--text-primary)'}}>{d}</div>{dayEvs.slice(0,3).map(e=>(<div key={e.id} onClick={ev=>{ev.stopPropagation();onSelect(e);}} style={{background:e.event_type?.colour||'#6366f1',color:'white',borderRadius:3,padding:'1px 5px',fontSize:11,marginBottom:2,cursor:'pointer',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{!e.all_day&&<span style={{marginRight:3}}>{fmtTime(e.start_at)}</span>}{e.title}</div>))}{dayEvs.length>3&&<div style={{fontSize:10,color:'var(--text-tertiary)'}}>+{dayEvs.length-3} more</div>}</div>);})}</div>))}</div>);
}
// ── Main Schedule Page ────────────────────────────────────────────────────────
export default function SchedulePage({ isToolManager, isMobile }) {
const { user } = useAuth();
const toast = useToast();
// Mobile: only day + schedule views
const allowedViews = isMobile ? ['schedule','day'] : ['schedule','day','week','month'];
const [view, setView] = useState('schedule');
const [selDate, setSelDate] = useState(new Date());
const [events, setEvents] = useState([]);
const [eventTypes, setEventTypes] = useState([]);
const [userGroups, setUserGroups] = useState([]);
const [panel, setPanel] = useState('calendar');
const [editingEvent, setEditingEvent] = useState(null);
const [detailEvent, setDetailEvent] = useState(null);
const [loading, setLoading] = useState(true);
const [createOpen, setCreateOpen] = useState(false);
const createRef = useRef(null);
const load = useCallback(() => {
Promise.all([api.getEvents(), api.getEventTypes(), api.getUserGroups()])
.then(([ev,et,ug]) => { setEvents(ev.events||[]); setEventTypes(et.eventTypes||[]); setUserGroups(ug.groups||[]); setLoading(false); })
.catch(() => setLoading(false));
}, []);
useEffect(() => { load(); }, [load]);
useEffect(() => {
if (!createOpen) return;
const h = e => { if (createRef.current && !createRef.current.contains(e.target)) setCreateOpen(false); };
document.addEventListener('mousedown', h);
return () => document.removeEventListener('mousedown', h);
}, [createOpen]);
const eventDates = new Set(events.map(e => e.start_at?.slice(0,10)));
const navDate = dir => {
const d = new Date(selDate);
if (view==='day') d.setDate(d.getDate()+dir);
else if (view==='week') d.setDate(d.getDate()+dir*7);
else d.setMonth(d.getMonth()+dir);
setSelDate(d);
};
const navLabel = () => {
if (view==='day') return `${DAYS[selDate.getDay()]} ${selDate.getDate()} ${MONTHS[selDate.getMonth()]} ${selDate.getFullYear()}`;
if (view==='week') { const ws=weekStart(selDate),we=new Date(ws); we.setDate(we.getDate()+6); return `${SHORT_MONTHS[ws.getMonth()]} ${ws.getDate()} ${SHORT_MONTHS[we.getMonth()]} ${we.getDate()} ${we.getFullYear()}`; }
return `${MONTHS[selDate.getMonth()]} ${selDate.getFullYear()}`;
};
const openDetail = async e => {
try { const { event } = await api.getEvent(e.id); setDetailEvent(event); } catch { toast('Failed to load event','error'); }
};
const handleSaved = () => { load(); setPanel('calendar'); setEditingEvent(null); };
const handleDelete = async e => {
if (!confirm(`Delete "${e.title}"?`)) return;
try { await api.deleteEvent(e.id); toast('Deleted','success'); load(); setDetailEvent(null); } catch(err) { toast(err.message,'error'); }
};
if (loading) return <div style={{display:'flex',alignItems:'center',justifyContent:'center',flex:1,color:'var(--text-tertiary)',fontSize:14}}>Loading schedule</div>;
// ── Sidebar width matches Messages (~280px) ───────────────────────────────
const SIDEBAR_W = isMobile ? 0 : 260;
return (
<div style={{ display:'flex', flex:1, overflow:'hidden', minHeight:0 }}>
{/* 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={{ fontSize:16, fontWeight:700, marginBottom:12, color:'var(--text-primary)' }}>Team Schedule</div>
{/* Create button — styled like new-chat-btn */}
{isToolManager && (
<div style={{ position:'relative', marginBottom:12 }} ref={createRef}>
<button className="newchat-btn" onClick={() => setCreateOpen(v=>!v)} style={{ width:'100%', justifyContent:'center', gap:8 }}>
<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="M12 4.5v15m7.5-7.5h-15" />
</svg>
Create Event
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
{createOpen && (
<div style={{ position:'absolute', top:'100%', left:0, right:0, zIndex:100, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius)', marginTop:4, boxShadow:'0 4px 16px rgba(0,0,0,0.12)' }}>
{[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);}],
['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);}],
['Bulk Event Import', ()=>{setPanel('bulkImport');setCreateOpen(false);}]
].map(([label,action])=>(
<button key={label} onClick={action} style={{display:'block',width:'100%',padding:'9px 16px',textAlign:'left',fontSize:14,background:'none',border:'none',cursor:'pointer',color:'var(--text-primary)'}}
onMouseEnter={e=>e.currentTarget.style.background='var(--background)'} onMouseLeave={e=>e.currentTarget.style.background=''}>{label}</button>
))}
</div>
)}
</div>
)}
</div>
{/* Mini calendar */}
<div style={{ padding:16 }}>
<MiniCalendar selected={selDate} onChange={d=>{setSelDate(d);setPanel('calendar');}} eventDates={eventDates}/>
</div>
</div>
)}
{/* Right panel */}
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0 }}>
{/* View toolbar */}
<div style={{ display:'flex', alignItems:'center', gap:8, padding:'8px 16px', borderBottom:'1px solid var(--border)', background:'var(--surface)', flexShrink:0, flexWrap:'nowrap' }}>
{/* Mobile title + create */}
{isMobile && (
<span style={{ fontSize:15, fontWeight:700, flex:1 }}>Team Schedule</span>
)}
{!isMobile && (
<>
<button className="btn btn-secondary btn-sm" onClick={() => setSelDate(new Date())}>Today</button>
<div style={{ display:'flex', gap:2 }}>
<button className="btn-icon" onClick={() => navDate(-1)} style={{ fontSize:16, padding:'2px 8px' }}></button>
<button className="btn-icon" onClick={() => navDate(1)} style={{ fontSize:16, padding:'2px 8px' }}></button>
</div>
{view !== 'schedule' && <span style={{ fontSize:13, fontWeight:600, color:'var(--text-primary)', whiteSpace:'nowrap' }}>{navLabel()}</span>}
<div style={{ marginLeft:'auto' }}/>
</>
)}
{/* View switcher */}
<div style={{ display:'flex', gap:2, background:'var(--surface-variant)', borderRadius:'var(--radius)', padding:3, flexShrink:0 }}>
{allowedViews.map(v => {
const labels = { schedule:'Schedule', day:'Day', week:'Week', month:'Month' };
return (
<button key={v} onClick={()=>{setView(v);setPanel('calendar');}} style={{ padding:'4px 10px', borderRadius:5, border:'none', cursor:'pointer', fontSize:12, fontWeight:600, background:view===v?'var(--surface)':'transparent', color:view===v?'var(--text-primary)':'var(--text-tertiary)', boxShadow:view===v?'0 1px 3px rgba(0,0,0,0.1)':'none', transition:'all 0.15s', whiteSpace:'nowrap' }}>
{labels[v]}
</button>
);
})}
</div>
</div>
{/* Mobile date picker */}
{isMobile && (
<MobileDatePicker selected={selDate} onChange={d=>{setSelDate(d);setPanel('calendar');}} eventDates={eventDates}/>
)}
{/* Calendar or panel content */}
<div style={{ flex:1, overflowY:'auto', overflowX: panel==='eventForm'?'auto':'hidden' }}>
{panel === 'calendar' && view === 'schedule' && <ScheduleView events={events} selectedDate={selDate} onSelect={openDetail}/>}
{panel === 'calendar' && view === 'day' && <DayView events={events} selectedDate={selDate} onSelect={openDetail}/>}
{panel === 'calendar' && view === 'week' && <WeekView events={events} selectedDate={selDate} onSelect={openDetail}/>}
{panel === 'calendar' && view === 'month' && <MonthView events={events} selectedDate={selDate} onSelect={openDetail} onSelectDay={d=>{setSelDate(d);setView('schedule');}}/>}
{panel === 'eventForm' && isToolManager && (
<div style={{ padding:28, maxWidth:1024 }}>
<h2 style={{ fontSize:17, fontWeight:700, marginBottom:24 }}>{editingEvent?'Edit Event':'New Event'}</h2>
<EventForm event={editingEvent} userGroups={userGroups} eventTypes={eventTypes} selectedDate={selDate} isToolManager={isToolManager}
onSave={handleSaved} onCancel={()=>{setPanel('calendar');setEditingEvent(null);}} onDelete={handleDelete}/>
</div>
)}
{panel === 'eventTypes' && isToolManager && (
<div style={{ padding:28 }}>
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:24 }}>
<h2 style={{ fontSize:17, fontWeight:700, margin:0 }}>Event Types</h2>
<button className="btn btn-secondary btn-sm" onClick={()=>setPanel('calendar')}> Back</button>
</div>
<EventTypesPanel eventTypes={eventTypes} userGroups={userGroups} onUpdated={load}/>
</div>
)}
{panel === 'bulkImport' && isToolManager && (
<div style={{ padding:28 }}>
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:24 }}>
<h2 style={{ fontSize:17, fontWeight:700, margin:0 }}>Bulk Event Import</h2>
<button className="btn btn-secondary btn-sm" onClick={()=>setPanel('calendar')}> Back</button>
</div>
<BulkImportPanel onImported={()=>{load();setPanel('calendar');}} onCancel={()=>setPanel('calendar')}/>
</div>
)}
</div>
</div>
{/* Event detail modal */}
{detailEvent && (
<EventDetailModal
event={detailEvent}
isToolManager={isToolManager}
onClose={() => setDetailEvent(null)}
onEdit={() => { setEditingEvent(detailEvent); setPanel('eventForm'); setDetailEvent(null); }}
onAvailabilityChange={() => openDetail(detailEvent)}
/>
)}
</div>
);
}