# Calendar Module

**Date:** 2026-05-30  
**Status:** Draft  
**Depends on:** `foundation-auth-rbac`, `customers-module`, `projects-module`, `tasks-board-engine`, `system-communications-notifications`  
**Referenced by:** `crm-support-center`, `tenant-portals`

---

## Overview

Calendar view of tasks, projects, and custom events. Two-way sync with Google Calendar and Microsoft Outlook. Optional scheduling integrations (Calendly, Acuity, moCal) for client-facing appointment booking. Events created via scheduling tools are automatically linked to customers and can trigger task/ticket creation.

---

## Data Model

```sql
calendar_events (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  created_by UUID NOT NULL,
  title TEXT NOT NULL,
  description TEXT,
  start_at TIMESTAMPTZ NOT NULL,
  end_at TIMESTAMPTZ NOT NULL,
  all_day BOOLEAN DEFAULT false,
  location TEXT,

  -- Source
  source TEXT DEFAULT 'manual',       -- 'manual' | 'task' | 'project' | 'google' | 'outlook' | 'calendly' | 'acuity' | 'mocal'
  task_id UUID,                        -- if sourced from a task due date
  project_id UUID,                     -- if sourced from project timeline
  customer_id UUID,                    -- if linked to a customer meeting

  -- External sync
  external_id TEXT,                    -- ID in Google/Outlook calendar
  external_calendar_id TEXT,           -- which calendar (user may have multiple)
  synced_at TIMESTAMPTZ,
  sync_status TEXT DEFAULT 'local',    -- 'local' | 'synced' | 'pending' | 'error'

  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
)

calendar_connections (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  user_id UUID NOT NULL,
  provider TEXT NOT NULL,              -- 'google' | 'outlook'
  external_user_id TEXT NOT NULL,      -- Google sub / Outlook oid
  access_token BYTEA NOT NULL,         -- AES-256-GCM encrypted
  refresh_token BYTEA NOT NULL,        -- AES-256-GCM encrypted
  token_expires_at TIMESTAMPTZ,
  selected_calendar_id TEXT,           -- calendar to sync (user picks after auth)
  sync_enabled BOOLEAN DEFAULT true,
  last_synced_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now()
)

scheduling_connections (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  provider TEXT NOT NULL,              -- 'calendly' | 'acuity' | 'mocal'
  api_key BYTEA NOT NULL,              -- AES-256-GCM encrypted
  webhook_uri TEXT,                    -- inbound webhook URL registered with provider
  settings JSONB,                      -- provider-specific config
  created_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE (tenant_id, provider)
)
```

---

## Calendar View (`/calendar`)

### Layout

```
┌─────────────────────────────────────────────────────────┐
│  ◄  May 2026  ►   [Day] [Week] [Month]  [+ New event]   │
│                                                         │
│  Mon  Tue  Wed  Thu  Fri  Sat  Sun                      │
│  ┌───┬───┬───┬───┬───┬───┬───┐                          │
│  │   │ ● │   │ ● │   │   │   │  ← month view           │
│  │   │   │ ■ │   │ ● │   │   │                          │
│  └───┴───┴───┴───┴───┴───┴───┘                          │
└─────────────────────────────────────────────────────────┘
```

Views: Day (time grid), Week (time grid), Month (day squares).

Event chips color-coded by source: tasks (blue), projects (green), manual (purple), external (grey).

Click event → popover with summary + "Open" link to task/project/detail.

### Event sources shown in calendar

1. **Tasks with due dates** — pulled from `tasks` where `due_date IS NOT NULL`
2. **Project milestones** — from `project_milestones` (if any)
3. **Manual events** — created directly in calendar
4. **External events** — synced from Google/Outlook

Filter controls: toggles per source type; user-level preference stored in `user_preferences`.

### Create event

"+ New event" → sheet form:
- Title (required)
- Date + time (all-day toggle)
- Description, location
- Customer, project links (optional)
- Participants (team members + customers via email)

Creates `calendar_events` record. If user has sync enabled: pushed to external calendar on save.

### RTL Layout (Hebrew)

**Column ordering:**
- Week starts Sunday (Israel standard) — same logical column order in both LTR and RTL
- `dir="rtl"` on the grid container reverses CSS grid column rendering: Sunday column appears on the **right** (rightmost visual = first logical in RTL = Israeli standard), Saturday on the **left**

**Time gutter:**
- LTR: time labels on left (`inset-inline-start: 0`)
- RTL: time labels on right — `inset-inline-start: 0` auto-mirrors via CSS logical properties
- Do NOT use `left: 0` / `right: 0` — logical properties only (per spec 81 CSS rules)

**Event blocks:**
- Text within event blocks: `dir="auto"` (user-generated, may be Hebrew or English)

**Navigation arrows:**
- Prev/Next chevrons mirror in RTL via `transform: scaleX(-1)` on directional arrows, or use `margin-inline-start` per spec 81 icon mirroring rules

---

## Google Calendar Sync

### OAuth Flow

