Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useJobForm } from '../../shared/useJobForm'
import { useCompensationForm } from '../../shared/useCompensationForm'
import { AddCompensationFormBody } from '../../shared/AddCompensationFormBody'
import styles from './AddAnotherJob.module.scss'
import { normalizeToDate } from '@/helpers/dateFormatting'
import { BaseBoundaries, BaseLayout, type CommonComponentInterface } from '@/components/Base'
import type { OnEventType } from '@/components/Base/useBase'
import { Form } from '@/components/Common/Form'
Expand Down Expand Up @@ -74,13 +73,6 @@ function Root({
// since useJobForm has already loaded it.
const primaryHireDate = jobForm.data.jobs?.find(j => j.primary)?.hireDate ?? undefined

const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)

const primaryHireDateParsed = normalizeToDate(primaryHireDate)
const minEffectiveDate =
primaryHireDateParsed && primaryHireDateParsed > tomorrow ? primaryHireDateParsed : tomorrow

const submitResult = composeSubmitHandler([jobForm, compensationForm], async () => {
const jobResult = await jobForm.actions.onSubmit({ employeeId, hireDate: primaryHireDate })
if (!jobResult) return
Expand Down Expand Up @@ -118,7 +110,6 @@ function Root({
submitCtaLabel={t('saveNewJobCta')}
isPending={isPending}
onCancel={onCancel}
minEffectiveDate={minEffectiveDate}
/>
</Form>
</BaseLayout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,6 @@ describe('management/EditCompensation', () => {
})

it('shows the secondary-jobs warning when scheduling a future FLSA change away from Nonexempt', async () => {
// Scheduling a future non-Nonexempt comp will delete secondary jobs at the
// effective date. The warning must fire in create mode too — the date field
// stays editable (unlike update mode where it is forced to today).
server.use(
Expand All @@ -415,7 +414,7 @@ describe('management/EditCompensation', () => {

expect(
await screen.findByText(
"Scheduling this classification change will delete the employee's additional jobs when it goes into effect.",
"Changing this employee's classification will immediately delete their additional jobs.",
),
).toBeInTheDocument()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import styles from './EditPendingCompensation.module.scss'
import { BaseBoundaries, BaseLayout, type CommonComponentInterface } from '@/components/Base'
import type { OnEventType } from '@/components/Base/useBase'
import { Form } from '@/components/Common/Form'
import { addDays, normalizeToDate } from '@/helpers/dateFormatting'
import { useComponentDictionary, useI18n } from '@/i18n'
import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler'
import { composeSubmitHandler } from '@/partner-hook-utils/form/composeSubmitHandler'
Expand Down Expand Up @@ -96,19 +95,6 @@ function Root({
return <BaseLayout isLoading error={loadingErrorHandling.errors} />
}

// Secondary new job: effective date must be on or after the secondary job's
// hire date (and at least tomorrow) so we don't set a date before the job
// even starts.
const minEffectiveDate =
isNewJob && !isPrimaryJob && jobForm.data.currentJob?.hireDate
? new Date(
Math.max(
addDays(new Date(), 1).getTime(),
(normalizeToDate(jobForm.data.currentJob.hireDate) ?? addDays(new Date(), 1)).getTime(),
),
)
: undefined

const submitResult = composeSubmitHandler([jobForm, compensationForm], async () => {
// For a primary new job, the user edits the hire date field. We read it
// back here and pass it to the comp submit so both the job's hire_date and
Expand Down Expand Up @@ -155,7 +141,6 @@ function Root({
submitCtaLabel={t('management.saveCta')}
isPending={isPending}
onCancel={onCancel}
minEffectiveDate={minEffectiveDate}
/>
</Form>
</BaseLayout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ActionsLayout, Flex } from '@/components/Common'
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
import { FLSA_OVERTIME_SALARY_LIMIT } from '@/shared/constants'
import useNumberFormatter from '@/hooks/useNumberFormatter'
import { addDays } from '@/helpers/dateFormatting'

export interface ManagementCompensationFormBodyProps {
jobForm: UseJobFormReady
Expand All @@ -17,9 +16,6 @@ export interface ManagementCompensationFormBodyProps {
submitCtaLabel: string
isPending: boolean
onCancel?: () => void
/** Override the minimum selectable date for the Effective date field. Defaults to tomorrow.
* Used by EditPendingCompensation for secondary new jobs where the floor is the job's hire date. */
minEffectiveDate?: Date
}

/**
Expand All @@ -36,7 +32,6 @@ export function ManagementCompensationFormBody({
submitCtaLabel,
isPending,
onCancel,
minEffectiveDate,
}: ManagementCompensationFormBodyProps) {
const { t } = useTranslation('Employee.Compensation')
const Components = useComponentContext()
Expand All @@ -51,7 +46,7 @@ export function ManagementCompensationFormBody({

{compensationForm.status.willDeleteSecondaryJobs && (
<Components.Alert
label={t('management.scheduledClassificationChangeNotification')}
label={t('validations.classificationChangeNotification')}
status="warning"
/>
)}
Expand Down Expand Up @@ -117,15 +112,10 @@ export function ManagementCompensationFormBody({
{CompFields.EffectiveDate && (
<CompFields.EffectiveDate
label={t('effectiveDateLabel')}
minDate={minEffectiveDate ?? addDays(new Date(), 1)}
maxDate={
compensationForm.data.maximumEffectiveDate
? new Date(compensationForm.data.maximumEffectiveDate)
: undefined
}
validationMessages={{
REQUIRED: t('validations.effectiveDate'),
EFFECTIVE_DATE_BEFORE_HIRE: t('validations.effectiveDateBeforeHire'),
EFFECTIVE_DATE_BEFORE_MIN: t('validations.effectiveDateBeforeMin'),
}}
formHookResult={compensationForm}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ describe('Compensation', () => {

expect(
screen.queryByText(
"Changing this employee's classification will delete the employee's additional pay rates.",
"Changing this employee's classification will immediately delete their additional jobs.",
),
).not.toBeInTheDocument()

Expand Down Expand Up @@ -631,7 +631,7 @@ describe('Compensation', () => {

expect(
screen.getByText(
"Changing this employee's classification will delete the employee's additional pay rates.",
"Changing this employee's classification will immediately delete their additional jobs.",
),
).toBeInTheDocument()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ export interface AddCompensationFormBodyProps {
submitCtaLabel: string
isPending: boolean
onCancel?: () => void
/** Override the lower bound for the effective date picker. When omitted, the hook's
* `minimumEffectiveDate` (the parent job's hire date) is used as the floor. */
minEffectiveDate?: Date
}

/**
Expand All @@ -35,7 +32,6 @@ export function AddCompensationFormBody({
submitCtaLabel,
isPending,
onCancel,
minEffectiveDate,
}: AddCompensationFormBodyProps) {
const { t } = useTranslation('Employee.Compensation')
const Components = useComponentContext()
Expand Down Expand Up @@ -137,18 +133,8 @@ export function AddCompensationFormBody({
validationMessages={{
REQUIRED: t('validations.effectiveDate'),
EFFECTIVE_DATE_BEFORE_HIRE: t('validations.effectiveDateBeforeHire'),
EFFECTIVE_DATE_BEFORE_MIN: t('validations.effectiveDateBeforeMin'),
}}
minDate={
minEffectiveDate ??
(compensationForm.data.minimumEffectiveDate
? new Date(compensationForm.data.minimumEffectiveDate)
: undefined)
}
maxDate={
compensationForm.data.maximumEffectiveDate
? new Date(compensationForm.data.maximumEffectiveDate)
: undefined
}
formHookResult={compensationForm}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ describe('composeSubmitHandler([useJobForm, useCompensationForm])', () => {
rate: 25,
paymentUnit: PAY_PERIODS.HOUR,
flsaStatus: FlsaStatus.NONEXEMPT,
effectiveDate: '2025-01-15',
},
// Onboarding does not surface an effectiveDate field — the server
// initializes it on the auto-created stub. Mirror the actual
// onboarding component behaviour here.
withEffectiveDateField: false,
shouldFocusError: false,
})
return { jobForm, compensationForm }
Expand Down Expand Up @@ -180,8 +183,8 @@ describe('composeSubmitHandler([useJobForm, useCompensationForm])', () => {
rate: 0,
paymentUnit: PAY_PERIODS.HOUR,
flsaStatus: FlsaStatus.NONEXEMPT,
effectiveDate: '2025-01-15',
},
withEffectiveDateField: false,
shouldFocusError: false,
})
return { jobForm, compensationForm }
Expand Down Expand Up @@ -233,8 +236,8 @@ describe('composeSubmitHandler([useJobForm, useCompensationForm])', () => {
rate: 25,
paymentUnit: PAY_PERIODS.HOUR,
flsaStatus: FlsaStatus.NONEXEMPT,
effectiveDate: '2025-01-15',
},
withEffectiveDateField: false,
shouldFocusError: false,
})
return { jobForm, compensationForm }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const CompensationErrorCodes = {
PAYMENT_UNIT_COMMISSION: 'PAYMENT_UNIT_COMMISSION',
RATE_COMMISSION_ZERO: 'RATE_COMMISSION_ZERO',
EFFECTIVE_DATE_BEFORE_HIRE: 'EFFECTIVE_DATE_BEFORE_HIRE',
EFFECTIVE_DATE_BEFORE_MIN: 'EFFECTIVE_DATE_BEFORE_MIN',
} as const

export type CompensationErrorCode =
Expand Down Expand Up @@ -163,6 +164,21 @@ export interface CompensationSchemaOptions {
* `EFFECTIVE_DATE_BEFORE_HIRE` issue when violated.
*/
hireDate?: string | null
/**
* Absolute lower bound for `effectiveDate`, enforced in both create and
* update modes whenever provided. Typically `addDays(today, 1)` (tomorrow)
* for management screens where effective dates must be in the future, or
* `max(tomorrow, hireDate)` for secondary new jobs. Surfaces an
* `EFFECTIVE_DATE_BEFORE_MIN` issue when violated.
*
* **Callers must only pass this when the carve-out cannot fire.** The
* carve-out (`willDeleteSecondaryJobs` in update mode) forces `effectiveDate`
* to today on a disabled field — passing `minEffectiveDate` for a primary
* job in update mode would cause a spurious validation failure on submit.
* Secondary jobs are safe because their FLSA field is hidden, preventing the
* carve-out from activating.
*/
minEffectiveDate?: string | null
/**
* When `false`, drops `effectiveDate` from the validated shape — the field
* becomes hook-managed (e.g. seeded from the parent job's `hireDate` during
Expand All @@ -177,6 +193,7 @@ export function createCompensationSchema(options: CompensationSchemaOptions = {}
mode = 'create',
optionalFieldsToRequire,
hireDate,
minEffectiveDate,
withEffectiveDateField = true,
} = options

Expand All @@ -188,22 +205,38 @@ export function createCompensationSchema(options: CompensationSchemaOptions = {}
excludeFields: withEffectiveDateField ? [] : ['effectiveDate'],
superRefine: (data, ctx) => {
validateFlsaRules(data, ctx)
// Only enforce the hire-date lower bound when picking a brand-new
// effective date (create mode). On update, the loaded effectiveDate
// can legitimately predate the parent job's hireDate (e.g. carried
// over from a stub or out-of-order data) and the API accepts the
// unchanged value or omitting it entirely. Blocking the submit here
// would trap partners whose flow doesn't render Fields.EffectiveDate.
// Enforce the hire-date lower bound in create mode always, and in
// update mode when `minEffectiveDate` is also set (management screens
// where the user is actively picking a new date). On plain update
// without `minEffectiveDate`, the loaded effectiveDate can legitimately
// predate the hire date (stub or out-of-order data) and the API
// accepts the unchanged value — blocking the submit would trap
// partners whose flow doesn't render Fields.EffectiveDate.
// When `withEffectiveDateField` is false the field is excluded from
// the shape and `data.effectiveDate` is undefined — this check
// naturally short-circuits.
if (mode === 'create' && hireDate && data.effectiveDate && data.effectiveDate < hireDate) {
if (
hireDate &&
data.effectiveDate &&
data.effectiveDate < hireDate &&
(mode === 'create' || minEffectiveDate)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['effectiveDate'],
message: CompensationErrorCodes.EFFECTIVE_DATE_BEFORE_HIRE,
})
}
// Enforce the caller-supplied minimum effective date in both modes.
// Callers must only pass this when the carve-out cannot fire (see
// CompensationSchemaOptions.minEffectiveDate for details).
if (minEffectiveDate && data.effectiveDate && data.effectiveDate < minEffectiveDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['effectiveDate'],
message: CompensationErrorCodes.EFFECTIVE_DATE_BEFORE_MIN,
})
}
},
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export type RateValidation = (typeof CompensationErrorCodes)[
| 'RATE_EXEMPT_THRESHOLD']
export type EffectiveDateValidation = (typeof CompensationErrorCodes)[
| 'REQUIRED'
| 'EFFECTIVE_DATE_BEFORE_HIRE']
| 'EFFECTIVE_DATE_BEFORE_HIRE'
| 'EFFECTIVE_DATE_BEFORE_MIN']

export type TitleFieldProps = HookFieldProps<TextInputHookFieldProps<RequiredValidation>>

Expand Down
Loading
Loading