import { useCallback, useEffect } from 'react'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import relativeTime from 'dayjs/plugin/relativeTime'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import useSettingsStore from '../store/settings'; import useLocalStorage from '../hooks/useLocalStorage'; dayjs.extend(duration); dayjs.extend(relativeTime); dayjs.extend(utc); dayjs.extend(timezone); export const convertToMs = (dateTime) => dayjs(dateTime).valueOf(); export const convertToSec = (dateTime) => dayjs(dateTime).unix(); export const initializeTime = (dateTime) => dayjs(dateTime); export const startOfDay = (dateTime) => dayjs(dateTime).startOf('day'); export const isBefore = (date1, date2) => dayjs(date1).isBefore(date2); export const isAfter = (date1, date2) => dayjs(date1).isAfter(date2); export const isSame = (date1, date2, unit = 'day') => dayjs(date1).isSame(date2, unit); export const add = (dateTime, value, unit) => dayjs(dateTime).add(value, unit); export const subtract = (dateTime, value, unit) => dayjs(dateTime).subtract(value, unit); export const diff = (date1, date2, unit = 'millisecond') => dayjs(date1).diff(date2, unit); export const format = (dateTime, formatStr) => dayjs(dateTime).format(formatStr); export const getNow = () => dayjs(); export const toFriendlyDuration = (dateTime, unit) => dayjs.duration(dateTime, unit).humanize(); export const formatExactDuration = (seconds) => { if (seconds < 60) return `${seconds.toFixed(1)} seconds`; if (seconds < 3600) { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m} minute${m !== 1 ? 's' : ''}, ${s} second${s !== 1 ? 's' : ''}`; } if (seconds < 86400) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); return `${h} hour${h !== 1 ? 's' : ''}, ${m} minute${m !== 1 ? 's' : ''}`; } const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); return `${d} day${d !== 1 ? 's' : ''}, ${h} hour${h !== 1 ? 's' : ''}`; }; export const fromNow = (dateTime) => dayjs(dateTime).fromNow(); export const setTz = (dateTime, timeZone) => dayjs(dateTime).tz(timeZone); export const setMonth = (dateTime, value) => dayjs(dateTime).month(value); export const setYear = (dateTime, value) => dayjs(dateTime).year(value); export const setDay = (dateTime, value) => dayjs(dateTime).date(value); export const setHour = (dateTime, value) => dayjs(dateTime).hour(value); export const setMinute = (dateTime, value) => dayjs(dateTime).minute(value); export const setSecond = (dateTime, value) => dayjs(dateTime).second(value); export const getMonth = (dateTime) => dayjs(dateTime).month(); export const getYear = (dateTime) => dayjs(dateTime).year(); export const getDay = (dateTime) => dayjs(dateTime).date(); export const getHour = (dateTime) => dayjs(dateTime).hour(); export const getMinute = (dateTime) => dayjs(dateTime).minute(); export const getSecond = (dateTime) => dayjs(dateTime).second(); export const getNowMs = () => Date.now(); export const roundToNearest = (dateTime, minutes) => { const current = initializeTime(dateTime); const minute = current.minute(); const snappedMinute = Math.round(minute / minutes) * minutes; return snappedMinute === 60 ? current.add(1, 'hour').minute(0) : current.minute(snappedMinute); }; export const useUserTimeZone = () => { const settings = useSettingsStore((s) => s.settings); const [timeZone, setTimeZone] = useLocalStorage( 'time-zone', dayjs.tz?.guess ? dayjs.tz.guess() : Intl.DateTimeFormat().resolvedOptions().timeZone ); useEffect(() => { const tz = settings?.['system_settings']?.value?.time_zone; if (tz && tz !== timeZone) { setTimeZone(tz); } }, [settings, timeZone, setTimeZone]); return timeZone; }; export const useTimeHelpers = () => { const timeZone = useUserTimeZone(); const toUserTime = useCallback( (value) => { if (!value) return dayjs(null); try { return initializeTime(value).tz(timeZone); } catch (error) { return initializeTime(value); } }, [timeZone] ); const userNow = useCallback(() => getNow().tz(timeZone), [timeZone]); return { timeZone, toUserTime, userNow }; }; export const RECURRING_DAY_OPTIONS = [ { value: 6, label: 'Sun' }, { value: 0, label: 'Mon' }, { value: 1, label: 'Tue' }, { value: 2, label: 'Wed' }, { value: 3, label: 'Thu' }, { value: 4, label: 'Fri' }, { value: 5, label: 'Sat' }, ]; export const useDateTimeFormat = () => { const [timeFormatSetting] = useLocalStorage('time-format', '12h'); const [dateFormatSetting] = useLocalStorage('date-format', 'mdy'); // Use user preference for time format const timeFormat = timeFormatSetting === '12h' ? 'h:mma' : 'HH:mm'; const dateFormat = dateFormatSetting === 'mdy' ? 'MMM D' : 'D MMM'; // Full format strings for detailed date-time displays const fullDateFormat = dateFormatSetting === 'mdy' ? 'MM/DD/YYYY' : 'DD/MM/YYYY'; const fullTimeFormat = timeFormatSetting === '12h' ? 'h:mm:ss A' : 'HH:mm:ss'; const fullDateTimeFormat = `${fullDateFormat}, ${fullTimeFormat}`; return { timeFormat, dateFormat, fullDateFormat, fullTimeFormat, fullDateTimeFormat, // Also return raw settings for cases that need them timeFormatSetting, dateFormatSetting, }; }; export const toTimeString = (value) => { if (!value) return '00:00'; if (typeof value === 'string') { const parsed = dayjs(value, ['HH:mm', 'HH:mm:ss', 'h:mm A'], true); if (parsed.isValid()) return parsed.format('HH:mm'); return value; } const parsed = initializeTime(value); return parsed.isValid() ? parsed.format('HH:mm') : '00:00'; }; export const parseDate = (value) => { if (!value) return null; const parsed = dayjs(value, ['YYYY-MM-DD', dayjs.ISO_8601], true); return parsed.isValid() ? parsed.toDate() : null; }; const TIMEZONE_FALLBACKS = [ 'UTC', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'America/Phoenix', 'America/Anchorage', 'Pacific/Honolulu', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Madrid', 'Europe/Warsaw', 'Europe/Moscow', 'Asia/Dubai', 'Asia/Kolkata', 'Asia/Shanghai', 'Asia/Tokyo', 'Asia/Seoul', 'Australia/Sydney', ]; const getSupportedTimeZones = () => { try { if (typeof Intl.supportedValuesOf === 'function') { return Intl.supportedValuesOf('timeZone'); } } catch (error) { console.warn('Unable to enumerate supported time zones:', error); } return TIMEZONE_FALLBACKS; }; const getTimeZoneOffsetMinutes = (date, timeZone) => { try { const dtf = new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', }); const parts = dtf.formatToParts(date).reduce((acc, part) => { if (part.type !== 'literal') acc[part.type] = part.value; return acc; }, {}); const asUTC = Date.UTC( Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour), Number(parts.minute), Number(parts.second) ); return (asUTC - date.getTime()) / 60000; } catch (error) { console.warn(`Failed to compute offset for ${timeZone}:`, error); return 0; } }; const formatOffset = (minutes) => { const rounded = Math.round(minutes); const sign = rounded < 0 ? '-' : '+'; const absolute = Math.abs(rounded); const hours = String(Math.floor(absolute / 60)).padStart(2, '0'); const mins = String(absolute % 60).padStart(2, '0'); return `UTC${sign}${hours}:${mins}`; }; export const buildTimeZoneOptions = (preferredZone) => { const zones = getSupportedTimeZones(); const referenceYear = new Date().getUTCFullYear(); const janDate = new Date(Date.UTC(referenceYear, 0, 1, 12, 0, 0)); const julDate = new Date(Date.UTC(referenceYear, 6, 1, 12, 0, 0)); const options = zones .map((zone) => { const janOffset = getTimeZoneOffsetMinutes(janDate, zone); const julOffset = getTimeZoneOffsetMinutes(julDate, zone); const currentOffset = getTimeZoneOffsetMinutes(new Date(), zone); const minOffset = Math.min(janOffset, julOffset); const maxOffset = Math.max(janOffset, julOffset); const usesDst = minOffset !== maxOffset; const labelParts = [`now ${formatOffset(currentOffset)}`]; if (usesDst) { labelParts.push( `DST range ${formatOffset(minOffset)} to ${formatOffset(maxOffset)}` ); } return { value: zone, label: `${zone} (${labelParts.join(' | ')})`, numericOffset: minOffset, }; }) .sort((a, b) => { if (a.numericOffset !== b.numericOffset) { return a.numericOffset - b.numericOffset; } return a.value.localeCompare(b.value); }); if ( preferredZone && !options.some((option) => option.value === preferredZone) ) { const currentOffset = getTimeZoneOffsetMinutes(new Date(), preferredZone); options.push({ value: preferredZone, label: `${preferredZone} (now ${formatOffset(currentOffset)})`, numericOffset: currentOffset, }); options.sort((a, b) => { if (a.numericOffset !== b.numericOffset) { return a.numericOffset - b.numericOffset; } return a.value.localeCompare(b.value); }); } return options; }; export const getDefaultTimeZone = () => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } catch (error) { return 'UTC'; } }; export const MONTH_NAMES = [ 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december', ]; export const MONTH_ABBR = [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec', ];