Skip to the content.
← Back to Table of Contents ← Back to Gatherings System

4.6.3 Gathering Schedule System

Last Updated: November 6, 2025
Status: Active
Primary Controller: GatheringsController (schedule-related methods)

Overview

The Gathering Schedule System allows event organizers to create detailed timetables for their gatherings. Each scheduled activity tracks when activities occur during an event, with support for:

Scheduled activities are distinct from gathering activities (the “what” - heavy combat, rapier, arts & sciences) and instead represent the “when” - the specific time slots during an event.

Database Schema

gathering_scheduled_activities

Column Type Notes
id int Primary key
gathering_id int Required. FK to gatherings.id (CASCADE on delete)
name varchar(255) Required. Activity name/title
description text Optional. Details about this time slot
location varchar(255) Optional. Specific location within event site
start_datetime datetime Required. Start time (stored as UTC)
end_datetime datetime Optional. End time (stored as UTC)
has_end_time boolean Flag indicating if end_datetime is meaningful
duration_hours decimal(5,2) Optional. Calculated duration in hours
staff_member_id int Optional. FK to gathering_staff.id for responsible staff
display_publicly boolean Whether to show on public landing page
created, modified datetime Standard audit columns
created_by, modified_by int Standard audit columns

Business Rules:

Timezone Implementation

Core Principle

Storage: All start_datetime and end_datetime values are stored in UTC.
Display: Times are converted to the gathering’s timezone (or user’s timezone as fallback).
Input: Forms accept times in gathering’s timezone and convert to UTC before saving.

Timezone Priority

When displaying or inputting scheduled activity times:

  1. Gathering’s timezone (gatherings.timezone) - Event location timezone
  2. User’s timezone (members.timezone) - Individual preference
  3. Application default (KMP.DefaultTimezone) - Kingdom-wide setting
  4. UTC - Universal fallback

Data Flow

┌─────────────────────────────────────────────────────────┐
│ User Input (datetime-local in gathering timezone)      │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ Edit Form: forInput() converts UTC → Gathering TZ      │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ Form Submission: Browser sends local datetime string   │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ Controller: toUtc() converts Gathering TZ → UTC        │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ Database: Stores as UTC DATETIME                       │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ Display: format() converts UTC → Gathering TZ          │
└─────────────────────────────────────────────────────────┘

Controller Methods

addScheduledActivity()

Route: POST /gatherings/{id}/add-scheduled-activity
Authorization: Requires edit permission on parent gathering

Process:

  1. Load parent gathering with ID validation
  2. Determine timezone using TimezoneHelper::getGatheringTimezone($gathering, $user)
  3. Convert start_datetime from gathering timezone to UTC
  4. Convert end_datetime from gathering timezone to UTC (if provided)
  5. Create new GatheringScheduledActivity entity
  6. Save to database
  7. Return AJAX response with success/error

Timezone Conversion:

$timezone = TimezoneHelper::getGatheringTimezone($gathering, $this->Authentication->getIdentity());
$data['start_datetime'] = TimezoneHelper::toUtc($data['start_datetime'], $timezone);
if (!empty($data['end_datetime'])) {
    $data['end_datetime'] = TimezoneHelper::toUtc($data['end_datetime'], $timezone);
}

editScheduledActivity()

Route: POST /gatherings/edit-scheduled-activity/{activityId}
Authorization: Requires edit permission on parent gathering

Process:

  1. Load existing scheduled activity
  2. Load parent gathering for timezone context
  3. Determine timezone using TimezoneHelper::getGatheringTimezone()
  4. Convert datetime inputs from gathering timezone to UTC
  5. Patch entity with converted data
  6. Save to database
  7. Return AJAX response

deleteScheduledActivity()

Route: POST /gatherings/delete-scheduled-activity/{activityId}
Authorization: Requires edit permission on parent gathering

Process:

  1. Load scheduled activity
  2. Load parent gathering for authorization
  3. Soft-delete (if using Muffin/Trash) or hard-delete
  4. Return AJAX response

