# Calendar Integration Settings UI

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 131  
**Tier:** All tiers  
**Depends on:** `calendar-module`, `settings-module`, `foundation-auth-rbac`  
**Referenced by:** `calendar-module`, `settings-module`

---

## Overview

Spec 19 (`calendar-module`) defines the `calendar_connections` table, OAuth flow, and sync mechanics, but the settings UI at `/settings/integrations/calendar` is documented in one sentence. This spec defines the full connection management page: per-user OAuth connect/disconnect, calendar picker, sync preferences, and connection health indicators.

---

## Route

`/settings/integrations/calendar` — accessible to all roles (each user manages their own calendar connections).

---

## Page Layout

```
┌──────────────────────────────────────────────────────────────┐
│  Settings > Integrations > Calendar Sync                     │
│                                                              │
│  Connect your personal calendar to Zync. Events and task     │
│  due dates sync automatically.                               │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  Google Calendar                                         │ │
│  │                                                          │ │
│  │  ○ Not connected                                         │ │
│  │                                                          │ │
│  │  [Connect Google Calendar →]                             │ │
│  └─────────────────────────────────────────────────────────┘ │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  Microsoft Outlook                                       │ │
│  │                                                          │ │
│  │  ○ Not connected                                         │ │
│  │                                                          │ │
│  │  [Connect Outlook →]                                     │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```

One connection per provider per user. Multiple providers can be connected simultaneously.

---

## Google Calendar — Connected State

After OAuth, card expands:

```
┌──────────────────────────────────────────────────────────────┐
│  Google Calendar                              [Disconnect]    │
│                                                              │
│  ✓ Connected as: alex@gmail.com                              │
│  Last sync: Today 14:23                                      │
│                                                              │
│  Which calendar to sync                                      │
│  ● Primary (alex@gmail.com)                                  │
│  ○ Work (acme@google.com)                                    │
│  ○ Shared: Team events                                       │
│  (list fetched from Google API on connect)                   │
│                                                              │
│  Sync direction                                              │
│  ● Two-way (Zync ↔ Google) — recommended                    │
│  ○ Read only (Google → Zync)                                 │
│  ○ Push only (Zync → Google)                                 │
│                                                              │
│  What to sync from Zync                                      │
│  ☑ Task due dates (creates 1-hour event at due time)         │
│  ☑ Manual calendar events                                    │
│  ☐ Customer meetings (from invoices/proposals)               │
│                                                              │
│  [Save preferences]                                          │
│                                                              │
│  ⓘ External Google events appear read-only in Zync.          │
└──────────────────────────────────────────────────────────────┘
```

---

## Microsoft Outlook — Connected State

```
┌──────────────────────────────────────────────────────────────┐
│  Microsoft Outlook                            [Disconnect]    │
│                                                              │
│  ✓ Connected as: alex@company.com                            │
│  Last sync: Today 09:15                                      │
│                                                              │
│  Which calendar to sync                                      │
│  ● Calendar (alex@company.com)                               │
│  ○ Shared: Team Calendar                                     │
│                                                              │
│  Sync direction                                              │
│  ● Two-way (Zync ↔ Outlook) — recommended                   │
│  ○ Read only  ○ Push only                                    │
│                                                              │
│  What to sync from Zync                                      │
│  ☑ Task due dates                                            │
│  ☑ Manual calendar events                                    │
│  ☐ Customer meetings                                         │
│                                                              │
│  [Save preferences]                                          │
└──────────────────────────────────────────────────────────────┘
```

---

## OAuth Connect Flow

**[Connect Google Calendar →]**:
1. `GET /api/auth/google-calendar/init` → returns Google OAuth URL
2. User redirected to Google OAuth consent screen
3. Google redirects to `GET /api/auth/google-calendar/callback?code=...`
4. Worker: exchange code for tokens, store encrypted in `calendar_connections`
5. Redirect back to `/settings/integrations/calendar?connected=google`
6. Page shows success toast: "Google Calendar connected"

**[Connect Outlook →]**: Same flow via Microsoft identity platform.

