/opt/mawid/apps/api/src/agent
NameSizeModeActions
agent.e2e.spec.ts132960644editdlrm
agent.module.ts8320644editdlrm
agent.service.ts81630644editdlrm
availability.spec.ts84820644editdlrm
availability.ts51830644editdlrm
booking-tools.service.ts144400644editdlrm
openai.client.ts22470644editdlrm
system-prompt.ts18550644editdlrm
tool-definitions.ts36140644editdlrm
tz.ts28820644editdlrm
Edit: /opt/mawid/apps/api/src/agent/tz.ts (2882B)
/** * Timezone helpers built on Intl — no extra dependency (PROJECT_PLAN §7.7). * Discipline: DB stores UTC; conversion happens only at these edges (§7.5). */ export interface WallTime { year: number; month: number; // 1-12 day: number; hour: number; minute: number; weekday: WeekdayKey; } export type WeekdayKey = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun'; const WEEKDAY_MAP: Record = { Mon: 'mon', Tue: 'tue', Wed: 'wed', Thu: 'thu', Fri: 'fri', Sat: 'sat', Sun: 'sun', }; const dtfCache = new Map(); function formatter(timeZone: string): Intl.DateTimeFormat { let dtf = dtfCache.get(timeZone); if (!dtf) { dtf = new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', weekday: 'short', hour12: false, }); dtfCache.set(timeZone, dtf); } return dtf; } /** Offset of `timeZone` from UTC at the given instant, in milliseconds. */ export function tzOffsetMs(timeZone: string, utc: Date): number { const parts = Object.fromEntries( formatter(timeZone) .formatToParts(utc) .map((p) => [p.type, p.value]), ); const asUtc = Date.UTC( Number(parts.year), Number(parts.month) - 1, Number(parts.day), parts.hour === '24' ? 0 : Number(parts.hour), Number(parts.minute), Number(parts.second), ); return asUtc - Math.floor(utc.getTime() / 1000) * 1000; } /** Convert a clinic-local wall time to the UTC instant. Two-pass handles DST. */ export function wallTimeToUtc( timeZone: string, year: number, month: number, day: number, hour: number, minute: number, ): Date { const guess = Date.UTC(year, month - 1, day, hour, minute); const first = guess - tzOffsetMs(timeZone, new Date(guess)); const second = guess - tzOffsetMs(timeZone, new Date(first)); return new Date(second); } /** Local wall-clock parts of a UTC instant in the given timezone. */ export function utcToWallTime(timeZone: string, utc: Date): WallTime { const parts = Object.fromEntries( formatter(timeZone) .formatToParts(utc) .map((p) => [p.type, p.value]), ); return { year: Number(parts.year), month: Number(parts.month), day: Number(parts.day), hour: parts.hour === '24' ? 0 : Number(parts.hour), minute: Number(parts.minute), weekday: WEEKDAY_MAP[parts.weekday], }; } /** Human label for patient-facing slot lists, in the clinic's local time. */ export function formatLocal(timeZone: string, utc: Date, locale = 'en-GB'): string { return new Intl.DateTimeFormat(locale, { timeZone, weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hour12: false, }).format(utc); }