From b2b09cb0d0176a127cf3dea29e051a8f13ebfce9 Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Tue, 17 Mar 2026 18:43:25 -0400 Subject: [PATCH] v0.9.60 Ramsey recommended feature adds --- .env.example | 2 +- backend/package.json | 2 +- backend/src/models/db.js | 12 ++-- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/SchedulePage.jsx | 92 +++++++++++++++++++++--- 6 files changed, 90 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index 5c6def5..953223c 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.59 +JAMA_VERSION=0.9.60 # App port — the host port Docker maps to the container PORT=3000 diff --git a/backend/package.json b/backend/package.json index fa8eef5..bf5a714 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.9.59", + "version": "0.9.60", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/models/db.js b/backend/src/models/db.js index 4a01e3b..8750ba0 100644 --- a/backend/src/models/db.js +++ b/backend/src/models/db.js @@ -432,21 +432,19 @@ 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) {} + // Delete the legacy "Default" type — "Event" is the canonical default + db.prepare("DELETE FROM event_types WHERE name = 'Default'").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(); - // 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 + // Remove duplicates — keep the one with is_default=1 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; i ( + + Going + + + ), + maybe: (color,size=15) => ( + + Maybe + + + ), + not_going: (color,size=15) => ( + + Not Going + + + ), +}; +const BELL_ICON = ( + + Awaiting your response + + +); // 30-minute time slots const TIME_SLOTS = (() => { @@ -170,7 +196,14 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc const [showTypeForm,setShowTypeForm]=useState(false); const [localTypes,setLocalTypes]=useState(eventTypes); // Sync localTypes when parent provides updated eventTypes (e.g. after async load) - useEffect(()=>{ setLocalTypes(eventTypes); },[eventTypes]); + // Also initialise typeId to the default event type for new events + 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 typeRef=useRef(null); // Track whether the user has manually changed the end time (vs auto-computed) @@ -373,7 +406,9 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
{event.event_type?.name&&{event.event_type.name}} - {!event.is_public&&Private} + {event.is_public + ? Public Event + : Private Event}
@@ -482,19 +517,22 @@ function BulkImportPanel({ onImported, onCancel }) { } // ── Calendar Views ──────────────────────────────────────────────────────────── -function ScheduleView({ events, selectedDate, onSelect }) { +function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filterTypeId='' }) { 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 kw=filterKeyword.toLowerCase().trim(); const filtered=events.filter(e=>{ const s=new Date(e.start_at); - return s>=monthStart && s<=monthEnd; + if(smonthEnd) return false; + if(filterTypeId && String(e.event_type_id)!==String(filterTypeId)) return false; + if(kw && ![ + e.title||'', e.location||'', e.description||'' + ].some(f=>f.toLowerCase().includes(kw))) return false; + return true; }); - 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&&( - - Awaiting your response - - + if(!filtered.length) return
{kw||filterTypeId?'No events match your filters':'No events in'} {!kw&&!filterTypeId&&`${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 ? RESP_ICON[e.my_response](RESP_COLOR[e.my_response]) : BELL_ICON )}
{e.location&&
{e.location}
}
);})}; } @@ -708,6 +746,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel const [userGroups, setUserGroups] = useState([]); const [panel, setPanel] = useState('calendar'); const [editingEvent, setEditingEvent] = useState(null); + const [filterKeyword, setFilterKeyword] = useState(''); + const [filterTypeId, setFilterTypeId] = useState(''); const [detailEvent, setDetailEvent] = useState(null); const [loading, setLoading] = useState(true); const [createOpen, setCreateOpen] = useState(false); @@ -797,6 +837,36 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
Filter Events
{setSelDate(d);setPanel('calendar');}} eventDates={eventDates}/>
+ + {/* List view filters — only shown in Schedule list view */} + {view==='schedule' && panel==='calendar' && ( +
+
Search
+ setFilterKeyword(e.target.value)} + style={{ marginBottom:8, fontSize:13 }} + /> + + {(filterKeyword||filterTypeId) && ( + + )} +
+ )}
@@ -843,7 +913,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel {/* Calendar or panel content */}
- {panel === 'calendar' && view === 'schedule' && } + {panel === 'calendar' && view === 'schedule' && } {panel === 'calendar' && view === 'day' && } {panel === 'calendar' && view === 'week' && } {panel === 'calendar' && view === 'month' && {setSelDate(d);setView('schedule');}}/>}