Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
13 KiB
Time Entry “Work Date” Plan (Option A)
Date: December 14, 2025
Status: Proposed
Owner: Time Tracking / Billing
Decisions (Confirmed)
work_dateis not user-editable.work_dateis computed in the user’s timezone (i.e., “the user’s calendar date”).- We will store the timezone used for computation on each entry as
work_timezone(for audit/debug).
Problem
Today, time entry → time sheet → time period mapping is effectively instant-based (start_time/end_time as timestamps) while time periods and the timesheet UX are calendar-based (“Apr 1–Apr 7”, “week of Apr 1”, etc.). Because instants are stored/compared in UTC-ish representations, users in non-UTC timezones can end up entering “Apr 1” locally while the system evaluates the entry against a different calendar day, creating confusing edge cases when determining the correct period_id/timesheet.
We want to keep the current architecture (persisted time_periods, time_sheets keyed by period_id, approvals, period CRUD, background period generation) while removing the conceptual mismatch between user-local calendar days and UTC instants.
Proposed Solution (Option A)
Introduce an explicit date-based bucketing field on time entries:
- Add
work_datetotime_entriesas a date (no time). - Define
work_dateas “the calendar date the work is attributed to” in the user’s timezone. - Determine the time period / timesheet association using
work_datevstime_periods.start_date/end_date(date-to-date comparisons), not using UTC instants.
We will still store start_time and end_time as instants for duration, auditability, ordering, and billing calculations, but grouping and period membership will use work_date.
Timezone Source of Truth
We need a stable source of truth for the user’s timezone at entry creation time.
Recommended resolution order:
users.timezone(preferred, already exists in schema)- Tenant default timezone (if we have one)
'UTC'(last resort)
Note: even if we fall back to a non-user timezone, we still persist work_timezone so we can detect and remediate missing user timezone configuration.
Goals
- Preserve existing persisted
time_periodsbehavior (listing, manual overrides, scheduled creation job). - Make timesheet bucketing consistent with “calendar day” as the UI expresses it.
- Minimize schema/code churn and keep backwards compatibility.
- Avoid retroactively changing historical timesheet membership when settings/timezones change.
Non-Goals
- Eliminating
time_periodsrecords. - Reworking the billing cycle subsystem.
- Changing approval semantics.
Data Model Changes
1) time_entries
Add:
work_date DATE NOT NULLwork_timezone TEXT NOT NULL- Stores the timezone used to compute
work_dateat create/update time (e.g.,America/Los_Angeles).
- Stores the timezone used to compute
Indexes (initial recommendation):
(tenant, user_id, work_date)(tenant, work_date)
2) (Optional) time_sheets snapshot fields
Not strictly required for Option A, but consider adding:
period_start_date DATEperiod_end_date DATE
This reduces join dependency for display and provides a historical snapshot even if periods are edited. If we keep period edit capability, we must decide whether edits should:
- update existing timesheets’ displayed boundaries, or
- be blocked once timesheets exist (current behavior blocks editing if sheets exist in
server/src/lib/actions/timePeriodsActions.ts).
Behavior Changes
A) Saving a time entry
When creating/updating a time entry:
- Compute
work_datefromstart_timeusing the resolved user timezone. - Persist
work_dateandwork_timezone. - Ignore any incoming
work_date/work_timezonefields from clients (not user-editable).
B) Choosing a timesheet / period for an entry
When an entry is created outside a timesheet context (API, quick-add):
- Find the
time_periodsrow where:start_date <= work_dateandend_date > work_date(treat periods as half-open[start,end)dates)
- Then
fetchOrCreateTimeSheet(user_id, period_id)and attachtime_sheet_id.
When an entry is created from a specific timesheet UI:
- Continue attaching directly to that
time_sheet_id/period_id. - Still compute/persist
work_datefor consistency and reporting.
C) Validation rule
Replace “time entry must fall within a valid time period” checks that currently compare instants with checks that compare work_date with period date boundaries.
Migration / Backfill Plan
Existing rows in time_entries need a backfilled work_date.
Constraints:
- Historical user timezone at time of entry may not be available.
Backfill strategy options:
- User timezone backfill (preferred): compute using
users.timezonewhen present. - Fallback backfill: if a user has no timezone, fall back to tenant default (if any) else
'UTC'.
Backfill must also populate work_timezone with whatever timezone was used per row so we can identify entries computed with fallback logic.
Implementation Surface (What Changes Where)
Project-Specific Gotchas / Clarifications
- Time periods are date-based: migrate
time_periods.start_date/end_datetoDATE(timezone-agnostic), keeping half-open[start,end)semantics. This eliminates server-timezone and cast-related ambiguity and makes period membership comparisons explicitly date-to-date. - Resolving
users.timezoneserver-side: server actions that computework_date(and “current period”) need a reliable way to fetch the current user and then readusers.timezone.- For interactive flows: resolve timezone from the authenticated user record.
- For non-interactive/system contexts (background jobs, service accounts): define an explicit fallback rule (tenant default or
'UTC') and persist that choice towork_timezone.
- Enterprise/Citus impact: EE has Citus distribution migrations that reference time/billing tables. Adding columns/indexes to
time_entriesshould be verified against EE distributed table behavior (and include any EE-specific migration steps if required). - Validation/test ripple: adding
work_date/work_timezonewill require updates across:- TypeScript interfaces (
server/src/interfaces/timeEntry.interfaces.ts) - Zod schemas used by UI/server actions (
server/src/lib/schemas/timeSheet.schemas.ts) and API schemas (server/src/lib/api/schemas/timeEntry.ts) - Test factories and fixtures that insert
time_entriesrows
- TypeScript interfaces (
- “Today” in UI: existing code uses UTC-derived strings (e.g.
new Date().toISOString().slice(0, 10)) for “today/current period” checks. These must be replaced with browser-local date logic anywhere they influence “current” labeling or default date selection.
Screens (UI)
server/src/components/time-management/time-entry/time-sheet/TimeEntryDialog.tsx- Ensure create/update calls do not send
work_date; server computes it. - Validate the date/time pickers produce correct instants for
start_time/end_time(avoid date-only parsing pitfalls).
- Ensure create/update calls do not send
server/src/components/time-management/time-entry/time-sheet/TimeSheetTable.tsx- Any “quick add” path that constructs timestamps from a displayed date should construct instants in the user’s timezone (not via
toISOString().slice(0,10)patterns).
- Any “quick add” path that constructs timestamps from a displayed date should construct instants in the user’s timezone (not via
server/src/components/time-management/time-entry/TimePeriodList.tsx- The “Current” period highlight should use the user’s local date (not UTC derived from
toISOString()).
- The “Current” period highlight should use the user’s local date (not UTC derived from
- “Create time entry from elsewhere” entry points that call
saveTimeEntry:server/src/components/time-management/interval-tracking/IntervalManagement.tsxserver/src/components/tickets/ticket/TicketDetails.tsxserver/src/components/projects/TaskForm.tsxserver/src/components/interactions/InteractionDetails.tsxserver/src/components/tickets/TicketingDashboard.tsxserver/src/components/user-activities/ActivityDetailViewerDrawer.tsx- Confirm these flows still land in the expected timesheet/period after
work_datebecomes authoritative.
Server Actions (Next.js “use server”)
server/src/lib/actions/timeEntryCrudActions.ts- Compute and persist
work_date/work_timezoneon create/update. - Ignore any client-supplied values for these fields.
- Compute and persist
server/src/lib/actions/timePeriodsActions.ts- Update
getCurrentTimePeriod()to compute “today” in the user’s timezone (not server timezone).
- Update
server/src/lib/actions/timeSheetOperations.ts- Ensure any “current period” or “entry belongs to period” logic is date-based where applicable.
API Services (REST-ish)
server/src/lib/api/services/TimeEntryService.ts- When creating entries (and when auto-creating timesheets), use
work_date(computed via user timezone) to find the correct period and timesheet. - Update
getTimePeriodForDate/getOrCreateTimeSheetlogic to compare using date values, notDateobjects interpreted in server timezone. - Update time-tracking session start/stop flows to persist
work_date/work_timezone.
- When creating entries (and when auto-creating timesheets), use
server/src/lib/api/services/TimeSheetService.ts- If any endpoints derive “current period” or generate periods based on “today”, ensure “today” is user-local where needed (or explicitly tenant-local if it’s admin/system).
Billing / Background Jobs
server/src/lib/billing/billingEngine.tsrolloverUnapprovedTime(...)mutatesstart_time/end_time; it must recompute and persistwork_dateaccordingly (using the entry’swork_timezoneif available, otherwise resolve via user timezone).
Types / Schemas
server/src/interfaces/timeEntry.interfaces.ts- Add
work_dateandwork_timezoneto the time entry interface(s) as appropriate.
- Add
- API Zod schemas in
server/src/lib/api/schemas/timeEntry.ts- Ensure
work_date/work_timezoneare not client-writable; return them in responses if useful.
- Ensure
Phases & To-Dos
Phase 0 — Confirm invariants + timezone readiness
- Confirm
users.timezoneis populated in real environments; if not, define how it gets set (e.g., profile screen, onboarding, or a one-time “set timezone” prompt). - Decide how strictly to enforce missing
users.timezone(warn + fallback vs block time entry creation until set). - Align on the definition of “user’s timezone” for service accounts/API clients (what timezone do we use when there is no interactive user?).
Phase 1 — Database migration + backfill
- Add
work_dateandwork_timezonecolumns totime_entries(start nullable). - Migrate
time_periods.start_date/end_datetoDATE(timezone-agnostic) and update any dependent code/queries accordingly. - Backfill existing rows:
- Join to
usersto computework_dateusingusers.timezonewhen present. - Use fallback timezone when missing, and persist the fallback into
work_timezone.
- Join to
- Add NOT NULL constraints after backfill.
- Add indexes for expected query patterns (
(tenant, user_id, work_date),(tenant, work_date)).
Phase 2 — Server actions write-path
- Update
saveTimeEntryflow to compute/persistwork_date/work_timezonefromstart_timein the user timezone. - Ensure updates that modify
start_timealso updatework_date(and keepwork_timezoneconsistent). - Ensure client-supplied
work_date/work_timezoneare ignored/rejected.
Phase 3 — “Current period” and period membership logic
- Update
getCurrentTimePeriod()to use the user’s local date (derived fromusers.timezone) when selecting a period. - Audit any remaining “find period for date” logic to ensure it compares using date values and consistent half-open boundaries.
Phase 4 — REST API alignment
- Update
TimeEntryServicecreate/update paths to compute/persistwork_date/work_timezone. - Update time-tracking session start/stop to populate these fields.
- Update period lookup + auto-timesheet creation to use
work_daterather than server-interpretedDatecomparisons. - Update API schemas to treat
work_date/work_timezoneas server-controlled fields.
Phase 5 — Frontend adjustments and regression fixes
- Update any UI “current period” indicator logic that uses UTC date strings to use the browser’s local date.
- Audit “quick add”/inline creation code paths that build timestamps from a displayed date and ensure they produce correct instants.
- Ensure all entry-creation entry points remain functional and land in the expected timesheet.
Phase 6 — Billing/job correctness
- Update
BillingEngine.rolloverUnapprovedTime(...)to recomputework_datewhenever it adjusts timestamps. - Search for any other code paths that mutate
time_entries.start_time/end_timeand ensure they also maintainwork_date.
Phase 7 — Tests, rollout, monitoring
- Add/extend tests covering timezone edge cases (entries around local midnight mapping to the expected period).
- Add a lightweight metric/log for “missing user timezone used fallback” to drive cleanup.
- Roll out behind a feature flag if desired, or deploy as a schema-first migration with dual-write then cutover.
Open Questions
- For API integrations (no browser context), what is the expected timezone rule for
work_date?