v0.9.52 minor bug fixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jama-frontend",
|
||||
"version": "0.9.51",
|
||||
"version": "0.9.52",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -159,25 +159,45 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
||||
const [localTypes,setLocalTypes]=useState(eventTypes);
|
||||
const typeRef=useRef(null);
|
||||
|
||||
// When event type changes: auto set duration and default group
|
||||
// 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
|
||||
|
||||
// When event type changes:
|
||||
// - Creating: always apply the type's duration to compute end time
|
||||
// - Editing: only apply duration if the type HAS a defined duration
|
||||
// (if no duration on type, keep existing saved 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){
|
||||
if(!start) return;
|
||||
if(!event) {
|
||||
// Creating new event — always apply duration (default 1hr)
|
||||
const dur=typ?.default_duration_hrs||1;
|
||||
setEd(toDateIn(addHours(start,dur)));
|
||||
setEt(toTimeIn(addHours(start,dur)));
|
||||
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
|
||||
}
|
||||
if(typ?.default_user_group_id&&!event) setGrps(prev=>new Set([...prev,Number(typ.default_user_group_id)]));
|
||||
},[typeId]);
|
||||
|
||||
// When start date changes: auto-match end date
|
||||
useEffect(()=>{ if(!event) setEd(sd); },[sd]);
|
||||
// When start date changes: match end date (both modes) unless user set it manually
|
||||
useEffect(()=>{
|
||||
if(!userSetEndTime.current) setEd(sd);
|
||||
},[sd]);
|
||||
|
||||
// When start time changes: auto-update end time preserving duration
|
||||
// When start time changes: recompute end using current duration offset
|
||||
useEffect(()=>{
|
||||
if(!sd||!st) return;
|
||||
if(userSetEndTime.current) return; // user already picked a specific end time — respect it
|
||||
const typ=localTypes.find(t=>t.id===Number(typeId));
|
||||
const dur=typ?.default_duration_hrs||1;
|
||||
const start=buildISO(sd,st);
|
||||
@@ -238,10 +258,10 @@ 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)} style={{width:120,flexShrink:0}}>
|
||||
<select className="input" value={et} onChange={e=>{setEt(e.target.value);userSetEndTime.current=true;}} 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}}/>
|
||||
<input type="date" className="input" value={ed} onChange={e=>{setEd(e.target.value);userSetEndTime.current=true;}} style={{width:150,flexShrink:0}}/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -467,11 +487,13 @@ function ScheduleView({ events, selectedDate, onSelect }) {
|
||||
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>);})}</>;
|
||||
}
|
||||
|
||||
const HOUR_H = 56; // px per hour row
|
||||
const HOUR_H = 52; // px per hour row
|
||||
const DAY_START = 0; // show from midnight
|
||||
const DAY_END = 24; // to midnight
|
||||
|
||||
function eventTopOffset(startDate) {
|
||||
const h=startDate.getHours(), m=startDate.getMinutes();
|
||||
return (h-7)*HOUR_H + (m/60)*HOUR_H;
|
||||
return (h - DAY_START)*HOUR_H + (m/60)*HOUR_H;
|
||||
}
|
||||
function eventHeightPx(startDate, endDate) {
|
||||
const diffMs=endDate-startDate;
|
||||
@@ -480,41 +502,42 @@ function eventHeightPx(startDate, endDate) {
|
||||
}
|
||||
|
||||
function DayView({ events, selectedDate, onSelect }) {
|
||||
const hours=Array.from({length:16},(_,i)=>i+7); // 7am–10pm
|
||||
const hours=Array.from({length:DAY_END - DAY_START},(_,i)=>i+DAY_START);
|
||||
const day=events.filter(e=>sameDay(new Date(e.start_at),selectedDate));
|
||||
const scrollRef = useRef(null);
|
||||
useEffect(()=>{ if(scrollRef.current) scrollRef.current.scrollTop = 7 * HOUR_H; },[selectedDate]);
|
||||
const fmtHour = h => h===0?'12 AM':h<12?`${h} AM`:h===12?'12 PM':`${h-12} PM`;
|
||||
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={{display:'flex',flexDirection:'column',height:'100%'}}>
|
||||
<div style={{display:'flex',borderBottom:'1px solid var(--border)',padding:'8px 0 8px 60px',fontSize:13,fontWeight:600,color:'var(--primary)',flexShrink:0}}>
|
||||
<div style={{textAlign:'center'}}><div>{DAYS[selectedDate.getDay()]}</div><div style={{fontSize:28,fontWeight:700}}>{selectedDate.getDate()}</div></div>
|
||||
</div>
|
||||
<div style={{position:'relative'}}>
|
||||
{/* Hour grid */}
|
||||
{hours.map(h=>(
|
||||
<div key={h} style={{display:'flex',borderBottom:'1px solid var(--border)',height:HOUR_H}}>
|
||||
<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 ref={scrollRef} style={{flex:1,overflowY:'auto',position:'relative'}}>
|
||||
<div style={{position:'relative'}}>
|
||||
{hours.map(h=>(
|
||||
<div key={h} style={{display:'flex',borderBottom:'1px solid var(--border)',height:HOUR_H}}>
|
||||
<div style={{width:60,flexShrink:0,fontSize:11,color:'var(--text-tertiary)',padding:'3px 10px 0',textAlign:'right'}}>{fmtHour(h)}</div>
|
||||
<div style={{flex:1}}/>
|
||||
</div>
|
||||
<div style={{flex:1}}/>
|
||||
</div>
|
||||
))}
|
||||
{/* Event blocks — absolutely positioned */}
|
||||
{day.map(e=>{
|
||||
const s=new Date(e.start_at), en=new Date(e.end_at);
|
||||
const top=eventTopOffset(s), height=eventHeightPx(s,en);
|
||||
return(
|
||||
<div key={e.id} onClick={()=>onSelect(e)} style={{
|
||||
position:'absolute', left:64, right:8,
|
||||
top, height,
|
||||
background:e.event_type?.colour||'#6366f1', color:'white',
|
||||
borderRadius:5, padding:'3px 8px', cursor:'pointer',
|
||||
fontSize:12, fontWeight:600, overflow:'hidden',
|
||||
boxShadow:'0 1px 3px rgba(0,0,0,0.2)',
|
||||
}}>
|
||||
<div style={{whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{e.title}</div>
|
||||
{height>32&&<div style={{fontSize:10,opacity:0.85}}>{fmtRange(e.start_at,e.end_at)}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
{day.map(e=>{
|
||||
const s=new Date(e.start_at), en=new Date(e.end_at);
|
||||
const top=eventTopOffset(s), height=eventHeightPx(s,en);
|
||||
return(
|
||||
<div key={e.id} onClick={()=>onSelect(e)} style={{
|
||||
position:'absolute', left:64, right:8,
|
||||
top, height,
|
||||
background:e.event_type?.colour||'#6366f1', color:'white',
|
||||
borderRadius:5, padding:'3px 8px', cursor:'pointer',
|
||||
fontSize:12, fontWeight:600, overflow:'hidden',
|
||||
boxShadow:'0 1px 3px rgba(0,0,0,0.2)',
|
||||
}}>
|
||||
<div style={{whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{e.title}</div>
|
||||
{height>32&&<div style={{fontSize:10,opacity:0.85}}>{fmtRange(e.start_at,e.end_at)}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -522,22 +545,24 @@ function DayView({ events, selectedDate, onSelect }) {
|
||||
|
||||
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();
|
||||
const hours=Array.from({length:DAY_END - DAY_START},(_,i)=>i+DAY_START), today=new Date();
|
||||
const scrollRef = useRef(null);
|
||||
useEffect(()=>{ if(scrollRef.current) scrollRef.current.scrollTop = 7 * HOUR_H; },[selectedDate]);
|
||||
const fmtHour = h => h===0?'12 AM':h<12?`${h} AM`:h===12?'12 PM':`${h-12} PM`;
|
||||
return(
|
||||
<div>
|
||||
<div style={{display:'flex',flexDirection:'column',height:'100%'}}>
|
||||
{/* Day headers */}
|
||||
<div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',borderBottom:'1px solid var(--border)',position:'sticky',top:0,background:'var(--surface)',zIndex:2}}>
|
||||
<div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',borderBottom:'1px solid var(--border)',background:'var(--surface)',flexShrink:0}}>
|
||||
<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>
|
||||
{/* Time grid with event columns */}
|
||||
{/* Scrollable time grid */}
|
||||
<div ref={scrollRef} style={{flex:1,overflowY:'auto'}}>
|
||||
<div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',position:'relative'}}>
|
||||
{/* Time labels column */}
|
||||
<div>
|
||||
{hours.map(h=>(
|
||||
<div key={h} style={{height:HOUR_H,borderBottom:'1px solid var(--border)',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 key={h} style={{height:HOUR_H,borderBottom:'1px solid var(--border)',fontSize:11,color:'var(--text-tertiary)',padding:'3px 10px 0',textAlign:'right'}}>{fmtHour(h)}</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Day columns */}
|
||||
@@ -566,6 +591,7 @@ function WeekView({ events, selectedDate, onSelect }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user