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:
poduck
2025-12-07 19:39:36 -05:00
parent 897a336d0b
commit f4332153f4
10 changed files with 783 additions and 43 deletions

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import axios from '../api/client';
import { X, Calendar, Clock, RotateCw, Zap } from 'lucide-react';
import { formatLocalDate } from '../utils/dateUtils';
interface ScheduledTask {
id: string;
@@ -79,7 +80,7 @@ const EditTaskModal: React.FC<EditTaskModalProps> = ({ task, isOpen, onClose, on
setScheduleMode('onetime');
if (task.run_at) {
const date = new Date(task.run_at);
setRunAtDate(date.toISOString().split('T')[0]);
setRunAtDate(formatLocalDate(date));
setRunAtTime(date.toTimeString().slice(0, 5));
}
} else if (task.schedule_type === 'INTERVAL') {

View File

@@ -41,6 +41,7 @@ import {
Resource,
TimeBlockListItem,
} from '../../types';
import { formatLocalDate } from '../../utils/dateUtils';
// Preset block types
const PRESETS = [
@@ -418,8 +419,8 @@ const TimeBlockCreatorModal: React.FC<TimeBlockCreatorModalProps> = ({
if (recurrenceType === 'NONE') {
if (selectedDates.length > 0) {
const sorted = [...selectedDates].sort((a, b) => a.getTime() - b.getTime());
data.start_date = sorted[0].toISOString().split('T')[0];
data.end_date = sorted[sorted.length - 1].toISOString().split('T')[0];
data.start_date = formatLocalDate(sorted[0]);
data.end_date = formatLocalDate(sorted[sorted.length - 1]);
}
} else if (recurrenceType === 'WEEKLY') {
data.recurrence_pattern = { days_of_week: daysOfWeek };

View File

@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
import { ChevronLeft, ChevronRight, CalendarDays, X } from 'lucide-react';
import { BlockedDate, TimeBlockListItem } from '../../types';
import { useBlockedDates, useTimeBlock } from '../../hooks/useTimeBlocks';
import { formatLocalDate } from '../../utils/dateUtils';
interface YearlyBlockCalendarProps {
resourceId?: string;
@@ -134,7 +135,7 @@ const YearlyBlockCalendar: React.FC<YearlyBlockCalendarProps> = ({
return <div key={`empty-${i}`} className="aspect-square" />;
}
const dateKey = day.toISOString().split('T')[0];
const dateKey = formatLocalDate(day);
const blocks = blockedDateMap.get(dateKey) || [];
const hasBlocks = blocks.length > 0;
const isToday = new Date().toDateString() === day.toDateString();