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:
- Run Database Migration:
cd app && bin/cake migrations migrate - Compile Assets:
npm run devornpm run prod - 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
- All dates and times are stored in UTC in the database
- UTC storage prevents timezone-related bugs and ensures consistent data
- Timezone conversion happens only at display/input boundaries
Display Priority
When displaying dates/times, the system resolves timezone in this order:
- Gathering’s Timezone - Event location timezone (
gatherings.timezone) - used when displaying event-specific dates - User’s Timezone - Individual member’s timezone preference (
members.timezone) - Application Default - System-wide default timezone (
KMP.DefaultTimezoneAppSetting) - 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
TimezoneHelper(src/KMP/TimezoneHelper.php) - Core timezone conversion logicTimezoneHelper(View) (src/View/Helper/TimezoneHelper.php) - Template-friendly wrapper- Migrations:
20251105000000_AddTimezoneToMembers.php- Addstimezonecolumn tomemberstable20251105000001_AddTimezoneToGatherings.php- Addstimezonecolumn togatheringstable
- Application Bootstrap - Initializes
KMP.DefaultTimezoneAppSetting (default: America/Chicago) insrc/Application.php - Entities:
src/Model/Entity/Member.php- Added timezone to accessible fieldssrc/Model/Entity/Gathering.php- Added timezone to accessible fields
- Tables:
src/Model/Table/MembersTable.php- Added timezone validationsrc/Model/Table/GatheringsTable.php- Added timezone validation
- View Setup:
src/View/AppView.php- Registered TimezoneHelper
- Examples:
templates/element/timezone_examples.php- Usage examples element
JavaScript Components
timezone-utils.js(assets/js/timezone-utils.js) - Client-side timezone handlingtimezone-input-controller.js(assets/js/controllers/timezone-input-controller.js) - Stimulus controller for automatic form handling- Global
KMP_Timezoneobject - Available throughout the application - Registration: Imported in
assets/js/index.js
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
- Automatic UTC to Local Conversion: Converts server-side UTC values to user’s local timezone on page load
- Automatic Local to UTC Conversion: Converts user input back to UTC before form submission
- Timezone Notice: Displays current timezone to the user (optional)
- Zero Setup: Works automatically with just data attributes
- Form Reset Support: Restores original times when form is reset
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:
- Page loads → UTC value converted to user’s local time (e.g., 9:30 AM for Chicago)
- User edits the time in their timezone
- Form submits → Local time converted back to UTC
- Server receives UTC value
When to Use
- Simple forms with datetime inputs → Use the Timezone Input Controller
- Complex JavaScript interactions → Use the
KMP_Timezoneutility directly - Display-only datetime values → Use PHP
TimezoneHelpermethods
Full Documentation
See Timezone Input Controller for:
- Complete API reference
- All configuration options
- Advanced patterns and examples
- Troubleshooting guide
- Integration with CakePHP forms
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:
- Detects UTC value from
data-utc-valueattribute - Converts to local time for the specified timezone
- Populates the input with local time
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:
- Scans for all
datetime-localinputs in the form - Converts each to UTC using the specified timezone
- Creates hidden inputs with UTC values for submission
- Disables original inputs to prevent duplicate submission
- Preserves conversion errors for debugging
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:
- Navigate to App Settings management
- Find
KMP.DefaultTimezone - 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:
America/Chicago- Central Time (US)America/New_York- Eastern Time (US)America/Denver- Mountain Time (US)America/Los_Angeles- Pacific Time (US)America/Phoenix- Arizona (MST - No DST)America/Anchorage- Alaska Time (US)Pacific/Honolulu- Hawaii Time (US)UTC- Coordinated Universal Time
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
- Use
TimezoneHelper::toUserTimezone()or$this->Timezone->format()for all datetime display - Consider using
$this->Timezone->smartRange()for date ranges - Add timezone notice with
$this->Timezone->notice()where helpful - Pass current user/identity to timezone helpers
For Form Input
- Use
$this->Timezone->forInput()to populate datetime-local inputs - Add timezone context help text to form fields
- Convert user input back to UTC using
TimezoneHelper::toUtc()before saving - Validate timezone values if allowing user to set timezone
For JavaScript
- Use
data-utc-valueattribute for inputs that should auto-convert - Call
KMP_Timezone.initializeDatetimeInputs()after dynamic content loads - Use
KMP_Timezone.formatDateTime()for client-side datetime display - Attach
KMP_Timezone.convertFormDatetimesToUTC()to form submit events
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:
- Migration runs successfully
- Seed creates AppSetting for
KMP.DefaultTimezone - User can set timezone in profile
- Datetimes display in user’s timezone
- Form inputs work correctly (datetime-local)
- Saved data is in UTC in database
- JavaScript utilities work on forms
- Timezone notice appears where appropriate
- Gathering timezone displays event times correctly
- Smart range formatting works for same-day and multi-day events
Manual Testing
- Set user timezone: Edit member profile, set timezone to different value
- Verify display: Check that datetimes show in user’s timezone
- Verify input: Create/edit records with datetime fields
- Verify storage: Check database - should still be UTC
- 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
- Check that
timezonefield is populated (user timezone) - Check that
KMP.DefaultTimezoneAppSetting exists - Ensure using
$this->Timezone->format()orTimezoneHelper::toUserTimezone() - Clear cache:
bin/cake cache clear_all
Form submissions saving wrong time
- Ensure converting input to UTC before saving with
TimezoneHelper::toUtc() - Check that timezone is being passed to conversion function
- Verify datetime-local input format is correct (YYYY-MM-DDTHH:mm)
Invalid timezone error
- Ensure timezone value is valid IANA identifier
- Use
TimezoneHelper::isValidTimezone()to validate - Check against
TimezoneHelper::getCommonTimezones()orgetTimezoneList()
JavaScript not converting times
- Check browser console for errors
- Ensure
timezone-utils.jsis loaded - Verify
KMP_Timezoneobject is available globally - Check that
data-utc-valueattribute is set correctly
Best Practices
- Always store in UTC - Never store local times in database
- Convert at boundaries - Convert to/from UTC only at display/input points
- Pass user context - Always pass current user to timezone helpers
- Provide feedback - Show user which timezone is being used
- Handle nulls - Allow null/empty timezones (use app default)
- Validate inputs - Validate timezone identifiers before saving
- Test edge cases - Test DST transitions, midnight times, date boundaries
- Document usage - Note timezone handling in code comments
Future Enhancements
Potential improvements for the timezone system:
- Session caching: Cache user timezone in session to reduce database queries
- Auto-detect on signup: Offer to use browser-detected timezone for new users
- Timezone by branch: Allow branches to have default timezones
- Multiple timezone display: Show event times in multiple timezones
- Timezone warnings: Warn when event timezone differs from user timezone
- Historical timezone: Handle timezone changes for past events
API Reference
For detailed information about every KMP_Timezone JavaScript method, see:
10.3.1 KMP_Timezone Utility API Reference - Complete API documentation including:
- Timezone detection methods
- All formatting functions (datetime, date, time)
- Input conversion methods (local ↔ UTC)
- Initialization and form handling
- Error handling and browser compatibility
- Common usage patterns and integration examples
Resources
- PHP Timezones - List of all IANA timezone identifiers
- DateTimeZone - PHP DateTimeZone class
- Intl.DateTimeFormat - JavaScript datetime formatting
- CakePHP DateTime - CakePHP DateTime utilities