v0.9.86 minor UI changes

This commit is contained in:
2026-03-19 12:10:10 -04:00
parent 6169ad5d99
commit 33b0264080
5 changed files with 113 additions and 10 deletions

View File

@@ -10,7 +10,7 @@
PROJECT_NAME=jama
# Image version to run (set by build.sh, or use 'latest')
JAMA_VERSION=0.9.85
JAMA_VERSION=0.9.86
# App port — the host port Docker maps to the container
PORT=3000

View File

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

View File

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

View File

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

View File

@@ -714,6 +714,98 @@ function BulkImportPanel({ onImported, onCancel }) {
// ── Calendar Views ────────────────────────────────────────────────────────────
// Parse keyword string into array of terms.
// Quoted phrases ("foo bar") count as one term; space-separated words are individual OR terms.
// ── Recurring event expansion ─────────────────────────────────────────────────
// Generates virtual occurrences from a recurring event's rule.
// Returns array of cloned event objects with adjusted start_at / end_at.
function expandRecurringEvent(ev, rangeStart, rangeEnd) {
const rule = ev.recurrence_rule;
if (!rule || !rule.freq) return [ev];
const origStart = new Date(ev.start_at);
const origEnd = new Date(ev.end_at);
const durMs = origEnd - origStart;
const occurrences = [];
let cur = new Date(origStart);
let count = 0;
const maxOccurrences = 500; // safety cap
// Step size based on freq/unit
const freq = rule.freq === 'custom' ? rule.unit : rule.freq.replace('ly','').replace('dai','day').replace('week','week').replace('month','month').replace('year','year');
const interval = rule.interval || 1;
const step = (d) => {
const n = new Date(d);
if (freq === 'day' || rule.freq === 'daily') n.setDate(n.getDate() + interval);
else if (freq === 'week' || rule.freq === 'weekly') n.setDate(n.getDate() + 7 * interval);
else if (freq === 'month' || rule.freq === 'monthly') n.setMonth(n.getMonth() + interval);
else if (freq === 'year' || rule.freq === 'yearly') n.setFullYear(n.getFullYear() + interval);
else n.setDate(n.getDate() + 7); // fallback weekly
return n;
};
// For weekly with byDay, generate per-day occurrences
const byDay = rule.byDay && rule.byDay.length > 0 ? rule.byDay : null;
const DAY_MAP = {SU:0,MO:1,TU:2,WE:3,TH:4,FR:5,SA:6};
// Determine end condition
const endDate = rule.ends === 'on' && rule.endDate ? new Date(rule.endDate + 'T23:59:59') : null;
const endCount = rule.ends === 'after' ? (rule.endCount || 13) : null;
// Start from original and step forward
while (count < maxOccurrences) {
// Check end conditions
if (endDate && cur > endDate) break;
if (endCount && occurrences.length >= endCount) break;
if (cur > rangeEnd) break;
if (byDay && (rule.freq === 'weekly' || freq === 'week')) {
// Emit one occurrence per byDay in this week
const weekStart = new Date(cur);
weekStart.setDate(cur.getDate() - cur.getDay()); // Sunday of this week
for (const dayKey of byDay) {
const dayNum = DAY_MAP[dayKey];
const occ = new Date(weekStart);
occ.setDate(weekStart.getDate() + dayNum);
occ.setHours(origStart.getHours(), origStart.getMinutes(), origStart.getSeconds());
if (occ >= rangeStart && occ <= rangeEnd) {
if (!endDate || occ <= endDate) {
const occEnd = new Date(occ.getTime() + durMs);
occurrences.push({...ev, start_at: occ.toISOString(), end_at: occEnd.toISOString(), _virtual: true});
}
}
}
cur = step(cur);
} else {
if (cur >= rangeStart && cur <= rangeEnd) {
const occEnd = new Date(cur.getTime() + durMs);
occurrences.push({...ev, start_at: cur.toISOString(), end_at: occEnd.toISOString(), _virtual: cur.toISOString() !== ev.start_at});
}
cur = step(cur);
}
count++;
}
// Always include the original even if before rangeStart
return occurrences.length > 0 ? occurrences : [ev];
}
// Expand all recurring events in a list within a date range
function expandEvents(events, rangeStart, rangeEnd) {
const result = [];
for (const ev of events) {
if (ev.recurrence_rule?.freq) {
const expanded = expandRecurringEvent(ev, rangeStart, rangeEnd);
result.push(...expanded);
} else {
result.push(ev);
}
}
// Sort by start_at
result.sort((a,b) => new Date(a.start_at) - new Date(b.start_at));
return result;
}
function parseKeywords(raw) {
const terms = [];
const re = /"([^"]+)"|(\S+)/g;
@@ -727,6 +819,9 @@ function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filter
const today=new Date(); today.setHours(0,0,0,0);
const terms=parseKeywords(filterKeyword);
const hasFilters = terms.length > 0 || !!filterTypeId || filterAvailability;
// Expand recurring events over a wide range (2 years forward)
const farFuture = new Date(today); farFuture.setFullYear(farFuture.getFullYear()+2);
const expandedEvents = expandEvents(events, new Date(y,m,1), farFuture);
const now = new Date(); // exact now for end-time comparison
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.
@@ -734,7 +829,7 @@ function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filter
// Filters active: today + all future events.
const from = hasFilters ? today : (isCurrentMonth ? today : new Date(y,m,1));
const to = hasFilters ? new Date(9999,11,31) : new Date(y,m+1,0,23,59,59);
const filtered=events.filter(e=>{
const filtered=expandedEvents.filter(e=>{
const s=new Date(e.start_at);
if(s<from||s>to) return false;
if(filterTypeId && String(e.event_type_id)!==String(filterTypeId)) return false;
@@ -857,7 +952,10 @@ function layoutEvents(evs) {
return result;
}
function DayView({ events, selectedDate, onSelect, onSwipe }) {
function DayView({ events: rawEvents, selectedDate, onSelect, onSwipe }) {
const dayStart = new Date(selectedDate); dayStart.setHours(0,0,0,0);
const dayEnd = new Date(selectedDate); dayEnd.setHours(23,59,59,999);
const events = expandEvents(rawEvents, dayStart, dayEnd);
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);
@@ -913,7 +1011,10 @@ function DayView({ events, selectedDate, onSelect, onSwipe }) {
);
}
function WeekView({ events, selectedDate, onSelect }) {
function WeekView({ events: rawEvents, selectedDate, onSelect }) {
const ws = weekStart(selectedDate);
const we = new Date(ws); we.setDate(we.getDate()+6); we.setHours(23,59,59,999);
const events = expandEvents(rawEvents, ws, we);
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:DAY_END - DAY_START},(_,i)=>i+DAY_START), today=new Date();
const scrollRef = useRef(null);
@@ -983,8 +1084,10 @@ function WeekView({ events, selectedDate, onSelect }) {
const MONTH_CELL_H = 90; // fixed cell height in px
function MonthView({ events, selectedDate, onSelect, onSelectDay }) {
function MonthView({ events: rawEvents, 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 monthStart = new Date(y,m,1), monthEnd = new Date(y,m+1,0,23,59,59,999);
const events = expandEvents(rawEvents, monthStart, monthEnd);
const cells=[]; for(let i=0;i<first;i++) cells.push(null); for(let d=1;d<=total;d++) cells.push(d);
while(cells.length%7!==0) cells.push(null);
const weeks=[]; for(let i=0;i<cells.length;i+=7) weeks.push(cells.slice(i,i+7));
@@ -1233,11 +1336,11 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
)}
{/* Calendar or panel content */}
<div style={{ flex:1, overflowY:'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 === '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 === 'month' && <MonthView events={events} selectedDate={selDate} onSelect={openDetail} onSelectDay={d=>{setSelDate(d);setView('schedule');}}/>}
{panel === 'calendar' && view === 'month' && <MonthView events={events} selectedDate={selDate} onSelect={openDetail} onSelectDay={d=>{setSelDate(d);setView('day');}}/>}
{panel === 'eventForm' && isToolManager && !isMobile && (
<div style={{ padding:28, maxWidth:1024 }}>