Fastify/Postgres backend + React frontend matching the stack's app conventions, plus a hand-rolled RFC 4791 CalDAV server (caldav-adapter turned out Koa-only in practice) so calendars subscribe as genuine two-way sync in Apple/Google/Outlook. v1 scope: multiple colour-coded calendars, department/staff event tagging via live Workforce lookups, month/week/day/list views, dashboard, file attachments, activity log, and an auto-synced UK bank holidays calendar. Verified end-to-end locally against real Postgres: REST CRUD, CalDAV discovery/PROPFIND/REPORT/PUT/sync-collection, all-day date handling, system-calendar write protection, and activity logging across both the web and CalDAV write paths. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
import type { EventSummary } from '../../types'
|
|
import {
|
|
startOfWeek, addDays, isSameDay, eventOccursOnDay,
|
|
formatDayHeader, formatTime, HOURS, HOUR_PX, timedLayout,
|
|
} from '../../dateUtils'
|
|
|
|
export default function WeekGrid({ events, date, onSelectDate, onSelectEvent }: {
|
|
events: EventSummary[]
|
|
date: Date
|
|
onSelectDate?: (d: Date) => void
|
|
onSelectEvent: (id: number) => void
|
|
}) {
|
|
const weekStart = startOfWeek(date)
|
|
const days = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i))
|
|
const today = new Date()
|
|
|
|
const allDayByDay = days.map(day => events.filter(ev => ev.all_day && eventOccursOnDay(ev, day)))
|
|
const timedByDay = days.map(day => events.filter(ev => !ev.all_day && eventOccursOnDay(ev, day)))
|
|
|
|
return (
|
|
<div className="cal-week-grid">
|
|
<div className="cal-week-head-cell" />
|
|
{days.map(day => (
|
|
<div
|
|
key={day.toISOString()}
|
|
className={`cal-week-head-cell${isSameDay(day, today) ? ' today' : ''}`}
|
|
onClick={() => onSelectDate?.(day)}
|
|
style={{ cursor: 'pointer' }}
|
|
>
|
|
{formatDayHeader(day)}
|
|
</div>
|
|
))}
|
|
|
|
<div />
|
|
{days.map((day, i) => (
|
|
<div key={`allday-${day.toISOString()}`} style={{ borderLeft: '1px solid var(--card-border)', padding: '3px' }}>
|
|
{allDayByDay[i].map(ev => (
|
|
<span
|
|
key={ev.id}
|
|
className="cal-event-chip"
|
|
style={{ background: ev.calendar_color, color: '#fff', marginBottom: 2 }}
|
|
onClick={() => onSelectEvent(ev.id)}
|
|
>
|
|
{ev.title}
|
|
</span>
|
|
))}
|
|
</div>
|
|
))}
|
|
|
|
<div className="cal-time-gutter">
|
|
{HOURS.map(h => (
|
|
<div key={h} className="cal-time-row">{h === 0 ? '' : `${h}:00`}</div>
|
|
))}
|
|
</div>
|
|
{days.map((day, i) => (
|
|
<div
|
|
key={`col-${day.toISOString()}`}
|
|
className="cal-day-col"
|
|
style={{ height: HOURS.length * HOUR_PX }}
|
|
onClick={() => onSelectDate?.(day)}
|
|
>
|
|
{HOURS.map(h => <div key={h} className="cal-day-col-slot" />)}
|
|
{timedByDay[i].map(ev => {
|
|
const { top, height } = timedLayout(ev, day)
|
|
return (
|
|
<div
|
|
key={ev.id}
|
|
className="cal-week-event"
|
|
style={{ top, height, background: ev.calendar_color, color: '#fff' }}
|
|
onClick={e => { e.stopPropagation(); onSelectEvent(ev.id) }}
|
|
title={ev.title}
|
|
>
|
|
{formatTime(ev.start_at)} {ev.title}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|