| 1 |
/** |
| 2 |
* Date and Time Formatting Utilities |
| 3 |
* Uses global Yatra settings for consistent date/time formatting across all pages |
| 4 |
*/ |
| 5 |
|
| 6 |
// Supported PHP date formats: |
| 7 |
// 'Y-m-d' -> 2025-12-15 |
| 8 |
// 'm/d/Y' -> 12/15/2025 |
| 9 |
// 'd/m/Y' -> 15/12/2025 |
| 10 |
// 'd-m-Y' -> 15-12-2025 |
| 11 |
// 'M d, Y' -> Dec 15, 2025 |
| 12 |
// 'F d, Y' -> December 15, 2025 |
| 13 |
// 'd M Y' -> 15 Dec 2025 |
| 14 |
// 'd F Y' -> 15 December 2025 |
| 15 |
|
| 16 |
// Supported PHP time formats: |
| 17 |
// 'H:i' -> 14:30 (24-hour) |
| 18 |
// 'h:i A' -> 02:30 PM (12-hour) |
| 19 |
// 'h:i a' -> 02:30 pm (12-hour lowercase) |
| 20 |
// 'H:i:s' -> 14:30:00 (24-hour with seconds) |
| 21 |
// 'h:i:s A' -> 02:30:00 PM (12-hour with seconds) |
| 22 |
|
| 23 |
/** |
| 24 |
* Get date format from Yatra settings |
| 25 |
*/ |
| 26 |
export function getDateFormat(): string { |
| 27 |
const w = window as any; |
| 28 |
return ( |
| 29 |
w?.yatraAdmin?.date_format || |
| 30 |
w?.yatraAdmin?.dateFormat || |
| 31 |
// Customer account pages localize their settings under yatraAccountPage. |
| 32 |
w?.yatraAccountPage?.date_format || |
| 33 |
"Y-m-d" |
| 34 |
); |
| 35 |
} |
| 36 |
|
| 37 |
/** |
| 38 |
* Get time format from Yatra settings |
| 39 |
*/ |
| 40 |
export function getTimeFormat(): string { |
| 41 |
const w = window as any; |
| 42 |
return ( |
| 43 |
w?.yatraAdmin?.time_format || |
| 44 |
w?.yatraAdmin?.timeFormat || |
| 45 |
w?.yatraAccountPage?.time_format || |
| 46 |
"H:i" |
| 47 |
); |
| 48 |
} |
| 49 |
|
| 50 |
/** |
| 51 |
* Get timezone from Yatra settings |
| 52 |
*/ |
| 53 |
export function getTimezone(): string { |
| 54 |
const w = window as any; |
| 55 |
return w?.yatraAdmin?.timezone || w?.yatraAccountPage?.timezone || "UTC"; |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* Parse a value into a Date, treating a date-only string ("YYYY-MM-DD") as a |
| 60 |
* LOCAL calendar date. |
| 61 |
* |
| 62 |
* `new Date("2026-08-01")` parses as UTC midnight, but the formatters read it |
| 63 |
* back with local getters (getDate/getMonth/…), so in any behind-UTC timezone |
| 64 |
* the day rolls back (Aug 1 → Jul 31) — e.g. a booking's travel_date showing |
| 65 |
* one day early. Building the Date from its parts pins it to the intended |
| 66 |
* calendar day regardless of timezone. Datetime strings (with a time part) and |
| 67 |
* Date objects are parsed/returned as before. |
| 68 |
*/ |
| 69 |
export function toDateValue(value: string | Date | null | undefined): Date { |
| 70 |
// Never hand back a non-Date. This previously returned `value` unchanged for |
| 71 |
// anything that wasn't a string, so a missing date came back as `undefined` |
| 72 |
// and the caller's `.toLocaleDateString()` threw — taking the whole admin |
| 73 |
// dashboard down with it via the error boundary. An Invalid Date keeps every |
| 74 |
// caller's date maths working (comparisons are simply false) and lets the |
| 75 |
// display helpers fall back to "-". |
| 76 |
if (value === null || value === undefined) return new Date(NaN); |
| 77 |
if (value instanceof Date) return value; |
| 78 |
if (typeof value !== "string") return new Date(NaN); |
| 79 |
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim()); |
| 80 |
if (dateOnly) { |
| 81 |
return new Date( |
| 82 |
Number(dateOnly[1]), |
| 83 |
Number(dateOnly[2]) - 1, |
| 84 |
Number(dateOnly[3]), |
| 85 |
); |
| 86 |
} |
| 87 |
return new Date(value); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Format a date string according to Yatra settings |
| 92 |
* @param dateString - Date string in any parseable format (ISO, YYYY-MM-DD, etc.) |
| 93 |
* @param includeTime - Whether to include time in the output |
| 94 |
* @returns Formatted date string |
| 95 |
*/ |
| 96 |
export function formatDate( |
| 97 |
dateString: string | Date | null | undefined, |
| 98 |
includeTime: boolean = false, |
| 99 |
): string { |
| 100 |
if (!dateString) return "-"; |
| 101 |
|
| 102 |
try { |
| 103 |
const date = toDateValue(dateString); |
| 104 |
|
| 105 |
if (isNaN(date.getTime())) { |
| 106 |
return String(dateString); |
| 107 |
} |
| 108 |
|
| 109 |
const phpDateFormat = getDateFormat(); |
| 110 |
const phpTimeFormat = getTimeFormat(); |
| 111 |
|
| 112 |
// Format date part |
| 113 |
const formattedDate = formatDatePart(date, phpDateFormat); |
| 114 |
|
| 115 |
// Format time part if requested |
| 116 |
if (includeTime) { |
| 117 |
const formattedTime = formatTimePart(date, phpTimeFormat); |
| 118 |
return `${formattedDate} ${formattedTime}`; |
| 119 |
} |
| 120 |
|
| 121 |
return formattedDate; |
| 122 |
} catch (e) { |
| 123 |
return String(dateString); |
| 124 |
} |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Format only the date part |
| 129 |
*/ |
| 130 |
function formatDatePart(date: Date, phpFormat: string): string { |
| 131 |
const year = date.getFullYear(); |
| 132 |
const month = date.getMonth(); |
| 133 |
const day = date.getDate(); |
| 134 |
|
| 135 |
const monthNames = [ |
| 136 |
"January", |
| 137 |
"February", |
| 138 |
"March", |
| 139 |
"April", |
| 140 |
"May", |
| 141 |
"June", |
| 142 |
"July", |
| 143 |
"August", |
| 144 |
"September", |
| 145 |
"October", |
| 146 |
"November", |
| 147 |
"December", |
| 148 |
]; |
| 149 |
const monthShort = [ |
| 150 |
"Jan", |
| 151 |
"Feb", |
| 152 |
"Mar", |
| 153 |
"Apr", |
| 154 |
"May", |
| 155 |
"Jun", |
| 156 |
"Jul", |
| 157 |
"Aug", |
| 158 |
"Sep", |
| 159 |
"Oct", |
| 160 |
"Nov", |
| 161 |
"Dec", |
| 162 |
]; |
| 163 |
|
| 164 |
const pad = (n: number) => n.toString().padStart(2, "0"); |
| 165 |
|
| 166 |
const dayNames = [ |
| 167 |
"Sunday", |
| 168 |
"Monday", |
| 169 |
"Tuesday", |
| 170 |
"Wednesday", |
| 171 |
"Thursday", |
| 172 |
"Friday", |
| 173 |
"Saturday", |
| 174 |
]; |
| 175 |
const dayShort = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; |
| 176 |
const dayOfWeek = date.getDay(); |
| 177 |
|
| 178 |
switch (phpFormat) { |
| 179 |
// Numeric formats |
| 180 |
case "Y-m-d": |
| 181 |
return `${year}-${pad(month + 1)}-${pad(day)}`; |
| 182 |
case "Y/m/d": |
| 183 |
return `${year}/${pad(month + 1)}/${pad(day)}`; |
| 184 |
case "m/d/Y": |
| 185 |
return `${pad(month + 1)}/${pad(day)}/${year}`; |
| 186 |
case "d/m/Y": |
| 187 |
return `${pad(day)}/${pad(month + 1)}/${year}`; |
| 188 |
case "d-m-Y": |
| 189 |
return `${pad(day)}-${pad(month + 1)}-${year}`; |
| 190 |
case "d.m.Y": |
| 191 |
return `${pad(day)}.${pad(month + 1)}.${year}`; |
| 192 |
// Month name formats with padded day |
| 193 |
case "M d, Y": |
| 194 |
return `${monthShort[month]} ${pad(day)}, ${year}`; |
| 195 |
case "F d, Y": |
| 196 |
return `${monthNames[month]} ${pad(day)}, ${year}`; |
| 197 |
case "d M Y": |
| 198 |
return `${pad(day)} ${monthShort[month]} ${year}`; |
| 199 |
case "d F Y": |
| 200 |
return `${pad(day)} ${monthNames[month]} ${year}`; |
| 201 |
// Month name formats with day without leading zero |
| 202 |
case "M j, Y": |
| 203 |
return `${monthShort[month]} ${day}, ${year}`; |
| 204 |
case "F j, Y": |
| 205 |
return `${monthNames[month]} ${day}, ${year}`; |
| 206 |
case "j M Y": |
| 207 |
return `${day} ${monthShort[month]} ${year}`; |
| 208 |
case "j F Y": |
| 209 |
return `${day} ${monthNames[month]} ${year}`; |
| 210 |
// Year first with month name |
| 211 |
case "Y M j": |
| 212 |
return `${year} ${monthShort[month]} ${day}`; |
| 213 |
case "Y F j": |
| 214 |
return `${year} ${monthNames[month]} ${day}`; |
| 215 |
// With weekday |
| 216 |
case "l, F j, Y": |
| 217 |
return `${dayNames[dayOfWeek]}, ${monthNames[month]} ${day}, ${year}`; |
| 218 |
case "D, M j, Y": |
| 219 |
return `${dayShort[dayOfWeek]}, ${monthShort[month]} ${day}, ${year}`; |
| 220 |
default: |
| 221 |
// Default to ISO format |
| 222 |
return `${year}-${pad(month + 1)}-${pad(day)}`; |
| 223 |
} |
| 224 |
} |
| 225 |
|
| 226 |
/** |
| 227 |
* Format only the time part |
| 228 |
*/ |
| 229 |
function formatTimePart(date: Date, phpFormat: string): string { |
| 230 |
const hours = date.getHours(); |
| 231 |
const minutes = date.getMinutes(); |
| 232 |
const seconds = date.getSeconds(); |
| 233 |
|
| 234 |
const pad = (n: number) => n.toString().padStart(2, "0"); |
| 235 |
|
| 236 |
const hours12 = hours % 12 || 12; |
| 237 |
const ampm = hours >= 12 ? "PM" : "AM"; |
| 238 |
|
| 239 |
switch (phpFormat) { |
| 240 |
case "H:i": |
| 241 |
return `${pad(hours)}:${pad(minutes)}`; |
| 242 |
case "h:i A": |
| 243 |
return `${pad(hours12)}:${pad(minutes)} ${ampm}`; |
| 244 |
case "h:i a": |
| 245 |
return `${pad(hours12)}:${pad(minutes)} ${ampm.toLowerCase()}`; |
| 246 |
case "H:i:s": |
| 247 |
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; |
| 248 |
case "h:i:s A": |
| 249 |
return `${pad(hours12)}:${pad(minutes)}:${pad(seconds)} ${ampm}`; |
| 250 |
default: |
| 251 |
return `${pad(hours)}:${pad(minutes)}`; |
| 252 |
} |
| 253 |
} |
| 254 |
|
| 255 |
/** |
| 256 |
* Format a date for display with time |
| 257 |
* @param dateString - Date string |
| 258 |
* @returns Formatted date and time string |
| 259 |
*/ |
| 260 |
export function formatDateTime( |
| 261 |
dateString: string | Date | null | undefined, |
| 262 |
): string { |
| 263 |
return formatDate(dateString, true); |
| 264 |
} |
| 265 |
|
| 266 |
/** |
| 267 |
* Format time only |
| 268 |
* @param dateString - Date string or time string |
| 269 |
* @returns Formatted time string |
| 270 |
*/ |
| 271 |
export function formatTime( |
| 272 |
dateString: string | Date | null | undefined, |
| 273 |
): string { |
| 274 |
if (!dateString) return "-"; |
| 275 |
|
| 276 |
try { |
| 277 |
const date = toDateValue(dateString); |
| 278 |
|
| 279 |
if (isNaN(date.getTime())) { |
| 280 |
// Try to parse as time only (HH:mm or HH:mm:ss) |
| 281 |
if ( |
| 282 |
typeof dateString === "string" && |
| 283 |
/^\d{1,2}:\d{2}(:\d{2})?$/.test(dateString) |
| 284 |
) { |
| 285 |
const [hours, minutes] = dateString.split(":").map(Number); |
| 286 |
const tempDate = new Date(); |
| 287 |
tempDate.setHours(hours, minutes, 0, 0); |
| 288 |
return formatTimePart(tempDate, getTimeFormat()); |
| 289 |
} |
| 290 |
return String(dateString); |
| 291 |
} |
| 292 |
|
| 293 |
return formatTimePart(date, getTimeFormat()); |
| 294 |
} catch (e) { |
| 295 |
return String(dateString); |
| 296 |
} |
| 297 |
} |
| 298 |
|
| 299 |
/** |
| 300 |
* Get relative time (e.g., "2 hours ago", "3 days ago") |
| 301 |
* @param dateString - Date string |
| 302 |
* @returns Relative time string |
| 303 |
*/ |
| 304 |
export function formatRelativeTime( |
| 305 |
dateString: string | Date | null | undefined, |
| 306 |
): string { |
| 307 |
if (!dateString) return "-"; |
| 308 |
|
| 309 |
try { |
| 310 |
const date = toDateValue(dateString); |
| 311 |
|
| 312 |
if (isNaN(date.getTime())) { |
| 313 |
return String(dateString); |
| 314 |
} |
| 315 |
|
| 316 |
const now = new Date(); |
| 317 |
const diffMs = now.getTime() - date.getTime(); |
| 318 |
const diffSecs = Math.floor(diffMs / 1000); |
| 319 |
const diffMins = Math.floor(diffSecs / 60); |
| 320 |
const diffHours = Math.floor(diffMins / 60); |
| 321 |
const diffDays = Math.floor(diffHours / 24); |
| 322 |
const diffWeeks = Math.floor(diffDays / 7); |
| 323 |
const diffMonths = Math.floor(diffDays / 30); |
| 324 |
const diffYears = Math.floor(diffDays / 365); |
| 325 |
|
| 326 |
if (diffSecs < 60) return "Just now"; |
| 327 |
if (diffMins < 60) |
| 328 |
return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`; |
| 329 |
if (diffHours < 24) |
| 330 |
return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`; |
| 331 |
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`; |
| 332 |
if (diffWeeks < 4) |
| 333 |
return `${diffWeeks} week${diffWeeks > 1 ? "s" : ""} ago`; |
| 334 |
if (diffMonths < 12) |
| 335 |
return `${diffMonths} month${diffMonths > 1 ? "s" : ""} ago`; |
| 336 |
return `${diffYears} year${diffYears > 1 ? "s" : ""} ago`; |
| 337 |
} catch (e) { |
| 338 |
return String(dateString); |
| 339 |
} |
| 340 |
} |
| 341 |
|
| 342 |
/** |
| 343 |
* Parse a date string to Date object |
| 344 |
* @param dateString - Date string in various formats |
| 345 |
* @returns Date object or null if invalid |
| 346 |
*/ |
| 347 |
export function parseDate(dateString: string | null | undefined): Date | null { |
| 348 |
if (!dateString) return null; |
| 349 |
|
| 350 |
try { |
| 351 |
const date = toDateValue(dateString); |
| 352 |
return isNaN(date.getTime()) ? null : date; |
| 353 |
} catch (e) { |
| 354 |
return null; |
| 355 |
} |
| 356 |
} |
| 357 |
|
| 358 |
/** |
| 359 |
* Format date for input fields (always YYYY-MM-DD for HTML date inputs) |
| 360 |
* @param dateString - Date string |
| 361 |
* @returns Date in YYYY-MM-DD format for input fields |
| 362 |
*/ |
| 363 |
/** |
| 364 |
* Today's date as a LOCAL "YYYY-MM-DD" string. |
| 365 |
* |
| 366 |
* Use this instead of `new Date().toISOString().split("T")[0]` for default/ |
| 367 |
* "today" date values: toISOString() yields the UTC date, which is the wrong |
| 368 |
* calendar day for a user whose local date differs from UTC at that moment |
| 369 |
* (e.g. late evening in a behind-UTC zone, or early morning ahead of UTC) — |
| 370 |
* saving a payment/booking/rule date a day off. This reads the browser's local |
| 371 |
* calendar day. |
| 372 |
*/ |
| 373 |
export function todayYmd(): string { |
| 374 |
return formatDateForInput(new Date()); |
| 375 |
} |
| 376 |
|
| 377 |
export function formatDateForInput( |
| 378 |
dateString: string | Date | null | undefined, |
| 379 |
): string { |
| 380 |
if (!dateString) return ""; |
| 381 |
|
| 382 |
try { |
| 383 |
const date = toDateValue(dateString); |
| 384 |
|
| 385 |
if (isNaN(date.getTime())) { |
| 386 |
return ""; |
| 387 |
} |
| 388 |
|
| 389 |
const year = date.getFullYear(); |
| 390 |
const month = (date.getMonth() + 1).toString().padStart(2, "0"); |
| 391 |
const day = date.getDate().toString().padStart(2, "0"); |
| 392 |
|
| 393 |
return `${year}-${month}-${day}`; |
| 394 |
} catch (e) { |
| 395 |
return ""; |
| 396 |
} |
| 397 |
} |
| 398 |
|