Skip to content

feat(votes): add close time and timezone pickers for vote deadlines (GH-2001) - #2193

Open
audigregorie wants to merge 6 commits into
mainfrom
feat/gh-2001-vote-close-tz-pickers
Open

feat(votes): add close time and timezone pickers for vote deadlines (GH-2001)#2193
audigregorie wants to merge 6 commits into
mainfrom
feat/gh-2001-vote-close-tz-pickers

Conversation

@audigregorie

Copy link
Copy Markdown
Contributor

Summary

Vote deadlines were date-only: organizers picked a close date and the vote closed at whatever time the stored timestamp landed on, displayed in each viewer's browser locale. This PR adds explicit close-time and timezone pickers to the vote form, stores the organizer's chosen IANA zone on the vote, and renders every deadline surface (form review, votes table, cast/results drawers) in the vote's own zone — with legacy votes that predate stored zones consistently read in Pacific time.

Resolves GH-2001

Behavior changes

Setting the close deadline

Before After
Organizers picked only a close date, and it had to be tomorrow or later. The close time was implicit and invisible. Organizers pick a close date, an exact close time (defaults to 11:59 PM), and a timezone (defaults to their own). Same-day deadlines are allowed as long as the combined date/time is still in the future in the chosen zone. The review step shows the full deadline with timezone, e.g. "Nov 15, 2026 5:00 PM PST".

Reading the deadline

Before After
Deadlines showed as a bare date ("Nov 15, 2026") in the viewer's local timezone, so the same vote could appear to close on different days for different viewers. Deadlines show the exact date, time, and timezone abbreviation in the vote's own zone everywhere — votes table, cast drawer, results drawer. Votes created before this change display in Pacific time, matching how their deadlines were originally authored.

Countdown chips

Before After
The "closes in N days" chips counted calendar days in the viewer's local zone, which could disagree with the displayed date. Countdowns count calendar days in the vote's zone, so the chip and the absolute deadline always agree. The results drawer now shows the absolute deadline as plain text next to the chip instead of hiding it in a tooltip.

Technical changes

packages/shared/src/interfaces/poll.interface.ts — Shared contracts

  • Added optional end_time_timezone (IANA zone) to Vote, CreateVoteRequest, and UpdateVoteRequest, and poll_end_time_timezone to VoteResultsResponse — optional so legacy votes stay valid
  • Added close_time (12-hour string) and timezone to VoteFormValue

packages/shared/src/constants/timezones.constants.ts — Constants

  • Added LEGACY_VOTE_TIMEZONE = 'America/Los_Angeles' as the canonical display zone for votes that predate stored timezones

packages/shared/src/utils/date-time.utils.ts — Date/time helpers

  • formatVoteDeadline() — formats a deadline as "MMM d, yyyy h:mm a zzz" in the vote's zone with legacy-Pacific fallback (also on invalid zone input)
  • daysUntilInTimezone() — whole-calendar-day countdown read in the vote's zone, single source for the table chip and drawer countdown
  • buildTimezoneOptions() — timezone select options with UTC offsets computed per deadline date so labels stay correct across DST boundaries

packages/shared/src/utils/vote.utils.ts — Vote mapping/builders

  • mapVoteToFormValue() hydrates close_date/close_time zoned to the stored zone (legacy votes hydrate in Pacific), defaulting time to 11:59 PM
  • All four request builders (create/update × live/draft) combine date + time + zone via combineDateTime() and send end_time_timezone

packages/shared/src/validators/vote.validators.ts — Validators

  • New voteDeadlineValidator() group validator: combined close date/time must be in the future in the chosen timezone (same-day allowed); moved from the generic date validators into the vote domain file

apps/lfx-one/src/app/modules/votes/vote-manage/vote-manage.component.ts — Vote form

  • New required close_time (default 11:59 PM) and timezone (default getUserTimezone()) controls; form deliberately stricter than the API contract since combineDateTime needs a zone
  • Group-level voteDeadlineValidator wired in; step-1 validity now requires valid time, zone, and a future deadline

apps/lfx-one/src/app/modules/votes/components/vote-basics/vote-basics.component.ts, vote-basics.component.html — Basics step

  • Close Time lfx-time-picker and filterable Timezone lfx-select with per-date DST-correct offset labels
  • Calendar min-date floor relaxed to today (cosmetic only — the group validator does the real zone-aware future check); the future-deadline error surfaces unconditionally in edit mode since a hydrated lapsed draft is invalid before any touch

apps/lfx-one/src/app/modules/votes/components/vote-review/vote-review.component.ts, vote-review.component.html — Review step

  • "Close Date" row is now "Close Date & Time", rendered via combineDateTime + formatVoteDeadline instead of a DatePipe on the raw date control

apps/lfx-one/src/app/shared/pipes/vote-deadline.pipe.ts, due-date-label.pipe.ts — Pipes

  • New voteDeadline pipe wrapping formatVoteDeadline; dueDateLabel pipe now accepts the vote's timezone and delegates to daysUntilInTimezone

