# Calendar Event Detail

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 102  
**Tier:** All tiers  
**Depends on:** `calendar-module`, `projects-module`, `marketing-leads-pipeline`, `foundation-auth-rbac`  
**Referenced by:** `calendar-module`

---

## Overview

Spec `calendar-module` defines `calendar_events` and the monthly/weekly calendar view. This spec defines the event detail panel (shown on click), the create/edit modal, attendee management, recurring event handling, and calendar-to-task linking.

---

## Data Model

Schema delta on `calendar_events`:

```sql
ALTER TABLE calendar_events ADD COLUMN recurrence TEXT;
-- RFC 5545 RRULE string, e.g. 'FREQ=WEEKLY;BYDAY=MO'
-- NULL = no recurrence. Only manual events (source='manual') support recurrence.

ALTER TABLE calendar_events ADD COLUMN attendees JSONB DEFAULT '[]';
-- Array of { email, name, status: 'pending'|'accepted'|'declined' }
-- Stored denormalized; no separate table needed for calendar-level attendees.

ALTER TABLE calendar_events ADD COLUMN color TEXT;
-- Optional event color override (OKLCH string, validated server-side).

ALTER TABLE calendar_events ADD COLUMN lead_id UUID REFERENCES leads(id);
-- Link to lead (for CRM context — kick-off calls, discovery sessions).

ALTER TABLE calendar_events ADD COLUMN parent_event_id UUID REFERENCES calendar_events(id) ON DELETE CASCADE;
-- For a recurring series edited "this event only": the detached one-off row points
-- at the master event. Distinct from external_id (Google/Outlook sync ID), which
-- must stay free for external-calendar correlation on the same row.
```

---

## Event Click: Detail Panel

Clicking any event on the calendar opens a side panel (non-blocking; calendar remains visible):

```
┌──────────────────────────────────────────────────────────────┐
│  Kick-off call — Acme Corp          [Edit] [Delete] [✕]      │
│                                                              │
│  📅  Monday, June 2, 2026   10:00 – 11:00                    │
│  📍  Zoom: https://zoom.us/j/123456                          │
│                                                              │
│  ── Linked ────────────────────────────────────────────────  │
│  👤  Customer: Acme Corp                                     │
│  📋  Project: Website redesign                               │
│  🎯  Lead: John Smith (PROPOSAL)                             │
│                                                              │
│  ── Attendees ──────────────────────────────────────────────  │
│  Dana Levi    dana@zync.is       ✓ Accepted                  │
│  John Smith   john@acme.com      ? Pending                   │
│                                                              │
│  ── Description ────────────────────────────────────────────  │
│  Discuss scope, timeline, and contract terms.                │
│                                                              │
│  🔄  Repeats weekly on Mondays                               │
└──────────────────────────────────────────────────────────────┘
```

---

## Create / Edit Modal

`/calendar` (modal overlay on calendar view):

```
┌──────────────────────────────────────────────────────────────┐
│  New event                                           [✕]     │
│                                                              │
│  Title:  [Kick-off call — Acme Corp___________________]      │
│                                                              │
│  Date:  [2026-06-02]   ☐ All day                             │
│  Time:  [10:00] — [11:00]                                    │
│                                                              │
│  Location:  [Zoom: https://zoom.us/j/123456__________]       │
│                                                              │
│  ── Link to ────────────────────────────────────────────── │
│  Customer:  [Acme Corp ▾]                                    │
│  Project:   [Website redesign ▾]                             │
│  Lead:      [John Smith ▾]                                   │
│                                                              │
│  ── Attendees ──────────────────────────────────────────── │
│  [Add attendee email_______________]  [+ Add]                │
│  dana@zync.is  [✕]                                           │
│  john@acme.com [✕]                                           │
│                                                              │
│  ── Recurrence ─────────────────────────────────────────── │
│  ○ Does not repeat                                           │
│  ○ Daily   ● Weekly on Monday   ○ Monthly   ○ Custom…        │
│                                                              │
│  ── Color ──────────────────────────────────────────────── │
│  ● Default  ○ Blue  ○ Green  ○ Red  ○ Orange  ○ Purple      │
│                                                              │
│  Description:  [Discuss scope, timeline...]                  │
│                                                              │
│  [Cancel]                              [Save event]          │
└──────────────────────────────────────────────────────────────┘
```

Quick-create: clicking empty calendar slot opens modal pre-filled with that date/time.

---

## Recurring Events

