From 47e048b6d22c83144656ce6626ba752174bbeeb6 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Sun, 1 Mar 2026 22:16:35 -0800 Subject: [PATCH] Updating calendar docs --- docs/api/calendar-export.md | 185 +++++++++++++++++ docs/api/information.md | 12 ++ docs/configuration/calendar-types.md | 6 +- docs/development/calendar-architecture.md | 229 ++++++++++++++++++++++ docs/how-tos/calendar-sync.md | 138 +++++++++++++ docs/reference/docker.md | 13 ++ docs/reference/localization.md | 23 +++ docs/web-app/calendar.md | 112 ++++++++++- 8 files changed, 708 insertions(+), 10 deletions(-) create mode 100644 docs/api/calendar-export.md create mode 100644 docs/development/calendar-architecture.md create mode 100644 docs/how-tos/calendar-sync.md diff --git a/docs/api/calendar-export.md b/docs/api/calendar-export.md new file mode 100644 index 0000000..1d5c4c1 --- /dev/null +++ b/docs/api/calendar-export.md @@ -0,0 +1,185 @@ +--- +sidebar_position: 6 +--- + +# Calendar Export API + +The Calendar Export API provides endpoints for exporting calendar events as iCalendar (`.ics`) files and managing calendar feed subscriptions. These endpoints are part of the v4 API and are served by the `CalendarExportController`. + +## Endpoints Overview + +| Endpoint | Method | Auth | Description | +|----------|--------|------|-------------| +| `v4/CalendarExport/ExportICalFile` | GET | Bearer token | Download a single event as `.ics` | +| `v4/CalendarExport/ExportDepartmentICalFeed` | GET | Bearer token | Download full department calendar as `.ics` | +| `v4/CalendarExport/GetCalendarSubscriptionUrl` | GET | Bearer token | Get or activate the calendar subscription URL | +| `v4/CalendarExport/RegenerateCalendarSubscriptionUrl` | POST | Bearer token | Regenerate the subscription URL (invalidates old) | +| `v4/CalendarExport/CalendarFeed/{token}` | GET | Anonymous | Fetch the iCal feed via encrypted token | + +## Export Single Event + +``` +GET /api/v4/CalendarExport/ExportICalFile?calendarItemId={id} +Authorization: Bearer {access_token} +``` + +Returns a single calendar event as a `.ics` file with content type `text/calendar`. + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `calendarItemId` | int | Yes | The ID of the calendar item to export | + +### Response + +- **Content-Type:** `text/calendar` +- **Content-Disposition:** attachment with `.ics` filename +- Body contains a valid iCalendar file with a single `VEVENT` + +## Export Department Calendar + +``` +GET /api/v4/CalendarExport/ExportDepartmentICalFeed +Authorization: Bearer {access_token} +``` + +Returns all calendar events for the authenticated user's department as a single `.ics` file. + +### Response + +- **Content-Type:** `text/calendar` +- Body contains a valid iCalendar file with multiple `VEVENT` entries + +## Get Calendar Subscription URL + +``` +GET /api/v4/CalendarExport/GetCalendarSubscriptionUrl +Authorization: Bearer {access_token} +``` + +Returns the user's calendar subscription URL. If the user has not yet activated calendar sync, it is activated automatically and a new subscription URL is generated. + +### Response + +```json +{ + "Data": { + "SubscriptionUrl": "https://api.resgrid.com/api/v4/CalendarExport/CalendarFeed/{encrypted_token}", + "WebCalUrl": "webcal://api.resgrid.com/api/v4/CalendarExport/CalendarFeed/{encrypted_token}", + "IsActive": true + }, + "Status": "success" +} +``` + +## Regenerate Calendar Subscription URL + +``` +POST /api/v4/CalendarExport/RegenerateCalendarSubscriptionUrl +Authorization: Bearer {access_token} +``` + +Generates a new sync key, invalidating all previously issued subscription URLs for this user. + +### Response + +```json +{ + "Data": { + "SubscriptionUrl": "https://api.resgrid.com/api/v4/CalendarExport/CalendarFeed/{new_encrypted_token}", + "WebCalUrl": "webcal://api.resgrid.com/api/v4/CalendarExport/CalendarFeed/{new_encrypted_token}", + "IsActive": true + }, + "Status": "success" +} +``` + +:::warning +Regenerating the subscription URL immediately invalidates the previous URL. Any external calendar application using the old URL will no longer receive updates. +::: + +## Calendar Feed (Anonymous) + +``` +GET /api/v4/CalendarExport/CalendarFeed/{token} +``` + +This endpoint does **not** require a Bearer token. Authentication is performed via the encrypted token embedded in the URL. This is the endpoint that external calendar applications (Google Calendar, Outlook, Apple Calendar) call to fetch events. + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `token` | string | Yes | URL-safe Base64-encoded encrypted token | + +### Response + +- **200 OK** — Content-Type `text/calendar` with the full department iCal feed +- **401 Unauthorized** — Token is invalid, expired, or the sync key has been regenerated + +### Token Format + +The encrypted token contains `{departmentId}|{userId}|{calendarSyncToken}` encrypted via the system's `IEncryptionService`. The token is made URL-safe using Base64 URL-safe encoding (`+` → `-`, `/` → `_`, trailing `=` trimmed). + +On each request, the system: +1. Decodes and decrypts the token +2. Extracts the department ID, user ID, and sync GUID +3. Loads the user profile and validates that `CalendarSyncToken` matches the embedded GUID +4. If valid, generates and returns the department calendar feed + +### Feature Flag + +The feed endpoint respects the `CalendarConfig.ICalFeedEnabled` configuration flag. When disabled, the endpoint returns an error response. + +## iCal Output Format + +All iCal output conforms to **RFC 5545** and is generated using the `Ical.Net` library. + +### Event Mapping + +| Calendar Item Field | iCal Property | Notes | +|---------------------|---------------|-------| +| Title | `SUMMARY` | | +| Description | `DESCRIPTION` | | +| Location | `LOCATION` | | +| Start | `DTSTART` | `VALUE=DATE` format when all-day | +| End | `DTEND` | `VALUE=DATE` format when all-day; exclusive (day after last event day) | +| IsAllDay | `DTSTART`/`DTEND` format | Date-only vs. date-time | +| Reminder | `VALARM` | `TRIGGER` based on `GetMinutesForReminder()` | +| Attendees | `ATTENDEE` | Only if populated | + +### All-Day and Multi-Day Events + +- **All-day events** use `VALUE=DATE` format (no time component) per RFC 5545 +- **Multi-day all-day events** set `DTEND` to one day after the last event day (iCal standard uses exclusive end dates for all-day events) +- **Timed events** use full date-time format with UTC offset + +### Recurrences + +Each materialized recurrence instance is emitted as a separate `VEVENT`. No `RRULE` properties are used because Resgrid pre-expands recurrences in the database. + +### PRODID + +The `PRODID` value is configurable via `CalendarConfig.ICalProductId` (default: `-//Resgrid//Calendar//EN`). + +## Calendar Items API Enhancement + +The existing `GetAllCalendarItems` v4 API endpoint response now includes an `IsMultiDay` boolean property on each calendar item, indicating whether the event spans multiple days (where `Start.Date != End.Date`). + +### Updated Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `IsMultiDay` | boolean | `true` if the event spans more than one day | +| `IsAllDay` | boolean | `true` if the event is an all-day event (existing field) | + +## FullCalendar JSON Enhancement + +The `GetV2CalendarEntriesForCal` endpoint (used by the web calendar view) now includes: + +| Field | Type | Description | +|-------|------|-------------| +| `allDay` | boolean | `true` for all-day events; enables FullCalendar banner rendering | + +For all-day events, the `end` date is set to one day after the last event day to match FullCalendar's exclusive end-date convention. diff --git a/docs/api/information.md b/docs/api/information.md index 0af594a..202208d 100644 --- a/docs/api/information.md +++ b/docs/api/information.md @@ -11,3 +11,15 @@ Here you can find some core information, mainly how to authenticate, about the R The Resgrid API Exposes our Swagger documentation to showcase every call and operation you can perform against the Resgrid API. You can view the documentation by navigating to the Resgrid API Swagger page. + +## Key API Areas + +| Area | Description | +|------|-------------| +| **Authentication** | JWT/OpenID Connect token management | +| **Statuses** | Personnel and unit status operations | +| **Calls** | Dispatch call management | +| **Calendar** | Calendar events, types, and RSVP | +| **Calendar Export** | iCal export, department feed subscriptions, and sync token management (see [Calendar Export API](calendar-export)) | +| **Contact Verification** | Email and phone verification | +| **Workflows** | Automated workflow management | diff --git a/docs/configuration/calendar-types.md b/docs/configuration/calendar-types.md index ce1347e..094145b 100644 --- a/docs/configuration/calendar-types.md +++ b/docs/configuration/calendar-types.md @@ -55,13 +55,16 @@ When creating or editing a calendar event, you select a type from the dropdown. Calendar events also support: - **Title and Description** — event details -- **Start/End Times** — with time zone support +- **All-Day Events** — span full days without requiring time selection; times auto-normalize to midnight boundaries +- **Start/End Times** — with time zone support (hidden when All Day is checked) +- **Multi-Day Events** — events spanning multiple days display as continuous banners on the calendar - **Location** — with geocoding for map display - **Recurrence** — none, weekly, monthly, or yearly - **Signup Type** — for RSVP-based events - **Entity Targeting** — target specific groups or the whole department - **Required/Optional Attendees** — for attendance tracking - **Reminders** — automated reminders before the event +- **iCal Export** — download individual events as `.ics` files or subscribe to the full calendar feed ## How Calendar Types Connect to Other Features @@ -71,6 +74,7 @@ Calendar events also support: | Reports | Events can be analyzed by type for operational reporting | | Notifications | New calendar events trigger notifications to targeted entities | | Permissions | Calendar event creation is controlled by the Create Calendar Entry permission | +| Calendar Sync | Exported iCal feeds include event type information | ## Common Errors and Resolutions diff --git a/docs/development/calendar-architecture.md b/docs/development/calendar-architecture.md new file mode 100644 index 0000000..41c4082 --- /dev/null +++ b/docs/development/calendar-architecture.md @@ -0,0 +1,229 @@ +--- +sidebar_position: 4 +--- + +# Calendar System Architecture + +This reference document describes the internal architecture of the Resgrid calendar system, including all-day and multi-day event handling, iCal export, calendar feed subscriptions, and the data model. + +## Data Model + +### CalendarItem + +The `CalendarItem` entity is the core model stored in the `CalendarItems` table. All changes in this enhancement are **backwards-compatible** with the existing table schema. + +| Property | Type | Description | +|----------|------|-------------| +| `Title` | string | Event name | +| `Start` | DateTime | Event start (always populated; auto-normalized for all-day events) | +| `End` | DateTime | Event end (always populated; auto-normalized for all-day events) | +| `IsAllDay` | bool | Whether the event is an all-day event | +| `Description` | string | Event description | +| `Location` | string | Event location | +| `RecurrenceType` | int | Recurrence schedule (none, weekly, monthly, yearly) | +| `RecurrenceEnd` | DateTime? | When recurrence stops | +| `Reminder` | int | Reminder type | +| `Attendees` | string | Event attendees | + +#### IsMultiDay() Helper + +A `[NotMapped]` pure helper method on `CalendarItem`: + +```csharp +public bool IsMultiDay() => Start.Date != End.Date; +``` + +This is used by the UI and API to determine display formatting without additional database queries. + +### UserProfile — CalendarSyncToken + +A new nullable column on the `UserProfiles` table stores the per-user calendar sync GUID: + +| Column | Type | Max Length | Nullable | Description | +|--------|------|-----------|----------|-------------| +| `CalendarSyncToken` | string | 128 | Yes | GUID for validating iCal feed subscription tokens | + +**Migration:** `M0047_AddingCalendarSyncToken` (SQL Server) and `M0047_AddingCalendarSyncTokenPg` (PostgreSQL). + +No new repository or DI registrations are required — `UserProfile` is managed by the existing `IUserProfilesRepository` with Dapper `SaveOrUpdate`. The new column is a nullable string on the existing entity. + +## All-Day Event Normalization + +When `IsAllDay` is `true`, the service layer automatically normalizes the `Start` and `End` times: + +| Field | Normalized Value | Purpose | +|-------|-----------------|---------| +| `Start` | `Start.Date` (midnight 00:00:00) | Ensures consistent date boundary | +| `End` | `End.Date.AddDays(1).AddTicks(-1)` (23:59:59.9999999) | Ensures the event covers the full end date | + +This normalization is applied in: +- `CalendarService.AddNewCalendarItemAsync` — after the existing UTC conversion block +- `CalendarService.UpdateCalendarItemAsync` — same location +- `CalendarItem.CreateRecurranceItem` — when generating recurrence children + +The `[Required]` attributes on `Start` and `End` are preserved — they are always populated. The "no time required" behavior is purely a UI/service-layer concern. + +## Calendar Sync Token System + +### Token Generation Flow + +1. User clicks **Activate Calendar Sync** in the web UI +2. `CalendarService.ActivateCalendarSyncAsync` generates a new GUID +3. The GUID is saved to `UserProfile.CalendarSyncToken` +4. An encrypted token payload is created: `"{departmentId}|{userId}|{calendarSyncToken}"` +5. The payload is encrypted via `IEncryptionService.Encrypt()` +6. The encrypted bytes are encoded as URL-safe Base64 (`+` → `-`, `/` → `_`, trailing `=` trimmed) +7. The full subscription URL is returned to the user + +### Token Validation Flow + +1. External calendar app requests `CalendarFeed/{token}` +2. Token is decoded from URL-safe Base64 +3. Token is decrypted via `IEncryptionService` +4. Payload is split into `departmentId`, `userId`, and `calendarSyncToken` +5. `UserProfile` is loaded and `CalendarSyncToken` is compared +6. If the GUID matches, the department iCal feed is generated and returned +7. If the GUID does not match (e.g., user regenerated the key), 401 is returned + +### Token Regeneration + +`RegenerateCalendarSyncAsync` overwrites the existing GUID with a new one, instantly invalidating all previously issued subscription URLs. + +## iCal Export Service + +The `CalendarExportService` (implementing `ICalendarExportService`) generates standard RFC 5545 iCalendar output using the `Ical.Net` NuGet package. + +### Service Methods + +| Method | Description | +|--------|-------------| +| `GenerateICalForItemAsync(int calendarItemId)` | Single `VEVENT` as `.ics` string | +| `GenerateICalForDepartmentAsync(int departmentId)` | Full department calendar as `.ics` with all items | + +### Event Mapping Rules + +| CalendarItem | Ical.Net CalendarEvent | Notes | +|--------------|----------------------|-------| +| `Title` | `Summary` | | +| `Description` | `Description` | | +| `Location` | `Location` | | +| `Start` | `DtStart` | `CalDateTime` with date-only when `IsAllDay` | +| `End` | `DtEnd` | Date-only + 1 day (exclusive) when all-day | +| `IsAllDay` | `IsAllDay` | Emits `VALUE=DATE` in output | +| `Reminder` | `Alarms` (VALARM) | Trigger from `GetMinutesForReminder()` | +| `Attendees` | `Attendees` | Only if populated | + +### Recurrence Handling + +Each materialized recurrence instance is emitted as a separate `VEVENT`. No `RRULE` properties are used because Resgrid pre-expands recurrences in the database. The `IsAllDay` flag is propagated to recurrence children. + +### Registration + +`CalendarExportService` is registered as `ICalendarExportService` in `ServicesModule.cs`. + +## Configuration + +Calendar-specific config values are stored in `CalendarConfig.cs` in the `Resgrid.Config` project, following existing patterns: + +```csharp +public static class CalendarConfig +{ + /// Feature flag to enable/disable the external iCal feed endpoint. + public static bool ICalFeedEnabled = true; + + /// PRODID value used in generated iCal files. + public static string ICalProductId = "-//Resgrid//Calendar//EN"; + + /// How long (in minutes) a feed response can be cached by the subscribing client. + public static int ICalFeedCacheDurationMinutes = 15; +} +``` + +These values can be overridden via `ResgridConfig.json` like all other config classes (handled by `ConfigProcessor`). + +## FullCalendar Upgrade (v3 → v6) + +The web calendar UI is upgraded from FullCalendar v3 to FullCalendar v6: + +| Aspect | v3 (Old) | v6 (New) | +|--------|----------|----------| +| API | jQuery plugin (`$('#calendar').fullCalendar({...})`) | Vanilla JS (`new FullCalendar.Calendar(el, {...})`) | +| Library | Single `fullcalendar` package | `@fullcalendar/core`, `daygrid`, `timegrid`, `interaction`, `list` | +| CSS | `fullcalendar.print.min.css` | FullCalendar 6 CSS bundles | +| All-day rendering | Manual handling | Native support via `allDay: true` in event JSON | +| Multi-day rendering | Limited | Native continuous banner rendering | + +### FullCalendar JSON Changes + +The `GetV2CalendarEntriesForCal` endpoint now includes: +- `allDay` boolean property (set from `IsAllDay`) +- For all-day events, `end` is set to `End.Date.AddDays(1)` (FullCalendar uses exclusive end dates) + +## Localization + +All new user-facing strings use the existing `IStringLocalizer` / `.resx` localization system. New keys are added to `Calendar.en.resx` and `Calendar.es.resx`: + +| Key | English | Spanish | +|-----|---------|---------| +| `AllDayEvent` | All Day Event | Evento de todo el día | +| `DateRange` | {0} – {1} | {0} – {1} | +| `DownloadIcs` | Download .ics | Descargar .ics | +| `SubscribeCalendar` | Subscribe to Calendar | Suscribirse al calendario | +| `CalendarSyncTitle` | Calendar Sync | Sincronización de calendario | +| `CalendarSyncDescription` | Subscribe to your department calendar in Google Calendar, Microsoft Outlook, Apple Calendar, or any application that supports iCal feeds. | Suscríbase al calendario de su departamento en Google Calendar, Microsoft Outlook, Apple Calendar o cualquier aplicación que admita fuentes iCal. | +| `ActivateCalendarSync` | Activate Calendar Sync | Activar sincronización de calendario | +| `RegenerateCalendarSync` | Regenerate Sync Key | Regenerar clave de sincronización | +| `CalendarSyncActivateHelp` | To sync your department calendar with an external calendar application, you must first activate calendar sync. This generates a unique subscription URL. If the URL is compromised, you can regenerate it to invalidate the old one. | Para sincronizar el calendario de su departamento con una aplicación de calendario externa, primero debe activar la sincronización del calendario. Esto genera una URL de suscripción única. Si la URL se ve comprometida, puede regenerarla para invalidar la anterior. | +| `CopyToClipboard` | Copy to Clipboard | Copiar al portapapeles | +| `SubscriptionUrl` | Subscription URL | URL de suscripción | +| `WebCalLink` | Open in Calendar App | Abrir en aplicación de calendario | +| `CalendarSyncActive` | Calendar sync is active. Use the URL below to subscribe. | La sincronización de calendario está activa. Use la URL a continuación para suscribirse. | +| `CalendarSyncInactive` | Calendar sync is not yet activated. Click the button below to generate your subscription URL. | La sincronización del calendario aún no está activada. Haga clic en el botón a continuación para generar su URL de suscripción. | + +## New and Modified Files Summary + +### New Files + +| File | Description | +|------|-------------| +| `CalendarConfig.cs` | Calendar-specific configuration values | +| `ICalendarExportService.cs` | Interface for iCal generation | +| `CalendarExportService.cs` | iCal generation implementation using Ical.Net | +| `CalendarExportController.cs` | v4 API controller for export and feed endpoints | +| `M0047_AddingCalendarSyncToken.cs` | SQL Server migration for UserProfiles column | +| `M0047_AddingCalendarSyncTokenPg.cs` | PostgreSQL migration for UserProfiles column | +| `CalendarExportServiceTests.cs` | Unit tests for iCal export | + +### Modified Files + +| File | Changes | +|------|---------| +| `CalendarItem.cs` | Added `IsMultiDay()` helper method | +| `UserProfile.cs` | Added `CalendarSyncToken` property | +| `ICalendarService.cs` | Added sync token management methods | +| `CalendarService.cs` | All-day normalization, sync token implementation | +| `ServicesModule.cs` | Registered `CalendarExportService` | +| `CalendarController.cs` (web) | All-day normalization, sync actions, view model updates | +| `CalendarController.cs` (v4 API) | Added `IsMultiDay` to calendar item results | +| `CalendarItemV2Json.cs` | Added `allDay` property | +| `GetAllCalendarItemResult.cs` | Added `IsMultiDay` property | +| `IndexView.cs` | Added `CalendarSyncToken` and `CalendarSubscriptionUrl` | +| `CalendarItemView.cs` | Added `ExportIcsUrl` | +| `Index.cshtml` | Calendar sync subscription panel | +| `View.cshtml` | All-day display, multi-day date range, .ics download | +| `New.cshtml` / `Edit.cshtml` | All-day date-only picker UX | +| `resgrid.calendar.index.js` | Rewritten for FullCalendar v6 API | +| `resgrid.calendar.newEntry.js` | All-day toggle hides time pickers | +| `_UserLayout.cshtml` | FullCalendar v6 CSS/JS references | +| `libman.json` | FullCalendar v6 library references | +| `Calendar.en.resx` / `Calendar.es.resx` | New localization keys | +| `CalendarServiceTests.cs` | Expanded with all-day, multi-day, sync token tests | + +## Backwards Compatibility + +All changes are backwards-compatible: +- The `CalendarItems` table schema is unchanged — no migration needed for calendar data +- The `UserProfiles` table gains a single nullable column — existing rows are unaffected +- Existing API contracts are additive only (new properties, new endpoints) +- The `IsAllDay` normalization only activates when `IsAllDay` is `true`; existing events with `IsAllDay = false` are not modified +- The FullCalendar v6 upgrade maintains the same data source URL (`GetV2CalendarEntriesForCal`) diff --git a/docs/how-tos/calendar-sync.md b/docs/how-tos/calendar-sync.md new file mode 100644 index 0000000..6931fb7 --- /dev/null +++ b/docs/how-tos/calendar-sync.md @@ -0,0 +1,138 @@ +--- +sidebar_position: 4 +--- + +# Calendar Sync & iCal Export + +This guide explains how to subscribe to your Resgrid department calendar from an external calendar application (Google Calendar, Microsoft Outlook, Apple Calendar, etc.) and how to export individual events as `.ics` files. + +## Overview + +Resgrid can publish your department calendar as a standard iCal feed that external calendar applications can subscribe to. Once subscribed, events are automatically kept in sync — new events appear, updated events are refreshed, and deleted events are removed on the next sync cycle. + +:::tip Supported Applications +Any application that supports iCal / ICS calendar subscriptions will work, including: +- **Google Calendar** +- **Microsoft Outlook** (desktop and web) +- **Apple Calendar** (macOS and iOS) +- **Thunderbird** +- **Yahoo Calendar** +::: + +--- + +## Step 1 — Activate Calendar Sync + +1. Navigate to **Calendar** in the left-hand menu +2. In the right sidebar, locate the **Calendar Sync** panel below the Types section +3. Click **Activate Calendar Sync** +4. A unique subscription URL is generated and displayed + +This URL is personal to your account and contains an encrypted token that identifies your department and user. No additional login is required when a calendar application fetches the feed. + +--- + +## Step 2 — Subscribe in Your Calendar Application + +### Google Calendar + +1. Copy the subscription URL from the Calendar Sync panel +2. Open [Google Calendar](https://calendar.google.com) +3. In the left sidebar, click the **+** next to "Other calendars" +4. Select **From URL** +5. Paste the subscription URL and click **Add calendar** + +Alternatively, click the **Google Calendar** quick-add link in the sync panel — it opens Google Calendar with the URL pre-filled. + +### Microsoft Outlook (Web) + +1. Copy the subscription URL +2. Open [Outlook Calendar](https://outlook.live.com/calendar) +3. Click **Add calendar** → **Subscribe from web** +4. Paste the URL, give it a name, and click **Import** + +### Apple Calendar (macOS / iOS) + +**macOS:** + +1. Click the **Open in Calendar App** button in the sync panel (uses `webcal://` protocol), or: +2. Open Apple Calendar → **File** → **New Calendar Subscription** +3. Paste the subscription URL and click **Subscribe** +4. Configure refresh interval (recommended: every 15 minutes) + +**iOS:** + +1. Copy the subscription URL +2. Go to **Settings** → **Calendar** → **Accounts** → **Add Account** → **Other** +3. Tap **Add Subscribed Calendar** +4. Paste the URL and tap **Next** → **Save** + +### Other Applications + +For any application that supports iCal subscriptions, use the subscription URL directly. The feed returns standard RFC 5545 iCalendar data with `text/calendar` content type. + +--- + +## Step 3 — Verify the Subscription + +After subscribing, your external calendar should display department events within a few minutes. All-day events appear as full-day banners. Multi-day events span their full date range. Events include titles, descriptions, locations, and reminders. + +:::note Sync Interval +External calendar applications control how often they refresh the feed. Most default to every 15–60 minutes. Check your calendar application settings if you need more frequent updates. +::: + +--- + +## Regenerating the Sync Key + +If you believe your subscription URL has been shared or compromised: + +1. Navigate to **Calendar** in the left-hand menu +2. In the Calendar Sync panel, click **Regenerate Sync Key** +3. The old URL is immediately invalidated +4. Copy the new URL and update it in all your subscribed calendar applications + +:::warning +Regenerating the sync key invalidates **all** previously issued subscription URLs for your account. Any calendar application using the old URL will stop receiving updates and must be re-subscribed with the new URL. +::: + +--- + +## Downloading Individual Events + +You can download any single calendar event as an `.ics` file: + +1. Navigate to the event detail page (click on an event in the calendar) +2. Click the **Download .ics** button +3. Open the downloaded file with your calendar application to add that single event + +This is useful for sharing a specific event with someone outside your department or adding it to a personal calendar without subscribing to the full feed. + +--- + +## How the iCal Feed Works + +| Aspect | Detail | +|--------|--------| +| **Format** | Standard iCalendar (RFC 5545) | +| **Content Type** | `text/calendar` | +| **Authentication** | Encrypted token in the URL (no login required) | +| **All-Day Events** | Exported with `VALUE=DATE` format (no time component) | +| **Multi-Day Events** | `DTEND` is exclusive (one day after the last event day), per iCal standard | +| **Recurrences** | Each recurrence instance is a separate `VEVENT` (no `RRULE`) | +| **Reminders** | Exported as `VALARM` components with appropriate trigger durations | +| **Locations** | Exported as `LOCATION` properties | +| **Attendees** | Exported as `ATTENDEE` properties (if populated) | + +--- + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| Calendar app says "unable to verify account" | Ensure the subscription URL is copied completely without extra spaces | +| Events not appearing after subscribing | Wait for the next sync cycle (up to 60 minutes depending on your app). Check that the URL starts with `https://` or `webcal://` | +| Events stopped updating | Your sync key may have been regenerated. Check the Calendar Sync panel for the current URL and re-subscribe | +| "Calendar sync is not yet activated" message | Click **Activate Calendar Sync** to generate your subscription URL | +| All-day events showing wrong dates | Ensure your calendar application is set to the correct time zone | +| Cannot see the Calendar Sync panel | Calendar sync may be disabled by your system administrator (`ICalFeedEnabled` config) | diff --git a/docs/reference/docker.md b/docs/reference/docker.md index a746559..9f81754 100644 --- a/docs/reference/docker.md +++ b/docs/reference/docker.md @@ -270,6 +270,19 @@ RESGRID__WebConfig__IngressProxyNetworkCidr=16 ASPNETCORE_ENVIRONMENT=Production ASPNETCORE_URLS=http://0.0.0.0:80 + +# ----------------------------------------------- +# --- Calendar Config Section ------------------- +# ----------------------------------------------- + +# Enable or disable the external iCal feed endpoint (default: true) +RESGRID__CalendarConfig__ICalFeedEnabled=true + +# PRODID value used in generated iCal files (default: -//Resgrid//Calendar//EN) +RESGRID__CalendarConfig__ICalProductId=-//Resgrid//Calendar//EN + +# How long (in minutes) a feed response can be cached by subscribing clients (default: 15) +RESGRID__CalendarConfig__ICalFeedCacheDurationMinutes=15 ``` ### System Behavior Config diff --git a/docs/reference/localization.md b/docs/reference/localization.md index 8869b40..038c642 100644 --- a/docs/reference/localization.md +++ b/docs/reference/localization.md @@ -39,6 +39,29 @@ The following localization keys are available for the contact verification featu These strings are defined in `EditProfile.en.resx` and `EditProfile.es.resx` and follow the existing marker-class localization pattern. +### Calendar Localization Strings + +The following localization keys are available for the calendar module's all-day events, multi-day events, and calendar sync features: + +| Key | English (en) | Description | +|-----|-------------|-------------| +| `AllDayEvent` | "All Day Event" | Label for all-day events on the calendar and detail views | +| `DateRange` | "{0} – {1}" | Format string for multi-day event date ranges | +| `DownloadIcs` | "Download .ics" | Button label for downloading a single event as iCal | +| `SubscribeCalendar` | "Subscribe to Calendar" | Button/panel label for calendar sync | +| `CalendarSyncTitle` | "Calendar Sync" | Panel heading for the calendar sync section | +| `CalendarSyncDescription` | "Subscribe to your department calendar in Google Calendar, Microsoft Outlook, Apple Calendar, or any application that supports iCal feeds." | Help text in the sync panel | +| `ActivateCalendarSync` | "Activate Calendar Sync" | Button to generate a subscription URL | +| `RegenerateCalendarSync` | "Regenerate Sync Key" | Button to invalidate and recreate the subscription URL | +| `CalendarSyncActivateHelp` | "To sync your department calendar with an external calendar application, you must first activate calendar sync. This generates a unique subscription URL. If the URL is compromised, you can regenerate it to invalidate the old one." | Detailed help text for first-time activation | +| `CopyToClipboard` | "Copy to Clipboard" | Button label for copying the subscription URL | +| `SubscriptionUrl` | "Subscription URL" | Label for the subscription URL field | +| `WebCalLink` | "Open in Calendar App" | Label for the webcal:// protocol link | +| `CalendarSyncActive` | "Calendar sync is active. Use the URL below to subscribe." | Status text when sync is active | +| `CalendarSyncInactive` | "Calendar sync is not yet activated. Click the button below to generate your subscription URL." | Status text when sync is not yet activated | + +These strings are defined in `Calendar.en.resx` and `Calendar.es.resx` (Spanish translations are also provided). + ## Localization Gaps The following areas or parts of the system may not be properly localized for your specific language: diff --git a/docs/web-app/calendar.md b/docs/web-app/calendar.md index 0039b32..c34ba7d 100644 --- a/docs/web-app/calendar.md +++ b/docs/web-app/calendar.md @@ -5,17 +5,20 @@ title: Calendar # Calendar -The Calendar module provides event scheduling, RSVP management, and recurring event support. It is managed by the `CalendarController`. +The Calendar module provides event scheduling, RSVP management, recurring event support, all-day and multi-day events, and external calendar sync via iCal feeds. It is managed by the `CalendarController`. ## Calendar View **Authorization:** `Schedule_View` policy The main calendar view displays: -- All department calendar events +- All department calendar events in a FullCalendar v6 interface - Color-coded by event type - IANA timezone conversion for proper display +- All-day events rendered as banners across their full date span +- Multi-day events displayed continuously across all applicable days - Upcoming items list +- Calendar sync subscription panel (see [Calendar Sync](#calendar-sync) below) ## Creating Events @@ -27,14 +30,32 @@ The main calendar view displays: |-------|----------|-------------| | Title | Yes | Event name | | Description | No | Event description (HTML supported, sanitized on display) | -| Start Date/Time | Yes | Event start (defaults to now + 3 hours) | -| End Date/Time | Yes | Event end (defaults to now + 4 hours) | +| All Day | No | When checked, the event spans full days and time pickers are hidden | +| Start Date/Time | Yes | Event start (defaults to now + 3 hours). Date-only when All Day is checked | +| End Date/Time | Yes | Event end (defaults to now + 4 hours). Date-only when All Day is checked | | Location | No | Event location (geocodable for map display) | | Item Type | No | Event category/type | | Entities | No | Target department or specific groups | +### All-Day Events + +When the **All Day** checkbox is checked: +- The date/time pickers switch to **date-only mode** (time selection is hidden) +- The service layer automatically normalizes times: **Start** is set to midnight (00:00:00) and **End** is set to 23:59:59.9999999 of the selected end date +- You only need to pick the start and end dates — valid times are filled in automatically +- On the calendar view and event detail page, all-day events display "All Day" instead of specific times + +### Multi-Day Events + +Events that span more than one day (where the start date differs from the end date) are multi-day events: +- On the calendar view, multi-day events render as a continuous banner across all days in the range +- On the event detail page, a date range is displayed (e.g., "Apr 20 – Apr 22") instead of a single date +- Multi-day events can be either all-day or time-specific +- The API response includes an `IsMultiDay` flag for programmatic detection + ### Validation - Start date must be before end date +- For all-day events, start and end may be the same date (single all-day event) - Recurrence end date validated if recurrence is set ### Recurrence @@ -44,7 +65,7 @@ Events can recur on these schedules: - **Monthly** — Repeats every month - **Yearly** — Repeats every year -Recurring events have a recurrence end date. +Recurring events have a recurrence end date. Recurrence children inherit the **All Day** flag and receive the same midnight/end-of-day time normalization when applicable. ### Notifications Event creation triggers a notification delivery to relevant personnel. Fires `CalendarEventAddedEvent`. @@ -53,7 +74,7 @@ Event creation triggers a notification delivery to relevant personnel. Fires `Ca **Authorization:** `Schedule_Update` policy + `CanUserModifyCalendarEntry` runtime check -Only department admins and the original event creator can modify events. +Only department admins and the original event creator can modify events. The same all-day normalization is applied when updating events. Fires `CalendarEventUpdatedEvent`. @@ -90,11 +111,62 @@ Calendar item types provide categorization and color coding. | Edit Type | `Schedule_Create` | Modify type (validates department ownership) | | Delete Type | `Schedule_Create` | Remove type (validates department ownership) | +## Calendar Sync + +The calendar sync feature allows personnel to subscribe to their department calendar from external calendar applications such as **Google Calendar**, **Microsoft Outlook**, and **Apple Calendar** using standard iCal (`.ics`) feeds. + +### How Calendar Sync Works + +1. **Activate** — Each user generates a unique, encrypted subscription URL by clicking "Activate Calendar Sync" on the calendar page +2. **Subscribe** — Copy the subscription URL into your calendar application, or use the quick-add links for Google Calendar, Outlook, or Apple Calendar +3. **Auto-Update** — The external calendar application periodically fetches the latest events from the feed URL + +The subscription URL contains an encrypted token that embeds your department ID, user ID, and a unique sync key. No additional authentication is required when the calendar application fetches the feed. + +### Activating Calendar Sync + +Navigate to the **Calendar** page. In the right sidebar below the Types section, you will find the **Calendar Sync** panel: + +- If sync is **not yet activated**, click **Activate Calendar Sync** to generate your subscription URL +- If sync is **already active**, the subscription URL is displayed along with: + - A **Copy to Clipboard** button + - An **Open in Calendar App** link (uses `webcal://` protocol for one-click subscription) + - Quick-add links for **Google Calendar** and **Microsoft Outlook** + - A **Regenerate Sync Key** button to invalidate the current URL and create a new one + +:::tip One-Click Subscription +The **Open in Calendar App** button uses the `webcal://` protocol, which most calendar applications recognize automatically. Clicking it will prompt your default calendar app to add the subscription. +::: + +### Regenerating the Sync Key + +If you believe your subscription URL has been compromised, click **Regenerate Sync Key**. This immediately invalidates the old URL — any calendar application using the previous URL will no longer receive updates and must be re-subscribed with the new URL. + +:::warning URL Invalidation +Regenerating the sync key invalidates **all** previously issued subscription URLs for your account. You will need to update the subscription in every external calendar application that was using the old URL. +::: + +### Downloading Individual Events + +On the event detail page, a **Download .ics** button allows you to download a single calendar event as an `.ics` file. This file can be opened in any calendar application to add that specific event. + +### Calendar Sync Configuration + +Calendar sync can be enabled or disabled system-wide via the `CalendarConfig.ICalFeedEnabled` configuration flag. When disabled, the feed endpoint returns an error and the sync panel is hidden. + +| Config Setting | Default | Description | +|----------------|---------|-------------| +| `ICalFeedEnabled` | `true` | Enable or disable the external iCal feed endpoint | +| `ICalProductId` | `-//Resgrid//Calendar//EN` | PRODID value used in generated iCal files | +| `ICalFeedCacheDurationMinutes` | `15` | How long (in minutes) a feed response can be cached by the subscribing client | + ## Calendar Data Formats -### FullCalendar v2 Format -The `GetV2CalendarEntriesForCal` endpoint returns events in FullCalendar-compatible JSON format: +### FullCalendar v6 Format +The `GetV2CalendarEntriesForCal` endpoint returns events in FullCalendar v6-compatible JSON format: - Start/end times in ISO format +- `allDay` boolean flag for all-day events +- For all-day events, `end` is set to the day after the last event day (FullCalendar uses exclusive end dates for all-day events) - Type-based background colors - URL links to event detail pages - Admin/creator flags for edit permissions @@ -104,6 +176,24 @@ The `GetDepartmentCalendarItems` endpoint returns all items with: - Time zone-converted timestamps - Recurrence parent validity check - `IsAdminOrCreator` flag for permissions +- `IsMultiDay` flag indicating whether the event spans multiple days + +## iCal Export & Feed + +The system generates standard iCalendar (RFC 5545) output using the `Ical.Net` library. + +### Single Event Export +Download a single calendar event as a `.ics` file containing one `VEVENT`. + +### Department Calendar Feed +The subscription feed returns all department calendar events as a single `.ics` file with multiple `VEVENT` entries: +- Each event (including materialized recurrence instances) is a separate `VEVENT` — no `RRULE` is used since recurrences are pre-expanded +- All-day events use `VALUE=DATE` format for `DTSTART`/`DTEND` +- Multi-day all-day events set `DTEND` to one day after the last event day (iCal standard: exclusive end date) +- Event title maps to `SUMMARY`, description to `DESCRIPTION`, location to `LOCATION` +- Reminders map to `VALARM` components with the appropriate trigger duration +- Attendees (if populated) map to `ATTENDEE` properties +- `PRODID` is set from the system configuration ## Map Integration @@ -114,10 +204,12 @@ The `GetMapDataForItem` endpoint geocodes a calendar item's location and returns | Endpoint | Purpose | |----------|---------| | `GetDepartmentCalendarItems` | All calendar items as JSON | -| `GetV2CalendarEntriesForCal` | FullCalendar v2 format events | +| `GetV2CalendarEntriesForCal` | FullCalendar v6 format events | | `GetDepartmentCalendarItemTypes` | Item types (includes "None" default) | | `GetDepartmentEnitites` | Department + groups as entities for targeting | | `GetMapDataForItem` | Geocoded coordinates for event location | +| `ActivateCalendarSync` | Generate a calendar sync subscription URL | +| `RegenerateCalendarSync` | Regenerate the sync key (invalidates old URL) | ## Interactions with Other Modules @@ -128,3 +220,5 @@ The `GetMapDataForItem` endpoint geocodes a calendar item's location and returns | **Mapping** | Event locations can be geocoded and displayed on maps | | **Notifications** | Event creation/update triggers notifications | | **Department Settings** | Module can be enabled/disabled | +| **User Profile** | Stores the calendar sync token for iCal feed subscriptions | +| **Encryption** | Subscription URL tokens are encrypted for security |