apps/lfx-one/src/app/modules/votes/components/votes-table, vote-cast-drawer, vote-results-drawer — Deadline surfaces

  • All end_time | date: 'MMM d, y' renderings replaced with voteDeadline: end_time_timezone; drawer countdown uses daysUntilInTimezone; results drawer shows the absolute deadline as visible text next to the countdown chip (tooltip removed)

packages/shared/src/utils/date-time.utils.spec.ts, vote.utils.spec.ts, validators/vote.validators.spec.ts — Tests

  • Coverage for the new formatting/countdown helpers, timezone hydration and request-building in vote utils, and the group deadline validator

…strictness (GH-2001)

Signed-off-by: Audi Young <audi.mycloud@gmail.com>
…s comments (GH-2001)

Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI balanced review requested due to automatic review settings September 4, 2026 10:35
@audigregorie
audigregorie requested a review from a team as a code owner September 4, 2026 10:35
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Essentials

Run ID: 2c992437-7227-4caa-92ed-013419fe348d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Sep 4, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes how vote deadlines are authored, validated, and sent to the API (end_time + end_time_timezone), with DST edge-case handling; incorrect logic could shift close times or block valid deadlines, though legacy Pacific fallback and tests mitigate regression risk.

Overview
Vote organizers now set an explicit close time and IANA timezone (defaults: 11:59 PM and the viewer’s zone) instead of a date-only close that implied an unclear instant. Create/update payloads combine date, time, and zone into end_time and send optional end_time_timezone; legacy votes without a stored zone still display in Pacific via LEGACY_VOTE_TIMEZONE.

Display and countdowns across the votes table, cast/results drawers, and review step use a new voteDeadline pipe and shared formatVoteDeadline / daysUntilInTimezone so absolute deadlines and “closes in N days” chips use the vote’s zone, not each viewer’s locale. The results drawer shows the full deadline beside the countdown chip (tooltip removed). Survey table countdowns pass the viewer’s timezone into dueDateLabel so chips align with the adjacent local date.

The basics step adds time picker, filterable timezone select (DST-correct offset labels), zone-aware calendar minDate, and group-level voteDeadlineValidator (future deadline + spring-forward nonexistent wall time rejection). Draft save paths use resolveDraftEndTime when fields are incomplete.

Reviewed by Cursor Bugbot for commit ee816fd. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

🚀 Deployment Status

Your branch has been deployed to: https://ui-pr-2193.dev.v2.cluster.linuxfound.info

Deployment Details:

  • Environment: Development
  • Namespace: ui-pr-2193
  • ArgoCD App: ui-pr-2193

The deployment will be automatically removed when this PR is closed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds timezone-aware vote deadlines for GH-2001, covering authoring, validation, persistence, and display.

Changes:

  • Adds close-time/timezone fields and shared deadline utilities.
  • Updates vote forms, tables, and drawers.
  • Adds unit coverage for timezone behavior.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/shared/src/validators/vote.validators.ts Adds deadline validation.
packages/shared/src/validators/vote.validators.spec.ts Tests deadline validation.
packages/shared/src/validators/date.validators.ts Uses type-only imports.
packages/shared/src/utils/vote.utils.ts Maps and builds zoned deadlines.
packages/shared/src/utils/vote.utils.spec.ts Tests deadline mapping/builders.
packages/shared/src/utils/date-time.utils.ts Adds formatting and countdown helpers.
packages/shared/src/utils/date-time.utils.spec.ts Tests timezone helpers.
packages/shared/src/interfaces/poll.interface.ts Extends vote contracts.
packages/shared/src/constants/timezones.constants.ts Defines the legacy vote zone.
apps/lfx-one/src/app/shared/pipes/vote-deadline.pipe.ts Adds deadline formatting pipe.
apps/lfx-one/src/app/shared/pipes/due-date-label.pipe.ts Makes countdowns timezone-aware.
apps/lfx-one/src/app/modules/votes/vote-manage/vote-manage.component.ts Adds deadline form controls.
apps/lfx-one/src/app/modules/votes/components/votes-table/votes-table.component.ts Registers deadline pipe.
apps/lfx-one/src/app/modules/votes/components/votes-table/votes-table.component.html Displays zoned deadlines.
apps/lfx-one/src/app/modules/votes/components/vote-review/vote-review.component.ts Builds the reviewed deadline.
apps/lfx-one/src/app/modules/votes/components/vote-review/vote-review.component.html Shows date, time, and zone.
apps/lfx-one/src/app/modules/votes/components/vote-results-drawer/vote-results-drawer.component.ts Computes zoned result deadlines.
apps/lfx-one/src/app/modules/votes/components/vote-results-drawer/vote-results-drawer.component.html Updates result deadline displays.
apps/lfx-one/src/app/modules/votes/components/vote-cast-drawer/vote-cast-drawer.component.ts Registers deadline pipe.
apps/lfx-one/src/app/modules/votes/components/vote-cast-drawer/vote-cast-drawer.component.html Displays the zoned deadline.
apps/lfx-one/src/app/modules/votes/components/vote-basics/vote-basics.component.ts Builds timezone options and date floor.
apps/lfx-one/src/app/modules/votes/components/vote-basics/vote-basics.component.html Adds time and timezone pickers.
Suppressed comments (1)

