/** * iDempiere's REST API serialises Timestamp columns as the server's *local* * (Europe/Berlin) wall-clock with a literal "Z" appended — e.g. * "2026-06-03T14:30:00Z", where 14:30 is ALREADY Berlin local time, not UTC. * * Parsing that string with `new Date()` makes the client re-apply the UTC * offset, pushing the displayed time 1h (CET, winter) / 2h (CEST, summer) into * the future. The old workaround hard-coded `setHours(getHours() - 1)`, which * is wrong half the year because it doesn't track daylight-saving time. * * This helper reads the wall-clock components verbatim and rebuilds a *local* * Date, so the timestamp renders exactly as iDempiere recorded it — no timezone * double-offset, no DST-dependent manual correction. It tolerates the common * shapes iDempiere / Postgres views emit (trailing `Z`, fractional seconds, a * real `±hh:mm` offset, a space instead of `T`, or a date-only value). * * Returns `null` for empty / unparseable input. */ export const parseIdempiereTimestamp = (value: any): Date | null => { if (value === null || value === undefined || value === '') return null if (value instanceof Date) return isNaN(value.getTime()) ? null : value const str = String(value) // YYYY-MM-DD[ T]HH:MM[:SS][.fff][Z|±hh:mm] — keep the wall-clock, drop the zone. const dt = str.match(/^(\d{4})-(\d{1,2})-(\d{1,2})[T ](\d{1,2}):(\d{2})(?::(\d{2}))?/) if (dt) { return new Date(+dt[1], +dt[2] - 1, +dt[3], +dt[4], +dt[5], +(dt[6] || 0)) } // Date-only "YYYY-MM-DD" → local midnight (avoids the UTC-parsing day shift). const d = str.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/) if (d) { return new Date(+d[1], +d[2] - 1, +d[3]) } // Fallback: epoch number or anything else the Date constructor understands. const parsed = new Date(value) return isNaN(parsed.getTime()) ? null : parsed }