// This app stores dates as date-only strings ("YYYY-MM-DD") and timestamps as // timestamptz, and is used from a single timezone. Every conversion here is // pinned to Europe/Vienna rather than the runtime's zone: the server renders // in UTC (Docker/Vercel) while the browser renders in Vienna, so an unpinned // formatter produces a different day on each side — wrong dates for the user // near midnight, and a React hydration mismatch. const TIMEZONE = "Europe/Vienna"; const dateTimeFormatter = new Intl.DateTimeFormat("de-AT", { day: "2-digit", month: "2-digit", year: "numeric", timeZone: TIMEZONE, }); // en-CA formats as "YYYY-MM-DD", which is the shape the rest of the app and // the database speak. const isoFormatter = new Intl.DateTimeFormat("en-CA", { day: "2-digit", month: "2-digit", year: "numeric", timeZone: TIMEZONE, }); const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/; // Today in Vienna. Deliberately not new Date().toISOString().slice(0, 10): // that is the *UTC* date, which for part of every day is already tomorrow // relative to Austria. export function todayIso(): string { return isoFormatter.format(new Date()); } // Normalizes either input to a "YYYY-MM-DD" string. A date-only string is // returned as-is — parsing it into a Date first would anchor it to UTC // midnight and shift it in any negative-offset zone. export function toIsoDate(value: string | Date): string { if (typeof value === "string") return DATE_ONLY.test(value) ? value : isoFormatter.format(new Date(value)); return isoFormatter.format(value); } export function fmtDate(date: string | Date | null | undefined): string { if (!date) return "–"; if (typeof date === "string" && DATE_ONLY.test(date)) { const [year, month, day] = date.split("-"); return `${day}.${month}.${year}`; } const d = typeof date === "string" ? new Date(date) : date; if (Number.isNaN(d.getTime())) return "–"; return dateTimeFormatter.format(d); } export function initials(firstName: string, lastName: string): string { const a = firstName.trim().charAt(0).toUpperCase(); const b = lastName.trim().charAt(0).toUpperCase(); return `${a}${b}`; } // "Dr. Max Mustermann, MSc MBA" — prefix titles precede the name, suffix // titles follow after a comma, both space-joined in the stored order. export function fmtFullName( firstName: string, lastName: string, titlePrefix: string[] | null | undefined, titleSuffix: string[] | null | undefined ): string { const prefix = titlePrefix && titlePrefix.length > 0 ? `${titlePrefix.join(" ")} ` : ""; const suffix = titleSuffix && titleSuffix.length > 0 ? `, ${titleSuffix.join(" ")}` : ""; return `${prefix}${firstName} ${lastName}${suffix}`; } // Whole years between two ISO dates. Compares the "MM-DD" tails as strings, // which is exact and needs no Date arithmetic at all. export function yearsBetweenIso(from: string, to: string): number { const years = Number(to.slice(0, 4)) - Number(from.slice(0, 4)); return to.slice(5) < from.slice(5) ? years - 1 : years; } export function fmtAge(birthDate: string | Date): number { return yearsBetweenIso(toIsoDate(birthDate), todayIso()); } export function tenure(entryDate: string | Date, endDate?: string | Date | null): string { const start = toIsoDate(entryDate); const end = endDate ? toIsoDate(endDate) : todayIso(); let years = Number(end.slice(0, 4)) - Number(start.slice(0, 4)); let months = Number(end.slice(5, 7)) - Number(start.slice(5, 7)); if (Number(end.slice(8, 10)) < Number(start.slice(8, 10))) months -= 1; if (months < 0) { years -= 1; months += 12; } if (years < 0) return "0 Monate"; const yearPart = years > 0 ? `${years} ${years === 1 ? "Jahr" : "Jahre"}` : ""; const monthPart = months > 0 ? `${months} ${months === 1 ? "Monat" : "Monate"}` : ""; if (yearPart && monthPart) return `${yearPart}, ${monthPart}`; return yearPart || monthPart || "unter 1 Monat"; } // Anchored at UTC midnight on both sides so the difference is a whole number // of calendar days regardless of DST transitions in between. export function daysBetweenIso(from: string, to: string = todayIso()): number { const start = Date.parse(`${from}T00:00:00Z`); const end = Date.parse(`${to}T00:00:00Z`); return Math.round((end - start) / 86_400_000); } export function addDaysIso(iso: string, days: number): string { const d = new Date(`${iso}T00:00:00Z`); d.setUTCDate(d.getUTCDate() + days); return d.toISOString().slice(0, 10); }