Skip to content

feat: disable past dates in Pay Schedule first pay date picker#1311

Open
larchai wants to merge 7 commits intoGusto:mainfrom
larchai:feat/SDK-530-disable-past-dates-pay-schedule
Open

feat: disable past dates in Pay Schedule first pay date picker#1311
larchai wants to merge 7 commits intoGusto:mainfrom
larchai:feat/SDK-530-disable-past-dates-pay-schedule

Conversation

@larchai
Copy link
Copy Markdown
Contributor

@larchai larchai commented Mar 20, 2026

Summary

  • Adds minDate, maxDate, and isDateDisabled props to the DatePicker component adapter interface
  • Passes minDate={new Date()} on the Pay Schedule anchorPayDate field to prevent past date selection
  • Dynamically renders processing time copy ("1 day" / "2 days" / "4 days") based on the company's actual ACH speed instead of hardcoding "2 days"
  • Fixes a UTC timezone bug in dateToCalendarDate identified by Copilot review

Resolves SDK-530
Partially addresses SDK-592 (Pay Schedule component only; Off-Cycle Pay Period and Contractor Payments still have hardcoded copy)

What changed

SDK-530: Disable past dates (3 files)

File Change
DatePickerTypes.ts Added 3 new optional props: minDate?: Date, maxDate?: Date, isDateDisabled?: (date: Date) => boolean
DatePicker.tsx Converts minDate→minValue, maxDate→maxValue, isDateDisabled→isDateUnavailable internally for react-aria. Uses local date extraction to avoid UTC shift.
Edit.tsx Passes minDate={new Date()} on the anchorPayDate DatePickerField

SDK-592: Dynamic processing time copy (4 files)

File Change
usePaySchedule.ts Added paymentSpeed?: string to PayScheduleContextType
PaySchedule.tsx Fetches company payment speed via usePaymentConfigsGet() (same hook used in Contractor/Payroll domains) and passes it through context
Edit.tsx Passes count parameter to i18n translation based on paymentSpeed (extracts number from "1-day"/"2-day"/"4-day"), defaults to 2
Company.PaySchedule.json Replaced hardcoded copy with i18next pluralized keys (_one / _other) using {{count}} interpolation

Copilot feedback: UTC timezone fix (1 file)

File Change
DatePicker.tsx dateToCalendarDate now uses local date methods (getFullYear/getMonth/getDate) instead of formatDateToStringDate (which uses toISOString()/UTC). Also returns undefined for invalid dates instead of throwing.

Auto-generated

File Change
component-inventory.md Auto-generated docs for new DatePicker adapter props
i18next.d.ts Auto-generated types for pluralized translation keys

Component adapter considerations

New DatePickerProps follow ecosystem conventions (surveyed MUI, Ant Design, react-datepicker, react-aria, shadcn/ui, Chakra UI):

  • minDate/maxDate with native JS Date type — most common naming convention, no @internationalized/date dependency for partners
  • isDateDisabled — combines react-aria's predicate pattern with universal "disabled" terminology
  • All props are optional and additive — existing custom adapters continue working unchanged

Design decisions

  • No Zod validation for past dates — The UI constraint (minDate) is sufficient. Adding schema validation would require converting to a dynamic schema factory for i18n support, which is disproportionate complexity.
  • No minDate enforcement for processing buffer — We considered setting minDate to account for ACH processing days but decided this is better enforced server-side via API validation rather than client-side in the SDK.
  • SDK-592 scoped to Pay Schedule only — The same hardcoded "2 days" issue exists in Off-Cycle Pay Period and Contractor Payments components but is left for a follow-up.
  • Local date extraction over formatDateToStringDatedateToCalendarDate uses getFullYear()/getMonth()/getDate() instead of the shared formatDateToStringDate utility because the utility uses toISOString() (UTC), which can shift the date by one day in non-UTC timezones. This was flagged by Copilot and verified locally.

Test plan

  • Verify past dates are greyed out / non-clickable in the First Pay Date calendar picker
  • Verify today's date is still selectable (minValue is inclusive)
  • Verify anchorEndOfPayPeriod (First Pay Period End Date) is NOT affected
  • Verify description copy dynamically reflects company ACH speed (1-day → "1 day", 2-day → "2 days", 4-day → "4 days")
  • Verify description falls back to "2 days" if payment config is unavailable
  • Verify component adapter compatibility — default react-aria DatePicker renders constraints correctly
  • Run existing tests pass: npm run test -- --run

🤖 Generated with Claude Code

