v0.9.86 minor UI changes
This commit is contained in:
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user