When `recurrence` is set:

- Calendar renders all instances by expanding the RRULE client-side (using `rrule.js`) up to 6 months ahead. No separate rows in DB per instance.
- Editing a recurring event: "Edit this event only" vs "Edit all future events".
  - "This only": creates a one-off `calendar_events` row with `recurrence = NULL` and `parent_event_id` referencing the master event (the instance's original date is stored so the master's RRULE expansion skips it).
  - "All future": updates the original event's `recurrence` with `UNTIL=` clause at the split point, creates new event row for the edited series.
- Delete: same two-option dialog.

---

## Calendar → Task Link

If `task_id` is set (source = 'task'), the detail panel shows:

```
│  📌  Task: "Deliver wireframes"  [View task]               │
│       Due: 2026-06-02  Status: IN_PROGRESS                 │
```

Calendar events from tasks are **read-only** (title, date reflect task; edit redirects to task detail).

---

## External Calendar Sync

Events with `source IN ('google', 'outlook')` show a sync badge. Editing pushes change to external calendar via the sync queue (spec `calendar-module` existing flow). Attendees from Google events are imported into `attendees` JSONB on sync.

---

## Attendee Email Notifications

When event is saved with attendees containing external emails (not `@{tenantDomain}`): send invite email via Resend with `.ics` attachment. `attendees[*].status` starts as `'pending'`; recipient can click Accept/Decline link in email (updates via `POST /api/calendar/events/:id/rsvp?token=...`).

---

## API

```
GET /api/calendar/events
    → list events in date range
      query: { start, end, userId? }
      Requires: calendar:read

POST /api/calendar/events
     → create event
       body: { title, start_at, end_at, all_day?, location?, description?,
               customer_id?, project_id?, lead_id?, attendees?, recurrence?, color? }
       Requires: calendar:write

GET /api/calendar/events/:id
    → event detail
      Requires: calendar:read

PATCH /api/calendar/events/:id
      → update event
        body: same as POST + { editScope?: 'this'|'future' }
        Requires: calendar:write

DELETE /api/calendar/events/:id
       → delete event
         query: { scope?: 'this'|'future' }
         Requires: calendar:write

POST /api/calendar/events/:id/rsvp
     → attendee RSVP response (token-auth, no session required)
       body: { token, status: 'accepted'|'declined' }
```

---

## Accessibility

The calendar widget and event detail panel must meet WCAG 2.1 AA. Key requirements:

**Calendar grid (month/week view):**
- Calendar cells: `role="gridcell"`, `aria-label="{date formatted long}"`, `aria-selected="true"` on selected date
- Current date cell: `aria-current="date"`
- Previous/next month navigation buttons: `aria-label="חודש קודם"` / `"חודש הבא"`
- Month/year heading: `aria-live="polite"` — announced when month changes via keyboard

### RTL

Event detail panel inherits `dir` from `<html>`. All layout must use CSS logical properties (`inset-inline-start` not `left`, `margin-inline-end` not `margin-right` — per spec 81). Event title and description: `dir="auto"` to handle user-generated Hebrew and English content.

**Date picker in Create/Edit modal:**

```tsx
// react-day-picker with locale — required config (see hebrew-locale-dates spec)
<DayPicker
  locale={locale === 'he' ? he : undefined}
  dir={locale === 'he' ? 'rtl' : 'ltr'}
  aria-label="בחר תאריך לאירוע"
/>
```

Keyboard: Arrow keys navigate between days. Enter/Space select. Escape closes the picker.

**Event cards on calendar grid:**
- Each event chip: `role="button"`, `aria-label="{title}, {time}, {type}"`
- Truncated text: full title in `title` attribute for tooltip on hover and screen reader access

**Side panel:**
- `role="complementary"` with `aria-label="פרטי אירוע"`
- Closes on Escape; focus returns to the calendar cell that was active before opening

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| RRULE expansion client-side | Not DB rows per instance | Storing N instance rows creates unbounded growth; RRULE in one row + JS expansion is the standard approach (Google Calendar, iCal) |
| Attendees in JSONB | Not separate table | Calendar attendees are lightweight (email + status); no need for relations, individual update granularity, or cross-event attendee identity |
| `.ics` invites via Resend | Not calendar API push | External attendees may not use Google/Outlook; `.ics` works universally; calendar API push is an optional enhancement for synced users |
| Side panel (not modal) | Not full-page | Calendar context is valuable; side panel keeps the month/week view visible while reading event detail |