packages/shared/src/utils/vote.utils.ts:335

  • The draft-update path has the same partial-form failure: a retained date plus an empty or invalid time produces end_time: '', causing the save request to fail instead of using the documented draft default. Fall back whenever the combined value is empty.
    end_time: formValue.close_date
      ? combineDateTime(formValue.close_date, formValue.close_time, formValue.timezone)
      : addDays(new Date(), DRAFT_VOTE_DEFAULT_DURATION_DAYS).toISOString(),
    end_time_timezone: formValue.timezone || undefined,

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/shared/src/utils/vote.utils.ts Outdated
Comment thread packages/shared/src/validators/vote.validators.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread apps/lfx-one/src/app/shared/pipes/due-date-label.pipe.ts
Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI review requested due to automatic review settings September 4, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

packages/shared/src/validators/vote.validators.ts:104

  • This accepts nonexistent wall-clock times during a DST spring-forward gap. For example, Mar 8, 2026 2:30 AM in America/New_York is normalized by fromZonedTime to a different instant, so the request is accepted but later renders as 1:30 AM rather than the organizer's chosen 2:30 AM. After combining, round-trip the instant into the selected zone and reject it when the resulting date/time differs from the controls, with a dedicated form error explaining that the time does not exist in that timezone.

…se-tz-pickers

Signed-off-by: Audi Young <audi.mycloud@gmail.com>

# Conflicts:
#	apps/lfx-one/src/app/modules/surveys/components/surveys-table/surveys-table.component.ts
Copilot AI review requested due to automatic review settings September 7, 2026 09:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

apps/lfx-one/src/app/modules/votes/components/vote-basics/vote-basics.component.ts:58

  • The deadline instant was resolved in the currently selected timezone, then reused to calculate every candidate zone's offset. On transition days that gives the offset at the same instant, not at the selected wall time in each option: at Nov 1, 2026 2:30 AM with New York selected, Los Angeles is labeled -07:00 (its offset at 07:30Z), but choosing Los Angeles schedules 2:30 AM after its fallback at -08:00. Build each option's instant by interpreting the selected date/time in that option's own zone.
      const combined = closeTime ? combineDateTime(closeDate, closeTime, timezone) : '';
      const deadline = combined ? new Date(combined) : closeDate;
      const options = buildTimezoneOptions(deadline);

Comment thread packages/shared/src/utils/vote.utils.ts Outdated
Comment thread packages/shared/src/validators/vote.validators.ts
Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI review requested due to automatic review settings September 7, 2026 12:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.

return titleValid && committeeValid && eligibleParticipantsValid && closeDateValid;
const closeTimeValid = !!form.get('close_time')?.valid;
const timezoneValid = !!form.get('timezone')?.valid;
const deadlineValid = !form.errors?.['futureDateTime'];
Comment on lines +267 to +268
const canCombine = !!closeDate && !!timezone && parseTime12Hour(closeTime) !== null;
const combined = canCombine ? combineDateTime(closeDate, closeTime, timezone) : '';

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ee816fd. Configure here.

// gap (e.g. Mar 8 2026 2:30 AM in America/New_York) — fromZonedTime normalizes it to a different
// local time, so reject rather than store a deadline ~1h off the organizer's exact selection.
if (!wallTimeExistsInTimezone(closeDate, closeTime, timezone)) {
return { nonexistentWallTime: true };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DST gap error does not block Next

Medium Severity

Step 1 validity only treats futureDateTime as a blocking group error, so a nonexistentWallTime DST-gap deadline still enables Next. The organizer can leave step 1 with the error visible, review a fromZonedTime-shifted close time, and only hit a silent form.invalid reject on Open Vote. Draft save still persists that shifted instant.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ee816fd. Configure here.

// date/time unconverted — converting through the selected zone first would label every other
// zone for the wrong wall time across a DST boundary (Mar 8 3:30 AM Tokyo → New York is -04:00, not -05:00).
const parsed = closeTime ? parseTime12Hour(closeTime) : null;
const deadline = parsed ? new Date(closeDate.getFullYear(), closeDate.getMonth(), closeDate.getDate(), parsed.hours, parsed.minutes) : closeDate;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timezone offsets use browser-local instant

Medium Severity

Offset labels now pass a browser-local Date into getTimezoneOffset, which reads the Date as a UTC instant, not as wall-clock fields in each candidate zone. When the viewer zone differs from the selected zone across a DST boundary, the selected timezone’s own offset is wrong — the case the previous instant-via-combineDateTime path had fixed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ee816fd. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let organizers set vote close date, time, and timezone

2 participants