From 5d21420ed974ee530d7fd551c6ee0fdfc5c7c5ed Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Tue, 17 Mar 2026 14:58:08 -0400 Subject: [PATCH] v0.9.51 minor schedule bug fixes. --- .env.example | 2 +- backend/package.json | 2 +- backend/src/models/db.js | 23 ++-- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/SchedulePage.jsx | 154 +++++++++++++++++++++-- 6 files changed, 162 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index db8c0f0..d465d80 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ PROJECT_NAME=jama # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.9.50 +JAMA_VERSION=0.9.51 # App port — the host port Docker maps to the container PORT=3000 diff --git a/backend/package.json b/backend/package.json index 5b17e7d..8b3fc6d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.9.50", + "version": "0.9.51", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/models/db.js b/backend/src/models/db.js index 690de55..4a01e3b 100644 --- a/backend/src/models/db.js +++ b/backend/src/models/db.js @@ -432,16 +432,23 @@ function initDb() { // Migration: add columns if missing (must run before inserts) try { db.exec("ALTER TABLE event_types ADD COLUMN is_protected INTEGER NOT NULL DEFAULT 0"); } catch(e) {} try { db.exec("ALTER TABLE event_types ADD COLUMN default_duration_hrs REAL"); } catch(e) {} - // Seed built-in event types - db.prepare("INSERT OR IGNORE INTO event_types (name, colour, is_default, is_protected) VALUES ('Default', '#9ca3af', 1, 1)").run(); - db.prepare("INSERT OR IGNORE INTO event_types (name, colour, is_protected, default_duration_hrs) VALUES ('Event', '#6366f1', 1, NULL)").run(); + // Seed built-in event types — "Event" is the primary default (1hr, protected, cannot edit/delete) + db.prepare("INSERT OR IGNORE INTO event_types (name, colour, is_default, is_protected, default_duration_hrs) VALUES ('Event', '#6366f1', 1, 1, 1.0)").run(); db.prepare("INSERT OR IGNORE INTO event_types (name, colour, default_duration_hrs) VALUES ('Game', '#22c55e', 3.0)").run(); db.prepare("INSERT OR IGNORE INTO event_types (name, colour, default_duration_hrs) VALUES ('Practice', '#f59e0b', 1.0)").run(); - // Ensure built-in types are protected (idempotent) - db.prepare("UPDATE event_types SET is_protected = 1 WHERE name IN ('Default', 'Event')").run(); - // Ensure Game/Practice have correct durations if they already existed without them - db.prepare("UPDATE event_types SET default_duration_hrs = 3.0 WHERE name = 'Game' AND default_duration_hrs IS NULL").run(); - db.prepare("UPDATE event_types SET default_duration_hrs = 1.0 WHERE name = 'Practice' AND default_duration_hrs IS NULL").run(); + // Rename legacy "Default" to "Event" if it exists + db.prepare("UPDATE event_types SET name = 'Event', is_protected = 1, default_duration_hrs = 1.0 WHERE name = 'Default'").run(); + // Remove the old separate "Event" type if it was created before this migration (was a duplicate) + // Keep whichever has is_default=1; delete duplicates + const evtTypes = db.prepare("SELECT id, is_default FROM event_types WHERE name = 'Event' ORDER BY is_default DESC").all(); + if (evtTypes.length > 1) { + for (let i=1; iString(n).padStart(2,'0'); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`; +} function sameDay(a,b) { return a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth()&&a.getDate()===b.getDate(); } function weekStart(d) { const r=new Date(d); r.setDate(d.getDate()-d.getDay()); r.setHours(0,0,0,0); return r; } function daysInMonth(y,m){ return new Date(y,m+1,0).getDate(); } @@ -215,8 +220,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
{isToolManager&&} {showTypeForm&&{setLocalTypes(p=>[...p,et]);setShowTypeForm(false);}} onClose={()=>setShowTypeForm(false)}/>} @@ -453,29 +457,157 @@ function BulkImportPanel({ onImported, onCancel }) { // ── Calendar Views ──────────────────────────────────────────────────────────── function ScheduleView({ events, selectedDate, onSelect }) { - const filtered=events.filter(e=>new Date(e.end_at)>=(selectedDate||new Date(0))); - if(!filtered.length) return
No upcoming events from selected date
; + const y=selectedDate.getFullYear(), m=selectedDate.getMonth(); + const monthStart=new Date(y,m,1), monthEnd=new Date(y,m+1,0,23,59,59); + const filtered=events.filter(e=>{ + const s=new Date(e.start_at); + return s>=monthStart && s<=monthEnd; + }); + if(!filtered.length) return
No events in {MONTHS[m]} {y}
; return <>{filtered.map(e=>{const s=new Date(e.start_at);const col=e.event_type?.colour||'#9ca3af';return(
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=''}>
{s.getDate()}
{SHORT_MONTHS[s.getMonth()]}, {DAYS[s.getDay()]}
{e.all_day?'All day':fmtRange(e.start_at,e.end_at)}
{e.event_type?.name&&{e.event_type.name}:}{e.title}{e.track_availability&&!e.my_response&&}
{e.location&&
{e.location}
}
);})}; } +const HOUR_H = 56; // px per hour row + +function eventTopOffset(startDate) { + const h=startDate.getHours(), m=startDate.getMinutes(); + return (h-7)*HOUR_H + (m/60)*HOUR_H; +} +function eventHeightPx(startDate, endDate) { + const diffMs=endDate-startDate; + const diffHrs=diffMs/(1000*60*60); + return Math.max(diffHrs*HOUR_H, HOUR_H*0.4); // min 40% of one hour row +} + function DayView({ events, selectedDate, onSelect }) { - const hours=Array.from({length:16},(_,i)=>i+7); + const hours=Array.from({length:16},(_,i)=>i+7); // 7am–10pm const day=events.filter(e=>sameDay(new Date(e.start_at),selectedDate)); - return(
{DAYS[selectedDate.getDay()]}
{selectedDate.getDate()}
{hours.map(h=>(
{h>12?`${h-12} PM`:h===12?'12 PM':`${h} AM`}
{day.filter(e=>new Date(e.start_at).getHours()===h).map(e=>(
onSelect(e)} style={{margin:'2px 0',padding:'5px 10px',borderRadius:5,background:e.event_type?.colour||'#6366f1',color:'white',fontSize:12,cursor:'pointer',fontWeight:600}}>{e.title} · {fmtRange(e.start_at,e.end_at)}
))}
))}
); + return( +
+
+
{DAYS[selectedDate.getDay()]}
{selectedDate.getDate()}
+
+
+ {/* Hour grid */} + {hours.map(h=>( +
+
+ {h>12?`${h-12} PM`:h===12?'12 PM':`${h} AM`} +
+
+
+ ))} + {/* 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( +
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)', + }}> +
{e.title}
+ {height>32&&
{fmtRange(e.start_at,e.end_at)}
} +
+ ); + })} +
+
+ ); } 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(); - return(
{days.map((d,i)=>
{DAYS[d.getDay()]} {d.getDate()}
)}
{hours.map(h=>(
{h>12?`${h-12} PM`:h===12?'12 PM':`${h} AM`}
{days.map((d,i)=>(
{events.filter(e=>sameDay(new Date(e.start_at),d)&&new Date(e.start_at).getHours()===h).map(e=>(
onSelect(e)} style={{background:e.event_type?.colour||'#6366f1',color:'white',borderRadius:3,padding:'2px 5px',fontSize:11,cursor:'pointer',marginBottom:1,fontWeight:600,overflow:'hidden',whiteSpace:'nowrap',textOverflow:'ellipsis'}}>{e.title}
))}
))}
))}
); + return( +
+ {/* Day headers */} +
+
+ {days.map((d,i)=>
{DAYS[d.getDay()]} {d.getDate()}
)} +
+ {/* Time grid with event columns */} +
+ {/* Time labels column */} +
+ {hours.map(h=>( +
+ {h>12?`${h-12} PM`:h===12?'12 PM':`${h} AM`} +
+ ))} +
+ {/* Day columns */} + {days.map((d,di)=>{ + const dayEvs=events.filter(e=>sameDay(new Date(e.start_at),d)); + return( +
+ {hours.map(h=>
)} + {dayEvs.map(e=>{ + const s=new Date(e.start_at),en=new Date(e.end_at); + const top=eventTopOffset(s), height=eventHeightPx(s,en); + return( +
onSelect(e)} style={{ + position:'absolute',top,left:2,right:2,height, + background:e.event_type?.colour||'#6366f1',color:'white', + borderRadius:3,padding:'2px 4px',cursor:'pointer', + fontSize:11,fontWeight:600,overflow:'hidden', + boxShadow:'0 1px 2px rgba(0,0,0,0.2)', + }}> +
{e.title}
+ {height>26&&
{fmtTime(e.start_at)}-{fmtTime(e.end_at)}
} +
+ ); + })} +
+ ); + })} +
+
+ ); } +const MONTH_CELL_H = 90; // fixed cell height in px + function MonthView({ events, selectedDate, onSelect, onSelectDay }) { const y=selectedDate.getFullYear(), m=selectedDate.getMonth(), first=new Date(y,m,1).getDay(), total=daysInMonth(y,m), today=new Date(); const cells=[]; for(let i=0;i
{DAYS.map(d=>
{d}
)}
{weeks.map((week,wi)=>(
{week.map((d,di)=>{if(!d)return
;const date=new Date(y,m,d),dayEvs=events.filter(e=>sameDay(new Date(e.start_at),date)),isToday=sameDay(date,today);return(
onSelectDay(date)} style={{borderRight:'1px solid var(--border)',borderBottom:'1px solid var(--border)',padding:'4px',minHeight:90,cursor:'pointer'}} onMouseEnter={el=>el.currentTarget.style.background='var(--background)'} onMouseLeave={el=>el.currentTarget.style.background=''}>
{d}
{dayEvs.slice(0,3).map(e=>(
{ev.stopPropagation();onSelect(e);}} style={{background:e.event_type?.colour||'#6366f1',color:'white',borderRadius:3,padding:'1px 5px',fontSize:11,marginBottom:2,cursor:'pointer',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{!e.all_day&&{fmtTime(e.start_at)}}{e.title}
))}{dayEvs.length>3&&
+{dayEvs.length-3} more
}
);})}
))}
); + return( +
+
+ {DAYS.map(d=>
{d}
)} +
+ {weeks.map((week,wi)=>( +
+ {week.map((d,di)=>{ + if(!d) return
; + const date=new Date(y,m,d), dayEvs=events.filter(e=>sameDay(new Date(e.start_at),date)), isToday=sameDay(date,today); + return( +
onSelectDay(date)} style={{borderRight:'1px solid var(--border)',borderBottom:'1px solid var(--border)',height:MONTH_CELL_H,padding:'3px',cursor:'pointer',overflow:'hidden',display:'flex',flexDirection:'column'}} + onMouseEnter={el=>el.currentTarget.style.background='var(--background)'} onMouseLeave={el=>el.currentTarget.style.background=''}> +
{d}
+ {dayEvs.slice(0,2).map(e=>( +
{ev.stopPropagation();onSelect(e);}} style={{ + background:e.event_type?.colour||'#6366f1',color:'white', + borderRadius:3,padding:'1px 4px',fontSize:11,marginBottom:1,cursor:'pointer', + whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',flexShrink:0, + }}> + {!e.all_day&&{fmtTime(e.start_at)}}{e.title} +
+ ))} + {dayEvs.length>2&&
+{dayEvs.length-2} more
} +
+ ); + })} +
+ ))} +
+ ); } // ── Main Schedule Page ──────────────────────────────────────────────────────── @@ -525,7 +657,7 @@ export default function SchedulePage({ isToolManager, isMobile }) { const navLabel = () => { if (view==='day') return `${DAYS[selDate.getDay()]} ${selDate.getDate()} ${MONTHS[selDate.getMonth()]} ${selDate.getFullYear()}`; if (view==='week') { const ws=weekStart(selDate),we=new Date(ws); we.setDate(we.getDate()+6); return `${SHORT_MONTHS[ws.getMonth()]} ${ws.getDate()} – ${SHORT_MONTHS[we.getMonth()]} ${we.getDate()} ${we.getFullYear()}`; } - return `${MONTHS[selDate.getMonth()]} ${selDate.getFullYear()}`; + return `${MONTHS[selDate.getMonth()]} ${selDate.getFullYear()}`; // schedule + month }; const openDetail = async e => { @@ -602,7 +734,7 @@ export default function SchedulePage({ isToolManager, isMobile }) {
- {view !== 'schedule' && {navLabel()}} + {navLabel()}
)}