---

## Error States

**Token expired / revoked:**

```
┌──────────────────────────────────────────────────────────────┐
│  Google Calendar                              [Disconnect]    │
│                                                              │
│  ⚠ Connection requires re-authorization                      │
│  Your Google authorization has expired or been revoked.      │
│                                                              │
│  [Reconnect →]                                               │
└──────────────────────────────────────────────────────────────┘
```

Token health checked: before any sync operation, attempt refresh. If refresh fails, set `calendar_connections.status = 'error'` and surface the banner.

**Sync error:**

```
│  Last sync: Today 09:15  ⚠ Sync error:                       │
│  "Invalid grant: token has been revoked"                     │
```

Sync error message stored in `calendar_connections.last_sync_error` (new column). Card shows the error text inline below the last-sync timestamp.

---

## Disconnect

**[Disconnect]** → confirmation dialog:

```
Disconnect Google Calendar?
Zync events pushed to Google will remain in Google Calendar.
Google events in Zync will become read-only and eventually expire.

[Cancel]    [Disconnect]
```

On confirm: revoke Google token (best-effort), delete `calendar_connections` record for this user + provider.

---

## Schema Delta

No new tables. Uses `calendar_connections` (spec 19). Add sync preference columns:

Note: `selected_calendar_id` already defined in spec 19's `calendar_connections` table. All new columns here are additive.

```sql
ALTER TABLE calendar_connections
  ADD COLUMN selected_calendar_name TEXT,     -- display name for selected calendar (e.g. 'alex@gmail.com')
  ADD COLUMN sync_direction TEXT NOT NULL DEFAULT 'two_way'
    CHECK (sync_direction IN ('two_way', 'read_only', 'push_only')),
  ADD COLUMN sync_task_due_dates BOOLEAN NOT NULL DEFAULT true,
  ADD COLUMN sync_manual_events BOOLEAN NOT NULL DEFAULT true,
  ADD COLUMN sync_customer_meetings BOOLEAN NOT NULL DEFAULT false,
  ADD COLUMN status TEXT NOT NULL DEFAULT 'active'
    CHECK (status IN ('active', 'error', 'disconnected')),
  ADD COLUMN last_sync_error TEXT;            -- null = no error; non-null = last error message
```

---

## API

```
GET /api/settings/calendar/connections
    → list current user's calendar connections + status
      Returns: [{ provider, status, connected_email, selected_calendar_name, sync_direction,
                  sync_task_due_dates, sync_manual_events, last_synced_at }]
      Requires: authenticated

GET /api/auth/google-calendar/init
    → initiate OAuth; returns redirect URL
      Requires: authenticated

GET /api/auth/google-calendar/callback
    → OAuth callback; stores tokens; redirects to settings page
      (no auth — callback from Google)

GET /api/auth/outlook/init
    → initiate OAuth; returns redirect URL
      Requires: authenticated

GET /api/auth/outlook/callback
    → OAuth callback; stores tokens; redirects to settings page

GET /api/settings/calendar/calendars?provider=google
    → fetch available calendars from provider API (live call)
      Returns: [{ id, name, primary }]
      Requires: authenticated, active connection

PATCH /api/settings/calendar/connections/:provider
      → update preferences for this connection
        body: { selected_calendar_id?, sync_direction?, sync_task_due_dates?,
                sync_manual_events?, sync_customer_meetings? }
        Requires: authenticated

DELETE /api/settings/calendar/connections/:provider
       → disconnect + revoke tokens
         Requires: authenticated
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Per-user connections | Not tenant-level | Each user has their own personal calendar; tenant-level would merge calendars incorrectly |
| Calendar picker from live API | Not manual entry | Calendar IDs are opaque; user can't enter them by hand; live fetch shows recognizable names |
| Status field on connection | Not derived from last sync | Allows explicit error state signaling without scanning sync logs; faster UI render |
| Sync direction preference | Not always two-way | Read-only is useful for users who want to see Zync events in Google without risk of cross-contamination |