Comment on lines +29 to +33
function dateToCalendarDate(date: Date): CalendarDate {
return parseDate(
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`,
)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd ask claude to check if there are existing date utilities in the codebase that could be leveraged here instead

OR

If not, does it make sense to move this function to be with existing date utils

Comment on lines +30 to +40
anchorPayDate: z
.date()
.refine(
date => {
const today = new Date()
today.setHours(0, 0, 0, 0)
return date >= today
},
{ message: 'First pay date cannot be in the past' },
)
.optional(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For simplicity we can likely just roll back this change and keep the schema as is. The UI will already prevent them from selecting a date in the past and this is kinda just an abundance of caution thing

if we do go with this, we'll need to update the schema to be a function so that we can dynamically provide translations here like elsewhere in the codebase, and then wire this up to the date picker error message so that it properly gets it. it's a lot of work for something where we could just rely on disabled dates working in the UI

Copy link
Copy Markdown
Member

@serikjensen serikjensen left a comment

Choose a reason for hiding this comment

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

Code looks good! we should figure out a way for you to check your changes

serikjensen

This comment was marked as duplicate.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to prevent selecting past dates for the Pay Schedule “First pay date” by adding date-constraint props to the DatePicker adapter interface, wiring them through the default react-aria DatePicker implementation, and applying a minimum date in the Pay Schedule edit form (with docs updates for adapter consumers).

Changes:

  • Extend DatePickerProps with minDate, maxDate, and isDateDisabled.
  • Map those props to react-aria minValue, maxValue, and isDateUnavailable in the default DatePicker implementation.
  • Apply minDate={new Date()} to the Pay Schedule anchorPayDate field and document the new adapter props.

Reviewed changes

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

File Description
src/components/Company/PaySchedule/_parts/Edit.tsx Applies a minimum selectable date to the “First pay date” field.
src/components/Common/UI/DatePicker/DatePickerTypes.ts Adds new optional date-constraint props to the adapter interface.
src/components/Common/UI/DatePicker/DatePicker.tsx Converts adapter props to react-aria constraint props and passes them through.
docs/component-adapter/component-inventory.md Documents the new DatePicker adapter props for partners.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +30 to +31
const dateString = formatDateToStringDate(date)
if (!dateString) throw new Error('Invalid date provided to dateToCalendarDate')
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

dateToCalendarDate relies on formatDateToStringDate, which uses Date#toISOString() (UTC) to derive the YYYY-MM-DD. For users not in UTC, this can shift the day (e.g., evening in US timezones becomes tomorrow in UTC), making minValue/maxValue off by one and potentially disabling today's date. Convert Date to a calendar date using local year/month/day (or a dedicated @internationalized/date helper) instead of toISOString().

Suggested change
const dateString = formatDateToStringDate(date)
if (!dateString) throw new Error('Invalid date provided to dateToCalendarDate')
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new Error('Invalid date provided to dateToCalendarDate')
}
const year = date.getFullYear()
const month = date.getMonth() + 1 // getMonth() is 0-indexed
const day = date.getDate()
const dateString = [
String(year).padStart(4, '0'),
String(month).padStart(2, '0'),
String(day).padStart(2, '0'),
].join('-')

Copilot uses AI. Check for mistakes.

function dateToCalendarDate(date: Date): CalendarDate {
const dateString = formatDateToStringDate(date)
if (!dateString) throw new Error('Invalid date provided to dateToCalendarDate')
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

dateToCalendarDate throws if passed an invalid Date. Since minDate/maxDate come from external adapters, a single invalid value would crash rendering. Consider handling invalid dates gracefully (e.g., treat as undefined and optionally log) rather than throwing.

Suggested change
if (!dateString) throw new Error('Invalid date provided to dateToCalendarDate')
if (!dateString || Number.isNaN(date.getTime())) {
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn('dateToCalendarDate received an invalid Date and will use a fallback value:', date)
}
const fallbackString = formatDateToStringDate(new Date()) || '1970-01-01'
return parseDate(fallbackString)
}

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +87
// Convert date constraint props to react-aria DateValue format
const minValue = minDate ? dateToCalendarDate(minDate) : undefined
const maxValue = maxDate ? dateToCalendarDate(maxDate) : undefined
const isDateUnavailable = isDateDisabled
? (dateValue: DateValue) => {
const jsDate = calendarDateValueToDate(dateValue)
return jsDate ? isDateDisabled(jsDate) : false
}
: undefined
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

New constraint props (minDate/maxDate/isDateDisabled) introduce user-facing behavior changes, but there are no accompanying tests to verify that past dates are actually disabled and that minDate is inclusive (today selectable). Since this component already has a test suite, please add coverage for the new props (and their conversion to react-aria’s minValue/maxValue/isDateUnavailable).

Copilot uses AI. Check for mistakes.
@jeffredodd jeffredodd force-pushed the feat/SDK-530-disable-past-dates-pay-schedule branch from 021c533 to 7581368 Compare March 25, 2026 17:52
@larchai
Copy link
Copy Markdown
Contributor Author

larchai commented Mar 27, 2026

Addressed Copilot feedback (latest commit)

Timezone bug fix: dateToCalendarDate was using formatDateToStringDate which internally calls Date#toISOString() (UTC). For users in western timezones, this could shift the day forward — e.g., 5pm PST on March 27 becomes March 28 in UTC, potentially disabling today's date incorrectly. Now uses local date parts (getFullYear/getMonth/getDate) to construct the date string, avoiding the UTC conversion entirely.

Graceful handling of invalid dates: Since minDate/maxDate are part of the public adapter interface, a partner could pass an invalid Date. Instead of throwing (which would crash rendering), dateToCalendarDate now returns undefined, and the constraint is simply skipped.

Not addressed (intentionally):

  • Copilot flagged the missing Zod .refine() — Steve already reviewed and approved omitting this (UI constraint is sufficient; adding schema validation would require dynamic schema factory for i18n).
  • Copilot suggested adding tests for the new props — this is a valid suggestion for a follow-up but out of scope for this PR per team discussion.

Comment on lines +230 to +241
if (offsetMinutes > 0) {
// Machine is west of UTC (Americas) — the bug manifests
expect(localBasedResult).toBe('2024-03-26')
expect(utcBasedResult).not.toBe(localBasedResult)
} else if (offsetMinutes === 0) {
// Machine is in UTC — both agree (bug hidden but still latent)
expect(localBasedResult).toBe('2024-03-27')
expect(utcBasedResult).toBe(localBasedResult)
} else {
// Machine is east of UTC (Europe, Asia) — local date is even further ahead
// At this UTC time, some eastern zones are already March 27 locally too
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should probably just remove this test. having conditionals in tests like this is not great practice and was likely just done to prove something out locally.

@larchai larchai enabled auto-merge (squash) March 27, 2026 22:17
larchai and others added 7 commits March 27, 2026 18:28
Adds minDate, maxDate, and isDateDisabled props to the DatePicker
component adapter interface, and uses minDate on the Pay Schedule
anchorPayDate field to prevent selection of past dates.

Also adds Zod validation on anchorPayDate as a safety net for
programmatic submissions.

Resolves SDK-530

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reuse existing formatDateToStringDate utility in dateToCalendarDate
  instead of duplicating date string formatting logic
- Revert Zod schema validation for anchorPayDate — the UI constraint
  (minDate) is sufficient and adding validation would require
  dynamic schema for i18n support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Regenerates component-inventory.md to include the new minDate,
maxDate, and isDateDisabled props added to DatePickerProps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetches company payment speed via usePaymentConfigsGet and renders
the Pay Schedule first pay date description dynamically (1/2/4 days)
instead of hardcoding "2 days". Falls back to 2 if unavailable.

Uses i18next count-based pluralization for correct "day" vs "days".

Partially addresses SDK-592 (Pay Schedule only; Off-Cycle Pay Period
and Contractor Payments still have hardcoded copy)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use local date parts (getFullYear/getMonth/getDate) instead of
  formatDateToStringDate which uses toISOString (UTC). This prevents
  timezone-related off-by-one errors where evening times in US
  timezones would shift to the next day in UTC.
- Return undefined instead of throwing on invalid dates, since
  minDate/maxDate come from external adapters and an invalid value
  should not crash rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds two test cases proving that formatDateToStringDate (which uses
toISOString/UTC) can return a different calendar date than the user's
local date. This documents the latent bug that motivated using local
date extraction (getFullYear/getMonth/getDate) in dateToCalendarDate.

Also includes auto-generated i18next type update for pluralized
anchorPayDateDescription keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove conditional test cases that were used to locally prove the
UTC timezone bug in formatDateToStringDate. As Steve noted, conditionals
in tests are bad practice — these served their purpose as local proof
and don't belong in the test suite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@larchai larchai force-pushed the feat/SDK-530-disable-past-dates-pay-schedule branch from 95f29b2 to ce13be7 Compare March 27, 2026 22:30
@jeffredodd jeffredodd disabled auto-merge March 27, 2026 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants