v0.10.4 fix event date/time functions

This commit is contained in:
2026-03-21 09:22:02 -04:00
parent 596fd0f969
commit f60730d0a5
5 changed files with 58 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-backend", "name": "jama-backend",
"version": "0.11.3", "version": "0.11.4",
"description": "TeamChat backend server", "description": "TeamChat backend server",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.11.3}" VERSION="${1:-0.11.4}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama" IMAGE_NAME="jama"

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-frontend", "name": "jama-frontend",
"version": "0.11.3", "version": "0.11.4",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -253,17 +253,22 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
finally { setSavingType(false); } finally { setSavingType(false); }
}; };
// When start date changes, match end date // When start date or start time changes, update end date/time to maintain duration
useEffect(() => { if(!event) setEd(sd); }, [sd]);
// When type or start time changes, auto-set end time
useEffect(() => { useEffect(() => {
if(!sd||!st) return; if(!sd||!st) return;
const typ = localTypes.find(t=>t.id===Number(typeId)); const typ = localTypes.find(t=>t.id===Number(typeId));
const dur = typ?.default_duration_hrs||1; const dur = typ?.default_duration_hrs||1;
const start = buildISO(sd,st); const start = buildISO(sd,st);
if(start && !event) { setEd(toDateIn(addHours(start,dur))); setEt(toTimeIn(addHours(start,dur))); } if(!start) return;
}, [typeId, st]); if(event) {
// Editing: only sync end date when start date changes, preserve manual end time
setEd(toDateIn(addHours(start, 0)));
} else {
// New event: always auto-set end to start + duration
setEd(toDateIn(addHours(start,dur)));
setEt(toTimeIn(addHours(start,dur)));
}
}, [sd, st, typeId]);
const handle = async () => { const handle = async () => {
if(!title.trim()) return toast('Title required','error'); if(!title.trim()) return toast('Title required','error');

View File

@@ -835,7 +835,7 @@ function parseKeywords(raw) {
return terms; return terms;
} }
function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filterTypeId='', filterAvailability=false, isMobile=false }) { function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filterTypeId='', filterAvailability=false, filterFromDate=null, isMobile=false }) {
const y=selectedDate.getFullYear(), m=selectedDate.getMonth(); const y=selectedDate.getFullYear(), m=selectedDate.getMonth();
const today=new Date(); today.setHours(0,0,0,0); const today=new Date(); today.setHours(0,0,0,0);
const terms=parseKeywords(filterKeyword); const terms=parseKeywords(filterKeyword);
@@ -845,11 +845,23 @@ function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filter
const expandedEvents = expandEvents(events, new Date(y,m,1), farFuture); const expandedEvents = expandEvents(events, new Date(y,m,1), farFuture);
const now = new Date(); // exact now for end-time comparison const now = new Date(); // exact now for end-time comparison
const isCurrentMonth = y === today.getFullYear() && m === today.getMonth(); const isCurrentMonth = y === today.getFullYear() && m === today.getMonth();
// No filters: show from today (if current month) or start of month (future months) to end of month. // from/to logic:
// Past months show nothing — navigate forward to see events. // - filterFromDate set (mini-calendar click): show from that date to end of its month
// Filters active: today + all future events. // - hasFilters (keyword/type/avail): show from today to far future
const from = hasFilters ? today : (isCurrentMonth ? today : new Date(y,m,1)); // - month nav (no filterFromDate, no filters): show full month, including past events in grey
const to = hasFilters ? new Date(9999,11,31) : new Date(y,m+1,0,23,59,59); let from, to;
if (filterFromDate) {
const fd = new Date(filterFromDate); fd.setHours(0,0,0,0);
from = fd;
to = new Date(fd.getFullYear(), fd.getMonth()+1, 0, 23, 59, 59);
} else if (hasFilters) {
from = today;
to = new Date(9999,11,31);
} else {
// Full month — start of month to end of month, past events included (shown grey)
from = new Date(y,m,1);
to = new Date(y,m+1,0,23,59,59);
}
const filtered=expandedEvents.filter(e=>{ const filtered=expandedEvents.filter(e=>{
const s=new Date(e.start_at); const s=new Date(e.start_at);
if(s<from||s>to) return false; if(s<from||s>to) return false;
@@ -1165,6 +1177,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
const [filterKeyword, setFilterKeyword] = useState(''); const [filterKeyword, setFilterKeyword] = useState('');
const [filterTypeId, setFilterTypeId] = useState(''); const [filterTypeId, setFilterTypeId] = useState('');
const [filterAvailability, setFilterAvailability] = useState(false); const [filterAvailability, setFilterAvailability] = useState(false);
const [filterFromDate, setFilterFromDate] = useState(null); // set by mini-calendar click
const [inputFocused, setInputFocused] = useState(false); // hides footer when keyboard open on mobile const [inputFocused, setInputFocused] = useState(false); // hides footer when keyboard open on mobile
const [detailEvent, setDetailEvent] = useState(null); const [detailEvent, setDetailEvent] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -1187,13 +1200,25 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
return () => document.removeEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h);
}, [createOpen]); }, [createOpen]);
const eventDates = new Set(events.map(e => e.start_at?.slice(0,10))); const eventDates = new Set(events.map(e => {
if (!e.start_at) return null;
const d = new Date(e.start_at);
const pad = n => String(n).padStart(2,'0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
}).filter(Boolean));
const navDate = dir => { const navDate = dir => {
const d = new Date(selDate); const d = new Date(selDate);
if (view==='day') d.setDate(d.getDate()+dir); if (view==='day') d.setDate(d.getDate()+dir);
else if (view==='week') d.setDate(d.getDate()+dir*7); else if (view==='week') d.setDate(d.getDate()+dir*7);
else d.setMonth(d.getMonth()+dir); else {
d.setMonth(d.getMonth()+dir);
// Month nav: clear mini-calendar filter and show full month
setFilterFromDate(null);
setFilterKeyword('');
setFilterTypeId('');
setFilterAvailability(false);
}
setSelDate(d); setSelDate(d);
}; };
@@ -1258,7 +1283,14 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
{/* Mini calendar */} {/* Mini calendar */}
<div style={{ padding:'8px 16px 16px' }}> <div style={{ padding:'8px 16px 16px' }}>
<div className="section-label" style={{ marginBottom:8 }}>Filter Events</div> <div className="section-label" style={{ marginBottom:8 }}>Filter Events</div>
<MiniCalendar selected={selDate} onChange={d=>{setSelDate(d);setPanel('calendar');}} eventDates={eventDates}/> <MiniCalendar selected={selDate} onChange={d=>{
setSelDate(d);
setPanel('calendar');
setFilterFromDate(d);
setFilterKeyword('');
setFilterTypeId('');
setFilterAvailability(false);
}} eventDates={eventDates}/>
</div> </div>
{/* List view filters — only shown in Schedule list view */} {/* List view filters — only shown in Schedule list view */}
@@ -1287,7 +1319,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
{(filterKeyword||filterTypeId||filterAvailability) && ( {(filterKeyword||filterTypeId||filterAvailability) && (
<button <button
className="btn btn-secondary btn-sm" className="btn btn-secondary btn-sm"
onClick={()=>{setFilterKeyword('');setFilterTypeId('');setFilterAvailability(false);}} onClick={()=>{setFilterKeyword('');setFilterTypeId('');setFilterAvailability(false);setFilterFromDate(null);}}
style={{ marginTop:8, width:'100%' }} style={{ marginTop:8, width:'100%' }}
>Clear filters</button> >Clear filters</button>
)} )}
@@ -1324,7 +1356,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
{allowedViews.map(v => { {allowedViews.map(v => {
const labels = { schedule:'Schedule', day:'Day', week:'Week', month:'Month' }; const labels = { schedule:'Schedule', day:'Day', week:'Week', month:'Month' };
return ( return (
<button key={v} onClick={()=>{setView(v);setPanel('calendar');setSelDate(new Date());setFilterKeyword('');setFilterTypeId('');setFilterAvailability(false);}} style={{ padding:'4px 10px', borderRadius:5, border:'none', cursor:'pointer', fontSize:12, fontWeight:600, background:view===v?'var(--surface)':'transparent', color:view===v?'var(--text-primary)':'var(--text-tertiary)', boxShadow:view===v?'0 1px 3px rgba(0,0,0,0.1)':'none', transition:'all 0.15s', whiteSpace:'nowrap' }}> <button key={v} onClick={()=>{setView(v);setPanel('calendar');setSelDate(new Date());setFilterKeyword('');setFilterTypeId('');setFilterAvailability(false);setFilterFromDate(null);}} style={{ padding:'4px 10px', borderRadius:5, border:'none', cursor:'pointer', fontSize:12, fontWeight:600, background:view===v?'var(--surface)':'transparent', color:view===v?'var(--text-primary)':'var(--text-tertiary)', boxShadow:view===v?'0 1px 3px rgba(0,0,0,0.1)':'none', transition:'all 0.15s', whiteSpace:'nowrap' }}>
{labels[v]} {labels[v]}
</button> </button>
); );
@@ -1355,7 +1387,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
{/* Calendar or panel content */} {/* Calendar or panel content */}
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow: view==='month' && panel==='calendar' ? 'hidden' : (panel==='eventForm'?'auto':'auto'), overflowX: panel==='eventForm'?'auto':'hidden' }}> <div style={{ flex:1, display:'flex', flexDirection:'column', overflow: view==='month' && panel==='calendar' ? 'hidden' : (panel==='eventForm'?'auto':'auto'), overflowX: panel==='eventForm'?'auto':'hidden' }}>
{panel === 'calendar' && view === 'schedule' && <div style={{paddingBottom: isMobile ? 80 : 0}}><ScheduleView events={events} selectedDate={selDate} onSelect={openDetail} filterKeyword={filterKeyword} filterTypeId={filterTypeId} filterAvailability={filterAvailability} isMobile={isMobile}/></div>} {panel === 'calendar' && view === 'schedule' && <div style={{paddingBottom: isMobile ? 80 : 0}}><ScheduleView events={events} selectedDate={selDate} onSelect={openDetail} filterKeyword={filterKeyword} filterTypeId={filterTypeId} filterAvailability={filterAvailability} filterFromDate={filterFromDate} isMobile={isMobile}/></div>}
{panel === 'calendar' && view === 'day' && <DayView events={events} selectedDate={selDate} onSelect={openDetail} onSwipe={isMobile ? dir => { const d=new Date(selDate); d.setDate(d.getDate()+dir); setSelDate(d); } : undefined}/>} {panel === 'calendar' && view === 'day' && <DayView events={events} selectedDate={selDate} onSelect={openDetail} onSwipe={isMobile ? dir => { const d=new Date(selDate); d.setDate(d.getDate()+dir); setSelDate(d); } : undefined}/>}
{panel === 'calendar' && view === 'week' && <WeekView events={events} selectedDate={selDate} onSelect={openDetail}/>} {panel === 'calendar' && view === 'week' && <WeekView events={events} selectedDate={selDate} onSelect={openDetail}/>}
{panel === 'calendar' && view === 'month' && <MonthView events={events} selectedDate={selDate} onSelect={openDetail} onSelectDay={d=>{setSelDate(d);setView('day');}}/>} {panel === 'calendar' && view === 'month' && <MonthView events={events} selectedDate={selDate} onSelect={openDetail} onSelectDay={d=>{setSelDate(d);setView('day');}}/>}