Files
AgendaPro/server/lib/time.ts
T
AgendaPro Dev d796538fd9 feat: rebrand AgendaPro -> AgendaMax + calendar fixes + current-week demo seed
- Rebrand all user-facing text to AgendaMax
- Fix sidebar scrolling: card container no longer overflows html (flex h-full min-h-0)
- Fix calendar toolbar hover: active buttons keep readable contrast
- Sticky calendar toolbar (month/week/day always visible on scroll)
- Seed guarantees 2-4 appointments per day for current week (Mon-Sat)
2026-07-27 10:11:16 -06:00

118 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/lib/time.ts
// Pure timezone helpers for business-local ("biz") date math.
// Mexico is UTC-6, so SQLite date('now') (= UTC) misattributes the evening rush.
// All functions are deterministic given an explicit instant (default = new Date()).
/** Returns the calendar date (YYYY-MM-DD) of the given instant in the given IANA tz. */
export function bizDateISO(instant: Date, tz: string): string {
return new Intl.DateTimeFormat("en-CA", {
timeZone: tz || "UTC",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(instant); // en-CA yields "YYYY-MM-DD"
}
/** Business "today" (YYYY-MM-DD) for a tz, at the given instant (default: server now). */
export function bizTodayISO(tz: string, now: Date = new Date()): string {
return bizDateISO(now, tz);
}
/** tz offset (in minutes) of the given instant, east of UTC positive. */
function tzOffsetMinutes(instant: Date, tz: string): number {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: tz || "UTC",
timeZoneName: "longOffset",
}).formatToParts(instant);
const off = parts.find((p) => p.type === "timeZoneName")?.value ?? "GMT+00:00";
// e.g. "GMT-06:00", "GMT+05:30", "GMT+00:00", or bare "GMT" for UTC
const m = off.match(/GMT([+-])(\d{1,2})(?::(\d{2}))?/);
if (!m) return 0; // bare "GMT" → UTC
const sign = m[1] === "-" ? -1 : 1;
const h = parseInt(m[2], 10);
const min = m[3] ? parseInt(m[3], 10) : 0;
return sign * (h * 60 + min);
}
/**
* Convert a business-local wall-clock (year, month, day, hh, mm, ss) in `tz` to a UTC Date.
* We first treat the wall-clock as if it were UTC, read the tz offset at that instant,
* then subtract the offset: a west tz (negative offset) is "behind" UTC, so the same
* wall-clock happens later in UTC → UTC = wall offset.
*/
export function wallToUtcDate(
tz: string,
y: number,
mo: number,
d: number,
hh: number,
mm: number,
ss: number
): Date {
const guess = new Date(Date.UTC(y, mo - 1, d, hh, mm, ss));
const offsetMin = tzOffsetMinutes(guess, tz);
return new Date(guess.getTime() - offsetMin * 60000);
}
/** Business-day UTC bounds (start = local 00:00:00, end = local 23:59:59) for today +/- offset. */
export function bizDayBounds(
tz: string,
dayOffset = 0,
now: Date = new Date()
): { start: Date; end: Date } {
const today = bizDateISO(now, tz);
const [y, m, d] = today.split("-").map(Number);
const base = new Date(Date.UTC(y, m - 1, d));
base.setUTCDate(base.getUTCDate() + dayOffset);
const ty = base.getUTCFullYear();
const tm = base.getUTCMonth() + 1;
const td = base.getUTCDate();
return {
start: wallToUtcDate(tz, ty, tm, td, 0, 0, 0),
end: wallToUtcDate(tz, ty, tm, td, 23, 59, 59),
};
}
/** Format a UTC instant as SQLite canonical "YYYY-MM-DD HH:MM:SS" (matches datetime() output). */
export function toSqliteUtc(d: Date): string {
return d.toISOString().slice(0, 19).replace("T", " ");
}
/** Format a UTC instant as ISO "YYYY-MM-DDTHH:MM:SSZ" (matches the stored start_at format). */
export function toIsoUtc(d: Date): string {
return d.toISOString().slice(0, 19) + "Z";
}
/** Business-day bounds in SQLite canonical format — pair with `datetime(column) >= ?`. */
export function bizDayBoundsSqlite(
tz: string,
dayOffset = 0,
now: Date = new Date()
): { start: string; end: string } {
const b = bizDayBounds(tz, dayOffset, now);
return { start: toSqliteUtc(b.start), end: toSqliteUtc(b.end) };
}
/** Business-day bounds in ISO-Z format — pair with raw `start_at >= ?`. */
export function bizDayBoundsIso(
tz: string,
dayOffset = 0,
now: Date = new Date()
): { start: string; end: string } {
const b = bizDayBounds(tz, dayOffset, now);
return { start: toIsoUtc(b.start), end: toIsoUtc(b.end) };
}
/** Convenience: convert a business-local wall-clock to a UTC ISO string. */
export function wallToUtcISO(
tz: string,
y: number,
mo: number,
d: number,
hh: number,
mm: number,
ss: number
): string {
return wallToUtcDate(tz, y, mo, d, hh, mm, ss).toISOString();
}