import { useState, useEffect, useRef } from 'react'; import { api } from '../utils/api.js'; import ColourPickerSheet from './ColourPickerSheet.jsx'; import { useToast } from '../contexts/ToastContext.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']; const DAY_PILLS = ['S','M','T','W','T','F','S']; const DAY_KEYS = ['SU','MO','TU','WE','TH','FR','SA']; 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; })(); 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); const pad=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`; } function fmtDateDisplay(iso) { if(!iso) return ''; const d=new Date(iso); return `${DAYS[d.getDay()]}, ${SHORT_MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; } function fmtTimeDisplay(slot) { const f=TIME_SLOTS.find(s=>s.value===slot); return f?f.label:slot; } const FREQ_OPTIONS = [ { value: '', label: 'Does not repeat' }, { value: 'daily', label: 'Every day' }, { value: 'weekly', label: 'Every week' }, { value: 'monthly', label: 'Every month' }, { value: 'yearly', label: 'Every year' }, { value: 'custom', label: 'Custom…' }, ]; function recurrenceLabel(rule) { if (!rule || !rule.freq) return 'Does not repeat'; if (rule.freq === 'custom') { const unit = (rule.interval||1)===1 ? rule.unit : `${rule.interval} ${rule.unit}s`; return `Every ${unit}`; } return FREQ_OPTIONS.find(o=>o.value===rule.freq)?.label || rule.freq; } // ── Toggle Switch ───────────────────────────────────────────────────────────── function Toggle({ checked, onChange }) { return (
onChange(!checked)} style={{ width:44,height:24,borderRadius:12,background:checked?'var(--primary)':'var(--surface-variant)',cursor:'pointer',position:'relative',transition:'background 0.2s',flexShrink:0 }}>
); } // ── Calendar Picker Overlay ─────────────────────────────────────────────────── function CalendarPicker({ value, onChange, onClose }) { const [cur, setCur] = useState(() => { const d = new Date(value||Date.now()); d.setDate(1); return d; }); const y=cur.getFullYear(), m=cur.getMonth(), first=new Date(y,m,1).getDay(), total=new Date(y,m+1,0).getDate(), today=new Date(); const cells=[]; for(let i=0;ie.target===e.currentTarget&&onClose()}>
Select Date
{selDate ? `${SHORT_MONTHS[selDate.getMonth()]} ${selDate.getDate()}, ${selDate.getFullYear()}` : '—'}
{MONTHS[m]} {y}
{['S','M','T','W','T','F','S'].map((d,i)=>
{d}
)} {cells.map((d,i) => { if(!d) return
; const date=new Date(y,m,d); const isSel = selDate && date.toDateString()===selDate.toDateString(); const isToday = date.toDateString()===today.toDateString(); return
onChange(`${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`)} style={{ textAlign:'center',padding:'8px 4px',borderRadius:'50%',cursor:'pointer',background:isSel?'var(--primary)':'transparent',color:isSel?'white':isToday?'var(--primary)':'var(--text-primary)',fontWeight:isToday&&!isSel?700:400,fontSize:14 }}>{d}
; })}
); } // ── Recurrence Sheet ────────────────────────────────────────────────────────── function RecurrenceSheet({ value, onChange, onClose }) { const rule = value || {}; const [showCustom, setShowCustom] = useState(rule.freq==='custom'); const [customRule, setCustomRule] = useState(rule.freq==='custom' ? rule : {freq:'custom',interval:1,unit:'week',byDay:[],ends:'never',endDate:'',endCount:13}); const selectFreq = (freq) => { if(freq==='custom') { setShowCustom(true); return; } onChange(freq ? {freq} : null); onClose(); }; const upd = (k,v) => setCustomRule(r=>({...r,[k]:v})); if(showCustom) return (
Custom recurrence
Repeats every
upd('interval',Math.max(1,parseInt(e.target.value)||1))} style={{ width:70,textAlign:'center',fontSize:16 }}/>
{(customRule.unit||'week')==='week' && (
Repeats on
{DAY_PILLS.map((d,i)=>{ const key=DAY_KEYS[i], sel=(customRule.byDay||[]).includes(key); return ; })}
)}
Ends
{[['never','Never'],['on','On'],['after','After']].map(([val,lbl])=>(
upd('ends',val)} style={{ width:20,height:20,borderRadius:'50%',border:`2px solid ${(customRule.ends||'never')===val?'var(--primary)':'var(--border)'}`,display:'flex',alignItems:'center',justifyContent:'center',cursor:'pointer',flexShrink:0 }}> {(customRule.ends||'never')===val&&
}
{lbl} {val==='on'&&(customRule.ends||'never')==='on'&&upd('endDate',e.target.value)} style={{ width:150 }}/>} {val==='after'&&(customRule.ends||'never')==='after'&&<>upd('endCount',parseInt(e.target.value)||1)} style={{ width:64,textAlign:'center' }}/>occurrences}
))}
); return (
e.target===e.currentTarget&&onClose()}>
{FREQ_OPTIONS.map(opt=>(
selectFreq(opt.value)} style={{ display:'flex',alignItems:'center',gap:12,padding:'14px 4px',borderBottom:'1px solid var(--border)',cursor:'pointer' }}>
{(rule.freq||'')===(opt.value)&&
}
{opt.label}
))}
); } // ── Row — must be defined OUTSIDE the component to avoid focus loss ───────────── function MobileRow({ icon, label, children, onPress, border=true }) { return (
{icon}
{label &&
{label}
} {children}
); } // ── Main Mobile Event Form ──────────────────────────────────────────────────── export default function MobileEventForm({ event, eventTypes, userGroups, 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 ? String(event.event_type_id) : ''); const [localTypes, setLocalTypes] = useState(eventTypes); const [showAddType, setShowAddType] = useState(false); const [newTypeName, setNewTypeName] = useState(''); const [newTypeColour, setNewTypeColour] = useState('#6366f1'); const [showTypeColourPicker, setShowTypeColourPicker] = useState(false); const [savingType, setSavingType] = useState(false); 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 [track, setTrack] = useState(!!event?.track_availability); const [isPrivate, setIsPrivate] = useState(event ? !event.is_public : false); const [groups, setGroups] = useState(new Set((event?.user_groups||[]).map(g=>g.id))); const [location, setLocation] = useState(event?.location||''); const [description, setDescription] = useState(event?.description||''); const [recRule, setRecRule] = useState(event?.recurrence_rule||null); const [saving, setSaving] = useState(false); // Overlay state const [showStartDate, setShowStartDate] = useState(false); const [showEndDate, setShowEndDate] = useState(false); const [showRecurrence, setShowRecurrence] = useState(false); const [showGroups, setShowGroups] = useState(false); // Sync and initialise typeId useEffect(() => { setLocalTypes(eventTypes); if(!event && typeId==='' && eventTypes.length>0) { const def = eventTypes.find(t=>t.is_default) || eventTypes[0]; if(def) setTypeId(String(def.id)); } }, [eventTypes]); const createEventType = async () => { if(!newTypeName.trim()) return; setSavingType(true); try { const r = await api.createEventType({ name: newTypeName.trim(), colour: newTypeColour }); setLocalTypes(prev => [...prev, r.eventType]); setTypeId(String(r.eventType.id)); setNewTypeName(''); setShowAddType(false); } catch(e) { toast(e.message, 'error'); } finally { setSavingType(false); } }; // When start date changes, match end date useEffect(() => { if(!event) setEd(sd); }, [sd]); // When type or start time changes, auto-set end time useEffect(() => { 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 && !event) { setEd(toDateIn(addHours(start,dur))); setEt(toTimeIn(addHours(start,dur))); } }, [typeId, st]); const handle = async () => { if(!title.trim()) return toast('Title required','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, description, isPublic:!isPrivate, trackAvailability:track, userGroupIds:[...groups], recurrenceRule:recRule||null }; 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 currentType = eventTypes.find(t=>t.id===Number(typeId)); return (
{/* Header */}
{event ? 'Edit Event' : 'New Event'}
{/* Title */}
setTitle(e.target.value)} placeholder="Add title" style={{ width:'100%',border:'none',background:'transparent',fontSize:22,fontWeight:700,color:'var(--text-primary)',outline:'none' }}/>
{/* Event Type */} } label="Event Type">
{isToolManager && ( )}
{/* All-day toggle */}
All day
{/* Start date/time */}
setShowStartDate(true)} style={{ flex:1,fontSize:15,cursor:'pointer' }}>{fmtDateDisplay(sd)} {!allDay && ( )}
{/* End date/time */}
setShowEndDate(true)} style={{ display:'flex',alignItems:'center',padding:'6px 20px 14px 56px',cursor:'pointer',borderBottom:'1px solid var(--border)' }}> {fmtDateDisplay(ed)} {!allDay && ( )}
{/* Recurrence */} } onPress={()=>setShowRecurrence(true)}> {recurrenceLabel(recRule)} {/* Track Availability */}
Track Availability
{/* Groups */}
setShowGroups(!showGroups)} style={{ display:'flex',alignItems:'center',padding:'14px 20px',borderBottom:'1px solid var(--border)',cursor:'pointer' }}> {groups.size>0 ? `${groups.size} group${groups.size!==1?'s':''} selected` : 'Add Groups'}
{showGroups && userGroups.map(g=>( ))}
{/* Private Event */}
Private Event
{/* Location */} }> setLocation(e.target.value)} placeholder="Add location" style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none' }}/> {/* Description */} } border={false}>