| @@ -23,11 +23,14 @@ | ||
| 23 | 23 | /** |
| 24 | 24 | * Get date format from Yatra settings |
| 25 | 25 | */ |
| 26 | 26 | export function getDateFormat(): string { |
| 27 | + const w = window as any; | |
| 27 | 28 | return ( |
| 28 | - (window as any)?.yatraAdmin?.date_format || | |
| 29 | - (window as any)?.yatraAdmin?.dateFormat || | |
| 29 | + w?.yatraAdmin?.date_format || | |
| 30 | + w?.yatraAdmin?.dateFormat || | |
| 31 | + // Customer account pages localize their settings under yatraAccountPage. | |
| 32 | + w?.yatraAccountPage?.date_format || | |
| 30 | 33 | "Y-m-d" |
| 31 | 34 | ); |
| 32 | 35 | } |
| 33 | 36 | |
| @@ -34,11 +37,13 @@ | ||
| 34 | 37 | /** |
| 35 | 38 | * Get time format from Yatra settings |
| 36 | 39 | */ |
| 37 | 40 | export function getTimeFormat(): string { |
| 41 | + const w = window as any; | |
| 38 | 42 | return ( |
| 39 | - (window as any)?.yatraAdmin?.time_format || | |
| 40 | - (window as any)?.yatraAdmin?.timeFormat || | |
| 43 | + w?.yatraAdmin?.time_format || | |
| 44 | + w?.yatraAdmin?.timeFormat || | |
| 45 | + w?.yatraAccountPage?.time_format || | |
| 41 | 46 | "H:i" |
| 42 | 47 | ); |
| 43 | 48 | } |
| 44 | 49 | |
| @@ -45,12 +50,45 @@ | ||
| 45 | 50 | /** |
| 46 | 51 | * Get timezone from Yatra settings |
| 47 | 52 | */ |
| 48 | 53 | export function getTimezone(): string { |
| 49 | - return (window as any)?.yatraAdmin?.timezone || "UTC"; | |
| 54 | + const w = window as any; | |
| 55 | + return w?.yatraAdmin?.timezone || w?.yatraAccountPage?.timezone || "UTC"; | |
| 50 | 56 | } |
| 51 | 57 | |
| 52 | 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 | +/** | |
| 53 | 91 | * Format a date string according to Yatra settings |
| 54 | 92 | * @param dateString - Date string in any parseable format (ISO, YYYY-MM-DD, etc.) |
| 55 | 93 | * @param includeTime - Whether to include time in the output |
| 56 | 94 | * @returns Formatted date string |
| @@ -61,10 +99,9 @@ | ||
| 61 | 99 | ): string { |
| 62 | 100 | if (!dateString) return "-"; |
| 63 | 101 | |
| 64 | 102 | try { |
| 65 | - const date = | |
| 66 | - typeof dateString === "string" ? new Date(dateString) : dateString; | |
| 103 | + const date = toDateValue(dateString); | |
| 67 | 104 | |
| 68 | 105 | if (isNaN(date.getTime())) { |
| 69 | 106 | return String(dateString); |
| 70 | 107 | } |
| @@ -236,10 +273,9 @@ | ||
| 236 | 273 | ): string { |
| 237 | 274 | if (!dateString) return "-"; |
| 238 | 275 | |
| 239 | 276 | try { |
| 240 | - const date = | |
| 241 | - typeof dateString === "string" ? new Date(dateString) : dateString; | |
| 277 | + const date = toDateValue(dateString); | |
| 242 | 278 | |
| 243 | 279 | if (isNaN(date.getTime())) { |
| 244 | 280 | // Try to parse as time only (HH:mm or HH:mm:ss) |
| 245 | 281 | if ( |
| @@ -270,10 +306,9 @@ | ||
| 270 | 306 | ): string { |
| 271 | 307 | if (!dateString) return "-"; |
| 272 | 308 | |
| 273 | 309 | try { |
| 274 | - const date = | |
| 275 | - typeof dateString === "string" ? new Date(dateString) : dateString; | |
| 310 | + const date = toDateValue(dateString); | |
| 276 | 311 | |
| 277 | 312 | if (isNaN(date.getTime())) { |
| 278 | 313 | return String(dateString); |
| 279 | 314 | } |
| @@ -312,9 +347,9 @@ | ||
| 312 | 347 | export function parseDate(dateString: string | null | undefined): Date | null { |
| 313 | 348 | if (!dateString) return null; |
| 314 | 349 | |
| 315 | 350 | try { |
| 316 | - const date = new Date(dateString); | |
| 351 | + const date = toDateValue(dateString); | |
| 317 | 352 | return isNaN(date.getTime()) ? null : date; |
| 318 | 353 | } catch (e) { |
| 319 | 354 | return null; |
| 320 | 355 | } |
| @@ -324,8 +359,22 @@ | ||
| 324 | 359 | * Format date for input fields (always YYYY-MM-DD for HTML date inputs) |
| 325 | 360 | * @param dateString - Date string |
| 326 | 361 | * @returns Date in YYYY-MM-DD format for input fields |
| 327 | 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 | + | |
| 328 | 377 | export function formatDateForInput( |
| 329 | 378 | dateString: string | Date | null | undefined, |
| 330 | 379 | ): string { |
| 331 | 380 | if (!dateString) return ""; |
| @@ -330,10 +379,9 @@ | ||
| 330 | 379 | ): string { |
| 331 | 380 | if (!dateString) return ""; |
| 332 | 381 | |
| 333 | 382 | try { |
| 334 | - const date = | |
| 335 | - typeof dateString === "string" ? new Date(dateString) : dateString; | |
| 383 | + const date = toDateValue(dateString); | |
| 336 | 384 | |
| 337 | 385 | if (isNaN(date.getTime())) { |
| 338 | 386 | return ""; |
| 339 | 387 | } |