/** * Date/Time Utility Functions * * TIMEZONE ARCHITECTURE: * - Database: All times stored in UTC * - API Communication: Always UTC (both directions) * - Frontend Display: Convert based on business_timezone setting * - If business_timezone is set: Display in that timezone * - If business_timezone is blank/null: Display in user's local timezone * * See CLAUDE.md for full architecture documentation. */ // ============================================================================ // SENDING TO API - Convert to UTC // ============================================================================ /** * Convert a local Date to UTC ISO string for API requests. * Use this when sending datetime values to the API. * * @example * const payload = { start_time: toUTC(selectedDate) }; * // Returns: "2024-12-08T19:00:00.000Z" */ export const toUTC = (date: Date): string => { return date.toISOString(); }; /** * Convert a date and time (in display timezone) to UTC ISO string. * Use when the user selects a time that should be interpreted in a specific timezone. * * @param date - The date portion * @param time - Time string "HH:MM" * @param timezone - IANA timezone the user is selecting in (business or local) * * @example * // User in Eastern selects 2pm, but display mode is "business" (Mountain) * // This means they selected 2pm Mountain time * toUTCFromTimezone(date, "14:00", "America/Denver") */ export const toUTCFromTimezone = ( date: Date, time: string, timezone: string ): string => { const [hours, minutes] = time.split(':').map(Number); const dateStr = formatLocalDate(date); // Create a date string that we'll parse in the target timezone const dateTimeStr = `${dateStr}T${time}:00`; // Use Intl to get the UTC offset for this timezone at this date/time const targetDate = new Date(dateTimeStr); const utcDate = convertTimezoneToUTC(targetDate, timezone); return utcDate.toISOString(); }; /** * Convert a Date object from a specific timezone to UTC. * The input Date's time values are interpreted as being in the given timezone. */ export const convertTimezoneToUTC = (date: Date, timezone: string): Date => { // Get the date/time components as they appear (treating as target timezone) const year = date.getFullYear(); const month = date.getMonth(); const day = date.getDate(); const hours = date.getHours(); const minutes = date.getMinutes(); const seconds = date.getSeconds(); // Create a formatter for the target timezone const formatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); // Find the UTC time that displays as our target time in the target timezone // We do this by creating a UTC date and adjusting based on offset const tempDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds)); // Get what this UTC time displays as in the target timezone const parts = formatter.formatToParts(tempDate); const getPart = (type: string) => parseInt(parts.find(p => p.type === type)?.value || '0'); const displayedHour = getPart('hour'); const displayedMinute = getPart('minute'); const displayedDay = getPart('day'); // Calculate the offset in minutes const displayedMinutes = displayedDay * 24 * 60 + displayedHour * 60 + displayedMinute; const targetMinutes = day * 24 * 60 + hours * 60 + minutes; const offsetMinutes = displayedMinutes - targetMinutes; // Adjust the UTC time by the offset return new Date(tempDate.getTime() - offsetMinutes * 60 * 1000); }; // ============================================================================ // RECEIVING FROM API - Convert from UTC for Display // ============================================================================ /** * Convert a UTC datetime string from API to a Date object in the display timezone. * * @param utcString - ISO string from API (always UTC) * @param businessTimezone - IANA timezone of the business (null = use local) */ export const fromUTC = ( utcString: string, businessTimezone?: string | null ): Date => { const utcDate = new Date(utcString); const targetTimezone = getDisplayTimezone(businessTimezone); return convertUTCToTimezone(utcDate, targetTimezone); }; /** * Convert a UTC Date to display in a specific timezone. * Returns a Date object with values adjusted for the target timezone. */ export const convertUTCToTimezone = (utcDate: Date, timezone: string): Date => { // Format the UTC date in the target timezone const formatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); const parts = formatter.formatToParts(utcDate); const getPart = (type: string) => parseInt(parts.find(p => p.type === type)?.value || '0'); // Create a new Date with the timezone-adjusted values // Note: This Date object's internal UTC value won't match, but the displayed values will be correct return new Date( getPart('year'), getPart('month') - 1, getPart('day'), getPart('hour'), getPart('minute'), getPart('second') ); }; /** * Get the timezone to use for display. * If businessTimezone is set, use it. Otherwise use user's local timezone. */ export const getDisplayTimezone = (businessTimezone?: string | null): string => { if (businessTimezone) { return businessTimezone; } // No business timezone set - use browser's local timezone return Intl.DateTimeFormat().resolvedOptions().timeZone; }; /** * Get the user's local timezone (browser timezone). */ export const getUserTimezone = (): string => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }; // ============================================================================ // FORMATTING FOR DISPLAY // ============================================================================ /** * Format a UTC datetime string for display, respecting timezone settings. * * @param utcString - ISO string from API * @param businessTimezone - IANA timezone of the business (null = use local) * @param options - Intl.DateTimeFormat options for customizing output * * @example * formatForDisplay("2024-12-08T19:00:00Z", "America/Denver") * // Returns: "Dec 8, 2024, 12:00 PM" (Mountain Time) * * formatForDisplay("2024-12-08T19:00:00Z", null) * // Returns time in user's local timezone */ export const formatForDisplay = ( utcString: string, businessTimezone?: string | null, options?: Intl.DateTimeFormatOptions ): string => { const utcDate = new Date(utcString); const timezone = getDisplayTimezone(businessTimezone); const defaultOptions: Intl.DateTimeFormatOptions = { timeZone: timezone, year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true, ...options, }; return utcDate.toLocaleString('en-US', defaultOptions); }; /** * Format just the time portion for display. * * @example * formatTimeForDisplay("2024-12-08T19:00:00Z", "America/Denver") * // Returns: "12:00 PM" */ export const formatTimeForDisplay = ( utcString: string, businessTimezone?: string | null ): string => { const utcDate = new Date(utcString); const timezone = getDisplayTimezone(businessTimezone); return utcDate.toLocaleString('en-US', { timeZone: timezone, hour: 'numeric', minute: '2-digit', hour12: true, }); }; /** * Format just the date portion for display. * * @example * formatDateForDisplay("2024-12-08T19:00:00Z", "America/Denver") * // Returns: "Dec 8, 2024" */ export const formatDateForDisplay = ( utcString: string, businessTimezone?: string | null, options?: Intl.DateTimeFormatOptions ): string => { const utcDate = new Date(utcString); const timezone = getDisplayTimezone(businessTimezone); const defaultOptions: Intl.DateTimeFormatOptions = { timeZone: timezone, year: 'numeric', month: 'short', day: 'numeric', ...options, }; return utcDate.toLocaleDateString('en-US', defaultOptions); }; /** * Format a datetime for datetime-local input, in the display timezone. * Returns: "YYYY-MM-DDTHH:MM" */ export const formatForDateTimeInput = ( utcString: string, businessTimezone?: string | null ): string => { const displayDate = fromUTC(utcString, businessTimezone); return formatLocalDateTime(displayDate); }; // ============================================================================ // DATE-ONLY HELPERS (for fields like time block start_date/end_date) // ============================================================================ /** * Format a Date object as YYYY-MM-DD string. * Uses the Date's local values (not UTC). * * For date-only fields, use this when you have a Date object * representing a calendar date selection. */ export const formatLocalDate = (date: Date): string => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; /** * Parse a YYYY-MM-DD string as a local date (at midnight local time). */ export const parseLocalDate = (dateString: string): Date => { const [year, month, day] = dateString.split('-').map(Number); return new Date(year, month - 1, day); }; /** * Format a Date as YYYY-MM-DDTHH:MM for datetime-local inputs. */ export const formatLocalDateTime = (date: Date): string => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day}T${hours}:${minutes}`; }; /** * Get today's date as YYYY-MM-DD in a specific timezone. * Useful for determining "today" in the business timezone. */ export const getTodayInTimezone = (timezone: string): string => { const now = new Date(); const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', }); return formatter.format(now); // Returns YYYY-MM-DD format }; /** * Check if a date string (YYYY-MM-DD) is today in the given timezone. */ export const isToday = (dateString: string, timezone: string): boolean => { return dateString === getTodayInTimezone(timezone); }; // ============================================================================ // UTILITY HELPERS // ============================================================================ /** * Check if two dates are the same day (ignoring time). */ export const isSameDay = (date1: Date, date2: Date): boolean => { return ( date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() ); }; /** * Get the start of a day (midnight local time). */ export const startOfDay = (date: Date): Date => { const result = new Date(date); result.setHours(0, 0, 0, 0); return result; }; /** * Get the end of a day (23:59:59.999 local time). */ export const endOfDay = (date: Date): Date => { const result = new Date(date); result.setHours(23, 59, 59, 999); return result; }; /** * Get timezone abbreviation for display (e.g., "MST", "EST"). */ export const getTimezoneAbbreviation = ( timezone: string, date: Date = new Date() ): string => { const formatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, timeZoneName: 'short', }); const parts = formatter.formatToParts(date); return parts.find(p => p.type === 'timeZoneName')?.value || timezone; }; /** * Format timezone for display (e.g., "Mountain Time (MST)"). */ export const formatTimezoneDisplay = (timezone: string): string => { const abbr = getTimezoneAbbreviation(timezone); const cityName = timezone.split('/').pop()?.replace(/_/g, ' ') || timezone; return `${cityName} (${abbr})`; };