Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 62 additions & 9 deletions Frontend/src/utils/dateUtils.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,74 @@
/**
* Unified Date Utility for HELPDESK.AI
* Fixes timezone shift issues by explicitly forcing local display.
* Compatible with Safari, Firefox, and Chrome across all supported Supabase date formats.
*/

export const formatTimelineDate = (dateStr) => {
const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:?\d{2})?$/;
const DATE_ONLY_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
const DATETIME_SPACE_REGEX = /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?$/;
const HAS_TIMEZONE = /[Zz]|[+-]\d{2}:?\d{2}$/;

/**
* Parse a date string into a Date object, with explicit handling for Safari compatibility.
* Safari is strict about ISO 8601 parsing — it rejects dates without TZ info and
* dates using space separators instead of 'T'.
*/
const parseDateSafely = (dateStr) => {
if (!dateStr) return null;

// Ensure the date string is interpreted as UTC if it's an ISO string from DB
let date;
if (typeof dateStr === 'string' && !dateStr.includes('Z') && !dateStr.includes('+')) {

if (typeof dateStr === 'number') {
return new Date(dateStr);
}

if (typeof dateStr === 'string') {
const trimmed = dateStr.trim();

// Handle "YYYY-MM-DD HH:MM:SS" format (space separator — Safari fails on this)
const spaceMatch = trimmed.match(DATETIME_SPACE_REGEX);
if (spaceMatch) {
const [, y, m, d, h, min, s, ms] = spaceMatch;
const msVal = ms ? parseInt(ms.slice(0, 3).padEnd(3, '0')) : 0;
return new Date(Date.UTC(
parseInt(y), parseInt(m) - 1, parseInt(d),
parseInt(h), parseInt(min), parseInt(s),
msVal
));
}

// Handle "YYYY-MM-DD" date-only format
const dateOnlyMatch = trimmed.match(DATE_ONLY_REGEX);
if (dateOnlyMatch) {
const [, y, m, d] = dateOnlyMatch;
return new Date(parseInt(y), parseInt(m) - 1, parseInt(d));
}

// Handle ISO 8601 with explicit timezone — safe in all browsers
if (trimmed.includes('T') && HAS_TIMEZONE.test(trimmed)) {
return new Date(trimmed);
}

// Handle full ISO without timezone (e.g. "2024-01-15T10:30:00")
// Safari interprets this as UTC, Chrome as local time.
// We normalise to UTC to match Supabase's UTC output convention.
if (trimmed.includes('T') && !HAS_TIMEZONE.test(trimmed)) {
return new Date(trimmed + 'Z');
}

// If it's a raw string without TZ, assume it was intended as UTC from our backend
date = new Date(dateStr + 'Z');
} else {
date = new Date(dateStr);
if (!HAS_TIMEZONE.test(trimmed)) {
return new Date(trimmed + 'Z');
}
}

if (isNaN(date.getTime())) return 'Invalid Date';
return new Date(dateStr);
};

export const formatTimelineDate = (dateStr) => {
if (!dateStr) return null;

const date = parseDateSafely(dateStr);
if (!date || isNaN(date.getTime())) return 'Invalid Date';

// Using the browser's default locale and timeZone (which is the user's local)
return date.toLocaleString(undefined, {
Expand Down