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()}` : '—'}
{const n=new Date(cur);n.setMonth(m-1);setCur(n);}} style={{ background:'none',border:'none',fontSize:20,cursor:'pointer',color:'var(--text-secondary)',padding:'4px 10px' }}>‹
{MONTHS[m]} {y}
{const n=new Date(cur);n.setMonth(m+1);setCur(n);}} style={{ background:'none',border:'none',fontSize:20,cursor:'pointer',color:'var(--text-secondary)',padding:'4px 10px' }}>›
{['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}
;
})}
Cancel
OK
);
}
// ── 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 (
setShowCustom(false)} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',display:'flex',alignItems:'center',gap:6,fontSize:14 }}>
Custom recurrence
{onChange(customRule);onClose();}} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--primary)',fontSize:14,fontWeight:700 }}>Done
{(customRule.unit||'week')==='week' && (
Repeats on
{DAY_PILLS.map((d,i)=>{
const key=DAY_KEYS[i], sel=(customRule.byDay||[]).includes(key);
return upd('byDay',sel?(customRule.byDay||[]).filter(x=>x!==key):[...(customRule.byDay||[]),key])} style={{ flex:1,aspectRatio:'1',borderRadius:'50%',border:'1px solid var(--border)',background:sel?'var(--primary)':'transparent',color:sel?'white':'var(--text-primary)',fontSize:12,fontWeight:600,cursor:'pointer',padding:4 }}>{d} ;
})}
)}
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'}
{saving?'…':'Save'}
{/* 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">
setTypeId(e.target.value)} style={{ background:'transparent',border:'none',fontSize:15,color:'var(--text-primary)',flex:1,outline:'none' }}>
{localTypes.map(t=>{t.name} )}
{isToolManager && (
setShowAddType(true)} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--primary)',fontSize:13,fontWeight:600,flexShrink:0,padding:'2px 4px' }}>
+ Type
)}
{/* All-day toggle */}
{/* Start date/time */}
setShowStartDate(true)} style={{ flex:1,fontSize:15,cursor:'pointer' }}>{fmtDateDisplay(sd)}
{!allDay && (
setSt(e.target.value)} style={{ fontSize:15,color:'var(--primary)',fontWeight:600,background:'transparent',border:'none',outline:'none',cursor:'pointer' }}>
{TIME_SLOTS.map(s=>{s.label} )}
)}
{/* 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 && (
setEt(e.target.value)} onClick={e=>e.stopPropagation()} style={{ fontSize:15,color:'var(--primary)',fontWeight:600,background:'transparent',border:'none',outline:'none' }}>
{TIME_SLOTS.map(s=>{s.label} )}
)}
{/* Recurrence */}
} onPress={()=>setShowRecurrence(true)}>
{recurrenceLabel(recRule)}
{/* 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=>(
setGroups(prev=>{const n=new Set(prev);n.has(g.id)?n.delete(g.id):n.add(g.id);return n;})} style={{ width:18,height:18,accentColor:'var(--primary)' }}/>
{g.name}
))}
{/* 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}>
{/* Delete */}
{event && isToolManager && (
onDelete(event)} style={{ width:'100%',padding:'14px',border:'1px solid var(--error)',borderRadius:'var(--radius)',background:'transparent',color:'var(--error)',fontSize:15,fontWeight:600,cursor:'pointer' }}>Delete Event
)}
{/* Overlays */}
{showStartDate &&
{setSd(v);setShowStartDate(false);}} onClose={()=>setShowStartDate(false)}/>}
{showEndDate && {setEd(v);setShowEndDate(false);}} onClose={()=>setShowEndDate(false)}/>}
{showRecurrence && {setRecRule(v);}} onClose={()=>setShowRecurrence(false)}/>}
{showTypeColourPicker && (
setShowTypeColourPicker(false)} title="Event Type Colour"/>
)}
{showAddType && (
e.target===e.currentTarget&&setShowAddType(false)}>
New Event Type
{setShowAddType(false);setNewTypeName('');setNewTypeColour('#6366f1');}} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:20,lineHeight:1 }}>✕
setNewTypeName(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&createEventType()}
placeholder="Type name…"
style={{ width:'100%',padding:'12px 14px',border:'1px solid var(--border)',borderRadius:'var(--radius)',fontSize:16,marginBottom:12,boxSizing:'border-box',background:'var(--background)',color:'var(--text-primary)' }}
/>
Colour
setShowTypeColourPicker(true)} style={{ flex:1,height:40,borderRadius:'var(--radius)',border:'2px solid var(--border)',background:newTypeColour,cursor:'pointer' }}/>
{savingType?'Creating…':'Create Type'}
)}
);
}