Skip to the content.

Timezone Handling in KMP

Overview

The KMP application now provides comprehensive timezone support, allowing dates and times to be displayed in users’ local timezones while storing everything in UTC in the database. This document explains the timezone system architecture and how to use it throughout the application.

Quick Start

For developers who want to quickly implement timezone support:

  1. Run Database Migration: cd app && bin/cake migrations migrate
  2. Compile Assets: npm run dev or npm run prod
  3. Set Default Timezone: Navigate to AppSettings and set KMP.DefaultTimezone (default: America/Chicago)

The default timezone is automatically initialized on first application run via Application.php bootstrap.

Architecture

Storage

Display Priority

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

  1. Gathering’s Timezone - Event location timezone (gatherings.timezone) - used when displaying event-specific dates
  2. User’s Timezone - Individual member’s timezone preference (members.timezone)
  3. Application Default - System-wide default timezone (KMP.DefaultTimezone AppSetting)
  4. UTC Fallback - Universal Coordinated Time (always available)

Note: The gathering timezone takes priority only when explicitly provided (e.g., when viewing a gathering’s details or schedule). For general date displays without gathering context, the system uses the user’s timezone.

Components

PHP Components

JavaScript Components

Usage Guide

PHP Usage

Basic Display in Controllers

use App\KMP\TimezoneHelper;

// Get current user's timezone
$timezone = TimezoneHelper::getUserTimezone($this->Authentication->getIdentity());

// Convert UTC to user's timezone for display
$localTime = TimezoneHelper::toUserTimezone($gathering->start_date, $this->Authentication->getIdentity());

// Format for display with timezone
$displayTime = TimezoneHelper::formatForDisplay(
    $gathering->start_date,
    $this->Authentication->getIdentity(),
    'l, F j, Y g:i A T',  // Include timezone abbreviation
    true                   // Include timezone
);
// Output: "Saturday, March 15, 2025 9:00 AM CDT"

Form Input Conversion

// When receiving form data (user's timezone -> UTC)
if ($this->request->is(['post', 'put'])) {
    $data = $this->request->getData();
    
    // Convert user's local time to UTC for storage
    $data['start_date'] = TimezoneHelper::toUtc(
        $data['start_date'],
        TimezoneHelper::getUserTimezone($this->Authentication->getIdentity())
    );
    
    $data['end_date'] = TimezoneHelper::toUtc(
        $data['end_date'],
        TimezoneHelper::getUserTimezone($this->Authentication->getIdentity())
    );
    
    $gathering = $this->Gatherings->patchEntity($gathering, $data);
    $this->Gatherings->save($gathering);
}

Template Usage

Basic Display

<!-- Display datetime in user's timezone -->
<?= $this->Timezone->format($gathering->start_date) ?>
<!-- Output: March 15, 2025 9:00 AM -->

