570 lines
25 KiB
Markdown
570 lines
25 KiB
Markdown
# Social Calendar Feature Plan
|
|
|
|
**Created:** 2026-03-06
|
|
**Status:** Planning Complete — Ready for Phase A Implementation
|
|
**Branch:** v2
|
|
**Feature Flag:** `enableSocialCalendar` (new, under SiteSettings)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
A layered personal and social calendar system. Each user gets their own calendar with multiple layers (system-populated, user-created, external feeds). Calendars can be shared between users at the item, category (layer), or whole-calendar level. Shared views allow multiple users' events to appear on a merged, color-coded calendar. Admin shared views can auto-include users by role.
|
|
|
|
### Design Principles
|
|
|
|
- **Layers are the core abstraction** — every event belongs to a layer, layers control visibility and sharing
|
|
- **System layers are virtual** — shifts, tickets, polls are queried live from source tables, not duplicated
|
|
- **Recurrence uses materialization** — consistent with existing ShiftSeries pattern (generate DB rows, allow exceptions)
|
|
- **Social-first** — friend relationships gate sharing; admin views are separate and only expose system data
|
|
- **Privacy by default** — layers default to PRIVATE; users explicitly opt into sharing
|
|
|
|
---
|
|
|
|
## Data Model
|
|
|
|
### CalendarLayer
|
|
|
|
Each user has multiple layers. System layers are auto-created on first calendar access.
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| id | String (cuid) | PK |
|
|
| userId | String | FK to User |
|
|
| name | String | "Personal", "Gym", "Google Cal", etc. |
|
|
| layerType | Enum | SYSTEM, USER, EXTERNAL |
|
|
| systemType | Enum? | SHIFTS, TICKETS, POLLS, PUBLIC_EVENTS (for SYSTEM layers only) |
|
|
| color | String | Hex color (#1890ff) |
|
|
| visibility | Enum | PRIVATE, FRIENDS, PUBLIC |
|
|
| isEnabled | Boolean | User can toggle layers on/off for themselves |
|
|
| sortOrder | Int | Display ordering |
|
|
| createdAt | DateTime | |
|
|
| updatedAt | DateTime | |
|
|
|
|
**System layers (auto-created per user):**
|
|
- My Shifts — from ShiftSignup records
|
|
- My Tickets — from EventTicket records
|
|
- My Polls — from SchedulingPollVote records
|
|
- Public Events — the existing Gancio/platform feed (togglable)
|
|
|
|
System layers are **virtual** — no CalendarItem rows are created. The API queries source tables directly and maps to the CalendarItem shape at response time.
|
|
|
|
### CalendarItem
|
|
|
|
User-created events, time blocks, and cached .ics feed entries.
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| id | String (cuid) | PK |
|
|
| userId | String | FK to User (owner) |
|
|
| layerId | String | FK to CalendarLayer |
|
|
| title | String | |
|
|
| description | String? | Text |
|
|
| date | DateTime | Date of this occurrence |
|
|
| startTime | String | HH:MM |
|
|
| endTime | String | HH:MM |
|
|
| isAllDay | Boolean | Default false |
|
|
| itemType | Enum | EVENT, TIME_BLOCK, REMINDER |
|
|
| location | String? | |
|
|
| color | String? | Override (null = inherit layer color) |
|
|
| visibility | Enum? | PRIVATE, FRIENDS, PUBLIC (null = inherit from layer) |
|
|
| busyStatus | Enum | BUSY, TENTATIVE, FREE (default BUSY) |
|
|
| showDetailsTo | Enum | NOBODY, FRIENDS, EVERYONE (default FRIENDS) |
|
|
| recurrenceRule | Json? | See Recurrence section |
|
|
| recurrenceEnd | DateTime? | When series stops |
|
|
| seriesId | String? | Groups recurring instances |
|
|
| isException | Boolean | Edited instance that broke from pattern |
|
|
| sourceType | Enum | MANUAL, ICS_FEED |
|
|
| sourceId | String? | External reference (ics UID, etc.) |
|
|
| createdAt | DateTime | |
|
|
| updatedAt | DateTime | |
|
|
|
|
**Notes:**
|
|
- System-layer items (shifts, tickets, polls) are NOT stored as CalendarItem rows — they're virtual
|
|
- .ics feed items ARE stored as CalendarItem rows (cached from external source, read-only to user)
|
|
- MANUAL items are user-created freeform events
|
|
|
|
### CalendarFeed (.ics import)
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| id | String (cuid) | PK |
|
|
| userId | String | FK to User |
|
|
| name | String | "Google Calendar", "Work" |
|
|
| url | String | .ics URL |
|
|
| layerId | String | FK to auto-created CalendarLayer |
|
|
| refreshInterval | Enum | FIFTEEN_MIN, HOURLY, SIX_HOUR, DAILY |
|
|
| lastFetchedAt | DateTime? | |
|
|
| lastStatus | Enum | OK, ERROR, PENDING |
|
|
| lastError | String? | Error message if failed |
|
|
| itemCount | Int | How many items imported |
|
|
| createdAt | DateTime | |
|
|
| updatedAt | DateTime | |
|
|
|
|
### SharedCalendarView
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| id | String (cuid) | PK |
|
|
| name | String | "Weekend Crew", "All Shift Admins" |
|
|
| description | String? | |
|
|
| ownerId | String | FK to User (creator) |
|
|
| viewType | Enum | MANUAL, ROLE_BASED |
|
|
| autoIncludeRoles | Json? | ["MAP_ADMIN", "USER"] (for ROLE_BASED) |
|
|
| includedLayerTypes | Json | ["shifts", "tickets", "personal-public"] |
|
|
| shareScope | Enum | MEMBERS, PUBLIC |
|
|
| shareToken | String? | Unique token for public share URL |
|
|
| createdAt | DateTime | |
|
|
| updatedAt | DateTime | |
|
|
|
|
**ROLE_BASED views:**
|
|
- Auto-include users matching specified roles
|
|
- Only pull system layers (shifts, tickets, polls) — never personal layers
|
|
- No notifications sent to included users (admin operational tool)
|
|
- Created/managed by SUPER_ADMIN or MAP_ADMIN
|
|
|
|
**MANUAL views:**
|
|
- Members are explicitly invited via notification system
|
|
- Can include personal layers (with member consent)
|
|
- Members can decline/leave
|
|
|
|
### SharedCalendarMember
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| id | String (cuid) | PK |
|
|
| viewId | String | FK to SharedCalendarView |
|
|
| userId | String | FK to User |
|
|
| status | Enum | INVITED, ACCEPTED, DECLINED |
|
|
| color | String | Auto-assigned from palette |
|
|
| joinedAt | DateTime? | |
|
|
| @@unique | [viewId, userId] | |
|
|
|
|
**Auto-color palette:**
|
|
```
|
|
#1890ff (blue), #52c41a (green), #fa8c16 (orange), #722ed1 (purple),
|
|
#eb2f96 (pink), #13c2c2 (cyan), #faad14 (gold), #f5222d (red),
|
|
#2f54eb (geekblue), #a0d911 (lime)
|
|
```
|
|
Assigned sequentially as members join: `PALETTE[memberIndex % length]`.
|
|
Users can override their assigned color per shared view.
|
|
|
|
### SharedViewComment
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| id | String (cuid) | PK |
|
|
| viewId | String | FK to SharedCalendarView |
|
|
| userId | String | FK to User |
|
|
| itemDate | String | YYYY-MM-DD (which date this comment is about) |
|
|
| itemId | String? | Optional: specific CalendarItem or source item ID |
|
|
| content | String | Text |
|
|
| createdAt | DateTime | |
|
|
|
|
### SharedViewReaction
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| id | String (cuid) | PK |
|
|
| viewId | String | FK to SharedCalendarView |
|
|
| userId | String | FK to User |
|
|
| itemId | String | CalendarItem or source item ID (e.g., "shift-abc123") |
|
|
| emoji | String | Single emoji or shortcode |
|
|
| createdAt | DateTime | |
|
|
| @@unique | [viewId, userId, itemId, emoji] | One reaction type per user per item |
|
|
|
|
### CalendarExportToken (.ics export)
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| id | String (cuid) | PK |
|
|
| userId | String | FK to User |
|
|
| token | String | Unique, random (for URL auth) |
|
|
| includePersonal | Boolean | Whether personal events are exported |
|
|
| includeLayers | Json? | Array of layer IDs (null = all enabled) |
|
|
| createdAt | DateTime | |
|
|
|
|
Export URL: `GET /api/calendar/feed/:userId/:token.ics`
|
|
|
|
---
|
|
|
|
## Recurrence Model
|
|
|
|
Uses **materialization** (consistent with existing ShiftSeries pattern):
|
|
|
|
1. User creates a recurring event with a recurrence rule
|
|
2. System generates CalendarItem rows for the next 3 months
|
|
3. Background job (BullMQ, daily) extends series forward by 1 month
|
|
4. Individual instances can be edited (becomes `isException: true`) or deleted
|
|
5. Editing the series template updates all non-exception future instances
|
|
|
|
### Recurrence Rule JSON
|
|
|
|
```json
|
|
{
|
|
"frequency": "DAILY | WEEKLY | BIWEEKLY | MONTHLY",
|
|
"daysOfWeek": [1, 3, 5],
|
|
"dayOfMonth": 15,
|
|
"interval": 1
|
|
}
|
|
```
|
|
|
|
- `WEEKLY` + `daysOfWeek: [1,3,5]` = every Mon/Wed/Fri
|
|
- `MONTHLY` + `dayOfMonth: 15` = 15th of every month
|
|
- `BIWEEKLY` + `daysOfWeek: [2,4]` = every other Tue/Thu
|
|
- `interval` for skip patterns (every 2 weeks, every 3 months)
|
|
|
|
### Recurrence Edit Options (UI)
|
|
|
|
When editing a recurring event instance:
|
|
- "This event only" — marks as exception, edits the single instance
|
|
- "This and future events" — updates template + regenerates future non-exception instances
|
|
- "All events in series" — updates template + all instances (including past, excluding exceptions)
|
|
|
|
When deleting:
|
|
- "This event only" — soft-delete the single instance
|
|
- "This and future events" — delete future instances, set recurrenceEnd on template
|
|
- "All events" — delete entire series
|
|
|
|
---
|
|
|
|
## Time Block Visibility (Configurable per item)
|
|
|
|
| `showDetailsTo` | Friends see | Public sees |
|
|
|-----------------|-------------|-------------|
|
|
| NOBODY | "Busy 2-4pm" | "Busy 2-4pm" |
|
|
| FRIENDS | "Dentist 2-4pm" | "Busy 2-4pm" |
|
|
| EVERYONE | "Dentist 2-4pm" | "Dentist 2-4pm" |
|
|
|
|
Combined with `busyStatus`:
|
|
- **BUSY** — solid color block
|
|
- **TENTATIVE** — dashed/lighter block
|
|
- **FREE** — no block shown (informational only, e.g., "Available for meetings")
|
|
|
|
---
|
|
|
|
## Notification Types (reusing existing system)
|
|
|
|
| Type | Message | Trigger |
|
|
|------|---------|---------|
|
|
| SHARED_VIEW_INVITE | "Alice invited you to 'Weekend Crew' calendar" | Manual shared view invite |
|
|
| SHARED_VIEW_ACCEPTED | "Bob accepted your invite to 'Weekend Crew'" | Member accepts |
|
|
| CALENDAR_EVENT_INVITE | "Alice added you to 'Planning Meeting' on Mar 10" | Phase B: event-level sharing |
|
|
| CALENDAR_REMINDER | "Reminder: Team standup in 15 minutes" | Future: optional reminders |
|
|
|
|
Role-based admin views do NOT trigger notifications (admin operational tool using only system data).
|
|
|
|
---
|
|
|
|
## Availability Finder (Phase B)
|
|
|
|
A dedicated mode within shared calendar views:
|
|
|
|
1. Toggle "Find Available Time" on a shared view
|
|
2. System overlays all members' BUSY/TENTATIVE time blocks
|
|
3. Highlights gaps where ALL members are free
|
|
4. Optional: filter by time range ("only show weekday 9am-5pm slots")
|
|
5. Click a free slot to create an event and auto-invite all members
|
|
|
|
Visual: green highlight on free slots, red/orange on conflicts, member avatars on busy blocks.
|
|
|
|
---
|
|
|
|
## API Routes
|
|
|
|
### Phase A (Personal Calendar)
|
|
|
|
```
|
|
# Layers
|
|
GET /api/calendar/layers — list user's layers
|
|
POST /api/calendar/layers — create custom layer
|
|
PATCH /api/calendar/layers/:id — update layer (name, color, visibility, enabled)
|
|
DELETE /api/calendar/layers/:id — delete custom layer (+ its items)
|
|
|
|
# Calendar Items
|
|
GET /api/calendar/items — list items in date range (all enabled layers merged)
|
|
POST /api/calendar/items — create item (event, time block, reminder)
|
|
PATCH /api/calendar/items/:id — update item
|
|
DELETE /api/calendar/items/:id — delete item
|
|
|
|
# Recurrence
|
|
POST /api/calendar/items/:id/series — edit series (this-only, this-and-future, all)
|
|
DELETE /api/calendar/items/:id/series — delete series (this-only, this-and-future, all)
|
|
|
|
# Unified personal view (merges system layers + user items)
|
|
GET /api/calendar/my — personal calendar (date range, layer filters)
|
|
```
|
|
|
|
### Phase B (Sharing + Social)
|
|
|
|
```
|
|
# Shared Views
|
|
GET /api/calendar/shared — list shared views I own or am a member of
|
|
POST /api/calendar/shared — create shared view
|
|
PATCH /api/calendar/shared/:id — update shared view
|
|
DELETE /api/calendar/shared/:id — delete shared view (owner only)
|
|
|
|
# Members
|
|
POST /api/calendar/shared/:id/invite — invite user(s) to shared view
|
|
PATCH /api/calendar/shared/:id/respond — accept/decline invite
|
|
DELETE /api/calendar/shared/:id/leave — leave a shared view
|
|
GET /api/calendar/shared/:id/members — list members + colors
|
|
|
|
# Merged calendar data
|
|
GET /api/calendar/shared/:id/items — merged items from all members
|
|
|
|
# Event-level sharing
|
|
POST /api/calendar/items/:id/share — share specific item with friend(s)
|
|
|
|
# Comments & Reactions (on shared views)
|
|
GET /api/calendar/shared/:id/comments?date=YYYY-MM-DD
|
|
POST /api/calendar/shared/:id/comments
|
|
DELETE /api/calendar/shared/:id/comments/:commentId
|
|
POST /api/calendar/shared/:id/reactions
|
|
DELETE /api/calendar/shared/:id/reactions/:reactionId
|
|
|
|
# Availability finder
|
|
GET /api/calendar/shared/:id/availability?start=&end=&dayStart=09:00&dayEnd=17:00
|
|
|
|
# Friend's public calendar
|
|
GET /api/calendar/user/:userId — view a friend's public items
|
|
```
|
|
|
|
### Phase C (.ics Integration)
|
|
|
|
```
|
|
# Feeds (import)
|
|
GET /api/calendar/feeds — list user's subscribed feeds
|
|
POST /api/calendar/feeds — subscribe to .ics URL
|
|
PATCH /api/calendar/feeds/:id — update feed settings
|
|
DELETE /api/calendar/feeds/:id — unsubscribe (deletes layer + cached items)
|
|
POST /api/calendar/feeds/:id/refresh — force refresh now
|
|
|
|
# Export
|
|
GET /api/calendar/export/token — get or create export token
|
|
DELETE /api/calendar/export/token — revoke export token
|
|
GET /api/calendar/feed/:userId/:token.ics — public .ics feed (no auth, token in URL)
|
|
```
|
|
|
|
### Phase D (Admin Shared Views)
|
|
|
|
```
|
|
# Admin role-based views (requireRole: SUPER_ADMIN, MAP_ADMIN)
|
|
POST /api/admin/calendar/shared — create role-based shared view
|
|
PATCH /api/admin/calendar/shared/:id — update
|
|
DELETE /api/admin/calendar/shared/:id — delete
|
|
GET /api/admin/calendar/shared/:id/items — merged system-layer data for matching users
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend Pages & Components
|
|
|
|
### Phase A
|
|
|
|
| Component | Location | Description |
|
|
|-----------|----------|-------------|
|
|
| MyCalendarPage | `volunteer/MyCalendarPage.tsx` | Personal calendar (main view) |
|
|
| CalendarLayerPanel | `components/calendar/CalendarLayerPanel.tsx` | Sidebar: layer list with toggles, colors, visibility |
|
|
| CalendarItemModal | `components/calendar/CalendarItemModal.tsx` | Create/edit event, time block, or reminder |
|
|
| RecurrenceEditor | `components/calendar/RecurrenceEditor.tsx` | Recurrence rule builder (frequency, days, end date) |
|
|
| PersonalCalendarView | `components/calendar/PersonalCalendarView.tsx` | Month/week/day calendar with layer color-coding |
|
|
| MobileDayView | `components/calendar/MobileDayView.tsx` | Day/3-day swipeable view for mobile |
|
|
|
|
**Mobile UX:** Day or 3-day swipeable view (not full month grid). Swipe left/right to navigate days. Layer toggles in a collapsible bottom sheet.
|
|
|
|
### Phase B
|
|
|
|
| Component | Location | Description |
|
|
|-----------|----------|-------------|
|
|
| SharedCalendarsPage | `volunteer/SharedCalendarsPage.tsx` | List of shared views I'm in |
|
|
| SharedCalendarView | `components/calendar/SharedCalendarView.tsx` | Merged multi-user calendar with member colors |
|
|
| SharedViewMembersPanel | `components/calendar/SharedViewMembersPanel.tsx` | Member list, color overrides, invite button |
|
|
| AvailabilityFinder | `components/calendar/AvailabilityFinder.tsx` | Free/busy overlay with slot highlighting |
|
|
| CalendarComments | `components/calendar/CalendarComments.tsx` | Comment thread for a date in shared view |
|
|
| CalendarReactions | `components/calendar/CalendarReactions.tsx` | Emoji reactions on items |
|
|
| FriendCalendarPage | `volunteer/FriendCalendarPage.tsx` | View a friend's public calendar |
|
|
|
|
### Phase C
|
|
|
|
| Component | Location | Description |
|
|
|-----------|----------|-------------|
|
|
| CalendarFeedsPanel | `components/calendar/CalendarFeedsPanel.tsx` | Manage .ics subscriptions |
|
|
| CalendarExportPanel | `components/calendar/CalendarExportPanel.tsx` | Export token management, copy URL |
|
|
|
|
### Phase D
|
|
|
|
| Component | Location | Description |
|
|
|-----------|----------|-------------|
|
|
| AdminSharedViewsPage | `pages/AdminSharedViewsPage.tsx` | Admin: create/manage role-based views |
|
|
| AdminCalendarOverview | `components/calendar/AdminCalendarOverview.tsx` | Big shift/event overview for admins |
|
|
|
|
---
|
|
|
|
## Navigation & Routing
|
|
|
|
### Volunteer Portal
|
|
- Footer nav: add "Calendar" tab (CalendarOutlined icon)
|
|
- `/volunteer/calendar` — MyCalendarPage
|
|
- `/volunteer/calendar/shared` — SharedCalendarsPage
|
|
- `/volunteer/calendar/shared/:id` — SharedCalendarView
|
|
- `/volunteer/calendar/friend/:userId` — FriendCalendarPage
|
|
|
|
### Admin
|
|
- Sidebar under existing section: "Calendar Overview"
|
|
- `/app/calendar/shared` — AdminSharedViewsPage
|
|
|
|
---
|
|
|
|
## Phase Breakdown
|
|
|
|
### Phase A: Personal Calendar + Layers + Freeform Events
|
|
**Scope:**
|
|
- [x] Prisma models: CalendarLayer, CalendarItem, CalendarFeed, SharedCalendarView, SharedCalendarMember, SharedViewComment, SharedViewReaction, CalendarExportToken (+ 12 enums)
|
|
- [x] Auto-create system layers on first calendar access (ensureSystemLayers)
|
|
- [x] CalendarItem CRUD (create, read, update, delete)
|
|
- [x] Recurrence: create series (materialize 3 months), edit/delete with scope options (THIS_ONLY/THIS_AND_FUTURE/ALL)
|
|
- [ ] BullMQ job: extend recurring series daily (add 1 month of future instances)
|
|
- [x] Personal calendar API: GET /api/calendar/my (merge system layers + user items)
|
|
- [x] System layer queries: shifts (from ShiftSignup), tickets (from Ticket), polls (from SchedulingPollVote)
|
|
- [x] Layer CRUD: create custom layers, toggle on/off, set color
|
|
- [x] Layer visibility settings (PRIVATE/FRIENDS/PUBLIC) — stored but not enforced until Phase B
|
|
- [x] MyCalendarPage: month view (desktop), day/3-day view (mobile)
|
|
- [x] CalendarLayerPanel: sidebar with layer toggles, color pickers, inline editing, grouped by type
|
|
- [x] CalendarItemModal: create/edit form with item type, recurrence, time block settings, scope selector
|
|
- [x] RecurrenceEditor: frequency/days/interval/end-date with preview text
|
|
- [x] PersonalCalendarView: desktop month view with layer-colored items
|
|
- [x] MobileDayView: day view with time grid, current time indicator, floating add button
|
|
- [x] Volunteer footer nav: "Calendar" tab (gated behind enableSocialCalendar)
|
|
- [x] Feature flag: enableSocialCalendar in SiteSettings, Zod schema, frontend types, FeatureGate
|
|
- [x] Settings page toggle added ("Social Calendar" in People & Engagement section)
|
|
|
|
### Phase B: Sharing + Social
|
|
**Scope:**
|
|
- [ ] Prisma models: SharedCalendarView, SharedCalendarMember, SharedViewComment, SharedViewReaction
|
|
- [ ] SharedCalendarView CRUD
|
|
- [ ] Invite flow: send invite via notification system, accept/decline/leave
|
|
- [ ] Merged calendar API: query all members' items with layer type filtering
|
|
- [ ] Auto-color assignment for members
|
|
- [ ] Layer visibility enforcement (PRIVATE/FRIENDS/PUBLIC filtering based on relationship)
|
|
- [ ] Event-level sharing: share a specific item with friend(s) via notification
|
|
- [ ] Comments on shared view dates/items
|
|
- [ ] Emoji reactions on shared view items
|
|
- [ ] Availability finder: free/busy overlay, slot highlighting, time range filter
|
|
- [ ] Friend's public calendar view
|
|
- [ ] SharedCalendarsPage, SharedCalendarView components
|
|
- [ ] AvailabilityFinder component
|
|
- [ ] CalendarComments, CalendarReactions components
|
|
- [ ] Public share URL (shareToken for unauthenticated view)
|
|
|
|
### Phase C: .ics Integration
|
|
**Scope:**
|
|
- [x] Prisma models: CalendarFeed, CalendarExportToken (already existed from Phase A migration)
|
|
- [x] .ics feed parser (node-ical v0.25.5)
|
|
- [x] BullMQ job: refresh feeds every 15 minutes (calendar-feed-refresh queue)
|
|
- [x] Feed CRUD: subscribe, update, delete, force refresh
|
|
- [x] Auto-create EXTERNAL layer per feed, cache items as CalendarItem rows (sourceType: ICS_FEED)
|
|
- [x] .ics export: generate feed from user's calendar via ical-generator v10, token-authenticated URL
|
|
- [x] Export token management (create, list, revoke)
|
|
- [x] CalendarFeedsPanel, CalendarExportPanel components
|
|
- [x] MyCalendarPage settings Drawer integration (gear icon)
|
|
|
|
### Phase D: Admin Shared Views
|
|
**Scope:**
|
|
- [ ] Role-based SharedCalendarView (viewType: ROLE_BASED)
|
|
- [ ] Auto-include users by role(s) — query live, no member rows needed
|
|
- [ ] Only expose system layers (shifts, tickets, polls) — no personal data
|
|
- [ ] No notifications to included users
|
|
- [ ] Admin routes (requireRole: SUPER_ADMIN, MAP_ADMIN)
|
|
- [ ] AdminSharedViewsPage
|
|
- [ ] AdminCalendarOverview (big shift/event dashboard)
|
|
|
|
---
|
|
|
|
## Implementation Notes
|
|
|
|
### Extending UnifiedCalendar
|
|
|
|
The existing `UnifiedCalendar` component and `unified-calendar.service.ts` remain as the **public** calendar. The new personal calendar service (`calendar.service.ts`) reuses the same source queries (shifts, Gancio, polls, ticketed events) but filters to the user's own records and merges with their CalendarItem rows.
|
|
|
|
### Recurrence Background Job
|
|
|
|
```typescript
|
|
// jobs/calendar-recurrence.job.ts
|
|
// Runs daily via BullMQ repeatable job
|
|
// 1. Find all CalendarItems with recurrenceRule where latest materialized date < now + 3 months
|
|
// 2. Generate new instances up to 3 months ahead
|
|
// 3. Skip dates that already have an instance (idempotent)
|
|
```
|
|
|
|
### .ics Feed Refresh Job
|
|
|
|
```typescript
|
|
// jobs/calendar-feed-refresh.job.ts
|
|
// Runs every 15 minutes via BullMQ repeatable job
|
|
// 1. Find feeds where lastFetchedAt + refreshInterval < now
|
|
// 2. Fetch .ics URL, parse events
|
|
// 3. Upsert CalendarItem rows (match on sourceId = ics UID)
|
|
// 4. Delete items no longer in feed
|
|
// 5. Update feed status
|
|
```
|
|
|
|
### Privacy Boundaries
|
|
|
|
| Scenario | What's visible |
|
|
|----------|---------------|
|
|
| Viewing own calendar | Everything (all layers, all items) |
|
|
| Friend views your calendar | Items on FRIENDS or PUBLIC visibility layers, plus items with individual FRIENDS/PUBLIC override |
|
|
| Public profile calendar | Only PUBLIC visibility layers and PUBLIC override items |
|
|
| Admin role-based view | Only system layers (shifts, tickets, polls) for users matching role filter |
|
|
| Shared view (MANUAL) | Items from includedLayerTypes on layers with appropriate visibility for the viewer |
|
|
| Time blocks (BUSY) | Title shown per showDetailsTo setting, always shows busy bar |
|
|
|
|
### Performance Considerations
|
|
|
|
- CalendarItem table will grow with materialized recurrence — add indexes on (userId, date), (layerId, date), (seriesId)
|
|
- System layers query source tables directly — leverage existing indexes on ShiftSignup, EventTicket, etc.
|
|
- .ics feed items are cached — only re-parsed on refresh interval
|
|
- Shared view queries can be expensive (N members x M layers) — cache merged results in Redis (2min TTL, bust on member change)
|
|
- Availability finder operates on time blocks only — narrow query scope
|
|
|
|
---
|
|
|
|
## Tracking Log
|
|
|
|
### 2026-03-06 — Planning Complete
|
|
- Brainstormed feature across 3 rounds of refinement
|
|
- Decided on layer-based architecture (system, user, external layers)
|
|
- Recurrence uses materialization (consistent with ShiftSeries pattern)
|
|
- Time block visibility is configurable per item (showDetailsTo: NOBODY/FRIENDS/EVERYONE)
|
|
- Shared views support manual (invite-based) and role-based (admin, system data only)
|
|
- Availability finder included in Phase B
|
|
- Comments and reactions on shared view items included in Phase B
|
|
- .ics import and export in Phase C
|
|
- Admin role-based views in Phase D (no personal data, no notifications)
|
|
- Reuse existing notification system for invites
|
|
- Auto-color assignment for shared view members with user override option
|
|
- Mobile UX: day/3-day swipeable view instead of month grid
|
|
|
|
### 2026-03-06 — Phase A Implementation Complete
|
|
- Schema: 8 models, 12 enums, migration `20260306203326_social_calendar_layers_items` applied
|
|
- Fixed pre-existing migration ordering issue (ticketed_events create must come before alter)
|
|
- Backend: calendar.service.ts (layer mgmt, item CRUD, recurrence materialization, personal calendar merge), calendar.routes.ts (9 endpoints), calendar.schemas.ts (Zod validation)
|
|
- Frontend: 5 new components (CalendarLayerPanel, CalendarItemModal, RecurrenceEditor, PersonalCalendarView, MobileDayView), MyCalendarPage
|
|
- Navigation: VolunteerFooterNav Calendar tab, App.tsx route, SettingsPage toggle
|
|
- Smoke tested: layers auto-create, item CRUD works, recurring events materialize correctly (Weekly Mon/Wed/Fri generated 11 instances through June)
|
|
- Both API and Admin compile with zero TypeScript errors
|
|
- Remaining Phase A item: BullMQ job for extending recurring series (not critical for launch, series materializes 3 months on creation)
|
|
|
|
### 2026-03-07 — Phase C Implementation Complete
|
|
- Backend: feed.schemas.ts (3 Zod schemas), feed.service.ts (feed CRUD, ICS parsing, RRULE materialization, export generation), feed.routes.ts (1 public + 8 auth routes), calendar-feed-queue.service.ts (BullMQ 15min repeatable job)
|
|
- Dependencies: node-ical v0.25.5 (ICS parsing), ical-generator v10.0.0 (ICS output)
|
|
- Feed import: streaming body read with 5MB limit, 1000 event cap, RRULE materialization via rrule.between(), stale event cleanup, status tracking (OK/ERROR/PENDING)
|
|
- Feed export: 32-byte random token, configurable layer/personal inclusion, past 1 month + future 3 months, standard iCalendar output with Content-Type: text/calendar
|
|
- Frontend: CalendarFeedsPanel (add/edit/delete/refresh with status badges), CalendarExportPanel (create/copy/revoke tokens), settings Drawer in MyCalendarPage (gear icon)
|
|
- Types: CalendarFeed, CalendarExportToken, CalendarFeedStatus, CalendarFeedInterval added to admin/src/types/api.ts
|
|
- server.ts: feedRoutes mounted before calendarRoutes (public .ics route needs no auth), queue worker started on bootstrap, graceful shutdown
|
|
- Smoke tested: Google US Holidays feed → 317 events imported with status OK; export token → valid .ics with VEVENT entries; revoke → 404
|
|
- Docker gotcha: anonymous volume `/app/node_modules` caches old dependencies — must `docker compose rm -sf api` to clear when adding new npm packages
|
|
- Both API and Admin compile with zero TypeScript errors
|