v0.11.5 event form bug fixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jama-frontend",
|
||||
"version": "0.11.4",
|
||||
"version": "0.11.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -204,7 +204,10 @@ function MobileRow({ icon, label, children, onPress, border=true }) {
|
||||
// ── 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);
|
||||
// Use local date for default, not UTC slice (avoids off-by-one for UTC- timezones)
|
||||
const defDate = selectedDate || new Date();
|
||||
const _pad = n => String(n).padStart(2,'0');
|
||||
const def = `${defDate.getFullYear()}-${_pad(defDate.getMonth()+1)}-${_pad(defDate.getDate())}`;
|
||||
const [title, setTitle] = useState(event?.title||'');
|
||||
const [typeId, setTypeId] = useState(event?.event_type_id ? String(event.event_type_id) : '');
|
||||
const [localTypes, setLocalTypes] = useState(eventTypes);
|
||||
@@ -217,6 +220,12 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
||||
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');
|
||||
// Track the saved event duration (minutes) so editing preserves it
|
||||
const savedDurMins = event
|
||||
? (new Date(event.end_at) - new Date(event.start_at)) / 60000
|
||||
: null;
|
||||
// Track previous typeId so we can detect a type change vs start time change
|
||||
const prevTypeIdRef = useRef(event?.event_type_id ? String(event.event_type_id) : '');
|
||||
const [allDay, setAllDay] = useState(!!event?.all_day);
|
||||
const [track, setTrack] = useState(!!event?.track_availability);
|
||||
const [isPrivate, setIsPrivate] = useState(event ? !event.is_public : false);
|
||||
@@ -253,25 +262,42 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
||||
finally { setSavingType(false); }
|
||||
};
|
||||
|
||||
// When start date or start time changes, update end date/time to maintain duration
|
||||
// Auto-calculate end date/time when start date, start time, or event type changes.
|
||||
// Rules:
|
||||
// - New event: use eventType duration (default 1hr)
|
||||
// - Editing + type changed: use new eventType duration
|
||||
// - Editing + type same: use saved event duration (preserve original length)
|
||||
// - Always: if end < start, advance end date by 1 day (overnight events)
|
||||
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) return;
|
||||
if(event) {
|
||||
// Editing: only sync end date when start date changes, preserve manual end time
|
||||
setEd(toDateIn(addHours(start, 0)));
|
||||
|
||||
const typeChanged = typeId !== prevTypeIdRef.current;
|
||||
prevTypeIdRef.current = typeId;
|
||||
|
||||
let durMins;
|
||||
if(!event || typeChanged) {
|
||||
// New event or type change: use eventType duration
|
||||
const typ = localTypes.find(t=>t.id===Number(typeId));
|
||||
durMins = (typ?.default_duration_hrs||1) * 60;
|
||||
} else {
|
||||
// New event: always auto-set end to start + duration
|
||||
setEd(toDateIn(addHours(start,dur)));
|
||||
setEt(toTimeIn(addHours(start,dur)));
|
||||
// Editing with same type: preserve the saved event duration
|
||||
durMins = savedDurMins || 60;
|
||||
}
|
||||
|
||||
const endIso = addHours(start, durMins/60);
|
||||
setEd(toDateIn(endIso));
|
||||
setEt(toTimeIn(endIso));
|
||||
}, [sd, st, typeId]);
|
||||
|
||||
const handle = async () => {
|
||||
if(!title.trim()) return toast('Title required','error');
|
||||
// Validation rules
|
||||
const startMs = new Date(buildISO(sd, allDay?'00:00':st)).getTime();
|
||||
const endMs = new Date(buildISO(ed, allDay?'23:59':et)).getTime();
|
||||
if(ed < sd) return toast('End date cannot be before start date','error');
|
||||
if(!allDay && endMs <= startMs && ed === sd) return toast('End time must be after start time, or set a later end date','error');
|
||||
setSaving(true);
|
||||
try {
|
||||
const body = { title:title.trim(), eventTypeId:typeId||null, startAt:allDay?buildISO(sd,'00:00'):buildISO(sd,st), endAt:allDay?buildISO(ed,'23:59'):buildISO(ed,et), allDay, location, description, isPublic:!isPrivate, trackAvailability:track, userGroupIds:[...groups], recurrenceRule:recRule||null };
|
||||
@@ -342,7 +368,17 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
||||
<div onClick={()=>setShowEndDate(true)} style={{ display:'flex',alignItems:'center',padding:'6px 20px 14px 56px',cursor:'pointer',borderBottom:'1px solid var(--border)' }}>
|
||||
<span style={{ flex:1,fontSize:15,color:'var(--text-secondary)' }}>{fmtDateDisplay(ed)}</span>
|
||||
{!allDay && (
|
||||
<select value={et} onChange={e=>setEt(e.target.value)} onClick={e=>e.stopPropagation()} style={{ fontSize:15,color:'var(--primary)',fontWeight:600,background:'transparent',border:'none',outline:'none' }}>
|
||||
<select value={et} onChange={e=>{
|
||||
const newEt = e.target.value;
|
||||
setEt(newEt);
|
||||
// If end time is earlier than start time on the same day, roll end date to next day
|
||||
if(sd === ed && newEt <= st) {
|
||||
const nextDay = addHours(buildISO(sd, st), 0);
|
||||
const d = new Date(nextDay); d.setDate(d.getDate()+1);
|
||||
const pad = n => String(n).padStart(2,'0');
|
||||
setEd(`${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`);
|
||||
}
|
||||
}} onClick={e=>e.stopPropagation()} style={{ fontSize:15,color:'var(--primary)',fontWeight:600,background:'transparent',border:'none',outline:'none' }}>
|
||||
{TIME_SLOTS.map(s=><option key={s.value} value={s.value}>{s.label}</option>)}
|
||||
</select>
|
||||
)}
|
||||
|
||||
@@ -156,7 +156,9 @@ function EventTypePopup({ userGroups, onSave, onClose, editing = null }) {
|
||||
function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCancel, onDelete, isToolManager }) {
|
||||
const toast = useToast();
|
||||
const today = new Date();
|
||||
const defaultDate = selectedDate ? selectedDate.toISOString().slice(0,10) : today.toISOString().slice(0,10);
|
||||
const _defD = selectedDate || today;
|
||||
const _p = n => String(n).padStart(2,'0');
|
||||
const defaultDate = `${_defD.getFullYear()}-${_p(_defD.getMonth()+1)}-${_p(_defD.getDate())}`;
|
||||
const [title, setTitle] = useState(event?.title || '');
|
||||
const [eventTypeId, setEventTypeId] = useState(event?.event_type_id || '');
|
||||
const [startDate, setStartDate] = useState(event ? toLocalDateInput(event.start_at) : defaultDate);
|
||||
@@ -173,23 +175,40 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
||||
const [showTypeForm, setShowTypeForm] = useState(false);
|
||||
const [localEventTypes, setLocalEventTypes] = useState(eventTypes);
|
||||
const typeRef = useRef(null);
|
||||
const savedDurMins = event
|
||||
? (new Date(event.end_at) - new Date(event.start_at)) / 60000
|
||||
: null;
|
||||
const prevTypeIdRef = useRef(event?.event_type_id ? String(event.event_type_id) : '');
|
||||
|
||||
// Auto-update end time when event type selected with default duration
|
||||
// Auto-update end time when event type or start time changes
|
||||
useEffect(() => {
|
||||
if (!eventTypeId || event) return;
|
||||
if (!startDate || !startTime) return;
|
||||
const et = localEventTypes.find(t => t.id === Number(eventTypeId));
|
||||
if (!et || !startDate || !startTime) return;
|
||||
const start = buildISO(startDate, startTime);
|
||||
setEndDate(toLocalDateInput(addHours(start, et.default_duration_hrs)));
|
||||
setEndTime(toLocalTimeInput(addHours(start, et.default_duration_hrs)));
|
||||
if (et.default_user_group_id && !event) setSelectedGroups(prev => new Set([...prev, et.default_user_group_id]));
|
||||
}, [eventTypeId]);
|
||||
if (!start) return;
|
||||
const typeChanged = String(eventTypeId) !== prevTypeIdRef.current;
|
||||
prevTypeIdRef.current = String(eventTypeId);
|
||||
if (!event || typeChanged) {
|
||||
// New event or type change: apply eventType duration
|
||||
const dur = et?.default_duration_hrs || 1;
|
||||
setEndDate(toLocalDateInput(addHours(start, dur)));
|
||||
setEndTime(toLocalTimeInput(addHours(start, dur)));
|
||||
} else {
|
||||
// Editing with same type: preserve saved duration
|
||||
const durMins = savedDurMins || 60;
|
||||
setEndDate(toLocalDateInput(addHours(start, durMins/60)));
|
||||
setEndTime(toLocalTimeInput(addHours(start, durMins/60)));
|
||||
}
|
||||
if (et?.default_user_group_id && !event) setSelectedGroups(prev => new Set([...prev, et.default_user_group_id]));
|
||||
}, [eventTypeId, startDate, startTime]);
|
||||
|
||||
const toggleGroup = (id) => setSelectedGroups(prev => { const n=new Set(prev); n.has(id)?n.delete(id):n.add(id); return n; });
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!title.trim()) return toast('Title required', 'error');
|
||||
if (!allDay && (!startDate||!startTime||!endDate||!endTime)) return toast('Start and end required', 'error');
|
||||
if (endDate < startDate) return toast('End date cannot be before start date', 'error');
|
||||
if (!allDay && endDate === startDate && buildISO(endDate, endTime) <= buildISO(startDate, startTime)) return toast('End time must be after start time, or use a later end date', 'error');
|
||||
setSaving(true);
|
||||
try {
|
||||
const body = {
|
||||
@@ -223,7 +242,14 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
||||
<input type="date" className="input" value={startDate} onChange={e => setStartDate(e.target.value)} style={{ width:150 }} />
|
||||
{!allDay && <input type="time" className="input" value={startTime} onChange={e => setStartTime(e.target.value)} style={{ width:120 }} />}
|
||||
<span style={{ color:'var(--text-tertiary)', fontSize:13 }}>to</span>
|
||||
{!allDay && <input type="time" className="input" value={endTime} onChange={e => setEndTime(e.target.value)} style={{ width:120 }} />}
|
||||
{!allDay && <input type="time" className="input" value={endTime} onChange={e => {
|
||||
const newEt = e.target.value; setEndTime(newEt);
|
||||
if(startDate === endDate && newEt <= startTime) {
|
||||
const d = new Date(buildISO(startDate, startTime)); d.setDate(d.getDate()+1);
|
||||
const p = n => String(n).padStart(2,'0');
|
||||
setEndDate(`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`);
|
||||
}
|
||||
}} style={{ width:120 }} />}
|
||||
<input type="date" className="input" value={endDate} onChange={e => setEndDate(e.target.value)} style={{ width:150 }} />
|
||||
</div>
|
||||
<label style={{ display:'flex', alignItems:'center', gap:8, marginTop:8, fontSize:13, cursor:'pointer' }}>
|
||||
|
||||
@@ -333,7 +333,9 @@ function FormRow({ label, children, required }) {
|
||||
// ── 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 _defD = selectedDate || new Date();
|
||||
const _p = n => String(n).padStart(2,'0');
|
||||
const def = `${_defD.getFullYear()}-${_p(_defD.getMonth()+1)}-${_p(_defD.getDate())}`;
|
||||
const [title,setTitle]=useState(event?.title||'');
|
||||
const [typeId,setTypeId]=useState(event?.event_type_id||'');
|
||||
const [sd,setSd]=useState(event?toDateIn(event.start_at):def);
|
||||
@@ -363,6 +365,11 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
||||
|
||||
// Track whether the user has manually changed the end time (vs auto-computed)
|
||||
const userSetEndTime = useRef(!!event); // editing mode: treat saved end as user-set
|
||||
// Duration of the saved event in minutes (preserved when editing with same type)
|
||||
const savedDurMins = event
|
||||
? (new Date(event.end_at) - new Date(event.start_at)) / 60000
|
||||
: null;
|
||||
const prevTypeIdRef = useRef(event?.event_type_id ? String(event.event_type_id) : '');
|
||||
|
||||
// When event type changes:
|
||||
// - Creating: always apply the type's duration to compute end time
|
||||
@@ -373,21 +380,16 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
||||
const typ=localTypes.find(t=>t.id===Number(typeId));
|
||||
const start=buildISO(sd,st);
|
||||
if(!start) return;
|
||||
if(!event) {
|
||||
// Creating new event — always apply duration (default 1hr)
|
||||
const typeChanged = typeId !== prevTypeIdRef.current;
|
||||
prevTypeIdRef.current = String(typeId);
|
||||
if(!event || typeChanged) {
|
||||
// New event or type change: apply eventType duration
|
||||
const dur=typ?.default_duration_hrs||1;
|
||||
setEd(toDateIn(addHours(start,dur)));
|
||||
setEt(toTimeIn(addHours(start,dur)));
|
||||
const endIso=addHours(start,dur);
|
||||
setEd(toDateIn(endIso)); setEt(toTimeIn(endIso));
|
||||
userSetEndTime.current = false;
|
||||
} else {
|
||||
// Editing — only update end time if the new type has an explicit duration
|
||||
if(typ?.default_duration_hrs) {
|
||||
setEd(toDateIn(addHours(start,typ.default_duration_hrs)));
|
||||
setEt(toTimeIn(addHours(start,typ.default_duration_hrs)));
|
||||
userSetEndTime.current = false;
|
||||
}
|
||||
// else: keep existing saved end time — do nothing
|
||||
}
|
||||
// Editing with same type: do not override end time here (start-time effect handles it)
|
||||
if(typ?.default_user_group_id&&!event) setGrps(prev=>new Set([...prev,Number(typ.default_user_group_id)]));
|
||||
},[typeId]);
|
||||
|
||||
@@ -396,17 +398,20 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
||||
if(!userSetEndTime.current) setEd(sd);
|
||||
},[sd]);
|
||||
|
||||
// When start time changes: recompute end using current duration offset
|
||||
// When start time changes: recompute end preserving duration
|
||||
useEffect(()=>{
|
||||
if(!sd||!st) return;
|
||||
if(userSetEndTime.current) return; // user already picked a specific end time — respect it
|
||||
if(userSetEndTime.current && event) {
|
||||
// Editing with user-preserved end: maintain saved duration
|
||||
const durMins = savedDurMins || 60;
|
||||
const start=buildISO(sd,st);
|
||||
if(start){ setEd(toDateIn(addHours(start,durMins/60))); setEt(toTimeIn(addHours(start,durMins/60))); }
|
||||
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)));
|
||||
}
|
||||
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;});
|
||||
@@ -416,6 +421,8 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
||||
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');
|
||||
if(ed<sd) return toast('End date cannot be before start date','error');
|
||||
if(!allDay&&ed===sd&&buildISO(ed,et)<=buildISO(sd,st)) return toast('End time must be after start time, or use a later end date','error');
|
||||
setSaving(true);
|
||||
try{
|
||||
const body={title:title.trim(),eventTypeId:typeId||null,startAt:allDay?buildISO(sd,'00:00'):buildISO(sd,st),endAt:allDay?buildISO(ed,'23:59'):buildISO(ed,et),allDay,location:loc,description:desc,isPublic:pub,trackAvailability:track,userGroupIds:[...grps],recurrenceRule:recRule||null};
|
||||
@@ -460,7 +467,11 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
||||
{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);userSetEndTime.current=true;}} style={{width:120,flexShrink:0}}>
|
||||
<select className="input" value={et} onChange={e=>{
|
||||
const newEt=e.target.value; setEt(newEt); userSetEndTime.current=true;
|
||||
// Overnight: if end < start on same day, advance end date
|
||||
if(sd===ed && newEt<=st){ const d=new Date(buildISO(sd,st)); d.setDate(d.getDate()+1); const p=n=>String(n).padStart(2,'0'); setEd(`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`); }
|
||||
}} 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);userSetEndTime.current=true;}} style={{width:150,flexShrink:0}}/>
|
||||
|
||||
Reference in New Issue
Block a user