<!-- Display datetime in gathering's timezone (event location) -->
<?= $this->Timezone->format($gathering->start_date, null, false, null, $gathering) ?>
<!-- Output: March 15, 2025 9:00 AM (in event's timezone) -->

<!-- With timezone abbreviation -->
<?= $this->Timezone->format($gathering->start_date, null, true) ?>
<!-- Output: March 15, 2025 9:00 AM CDT -->

<!-- Custom format -->
<?= $this->Timezone->format($gathering->start_date, 'l, F j, Y \a\t g:i A') ?>
<!-- Output: Saturday, March 15, 2025 at 9:00 AM -->

Date and Time Only

<!-- Date only -->
<?= $this->Timezone->date($gathering->start_date) ?>
<!-- Output: March 15, 2025 -->

<!-- Time only -->
<?= $this->Timezone->time($gathering->start_date) ?>
<!-- Output: 9:00 AM -->

<!-- Custom date format -->
<?= $this->Timezone->date($gathering->start_date, 'M j, Y') ?>
<!-- Output: Mar 15, 2025 -->

Date Ranges

<!-- Basic range -->
<?= $this->Timezone->range($gathering->start_date, $gathering->end_date) ?>
<!-- Output: March 15, 2025 9:00 AM - March 17, 2025 5:00 PM -->

<!-- Smart range (same day shows time range only) -->
<?= $this->Timezone->smartRange($activity->start_datetime, $activity->end_datetime) ?>
<!-- Same day: March 15, 2025 9:00 AM - 5:00 PM -->
<!-- Different days: March 15, 2025 - March 17, 2025 -->

Form Inputs

<!-- datetime-local input with timezone conversion -->
<?= $this->Form->control('start_date', [
    'type' => 'datetime-local',
    'value' => $this->Timezone->forInput($gathering->start_date),
    'label' => 'Start Date/Time',
    'help' => 'Times are in your timezone: ' . $this->Timezone->getUserTimezone()
]) ?>

<!-- Show timezone notice to user -->
<?= $this->Timezone->notice('text-muted small mt-2') ?>
<!-- Output: <div class="text-muted small mt-2">
         <i class="bi bi-clock"></i> Times shown in America/Chicago (CDT)
     </div> -->

Timezone Selector

<!-- Add timezone selector to member profile form -->
<?= $this->Form->control('timezone', [
    'type' => 'select',
    'options' => $this->Timezone->getTimezoneOptions(true), // common timezones only
    'empty' => '(Use Application Default)',
    'label' => 'Your Timezone',
    'help' => 'Select your timezone for personalized date/time display'
]) ?>

<!-- All timezones (for advanced users) -->
<?= $this->Form->control('timezone', [
    'type' => 'select',
    'options' => $this->Timezone->getTimezoneOptions(false), // all timezones
    'empty' => '(Use Application Default)'
]) ?>

Stimulus Controller: Timezone Input

For automatic timezone handling in HTML forms, KMP provides the Timezone Input Controller (timezone-input-controller.js). This Stimulus controller eliminates the need to manually call timezone conversion methods.

Key Features

Quick Example

<form data-controller="timezone-input">
    <input type="datetime-local" 
           name="start_date"
           data-timezone-input-target="datetimeInput"
           data-utc-value="2025-03-15T14:30:00Z"
           class="form-control">
    
    <!-- Optional: Show timezone notice -->
    <small data-timezone-input-target="notice" class="text-muted"></small>
</form>

What happens:

  1. Page loads → UTC value converted to user’s local time (e.g., 9:30 AM for Chicago)
  2. User edits the time in their timezone
  3. Form submits → Local time converted back to UTC
  4. Server receives UTC value

When to Use

Full Documentation

See Timezone Input Controller for:


JavaScript Usage - KMP_Timezone Object

The KMP_Timezone object provides low-level client-side timezone utilities for display and form handling. It is automatically available globally after page load. For most form use cases, use the Timezone Input Controller instead.

Timezone Detection

// Detect user's browser timezone using Intl API
const userTz = KMP_Timezone.detectTimezone();
console.log(userTz); // "America/Chicago"

// Get timezone from element or detect
const tz = KMP_Timezone.getTimezone(element);

Formatting Dates and Times

The KMP_Timezone object provides methods to format UTC datetimes in the user’s local timezone:

// Format complete datetime
const utcString = "2025-03-15T14:30:00Z";
const displayed = KMP_Timezone.formatDateTime(utcString, "America/Chicago");
// Output: "3/15/2025, 9:30:00 AM"

// Format date only
const dateOnly = KMP_Timezone.formatDate(utcString, "America/Chicago");
// Output: "March 15, 2025"

// Format time only
const timeOnly = KMP_Timezone.formatTime(utcString, "America/Chicago");
// Output: "9:30 AM"

// Custom format with options
const custom = KMP_Timezone.formatDateTime(utcString, "America/Chicago", {
    dateStyle: 'full',
    timeStyle: 'short'
});
// Output: "Saturday, March 15, 2025 at 9:30 AM"

HTML5 Datetime Input Conversion

For use with datetime-local HTML inputs (which don’t include timezone info):

// Convert UTC to local time for input value (YYYY-MM-DDTHH:mm format)
const utcDate = "2025-03-15T14:30:00Z";
const inputValue = KMP_Timezone.toLocalInput(utcDate, "America/Chicago");
// Output: "2025-03-15T09:30"

// Convert user's local input back to UTC for submission
const localInput = "2025-03-15T09:30";
const utcValue = KMP_Timezone.toUTC(localInput, "America/Chicago");
// Output: "2025-03-15T14:30:00.000Z"

// Get timezone offset in minutes
const offsetMinutes = KMP_Timezone.getTimezoneOffset("America/Chicago");
// Output: -300 (UTC-5 hours) or -360 (UTC-6 hours during DST)

// Get timezone abbreviation
const abbr = KMP_Timezone.getAbbreviation("America/Chicago");
// Output: "CDT" or "CST" depending on DST

Automatic Form Initialization

The most convenient approach: use data attributes for automatic timezone handling:

<!-- HTML: Add data-utc-value to datetime-local inputs -->
<input type="datetime-local" 
       name="start_date"
       data-timezone="America/Chicago"
       data-utc-value="2025-03-15T14:30:00Z"
       class="form-control">

The KMP_Timezone object automatically initializes all such inputs on page load:

For dynamically-added content, manually initialize:

// Initialize inputs in a specific container (e.g., after modal opens)
const modal = document.getElementById('myModal');
KMP_Timezone.initializeDatetimeInputs(modal);

Form Submission Conversion

To automatically convert all local time inputs back to UTC before form submission:

// Add to form submit event
document.getElementById('myForm').addEventListener('submit', function(e) {
    e.preventDefault();
    
    const timezone = KMP_Timezone.detectTimezone();
    
    // Convert all datetime-local inputs to UTC
    // Creates hidden inputs with UTC values, disables originals
    KMP_Timezone.convertFormDatetimesToUTC(this, timezone);
    
    // Now submit the form
    this.submit();
});

This utility:

Configuration

Setting User Timezone

Users can set their timezone in their member profile:

// In MembersController::edit()
$member->timezone = 'America/New_York';
$this->Members->save($member);

Setting Application Default

Administrators can change the application default timezone through AppSettings:

  1. Navigate to App Settings management
  2. Find KMP.DefaultTimezone
  3. Set to desired IANA timezone identifier (e.g., America/New_York, UTC)

Or programmatically:

use App\KMP\StaticHelpers;

StaticHelpers::setAppSetting('KMP.DefaultTimezone', 'America/New_York');

Common Timezones

The system provides shortcuts for common US timezones:

Migration and Deployment

Running the Migration

cd /workspaces/KMP/app
bin/cake migrations migrate

This will add the timezone column to the members table.

Running the Seed

cd /workspaces/KMP/app
bin/cake migrations seed --seed DefaultTimezoneSeed

This will create the KMP.DefaultTimezone AppSetting.

Asset Compilation

After pulling timezone changes, recompile assets:

cd /workspaces/KMP/app
npm run dev    # Development
npm run prod   # Production

Implementation Checklist

When implementing timezone support in a new feature:

For Display

For Form Input

For JavaScript

Examples from KMP

Gatherings Calendar

// Controller
$gathering = $this->Gatherings->get($id);
$this->set('gathering', $gathering);

// Template
<h1><?= h($gathering->name) ?></h1>
<p>
    <strong>When:</strong> 
    <?= $this->Timezone->smartRange($gathering->start_date, $gathering->end_date) ?>
    <?= $this->Timezone->notice() ?>
</p>

Gathering Schedule Activities

// Controller - when saving
$data = $this->request->getData();
$data['start_datetime'] = TimezoneHelper::toUtc(
    $data['start_datetime'],
    TimezoneHelper::getUserTimezone($this->Authentication->getIdentity())
);

// Template - when editing
<?= $this->Form->control('start_datetime', [
    'type' => 'datetime-local',
    'value' => $this->Timezone->forInput($activity->start_datetime),
    'min' => $this->Timezone->forInput($gathering->start_date),
    'max' => $this->Timezone->forInput($gathering->end_date)
]) ?>

Member Profile

// Add timezone selector to member edit form
<?= $this->Form->control('timezone', [
    'type' => 'select',
    'options' => $this->Timezone->getTimezoneOptions(),
    'empty' => sprintf('(Use Application Default: %s)', 
        \App\KMP\TimezoneHelper::getAppTimezone() ?? 'UTC'),
    'label' => 'Your Timezone',
    'help' => 'Select your timezone to see dates and times in your local time'
]) ?>

Gathering Timezone (Event Location)

Gatherings can have their own timezone based on the event location. This ensures that event times are always displayed consistently in the event’s local time, regardless of where users are viewing from.

Setting Gathering Timezone

// In gathering add/edit form
<?= $this->Form->control('timezone', [
    'type' => 'select',
    'options' => $this->Timezone->getTimezoneOptions(),
    'empty' => '(Use event timezone from location)',
    '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.'
]) ?>

Displaying Event Times in Event Timezone

// In gathering view - display times in event's timezone
<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>

<!-- Note: Passing $gathering as the 5th parameter ensures times display in event timezone -->

Scheduled Activities with Gathering Timezone

// In schedule display - activities inherit 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; ?>

Controller: Converting with Gathering Timezone

// When saving gathering schedule, use gathering's timezone for input conversion
if ($this->request->is(['post', 'put'])) {
    $data = $this->request->getData();
    
    // Get the timezone to use (gathering's or user's)
    $timezone = TimezoneHelper::getGatheringTimezone($gathering, $this->Authentication->getIdentity());
    
    // Convert from gathering timezone to UTC
    $data['start_datetime'] = TimezoneHelper::toUtc($data['start_datetime'], $timezone);
    $data['end_datetime'] = TimezoneHelper::toUtc($data['end_datetime'], $timezone);
    
    $activity = $this->GatheringScheduledActivities->patchEntity($activity, $data);
    $this->GatheringScheduledActivities->save($activity);
}

Testing

Testing Checklist

When implementing or verifying timezone support:

Manual Testing

  1. Set user timezone: Edit member profile, set timezone to different value
  2. Verify display: Check that datetimes show in user’s timezone
  3. Verify input: Create/edit records with datetime fields
  4. Verify storage: Check database - should still be UTC
  5. Verify conversion: User in Chicago sees 9:00 AM, user in New York sees 10:00 AM (same UTC time)

Automated Testing

// Test timezone conversion
public function testTimezoneConversion()
{
    $member = $this->Members->newEntity(['timezone' => 'America/Chicago']);
    
    $utcDate = new DateTime('2025-03-15 14:30:00', new DateTimeZone('UTC'));
    $localDate = TimezoneHelper::toUserTimezone($utcDate, $member);
    
    $this->assertEquals('2025-03-15 09:30:00', $localDate->format('Y-m-d H:i:s'));
}

Testing

Testing Checklist

Form submissions saving wrong time

Invalid timezone error

JavaScript not converting times

Best Practices

  1. Always store in UTC - Never store local times in database
  2. Convert at boundaries - Convert to/from UTC only at display/input points
  3. Pass user context - Always pass current user to timezone helpers
  4. Provide feedback - Show user which timezone is being used
  5. Handle nulls - Allow null/empty timezones (use app default)
  6. Validate inputs - Validate timezone identifiers before saving
  7. Test edge cases - Test DST transitions, midnight times, date boundaries
  8. Document usage - Note timezone handling in code comments

Future Enhancements

Potential improvements for the timezone system:

API Reference

For detailed information about every KMP_Timezone JavaScript method, see:

10.3.1 KMP_Timezone Utility API Reference - Complete API documentation including:

Resources