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
{MONTHS[m]} {y}
{DAYS.map(d=>
{d[0]}
)} {cells.map((d,i)=>{ if(!d) return
; 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 (
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&&}
); })}
); } // ── 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 {open && (
{DAYS.map(d=>
{d[0]}
)} {cells.map((d,i)=>{ if(!d) return
; 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 (
{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&&}
); })}
)}
); } // ── 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 (
setName(e.target.value)} style={{marginTop:4}} autoFocus/>
setColour(e.target.value)} style={{marginTop:4,width:'100%',height:32,padding:2,borderRadius:4,border:'1px solid var(--border)'}}/>
{useDur&&}
); } // ── 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})=>(
{label}{required&& *}
{children}
); return (
{/* Title */}
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%'}}/>
{/* Availability (first — if enabled, groups become required) */} {/* Groups — required when tracking */}
{userGroups.length===0 ?
No user groups yet
:userGroups.map(g=>( ))}

{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`}

{/* Visibility — only shown if groups selected OR tracking */} {(grps.size>0||track) && ( )} {/* Date/Time */}
setSd(e.target.value)} style={{width:150,flexShrink:0}}/> {!allDay&&( <> to setEd(e.target.value)} style={{width:150,flexShrink:0}}/> )}
{/* Event Type */}
{isToolManager&&} {showTypeForm&&{setLocalTypes(p=>[...p,et]);setShowTypeForm(false);}} onClose={()=>setShowTypeForm(false)}/>}
{/* Location */} setLoc(e.target.value)}/> {/* Description */}