Skip to the content.
← Back to Table of Contents ← Back to Core Modules

4.6 Gatherings System

Last Updated: November 4, 2025
Status: Active
Primary Module: app/src/Controller/GatheringsController.php

Overview

The Gatherings system provides a comprehensive workflow for creating and managing kingdom events. A gathering stores the hosting branch, type, dates, location, staff, attached activities, and member attendance. The system includes:

Table of Contents

Module Snapshot

Data Model

The Gatherings module stores only the data it owns and relies on other modules for auxiliary behavior (e.g., waivers). Soft deletes are implemented through the deleted timestamp provided by the Muffin/Trash behavior.

erDiagram
    BRANCHES ||--o{ GATHERINGS : hosts
    GATHERING_TYPES ||--o{ GATHERINGS : categorizes
    MEMBERS ||--o{ GATHERINGS : creates
    GATHERINGS ||--o{ GATHERING_STAFF : has_staff
    MEMBERS ||--o{ GATHERING_STAFF : may_be_linked
    GATHERINGS ||--o{ GATHERINGS_GATHERING_ACTIVITIES : includes
    GATHERING_ACTIVITIES ||--o{ GATHERINGS_GATHERING_ACTIVITIES : reused_as
    GATHERINGS ||--o{ GATHERING_ATTENDANCES : records
    MEMBERS ||--o{ GATHERING_ATTENDANCES : participates

gatherings

Core event record. End dates default to the start date when omitted; validation enforces end_date >= start_date. Start and end dates support both date and time to allow specific event scheduling.

Column Type Notes
id int Primary key
public_id varchar(36) UUID for public URLs; generated automatically
branch_id int Required. FK to branches.id
gathering_type_id int Required. FK to gathering_types.id
name varchar(255) Required display name
description text Optional Markdown description rendered in the detail tab
start_date datetime Required start date and time (stored as UTC DATETIME)
end_date datetime Required end date and time (stored as UTC DATETIME); may equal start_date for single-day events
location varchar(255) Optional venue/address text
timezone varchar(50) Optional IANA timezone identifier (e.g., “America/Chicago”); falls back to user/app timezone when null
latitude decimal(10,8) Optional geocode used by the map tab
longitude decimal(11,8) Optional geocode used by the map tab
public_page_enabled boolean When true, enables public landing page access
show_host_branch_on_public_page boolean Controls branch visibility on public page
cancelled_at datetime When set, the gathering is cancelled. Null = active
cancellation_reason text Optional reason for cancellation
created, modified datetime Managed by the Timestamp behavior
created_by, modified_by int Populated by Muffin/Footprint
deleted datetime Soft-delete marker (null when active)

Migration History:

gathering_types

Reusable labels that describe the nature of a gathering (tournament, practice, court, etc.).

Column Type Notes
id int Primary key
name varchar(255) Required, unique
description text Optional helper text displayed in management UI
clonable boolean Controls whether gatherings of this type expose the “Clone” action
color varchar(7) Hex value used to tint calendar badges
Standard audit columns   Added by Muffin behaviors

gathering_staff

Staff management system for gatherings. Supports both stewards (who must be AMP members with contact info) and other staff (who can be AMP members or generic SCA names).

Column Type Notes
id int Primary key
gathering_id int Required. FK to gatherings.id (CASCADE on delete)
member_id int Optional FK to members.id for AMP member staff
sca_name varchar(255) Optional SCA name for non-AMP staff (XOR with member_id)
role varchar(100) Required role name (e.g., “Steward”, “Herald”, “List Master”)
is_steward boolean True for event stewards; requires contact info
email varchar(255) Contact email (auto-populated from member, editable for privacy)
phone varchar(50) Contact phone (auto-populated from member, editable for privacy)
contact_notes text Optional contact preferences (e.g., “text only”, “no calls after 9 PM”)
sort_order int Display ordering (stewards: 0-99, other staff: 100+)
show_on_public_page boolean Whether to display this staff member on the public landing page
created, modified datetime Standard audit columns
created_by, modified_by int Standard audit columns
deleted datetime Soft-delete marker

Business Rules:

gathering_activities

Configuration catalog of activities (heavy combat, archery, A&S, etc.). The description here is the default that can be overridden per gathering.

Column Type Notes
id int Primary key
name varchar(255) Required
description text Optional default description
Standard audit columns    

gatherings_gathering_activities

Join table that pairs activities with specific gatherings while preserving per-gathering customization.

Column Type Notes
id int Primary key
gathering_id int FK to gatherings.id
gathering_activity_id int FK to gathering_activities.id
sort_order int Display ordering within the activities tab
custom_description text Optional override shown instead of the activity default
Standard audit columns    
Unique index (gathering_id, gathering_activity_id) Prevents duplicates

gathering_attendances

Attendance records are intentionally simple: a member either has a single record for the gathering or they do not. There is no RSVP status enum—attendance is implied by the record’s existence.

Column Type Notes
id int Primary key
gathering_id int FK to gatherings.id (cascade delete)
member_id int FK to members.id (cascade delete)
public_note text Optional note shown wherever sharing rules allow
share_with_kingdom boolean Visible to kingdom-level officers
share_with_hosting_group boolean Visible to officers for the hosting branch
share_with_crown boolean Reserved for high-office reporting
is_public boolean Visible to any authenticated user with gathering access
Standard audit columns   Includes soft-delete marker
Unique index (gathering_id, member_id) Enforced by validation and the database

Gathering Types

Gathering types are managed through GatheringTypesController and exposed in the gathering create/edit forms. They provide three key behaviors:

  1. Categorisation: Each gathering must reference a type; the association is eager-loaded for list, calendar, and detail views.
  2. Color Coding: The optional color column is consumed by the calendar elements (calendar_month.php, calendar_week.php, calendar_list.php). When blank, the UI falls back to Bootstrap blue.
  3. Clone Eligibility: clonable = false hides the “Clone” modal in templates/Gatherings/view.php, preventing users from using that type as a template.

Administrators can extend the catalog without code changes. Validation ensures unique names and hex-formatted colors.

Template Activities

Controller: GatheringTypesController
Methods: addActivity(), removeActivity()
Join Table: gathering_type_gathering_activities

Gathering types can have template activities that are automatically added to new gatherings of that type. This streamlines event creation by pre-populating common activities.

Features:

  1. Automatic Application: When a gathering is created with a specific type, all template activities for that type are automatically added to the gathering
  2. Not Removable Flag: Template activities can be marked as “not removable,” preventing event organizers from removing them from individual gatherings
  3. Tab Interface: Template activities are managed via a dedicated tab on the gathering type view page
  4. Modal-based Management: Add activities via modal form, remove with confirmation

Workflow:

  1. Create gathering type
  2. Navigate to “Template Activities” tab
  3. Click “Add Template Activity”
  4. Select activity from dropdown
  5. Optionally check “Not Removable” to lock it on gatherings
  6. Click “Add Activity”

Use Cases:

Standard Tournament Activities:

Feast Requirements:

Practice Defaults:

Authorization:

Database Structure:

gathering_type_gathering_activities
├── id (primary key)
├── gathering_type_id (FK to gathering_types)
├── gathering_activity_id (FK to gathering_activities)
├── not_removable (boolean)
└── audit columns (created, modified, created_by_id, modified_by_id)

Synchronization: Template activities sync to gatherings happens automatically in GatheringsTable::afterSave() when:

Event Timezone Support

Migration: 20251105000001_AddTimezoneToGatherings.php
Entity Field: Gathering::$timezone
Helper: TimezoneHelper::getGatheringTimezone()

KMP supports event-specific timezones for gatherings, allowing events to be displayed consistently in their location’s timezone regardless of where users are viewing from. This is especially important for:

Timezone Priority for Gatherings

When displaying gathering dates/times, the system resolves timezone in this order:

  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

Database Schema

The gatherings table includes a timezone column:

Usage in Controllers

When saving gathering data with datetime fields, convert from gathering timezone to UTC:

use App\KMP\TimezoneHelper;

// When creating or editing a gathering
if ($this->request->is(['post', 'put'])) {
    $data = $this->request->getData();
    
    // Get the appropriate timezone (gathering's or user's)
    $timezone = TimezoneHelper::getGatheringTimezone($gathering, $this->Authentication->getIdentity());
    
    // Convert from event timezone to UTC for storage
    $data['start_date'] = TimezoneHelper::toUtc($data['start_date'], $timezone);
    $data['end_date'] = TimezoneHelper::toUtc($data['end_date'], $timezone);
    
    $gathering = $this->Gatherings->patchEntity($gathering, $data);
    $this->Gatherings->save($gathering);
}

Usage in Templates

Display gathering times in the event’s timezone:

<!-- Gathering view page -->
<h3><?= h($gathering->name) ?></h3>
<p>
    <strong>When:</strong>
    <?= $this->Timezone->format($gathering->start_date, 'l, F j, Y g:i A T', true, null, $gathering) ?>
    to
    <?= $this->Timezone->format($gathering->end_date, 'l, F j, Y g:i A T', true, null, $gathering) ?>
</p>

<!-- Scheduled activities in gathering timezone -->
<?php foreach ($gathering->gathering_scheduled_activities as $activity): ?>
    <div class="activity">
        <strong><?= h($activity->name) ?></strong><br>
        <?= $this->Timezone->format($activity->start_datetime, 'g:i A', false, null, $gathering) ?>
        <?php if ($activity->has_end_time): ?>
            - <?= $this->Timezone->time($activity->end_datetime, null, null, $gathering) ?>
        <?php endif; ?>
    </div>
<?php endforeach; ?>

Form Input

Add timezone selector to gathering add/edit forms:

<?= $this->Form->control('timezone', [
    'type' => 'select',
    'options' => $this->Timezone->getTimezoneOptions(),
    'empty' => '(Use user timezone)',
    'label' => 'Event Timezone',
    'help' => 'Set the timezone for this event based on its location. If not set, times will display in each user\'s timezone.'
]) ?>

<?= $this->Form->control('start_date', [
    'type' => 'datetime-local',
    'value' => $this->Timezone->forInput($gathering->start_date, null, null, $gathering)
]) ?>

Benefits

  1. Event Consistency: Event times display consistently in the event’s location timezone
  2. User Flexibility: Users can still set their own timezone for non-event views
  3. Location Accuracy: Multi-timezone kingdoms can specify exact timezone per event
  4. Clear Communication: Event times are unambiguous when gathering timezone is set
  5. Backward Compatible: Events without timezone still work (use user/app default)

See Also

For complete timezone implementation details, see 10.3 Timezone Handling.

Gathering Staff Management

Controller: GatheringStaffController
Models: GatheringStaffTable, GatheringStaff entity
View Element: templates/element/gatherings/staffTab.php

The staff management system allows gatherings to have multiple staff members with customizable roles. Staff members are categorized into two types:

Stewards

Event stewards are the primary contacts for a gathering and must meet stricter requirements:

Other Staff

Non-steward staff provide flexibility for volunteers and roles:

Features

Adding Staff:

  1. Click “Add Staff Member” button in the Staff tab
  2. Select whether person is a steward
  3. Choose AMP member from dropdown OR enter SCA name
  4. For stewards, contact info auto-populates (editable)
  5. Specify role name
  6. Optionally add contact notes
  7. Choose whether to display on public landing page

AJAX Contact Lookup:

Sort Ordering:

Authorization:

Use Cases

Multiple Co-Stewards: Large events can assign multiple stewards, each with their own contact preferences and information.

Privacy-Conscious Contacts: Stewards can override their personal AMP contact info with event-specific email addresses or phone numbers.

Mixed Staff Rosters: Typical gathering might include:

Public Page Display

Staff members can opt-in to display on the public landing page via the show_on_public_page boolean. This allows stewards to control their privacy while still providing event contact information to potential attendees.

Calendar Download Feature

Service: ICalendarService
Controller Action: GatheringsController::downloadCalendar()
Routes:

The calendar download feature generates RFC 5545 compliant iCalendar (.ics) files that can be imported into any calendar application (Google Calendar, Outlook, Apple Calendar, Android, etc.).

Features

Universal Compatibility:

Event Information Included:

Access Patterns:

Event Formats:

UI Integration

Download buttons appear in three locations:

  1. Gathering View Page (/gatherings/view/{id})
    • Prominent button with calendar icon
    • First in action buttons row
    • Tooltip: “Download calendar file (.ics) for Outlook, Google Calendar, iOS, etc.”
  2. Calendar Quick View Modal
    • Appears when clicking event in calendar view
    • Positioned before “Full Details” button
    • Allows quick download without navigating away
  3. Public Landing Page (/gatherings/public-landing/{publicId})
    • Large, visible button in hero section
    • Available to all visitors (no login required)
    • Allows potential attendees to add event before registering

Browser Behavior

Testing

Unit tests in tests/TestCase/Services/ICalendarServiceTest.php validate:

Calendar Subscription Feeds

Route: /gatherings/feed (public, no authentication required)

The calendar subscription feed provides a multi-event iCalendar endpoint that calendar applications can subscribe to. Unlike the single-event download, this feed is polled periodically by the calendar app and automatically updates with new, changed, or removed events.

Endpoint

GET /gatherings/feed[?filter[branch_id][]=Ab3kX9pQ&filter[gathering_type_id][]=3]

Query Parameters (all optional):

The feed accepts the same filter[column][] query parameters as the calendar grid.

Parameter Description Example
filter[branch_id][] Filter by branch public ID (repeatable) ?filter[branch_id][]=mfoBwnBC
filter[gathering_type_id][] Filter by gathering type ID (repeatable) ?filter[gathering_type_id][]=3

Parameters can be combined: ?filter[branch_id][]=mfoBwnBC&filter[gathering_type_id][]=3

Feed Behavior

Subscribing from Calendar Apps

App How to Subscribe
Google Calendar Settings → Add calendar → From URL → paste feed URL
Apple Calendar File → New Calendar Subscription → paste feed URL
Outlook Add calendar → Subscribe from web → paste feed URL
Thunderbird New Calendar → On the Network → paste feed URL

UI Integration

The calendar toolbar includes a Subscribe dropdown button that:

  1. Builds the feed URL based on current grid filter selections
  2. Displays a copyable URL in a dropdown panel
  3. Provides one-click copy to clipboard

Implementation

Gathering Activities

Activities act as reusable building blocks. When attaching activities to a gathering:

GatheringsController::addActivity() and removeActivity() enforce uniqueness through ORM checks before the database constraint fires, producing user-friendly errors.

Attendance Tracking

Attendance is recorded through AJAX modals driven by the Stimulus controller.

GatheringAttendancesTable supplies helper finders (findShared, findSharedWithKingdom, etc.) that higher-level reporting can reuse.

Public Landing Pages

Template: templates/Gatherings/public_landing.php
Element: templates/element/gatherings/public_content.php
Route: /gatherings/public-landing/{publicId}

Public landing pages allow events to be shared with non-authenticated users for promotion and information gathering. Each gathering has a unique public_id (UUID) used in the public URL.

Features

Activation:

Content Display:

QR Code Integration:

Branch Visibility Control:

Security:

Use Cases

Event Promotion: Share public URL via social media, email, or printed materials. QR code can be displayed on posters for easy mobile access.

Pre-Registration Information: Allow potential attendees to view event details and add to their calendar before creating an AMP account or registering.

Cross-Kingdom Sharing: Share events with members of other kingdoms or regions who may not have AMP access.

Location Maps

Template Element: templates/element/gatherings/mapTab.php
JavaScript: Leaflet library integration

Interactive location maps provide visual event location information using OpenStreetMap (OSM) and Leaflet.

Features

Map Display:

Geocoding:

CSP Configuration: Content Security Policy (CSP) headers are configured to allow OSM tile loading:

Leaflet Integration:

Configuration

CSP settings in Application.php (middleware stack):

$csp = "default-src 'self'; "
     . "img-src 'self' data: https://*.tile.openstreetmap.org https://*.openstreetmap.org; "
     . "connect-src 'self' https://*.tile.openstreetmap.org;";

This configuration allows the application to load map tiles while maintaining security.

Event Cloning

Modal: templates/element/gatherings/cloneModal.php
Controller Action: GatheringsController::clone()

Event cloning allows organizers to quickly create new gatherings based on existing events, preserving structure while allowing customization.

Features

Clonable Types:

Cloned Data:

Cloning Process:

  1. Click “Clone This Gathering” button (only if type is clonable)
  2. Modal opens with pre-filled form
  3. Adjust name, dates, and other details
  4. System creates new gathering with copied activities
  5. Redirect to new gathering’s view page

Use Cases:

Recurring Events: Clone monthly practices or quarterly tournaments with consistent structure.

Event Series: Create series of related events (e.g., “Spring War”, “Summer War”, “Fall War”) with similar activities and descriptions.

Multi-Location Events: Clone event template for different locations or branches.

Authorization

Clone action requires:

Cancel & Restore

Gatherings can be cancelled without deleting them, preserving all associated data (waivers, attendance, staff, activities).

Cancellation

Restoration

Impact on Other Features

Feature When Cancelled
Waiver uploads Blocked — returns 403 error
Existing waivers Remain viewable for audit
Attendance No new attendance recording
Calendar views Shown with cancelled indicator
Public landing page Displays cancellation notice
Cloning Still possible from cancelled source

Entity Virtual Field

// Gathering entity
protected function _getIsCancelled(): bool
{
    return $this->cancelled_at !== null;
}

User Interface Surfaces

Authorization Model

Integrations

Testing & Fixtures

Future Enhancements

The current implementation provides comprehensive event management. Potential follow-ups include:

  1. Recurring Gatherings: Template recurring series rather than manual cloning.
  2. Attendance Reporting: Aggregate dashboards leveraging the finders in GatheringAttendancesTable.
  3. Reminder Emails: Notify attendees as gatherings approach using stored attendance preferences.
  4. Staff Scheduling: Expand staff management to include shift scheduling and volunteer coordination.
  5. Advanced Map Features: Add route planning, nearby amenities, parking information.
  6. Public Registration: Allow non-authenticated users to pre-register via public landing page.

💡 Implemented since initial release: Event time customization (datetime support with timezone), cancel/restore workflow, steward editing permissions, calendar subscription feeds.

Evaluate these against kingdom needs before expanding the data model.