1. User clicks "Connect Google Calendar" in `/settings/integrations/calendar`
2. Redirect to Google OAuth with scopes: `calendar.readonly calendar.events`
3. Callback: `GET /api/auth/google-calendar/callback` → store encrypted tokens in `calendar_connections`
4. User picks which calendar to sync (list from Google API)

### Sync Mechanism

**Push from Zync → Google:** on create/update/delete of manual events and task due-date events, push to Google via `events.insert` / `events.update` / `events.delete`.

**Pull from Google → Zync:** Google Push Notifications (`events.watch`) → sends HTTP POST to `POST /api/webhooks/calendar/google/{connectionId}` on changes. Worker fetches changed events, upserts into `calendar_events`.

**Token refresh:** access token stored with `token_expires_at`. Before any Google API call: check expiry, refresh if within 5 minutes.

**Sync cron:** `POST /api/cron/calendar-sync` — daily full re-sync (catch webhook gaps). Fetches Google events modified since `last_synced_at`.

### Conflict resolution

Zync is the source of truth for task/project events. Google is the source of truth for manually created external events. Manual events created in Zync propagate to Google; external-only events from Google appear read-only in Zync (editable only via Google).

---

## Microsoft Outlook Sync

Same pattern as Google:
- OAuth: Microsoft identity platform, scopes `Calendars.ReadWrite`
- Callback: `GET /api/auth/outlook-calendar/callback`
- Webhooks: Microsoft Graph subscriptions → `POST /api/webhooks/calendar/outlook/{connectionId}`
- Token refresh: Microsoft refresh token flow

---

## Scheduling Integrations

### Calendly

When a new booking is created in Calendly:
1. Calendly webhook → `POST /api/webhooks/scheduling/calendly`
2. Verify webhook signature (`X-Calendly-Webhook-Subscription-Uuid` + HMAC)
3. Create `calendar_events` record with `source = 'calendly'`
4. Match invitee email against `customer_contacts` → link `customer_id`
5. Optionally: create a new support ticket or task (configurable per event type in settings)

Setup: staff pastes Calendly API key in `/settings/integrations/scheduling`. System registers webhook on Calendly.

### Acuity Scheduling

Same pattern. Acuity webhook → `POST /api/webhooks/scheduling/acuity`. HMAC verification using Acuity API key.

### moCal

Israeli scheduling platform. Same pattern. REST API + webhook. `POST /api/webhooks/scheduling/mocal`.

---

## Tenant Configuration (`/settings/integrations/calendar`)

- Google Calendar: connect / disconnect / pick calendar
- Outlook: connect / disconnect / pick calendar
- Scheduling integrations: Calendly / Acuity / moCal (API key input + test connection)
- On new booking: configure action (create event only / create event + task / create event + ticket)
- Notification on new booking: in-app + email to assignee

---

## Permissions

| Action | Required permission |
|--------|-------------------|
| View calendar | `calendar:read` |
| Create/edit/delete manual events | `calendar:write` |
| Connect personal calendar | `calendar:connect` |
| Manage scheduling integrations | `settings:write` |

---

## API Endpoints

```
GET    /api/calendar/events                    → list events in range (?start=&end=)
POST   /api/calendar/events                    → create manual event
GET    /api/calendar/events/:id                → event detail
PATCH  /api/calendar/events/:id                → update event
DELETE /api/calendar/events/:id                → delete event

GET    /api/calendar/connections               → list user's calendar connections
DELETE /api/calendar/connections/:id           → disconnect calendar
GET    /api/auth/google-calendar/start         → initiate Google OAuth
GET    /api/auth/google-calendar/callback      → Google OAuth callback
GET    /api/auth/outlook-calendar/start        → initiate Outlook OAuth
GET    /api/auth/outlook-calendar/callback     → Outlook OAuth callback

POST   /api/webhooks/calendar/google/:connId   → Google Push Notification
POST   /api/webhooks/calendar/outlook/:connId  → MS Graph subscription notification
POST   /api/webhooks/scheduling/calendly       → Calendly booking webhook
POST   /api/webhooks/scheduling/acuity         → Acuity booking webhook
POST   /api/webhooks/scheduling/mocal          → moCal booking webhook
```

---

## Webhooks

| Event | Payload |
|-------|---------|
| `calendar.booking_created` | `{ eventId, customerId, provider, title, startAt }` |

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Google Push Notifications | Not polling | Push = real-time sync; polling every 5min = KV/CPU waste |
| Tokens encrypted in DB | `INTEGRATION_ENCRYPTION_KEY` | OAuth tokens are long-lived credentials; same pattern as other adapter creds |
| Zync source-of-truth for task events | Not Google | Tasks drive calendar, not reverse; external edits to task-sourced events are ignored |
| External-only events read-only in Zync | Not bi-directional for all | Prevents sync conflicts; user edits external events in Google/Outlook |
| Scheduling webhooks auto-match customer | By email | Avoids manual linking; fallback: create unlinked event with note |
| Scheduling action configurable | Create event + optional task/ticket | Different tenants have different post-booking flows; settings hub controls this |