Templates and UI

Schedule Tab

File: app/templates/element/gatherings/scheduleTab.php

Features:

Display Example:

<div class="activity-item">
    <strong><?= h($activity->name) ?></strong><br>
    <?= $this->Timezone->format($activity->start_datetime, 'l, F j, Y g:i A', false, null, $gathering) ?>
    <?php if ($activity->has_end_time): ?>
        to <?= $this->Timezone->time($activity->end_datetime, null, null, $gathering) ?>
    <?php endif; ?>
</div>

Add Schedule Modal

File: app/templates/element/gatherings/addScheduleModal.php

Features:

Timezone Notice:

<div class="alert alert-info">
    <i class="bi bi-info-circle"></i>
    All times in <strong><?= h($this->Timezone->getGatheringTimezone($gathering)) ?></strong>
</div>

Edit Schedule Modal

File: app/templates/element/gatherings/editScheduleModal.php

Features:

Data Attribute Population:

<button type="button" 
    class="btn btn-sm btn-primary edit-activity-btn"
    data-activity-id="<?= $activity->id ?>"
    data-name="<?= h($activity->name) ?>"
    data-start="<?= $this->Timezone->forInput($activity->start_datetime, null, null, $gathering) ?>"
    data-end="<?= $this->Timezone->forInput($activity->end_datetime, null, null, $gathering) ?>"
    ...>
    Edit
</button>

Public Content Display

File: app/templates/element/gatherings/public_content.php

Features:

JavaScript Integration

The schedule modals use standard Bootstrap 5 modal JavaScript with AJAX form submission. Timezone conversion happens server-side, so JavaScript only needs to:

  1. Populate form fields from data attributes
  2. Handle form submission via AJAX
  3. Reload schedule tab content on success
  4. Display error messages on failure

No Client-Side Timezone Conversion

Unlike some other datetime features, the schedule system does NOT use the timezone-input-controller.js Stimulus controller. This is intentional:

User Experience Flow

Adding a Scheduled Activity

  1. User clicks “Add Scheduled Activity” button in Schedule tab
  2. Modal opens showing timezone notice (e.g., “All times in America/Chicago”)
  3. User fills in activity details
  4. User selects start time using datetime-local picker (in gathering timezone)
  5. Optionally selects end time
  6. User submits form
  7. Controller converts times to UTC
  8. Database stores UTC values
  9. Schedule tab refreshes showing new activity in gathering timezone

Editing a Scheduled Activity

  1. User clicks “Edit” button on existing activity
  2. Modal opens with pre-populated fields
  3. Times shown in gathering timezone (converted from UTC)
  4. User modifies values
  5. User submits form
  6. Controller converts back to UTC
  7. Database updates UTC values
  8. Schedule tab refreshes with updated times in gathering timezone

Viewing Schedule (Different Timezones)

Scenario: Event in Chicago (Central Time), User in New York (Eastern Time)

Without Gathering Timezone:

With Gathering Timezone:

Integration with Parent Gathering

Timezone Inheritance

Scheduled activities inherit timezone context from their parent gathering:

// In controller
$timezone = TimezoneHelper::getGatheringTimezone($gathering, $user);

// In template
$this->Timezone->format($activity->start_datetime, 'g:i A', false, null, $gathering);

The gathering object provides:

Date Range Constraints

Forms should validate that scheduled activities fall within the gathering’s date range:

'start_datetime' => [
    'dateTime' => [
        'rule' => 'dateTime',
        'message' => 'Please enter a valid date and time'
    ],
    'withinGatheringDates' => [
        'rule' => function ($value, $context) {
            $gathering = $this->get($context['data']['gathering_id']);
            return $value >= $gathering->start_date && $value <= $gathering->end_date;
        },
        'message' => 'Activity must occur during the gathering dates'
    ]
]

Best Practices

Display

DO:

DON’T:

Input Conversion

DO:

DON’T:

Forms

DO:

DON’T:

Testing Checklist

When implementing or modifying scheduled activities:

See Also