Add calendar app — shared events calendar with departments, staff tagging, bank holidays and two-way phone sync
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>
This commit is contained in:
commit
bf5557d277
53 changed files with 12529 additions and 0 deletions
51
frontend/src/components/views/MonthGrid.tsx
Normal file
51
frontend/src/components/views/MonthGrid.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import type { EventSummary } from '../../types'
|
||||
import { monthGridDays, isSameDay, eventOccursOnDay, formatTime, WEEKDAY_LABELS } from '../../dateUtils'
|
||||
|
||||
const MAX_VISIBLE = 3
|
||||
|
||||
export default function MonthGrid({ events, date, onSelectDate, onSelectEvent }: {
|
||||
events: EventSummary[]
|
||||
date: Date
|
||||
onSelectDate?: (d: Date) => void
|
||||
onSelectEvent: (id: number) => void
|
||||
}) {
|
||||
const days = monthGridDays(date)
|
||||
const today = new Date()
|
||||
|
||||
return (
|
||||
<div className="cal-grid">
|
||||
{WEEKDAY_LABELS.map(d => (
|
||||
<div key={d} className="cal-day-header">{d}</div>
|
||||
))}
|
||||
{days.map(day => {
|
||||
const dayEvents = events
|
||||
.filter(ev => eventOccursOnDay(ev, day))
|
||||
.sort((a, b) => a.start_at.localeCompare(b.start_at))
|
||||
const cls = [
|
||||
'cal-day',
|
||||
day.getMonth() !== date.getMonth() ? 'other-month' : '',
|
||||
isSameDay(day, today) ? 'today' : '',
|
||||
].filter(Boolean).join(' ')
|
||||
return (
|
||||
<div key={day.toISOString()} className={cls} onClick={() => onSelectDate?.(day)}>
|
||||
<div className="cal-day-num">{day.getDate()}</div>
|
||||
{dayEvents.slice(0, MAX_VISIBLE).map(ev => (
|
||||
<span
|
||||
key={ev.id}
|
||||
className="cal-event-chip"
|
||||
style={{ background: ev.calendar_color, color: '#fff' }}
|
||||
title={ev.title}
|
||||
onClick={e => { e.stopPropagation(); onSelectEvent(ev.id) }}
|
||||
>
|
||||
{!ev.all_day && `${formatTime(ev.start_at)} `}{ev.title}
|
||||
</span>
|
||||
))}
|
||||
{dayEvents.length > MAX_VISIBLE && (
|
||||
<span className="cal-event-more">+{dayEvents.length - MAX_VISIBLE} more</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue