feat(votes): add close time and timezone pickers for vote deadlines (GH-2001) - #2193
feat(votes): add close time and timezone pickers for vote deadlines (GH-2001)#2193audigregorie wants to merge 6 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Essentials Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
PR SummaryMedium Risk Overview Display and countdowns across the votes table, cast/results drawers, and review step use a new The basics step adds time picker, filterable timezone select (DST-correct offset labels), zone-aware calendar Reviewed by Cursor Bugbot for commit ee816fd. Bugbot is set up for automated code reviews on this repo. Configure here. |
🚀 Deployment StatusYour branch has been deployed to: https://ui-pr-2193.dev.v2.cluster.linuxfound.info Deployment Details:
The deployment will be automatically removed when this PR is closed. |
There was a problem hiding this comment.
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.
Signed-off-by: Audi Young <audi.mycloud@gmail.com>
There was a problem hiding this comment.
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 AMinAmerica/New_Yorkis normalized byfromZonedTimeto 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
There was a problem hiding this comment.
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
deadlineinstant 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: atNov 1, 2026 2:30 AMwith 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);
Signed-off-by: Audi Young <audi.mycloud@gmail.com>
| return titleValid && committeeValid && eligibleParticipantsValid && closeDateValid; | ||
| const closeTimeValid = !!form.get('close_time')?.valid; | ||
| const timezoneValid = !!form.get('timezone')?.valid; | ||
| const deadlineValid = !form.errors?.['futureDateTime']; |
| const canCombine = !!closeDate && !!timezone && parseTime12Hour(closeTime) !== null; | ||
| const combined = canCombine ? combineDateTime(closeDate, closeTime, timezone) : ''; |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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 }; |
There was a problem hiding this comment.
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.
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; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit ee816fd. Configure here.


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
Reading the deadline
Countdown chips
Technical changes
packages/shared/src/interfaces/poll.interface.ts— Shared contractsend_time_timezone(IANA zone) toVote,CreateVoteRequest, andUpdateVoteRequest, andpoll_end_time_timezonetoVoteResultsResponse— optional so legacy votes stay validclose_time(12-hour string) andtimezonetoVoteFormValuepackages/shared/src/constants/timezones.constants.ts— ConstantsLEGACY_VOTE_TIMEZONE = 'America/Los_Angeles'as the canonical display zone for votes that predate stored timezonespackages/shared/src/utils/date-time.utils.ts— Date/time helpersformatVoteDeadline()— 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 countdownbuildTimezoneOptions()— timezone select options with UTC offsets computed per deadline date so labels stay correct across DST boundariespackages/shared/src/utils/vote.utils.ts— Vote mapping/buildersmapVoteToFormValue()hydratesclose_date/close_timezoned to the stored zone (legacy votes hydrate in Pacific), defaulting time to 11:59 PMcombineDateTime()and sendend_time_timezonepackages/shared/src/validators/vote.validators.ts— ValidatorsvoteDeadlineValidator()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 fileapps/lfx-one/src/app/modules/votes/vote-manage/vote-manage.component.ts— Vote formclose_time(default 11:59 PM) andtimezone(defaultgetUserTimezone()) controls; form deliberately stricter than the API contract sincecombineDateTimeneeds a zonevoteDeadlineValidatorwired in; step-1 validity now requires valid time, zone, and a future deadlineapps/lfx-one/src/app/modules/votes/components/vote-basics/vote-basics.component.ts, vote-basics.component.html— Basics steplfx-time-pickerand filterable Timezonelfx-selectwith per-date DST-correct offset labelsapps/lfx-one/src/app/modules/votes/components/vote-review/vote-review.component.ts, vote-review.component.html— Review stepcombineDateTime+formatVoteDeadlineinstead of aDatePipeon the raw date controlapps/lfx-one/src/app/shared/pipes/vote-deadline.pipe.ts, due-date-label.pipe.ts— PipesvoteDeadlinepipe wrappingformatVoteDeadline;dueDateLabelpipe now accepts the vote's timezone and delegates todaysUntilInTimezoneapps/lfx-one/src/app/modules/votes/components/votes-table, vote-cast-drawer, vote-results-drawer— Deadline surfacesend_time | date: 'MMM d, y'renderings replaced withvoteDeadline: end_time_timezone; drawer countdown usesdaysUntilInTimezone; 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