| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- /**
- * Date utilities for handling UTC timestamps from the backend.
- *
- * The backend stores all timestamps in UTC without timezone indicators.
- * These utilities ensure dates are properly interpreted as UTC and
- * displayed in the user's local timezone.
- */
- export type TimeFormat = 'system' | '12h' | '24h';
- export type DateFormat = 'system' | 'us' | 'eu' | 'iso';
- /**
- * Get the date input placeholder based on format setting.
- */
- export function getDatePlaceholder(dateFormat: DateFormat = 'system'): string {
- switch (dateFormat) {
- case 'us':
- return 'MM/DD/YYYY';
- case 'eu':
- return 'DD/MM/YYYY';
- case 'iso':
- return 'YYYY-MM-DD';
- case 'system':
- default: {
- // Try to detect system format
- const testDate = new Date(2000, 11, 31); // Dec 31, 2000
- const formatted = testDate.toLocaleDateString();
- if (formatted.startsWith('12')) return 'MM/DD/YYYY';
- if (formatted.startsWith('31')) return 'DD/MM/YYYY';
- return 'YYYY-MM-DD';
- }
- }
- }
- /**
- * Get the time input placeholder based on format setting.
- */
- export function getTimePlaceholder(timeFormat: TimeFormat = 'system'): string {
- switch (timeFormat) {
- case '12h':
- return 'HH:MM AM/PM';
- case '24h':
- return 'HH:MM';
- case 'system':
- default: {
- // Try to detect system format
- const testDate = new Date(2000, 0, 1, 14, 30);
- const formatted = testDate.toLocaleTimeString();
- if (formatted.includes('PM') || formatted.includes('AM')) return 'HH:MM AM/PM';
- return 'HH:MM';
- }
- }
- }
- /**
- * Format a Date object to a date string based on format setting.
- */
- export function formatDateInput(date: Date, dateFormat: DateFormat = 'system'): string {
- const day = String(date.getDate()).padStart(2, '0');
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const year = date.getFullYear();
- switch (dateFormat) {
- case 'us':
- return `${month}/${day}/${year}`;
- case 'eu':
- return `${day}/${month}/${year}`;
- case 'iso':
- return `${year}-${month}-${day}`;
- case 'system':
- default:
- return date.toLocaleDateString();
- }
- }
- /**
- * Format a Date object to a time string based on format setting.
- */
- export function formatTimeInput(date: Date, timeFormat: TimeFormat = 'system'): string {
- const hours24 = date.getHours();
- const minutes = String(date.getMinutes()).padStart(2, '0');
- switch (timeFormat) {
- case '12h': {
- const hours12 = hours24 % 12 || 12;
- const ampm = hours24 < 12 ? 'AM' : 'PM';
- return `${hours12}:${minutes} ${ampm}`;
- }
- case '24h':
- return `${String(hours24).padStart(2, '0')}:${minutes}`;
- case 'system':
- default:
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
- }
- }
- /**
- * Split a date string by common separators (/, ., -).
- */
- function splitDateParts(value: string): string[] | null {
- // Try common separators: /, ., -
- for (const sep of ['/', '.', '-']) {
- const parts = value.split(sep);
- if (parts.length === 3) return parts;
- }
- return null;
- }
- /**
- * Parse a date string based on format setting.
- * Returns null if parsing fails.
- * Supports common separators: / . -
- */
- export function parseDateInput(value: string, dateFormat: DateFormat = 'system'): Date | null {
- if (!value) return null;
- let day: number, month: number, year: number;
- try {
- switch (dateFormat) {
- case 'us': {
- // MM/DD/YYYY (also accepts . and - separators)
- const parts = splitDateParts(value);
- if (!parts) return null;
- month = parseInt(parts[0], 10);
- day = parseInt(parts[1], 10);
- year = parseInt(parts[2], 10);
- break;
- }
- case 'eu': {
- // DD/MM/YYYY (also accepts . and - separators)
- const parts = splitDateParts(value);
- if (!parts) return null;
- day = parseInt(parts[0], 10);
- month = parseInt(parts[1], 10);
- year = parseInt(parts[2], 10);
- break;
- }
- case 'iso': {
- // YYYY-MM-DD (also accepts . and / separators)
- const parts = splitDateParts(value);
- if (!parts) return null;
- year = parseInt(parts[0], 10);
- month = parseInt(parts[1], 10);
- day = parseInt(parts[2], 10);
- break;
- }
- case 'system':
- default: {
- // Detect system format and parse accordingly
- const testDate = new Date(2000, 11, 31); // Dec 31, 2000
- const formatted = testDate.toLocaleDateString();
- const parts = splitDateParts(value);
- if (parts) {
- // Detect format from system locale
- if (formatted.startsWith('12')) {
- // US format: MM/DD/YYYY
- month = parseInt(parts[0], 10);
- day = parseInt(parts[1], 10);
- year = parseInt(parts[2], 10);
- } else if (formatted.startsWith('31')) {
- // EU format: DD/MM/YYYY
- day = parseInt(parts[0], 10);
- month = parseInt(parts[1], 10);
- year = parseInt(parts[2], 10);
- } else {
- // ISO format: YYYY-MM-DD
- year = parseInt(parts[0], 10);
- month = parseInt(parts[1], 10);
- day = parseInt(parts[2], 10);
- }
- break;
- }
- return null;
- }
- }
- if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
- if (month < 1 || month > 12) return null;
- if (day < 1 || day > 31) return null;
- if (year < 1900 || year > 2100) return null;
- return new Date(year, month - 1, day);
- } catch {
- return null;
- }
- }
- /**
- * Parse a time string. Handles both 12h (with AM/PM) and 24h formats.
- * Returns { hours, minutes } or null if parsing fails.
- */
- export function parseTimeInput(value: string): { hours: number; minutes: number } | null {
- if (!value) return null;
- try {
- const trimmed = value.trim().toUpperCase();
- // Check for 12h format with AM/PM
- const ampmMatch = trimmed.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
- if (ampmMatch) {
- let hours = parseInt(ampmMatch[1], 10);
- const minutes = parseInt(ampmMatch[2], 10);
- const ampm = ampmMatch[3]?.toUpperCase();
- if (ampm === 'PM' && hours < 12) hours += 12;
- if (ampm === 'AM' && hours === 12) hours = 0;
- if (hours < 0 || hours > 23) return null;
- if (minutes < 0 || minutes > 59) return null;
- return { hours, minutes };
- }
- // Try 24h format HH:MM
- const match24 = trimmed.match(/^(\d{1,2}):(\d{2})$/);
- if (match24) {
- const hours = parseInt(match24[1], 10);
- const minutes = parseInt(match24[2], 10);
- if (hours < 0 || hours > 23) return null;
- if (minutes < 0 || minutes > 59) return null;
- return { hours, minutes };
- }
- return null;
- } catch {
- return null;
- }
- }
- /**
- * Convert a Date object to datetime-local input value (ISO format).
- */
- export function toDateTimeLocalValue(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}`;
- }
- /**
- * Apply time format setting to Intl.DateTimeFormatOptions.
- * This modifies the options object in place and returns it.
- */
- export function applyTimeFormat(
- options: Intl.DateTimeFormatOptions,
- timeFormat: TimeFormat = 'system'
- ): Intl.DateTimeFormatOptions {
- if (timeFormat === '12h') {
- options.hour12 = true;
- } else if (timeFormat === '24h') {
- options.hour12 = false;
- }
- // 'system' leaves hour12 undefined, letting the browser decide
- return options;
- }
- /**
- * Parse a date string from the backend as UTC.
- * Handles ISO 8601 strings with or without timezone indicators.
- *
- * @param dateStr - Date string from backend (e.g., "2026-01-09T12:03:36.288768")
- * @returns Date object in local timezone
- */
- export function parseUTCDate(dateStr: string | null | undefined): Date | null {
- if (!dateStr) return null;
- // If the string already has a timezone indicator, parse as-is
- if (dateStr.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(dateStr)) {
- return new Date(dateStr);
- }
- // Otherwise, append 'Z' to interpret as UTC
- return new Date(dateStr + 'Z');
- }
- /**
- * Format a UTC date string to a localized date/time string.
- *
- * @param dateStr - Date string from backend
- * @param options - Intl.DateTimeFormat options (defaults to showing date and time)
- * @returns Formatted date string in user's locale and timezone
- */
- export function formatDate(
- dateStr: string | null | undefined,
- options?: Intl.DateTimeFormatOptions
- ): string {
- const date = parseUTCDate(dateStr);
- if (!date) return '';
- const defaultOptions: Intl.DateTimeFormatOptions = {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- };
- return date.toLocaleString(undefined, options ?? defaultOptions);
- }
- /**
- * Format a UTC date string to a localized date-only string.
- *
- * @param dateStr - Date string from backend
- * @param options - Intl.DateTimeFormat options
- * @returns Formatted date string in user's locale and timezone
- */
- export function formatDateOnly(
- dateStr: string | null | undefined,
- options?: Intl.DateTimeFormatOptions
- ): string {
- const date = parseUTCDate(dateStr);
- if (!date) return '';
- const defaultOptions: Intl.DateTimeFormatOptions = {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- };
- return date.toLocaleDateString(undefined, options ?? defaultOptions);
- }
- /**
- * Format a UTC date string to a localized date/time string with time format support.
- *
- * @param dateStr - Date string from backend
- * @param timeFormat - Time format setting ('system', '12h', '24h')
- * @param options - Intl.DateTimeFormat options (defaults to showing date and time)
- * @returns Formatted date string in user's locale and timezone
- */
- export function formatDateTime(
- dateStr: string | null | undefined,
- timeFormat: TimeFormat = 'system',
- options?: Intl.DateTimeFormatOptions
- ): string {
- const date = parseUTCDate(dateStr);
- if (!date) return '';
- const defaultOptions: Intl.DateTimeFormatOptions = {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- };
- const finalOptions = applyTimeFormat(options ?? defaultOptions, timeFormat);
- return date.toLocaleString(undefined, finalOptions);
- }
- /**
- * Format a Date object to a localized time string with time format support.
- *
- * @param date - Date object
- * @param timeFormat - Time format setting ('system', '12h', '24h')
- * @param options - Additional Intl.DateTimeFormat options
- * @returns Formatted time string
- */
- export function formatTimeOnly(
- date: Date,
- timeFormat: TimeFormat = 'system',
- options?: Intl.DateTimeFormatOptions
- ): string {
- const defaultOptions: Intl.DateTimeFormatOptions = {
- hour: '2-digit',
- minute: '2-digit',
- };
- const finalOptions = applyTimeFormat({ ...defaultOptions, ...options }, timeFormat);
- return date.toLocaleTimeString([], finalOptions);
- }
- /**
- * Calculate and format an ETA based on remaining minutes from now.
- *
- * @param remainingMinutes - Minutes until completion
- * @param timeFormat - Time format setting ('system', '12h', '24h')
- * @param t - Optional i18n translation function
- * @returns Formatted ETA string (e.g., "3:45 PM", "Tomorrow 9:30 AM", "Wed 2:00 PM")
- */
- export function formatETA(
- remainingMinutes: number,
- timeFormat: 'system' | '12h' | '24h' = 'system',
- t?: (key: string) => string
- ): string {
- const now = new Date();
- const eta = new Date(now.getTime() + remainingMinutes * 60 * 1000);
-
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- const etaDay = new Date(eta);
- etaDay.setHours(0, 0, 0, 0);
- const timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };
- if (timeFormat === '12h') timeOptions.hour12 = true;
- else if (timeFormat === '24h') timeOptions.hour12 = false;
- const timeStr = eta.toLocaleTimeString([], timeOptions);
- const dayDiff = Math.floor((etaDay.getTime() - today.getTime()) / 86400000);
- if (dayDiff === 0) return timeStr;
- if (dayDiff === 1) return `${t?.('common.tomorrow') ?? 'Tomorrow'} ${timeStr}`;
- return `${eta.toLocaleDateString([], { weekday: 'short' })} ${timeStr}`;
- }
- /**
- * Format a duration in seconds to a human-readable string, with null handling.
- *
- * @param seconds - Duration in seconds, or null/undefined
- * @returns Formatted string (e.g., "2h 30m", "45m") or "--" if no value
- */
- export function formatDuration(seconds: number | null | undefined): string {
- if (seconds == null || seconds < 0) return '--';
-
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
-
- return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
- }
|