Skip to main content

27. Time-entry modes, segments, and facility timezone

Date: 2026-06-07

Status

Accepted

Context

Contractors originally logged one hours number per day (ADR-0016). The owner wants richer, real time tracking — a live punch clock, from–to time pairs (multiple per day), a weekly-table bulk entry, plus the original daily-total entry — and wants which methods are available to be configurable, not hardcoded (the prod Setup-Menu principle). Times must be shown in the assigned facility's timezone, which the schema did not store. The tz→business-day conversion was deferred in ADR-0012/0024.

Decision

  • Segments, not a new aggregate. Add time_segments (a punch in/out or a from–to pair; ended_at null = an open punch, with a partial unique index enforcing one open punch per contractor). A day's time_entries.hours is recomputed as Σ closed-segment durations by recomputeDayEntry (src/server/services/timeEntry.ts). time_entries stays the single daily aggregate every downstream consumer (pay, payroll, invoice variance, performance, overage, calendar) already reads — so no consumer changes.
  • Four configurable modes (punch, weekly_table, calendar_breakdown, calendar_total) stored in settings.time_entry_modes (enabled set + default), edited on an Owner-only Setup Menu (/admin/setup). The contractor calendar offers only the enabled modes.
  • Per-facility timezone. facilities.timezone (IANA; default America/New_York/EST·EDT, also the company operating_timezone default now). src/lib/dates/tz.ts is the pure wall-clock↔UTC boundary: businessDayInTz decides the day a punch belongs to; manual pairs are stored as UTC instants via zonedWallTimeToUtc. The rest of the date layer stays timezone-free UTC day math (ADR's existing invariant).
  • New week colours: weekColor → green = contracted met, yellow = overage, red = under (red as soon as a week is below contracted, including empty/future weeks).

Consequences

  • All time-entry writes still go through vetted service-role server actions (ADR-0004); RLS on time_segments mirrors time_entries as the read-path safety net.
  • The daily-total invariant means migrations to richer entry are non-breaking and reversible by mode toggle. Legacy hours-only days (and Hubstaff-fed in-house hours) keep working — they simply have no segments and display their total.
  • Overnight punches are attributed to the started_at business day; a day's hours clamp at 24.
  • src/db/types.ts regenerated from the live schema after migration 0020.