feat: Add timezone architecture for consistent date/time handling
- Create dateUtils.ts with helpers for UTC conversion and timezone display - Add TimezoneSerializerMixin to include business_timezone in API responses - Update GeneralSettings timezone dropdown with IANA identifiers - Apply timezone mixin to Event, TimeBlock, and field mobile serializers - Document timezone architecture in CLAUDE.md All times stored in UTC, converted for display based on business timezone. If business_timezone is null, uses user's local timezone. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import Portal from '../components/Portal';
|
||||
import EventAutomations from '../components/EventAutomations';
|
||||
import TimeBlockCalendarOverlay from '../components/time-blocks/TimeBlockCalendarOverlay';
|
||||
import { getOverQuotaResourceIds } from '../utils/quotaUtils';
|
||||
import { formatLocalDate } from '../utils/dateUtils';
|
||||
|
||||
// Time settings
|
||||
const START_HOUR = 0; // Midnight
|
||||
@@ -87,8 +88,8 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
|
||||
// Fetch blocked dates for the calendar overlay
|
||||
const blockedDatesParams = useMemo(() => ({
|
||||
start_date: dateRange.startDate.toISOString().split('T')[0],
|
||||
end_date: dateRange.endDate.toISOString().split('T')[0],
|
||||
start_date: formatLocalDate(dateRange.startDate),
|
||||
end_date: formatLocalDate(dateRange.endDate),
|
||||
include_business: true,
|
||||
}), [dateRange]);
|
||||
const { data: blockedDates = [] } = useBlockedDates(blockedDatesParams);
|
||||
|
||||
@@ -33,28 +33,134 @@ const GeneralSettings: React.FC = () => {
|
||||
setFormState(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
// Common timezones grouped by region
|
||||
const commonTimezones = [
|
||||
{ value: 'America/New_York', label: 'Eastern Time (New York)' },
|
||||
{ value: 'America/Chicago', label: 'Central Time (Chicago)' },
|
||||
{ value: 'America/Denver', label: 'Mountain Time (Denver)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific Time (Los Angeles)' },
|
||||
{ value: 'America/Anchorage', label: 'Alaska Time' },
|
||||
{ value: 'Pacific/Honolulu', label: 'Hawaii Time' },
|
||||
{ value: 'America/Phoenix', label: 'Arizona (no DST)' },
|
||||
{ value: 'America/Toronto', label: 'Eastern Time (Toronto)' },
|
||||
{ value: 'America/Vancouver', label: 'Pacific Time (Vancouver)' },
|
||||
{ value: 'Europe/London', label: 'London (GMT/BST)' },
|
||||
{ value: 'Europe/Paris', label: 'Central European Time' },
|
||||
{ value: 'Europe/Berlin', label: 'Berlin' },
|
||||
{ value: 'Asia/Tokyo', label: 'Japan Time' },
|
||||
{ value: 'Asia/Shanghai', label: 'China Time' },
|
||||
{ value: 'Asia/Singapore', label: 'Singapore Time' },
|
||||
{ value: 'Asia/Dubai', label: 'Dubai (GST)' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney (AEST)' },
|
||||
{ value: 'Australia/Melbourne', label: 'Melbourne (AEST)' },
|
||||
{ value: 'Pacific/Auckland', label: 'New Zealand Time' },
|
||||
{ value: 'UTC', label: 'UTC' },
|
||||
// IANA timezones grouped by region
|
||||
const timezoneGroups = [
|
||||
{
|
||||
label: 'United States',
|
||||
timezones: [
|
||||
'America/New_York',
|
||||
'America/Chicago',
|
||||
'America/Denver',
|
||||
'America/Los_Angeles',
|
||||
'America/Anchorage',
|
||||
'Pacific/Honolulu',
|
||||
'America/Phoenix',
|
||||
'America/Detroit',
|
||||
'America/Indiana/Indianapolis',
|
||||
'America/Boise',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Canada',
|
||||
timezones: [
|
||||
'America/Toronto',
|
||||
'America/Vancouver',
|
||||
'America/Edmonton',
|
||||
'America/Winnipeg',
|
||||
'America/Halifax',
|
||||
'America/St_Johns',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Mexico & Central America',
|
||||
timezones: [
|
||||
'America/Mexico_City',
|
||||
'America/Tijuana',
|
||||
'America/Cancun',
|
||||
'America/Guatemala',
|
||||
'America/Costa_Rica',
|
||||
'America/Panama',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'South America',
|
||||
timezones: [
|
||||
'America/Sao_Paulo',
|
||||
'America/Argentina/Buenos_Aires',
|
||||
'America/Santiago',
|
||||
'America/Bogota',
|
||||
'America/Lima',
|
||||
'America/Caracas',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Europe',
|
||||
timezones: [
|
||||
'Europe/London',
|
||||
'Europe/Dublin',
|
||||
'Europe/Paris',
|
||||
'Europe/Berlin',
|
||||
'Europe/Madrid',
|
||||
'Europe/Rome',
|
||||
'Europe/Amsterdam',
|
||||
'Europe/Brussels',
|
||||
'Europe/Vienna',
|
||||
'Europe/Zurich',
|
||||
'Europe/Stockholm',
|
||||
'Europe/Oslo',
|
||||
'Europe/Copenhagen',
|
||||
'Europe/Helsinki',
|
||||
'Europe/Athens',
|
||||
'Europe/Bucharest',
|
||||
'Europe/Warsaw',
|
||||
'Europe/Prague',
|
||||
'Europe/Moscow',
|
||||
'Europe/Istanbul',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Asia',
|
||||
timezones: [
|
||||
'Asia/Tokyo',
|
||||
'Asia/Seoul',
|
||||
'Asia/Shanghai',
|
||||
'Asia/Hong_Kong',
|
||||
'Asia/Taipei',
|
||||
'Asia/Singapore',
|
||||
'Asia/Kuala_Lumpur',
|
||||
'Asia/Bangkok',
|
||||
'Asia/Ho_Chi_Minh',
|
||||
'Asia/Jakarta',
|
||||
'Asia/Manila',
|
||||
'Asia/Kolkata',
|
||||
'Asia/Mumbai',
|
||||
'Asia/Dubai',
|
||||
'Asia/Riyadh',
|
||||
'Asia/Jerusalem',
|
||||
'Asia/Karachi',
|
||||
'Asia/Dhaka',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Australia & Pacific',
|
||||
timezones: [
|
||||
'Australia/Sydney',
|
||||
'Australia/Melbourne',
|
||||
'Australia/Brisbane',
|
||||
'Australia/Perth',
|
||||
'Australia/Adelaide',
|
||||
'Australia/Darwin',
|
||||
'Pacific/Auckland',
|
||||
'Pacific/Fiji',
|
||||
'Pacific/Guam',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Africa',
|
||||
timezones: [
|
||||
'Africa/Johannesburg',
|
||||
'Africa/Cairo',
|
||||
'Africa/Lagos',
|
||||
'Africa/Nairobi',
|
||||
'Africa/Casablanca',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Other',
|
||||
timezones: [
|
||||
'UTC',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -146,10 +252,14 @@ const GeneralSettings: React.FC = () => {
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
>
|
||||
{commonTimezones.map(tz => (
|
||||
<option key={tz.value} value={tz.value}>
|
||||
{tz.label}
|
||||
</option>
|
||||
{timezoneGroups.map(group => (
|
||||
<optgroup key={group.label} label={group.label}>
|
||||
{group.timezones.map(tz => (
|
||||
<option key={tz} value={tz}>
|
||||
{tz}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
|
||||
Reference in New Issue
Block a user