| ← 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:
- Specific start and optional end times
- Duration tracking (calculated in hours)
- Location within the event
- Staff member assignments
- Timezone-aware display and input
- Public visibility controls
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:
- All datetime values stored in UTC in database
- Display and input conversion uses gathering timezone (if set), else user timezone
has_end_timeflag prevents false positives from NULLend_datetimeduration_hourscalculated from start/end times when both present
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:
- Gathering’s timezone (
gatherings.timezone) - Event location timezone - User’s timezone (
members.timezone) - Individual preference - Application default (
KMP.DefaultTimezone) - Kingdom-wide setting - 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:
- Load parent gathering with ID validation
- Determine timezone using
TimezoneHelper::getGatheringTimezone($gathering, $user) - Convert
start_datetimefrom gathering timezone to UTC - Convert
end_datetimefrom gathering timezone to UTC (if provided) - Create new
GatheringScheduledActivityentity - Save to database
- 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:
- Load existing scheduled activity
- Load parent gathering for timezone context
- Determine timezone using
TimezoneHelper::getGatheringTimezone() - Convert datetime inputs from gathering timezone to UTC
- Patch entity with converted data
- Save to database
- Return AJAX response
deleteScheduledActivity()
Route: POST /gatherings/delete-scheduled-activity/{activityId}
Authorization: Requires edit permission on parent gathering
Process:
- Load scheduled activity
- Load parent gathering for authorization
- Soft-delete (if using Muffin/Trash) or hard-delete
- Return AJAX response
Templates and UI
Schedule Tab
File: app/templates/element/gatherings/scheduleTab.php
Features:
- Displays all scheduled activities for the gathering
- Times shown in gathering timezone using
$this->Timezone->format() - Edit buttons with data attributes containing timezone-converted values
- Delete buttons with confirmation
- “Add Scheduled Activity” button
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:
- Modal form with datetime-local inputs
- Info alert showing current timezone: “All times in [timezone]”
- Name, description, location fields
- Start datetime (required)
- End datetime (optional)
- Staff member selector
- Public display toggle
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:
- Pre-populated form with existing values
- Times converted to gathering timezone for editing
- Same fields as add modal
- Timezone notice consistent with add modal
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:
- Shows scheduled activities on public landing page
- Respects
display_publiclyflag - Times displayed in gathering timezone
- Compact format suitable for public viewing
JavaScript Integration
Modal Form Handling
The schedule modals use standard Bootstrap 5 modal JavaScript with AJAX form submission. Timezone conversion happens server-side, so JavaScript only needs to:
- Populate form fields from data attributes
- Handle form submission via AJAX
- Reload schedule tab content on success
- 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:
- Server-side conversion is more reliable
- Avoids timezone library dependencies
- Simplifies client-side code
- Maintains single source of truth (server)
User Experience Flow
Adding a Scheduled Activity
- User clicks “Add Scheduled Activity” button in Schedule tab
- Modal opens showing timezone notice (e.g., “All times in America/Chicago”)
- User fills in activity details
- User selects start time using datetime-local picker (in gathering timezone)
- Optionally selects end time
- User submits form
- Controller converts times to UTC
- Database stores UTC values
- Schedule tab refreshes showing new activity in gathering timezone
Editing a Scheduled Activity
- User clicks “Edit” button on existing activity
- Modal opens with pre-populated fields
- Times shown in gathering timezone (converted from UTC)
- User modifies values
- User submits form
- Controller converts back to UTC
- Database updates UTC values
- 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:
- Activity shows as “9:00 AM” (user’s Eastern time)
- Confusing for event that starts at 9:00 AM Central
With Gathering Timezone:
- Activity shows as “9:00 AM CDT” (gathering’s Central time)
- Clear that event starts at 9:00 AM in Chicago
- User knows to adjust for their own travel
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:
- Timezone field value (if set)
- Context for timezone resolution
- Consistent timezone across all activity displays
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:
- Use
$this->Timezone->format()with gathering parameter - Show timezone abbreviation for clarity (CDT, EST, etc.)
- Include timezone notice in forms
- Display consistent timezone across all schedule views
❌ DON’T:
- Display raw UTC times to users
- Assume user’s timezone matches event timezone
- Forget to pass gathering context to timezone helpers
- Mix timezones within a single schedule view
Input Conversion
✅ DO:
- Convert all datetime inputs to UTC before saving
- Use
TimezoneHelper::getGatheringTimezone()for timezone resolution - Validate datetime format before conversion
- Handle NULL/empty values gracefully
❌ DON’T:
- Store local times in database
- Skip timezone conversion on edit operations
- Assume datetime-local input is in UTC
- Forget to convert both start and end times
Forms
✅ DO:
- Use
datetime-localinput type - Show timezone notice to users
- Pre-populate edit forms with converted times
- Validate date ranges against parent gathering
❌ DON’T:
- Use text inputs for datetime
- Hide timezone information from users
- Populate edit forms with UTC values
- Allow activities outside gathering date range
Testing Checklist
When implementing or modifying scheduled activities:
- Create scheduled activity with specific time
- Verify time saves as UTC in database
- View schedule tab - times display in gathering timezone
- Edit scheduled activity - form shows time in gathering timezone
- Save edited activity - converts back to UTC correctly
- View on public landing - times display in gathering timezone
- Test with gathering that has explicit timezone set
- Test with gathering without timezone (uses user timezone)
- Test with different user timezones
- Verify activities without end times display correctly
- Verify duration calculation works correctly
- Test staff member assignment
- Test public visibility toggle
See Also
- 4.6 Gatherings System - Parent documentation
- 10.3 Timezone Handling - Complete timezone documentation
- 4.6.2 Gathering Staff Management - Staff integration