From fd54ec8ac42b2953b6bdaf19cd5bbab55a7195c8 Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Wed, 3 Jun 2026 09:54:59 -0600 Subject: [PATCH 1/4] feat: extract Deductions card from DashboardFlow into standalone management block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates the Deductions card on the Dashboard's Job & Pay tab into a card-as-block following the pattern documented in .claude/skills/migrate-dashboard-card-to-block. Adds four standalone-consumable surfaces: - `EmployeeManagement.Deductions` — orchestrated block (card + add/edit form + success-alert chrome), wired via a local robot3 state machine. - `EmployeeManagement.DeductionsCard` — self-fetching standalone card that emits scoped `EMPLOYEE_DEDUCTION_MANAGEMENT_*` events. - `EmployeeManagement.DeductionsEditForm` — standalone add/edit form that adapts the shared onboarding `DeductionsForm` and emits scoped events on save/cancel. - `useDeductionsList` (existing) — already returns the `BaseHookReady` shape; now consumed directly by the card and the edit form. Dashboard integration: - `dashboardStateMachine` and `DashboardComponents` retargeted to the scoped `EMPLOYEE_DEDUCTION_MANAGEMENT_*` events; the dashboard's Job & Pay view now renders `` inline and routes to `` via the renamed `editDeduction` state. - Card chrome strings and success-alert labels relocated from `Employee.Dashboard:jobAndPay.deductions.*` into the new `Employee.Deductions.Management.json` namespace; alert labels duplicated into `Employee.Dashboard.json` so dashboard chrome still renders the toast above the tabs. Testing: - `DeductionsCard.test.tsx` — card-in-isolation contract (loading, ready, empty, scoped event emission for add/edit/delete). - `Deductions.test.tsx` — block integration (card↔edit transitions, cancel, delete + alert dismissal). - `Dashboard.test.tsx` — existing assertions retargeted to the scoped event names; all other dashboard coverage retained. Dev app: `npx tsx sdk-app/scripts/analyze-component-props.ts` re-run; the registry now surfaces `EmployeeManagement.Deductions`, `EmployeeManagement.DeductionsCard`, and `EmployeeManagement.DeductionsEditForm` under the Employee Management sidebar section. Docs: partner-facing entry added to `docs/workflows-overview/employee-management/employee-management.md` covering the block, the per-piece composition pattern, props, and the full scoped event surface. Co-authored-by: Cursor --- .../employee-management.md | 139 +++++++++++++- sdk-app/src/generated-registry-data.ts | 3 + .../Employee/Dashboard/Dashboard.test.tsx | 17 +- .../Employee/Dashboard/Dashboard.tsx | 14 -- .../Dashboard/DashboardComponents.tsx | 44 +---- .../Employee/Dashboard/JobAndPayView.tsx | 148 +-------------- .../Dashboard/dashboardStateMachine.ts | 36 ++-- .../Deductions/management/Deductions.test.tsx | 177 ++++++++++++++++++ .../Deductions/management/Deductions.tsx | 52 +++++ .../DeductionsCard/DeductionsCard.test.tsx | 168 +++++++++++++++++ .../DeductionsCard/DeductionsCard.tsx | 167 +++++++++++++++++ .../management/DeductionsCard/index.ts | 2 + .../management/DeductionsComponents.tsx | 50 +++++ .../management/DeductionsEditForm.tsx | 82 ++++++++ .../management/deductionsStateMachine.ts | 92 +++++++++ .../Employee/Deductions/management/index.ts | 6 + .../shared/DeleteDeductionDialog.tsx | 27 ++- .../Employee/exports/employeeManagement.ts | 6 + src/i18n/en/Employee.Dashboard.json | 15 -- .../en/Employee.Management.Deductions.json | 31 +++ src/shared/constants.ts | 12 ++ src/types/i18next.d.ts | 48 +++-- 22 files changed, 1077 insertions(+), 259 deletions(-) create mode 100644 src/components/Employee/Deductions/management/Deductions.test.tsx create mode 100644 src/components/Employee/Deductions/management/Deductions.tsx create mode 100644 src/components/Employee/Deductions/management/DeductionsCard/DeductionsCard.test.tsx create mode 100644 src/components/Employee/Deductions/management/DeductionsCard/DeductionsCard.tsx create mode 100644 src/components/Employee/Deductions/management/DeductionsCard/index.ts create mode 100644 src/components/Employee/Deductions/management/DeductionsComponents.tsx create mode 100644 src/components/Employee/Deductions/management/DeductionsEditForm.tsx create mode 100644 src/components/Employee/Deductions/management/deductionsStateMachine.ts create mode 100644 src/components/Employee/Deductions/management/index.ts create mode 100644 src/i18n/en/Employee.Management.Deductions.json diff --git a/docs/workflows-overview/employee-management/employee-management.md b/docs/workflows-overview/employee-management/employee-management.md index b2d42fbe5..07fb8dcca 100644 --- a/docs/workflows-overview/employee-management/employee-management.md +++ b/docs/workflows-overview/employee-management/employee-management.md @@ -33,6 +33,8 @@ Employee management components can be used to compose your own workflow, or can - [EmployeeManagement.DashboardFlow](#employeemanagementdashboardflow) - [EmployeeManagement.PaymentMethod](#employeemanagementpaymentmethod) - [Composing from EmployeeManagement.PaymentMethodCard directly](#composing-from-employeemanagementpaymentmethodcard-directly) +- [EmployeeManagement.Deductions](#employeemanagementdeductions) + - [Composing from EmployeeManagement.DeductionsCard and EmployeeManagement.DeductionsEditForm directly](#composing-from-employeemanagementdeductionscard-and-employeemanagementdeductionseditform-directly) - [EmployeeManagement.Profile](#employeemanagementprofile) - [Composing from EmployeeManagement.ProfileCard and EmployeeManagement.ProfileEditForm directly](#composing-from-employeemanagementprofilecard-and-employeemanagementprofileeditform-directly) - [EmployeeManagement.HomeAddress](#employeemanagementhomeaddress) @@ -94,11 +96,11 @@ The dashboard forwards every event emitted by its card surfaces and edit screens | EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_CARD_BANK_ACCOUNT_DELETED | Fired after a bank account is deleted; surfaces the "Bank account deleted" alert | Response from the Delete a bank account endpoint | | EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_CARD_SPLIT_REQUESTED | Fired when the "Split paycheck" CTA is clicked on the Payment card | None | | EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_SPLIT_FORM_SUBMITTED | Fired after a split-paycheck save succeeds; surfaces the "Split updated" alert | Response from the Update payment method endpoint | -| EMPLOYEE_DEDUCTION_ADD | Fired when the "Add deduction" CTA is clicked | { employeeId: string } | -| EMPLOYEE_DEDUCTION_EDIT | Fired when an existing deduction is selected for editing | The `Garnishment` entity being edited | -| EMPLOYEE_DEDUCTION_CREATED | Fired after a new deduction is created; surfaces the "Deduction added" alert | Response from the Create a garnishment endpoint | -| EMPLOYEE_DEDUCTION_UPDATED | Fired after a deduction is updated; surfaces the "Deduction updated" alert | Response from the Update a garnishment endpoint | -| EMPLOYEE_DEDUCTION_DELETED | Fired after a deduction is deleted; surfaces the "Deduction deleted" alert | Response from the Update a garnishment endpoint | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED | Fired when the "Add deduction" CTA is clicked on the Deductions card | { employeeId: string } | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED | Fired when a row's "Edit" menu item is chosen on the Deductions card | The `Garnishment` row being edited | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED | Fired after a new deduction is created; surfaces the "Deduction added" alert | The created `Garnishment` | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED | Fired after a deduction is updated; surfaces the "Deduction updated" alert | The updated `Garnishment` | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED | Fired after the soft-delete dialog is confirmed; surfaces the "Deduction deleted" alert | The now-inactive `Garnishment` | | EMPLOYEE_FEDERAL_TAXES_EDIT | Fired when the "Edit" CTA is clicked on the Federal taxes card | { employeeId: string, federalTaxes: EmployeeFederalTax } | | EMPLOYEE_FEDERAL_TAXES_DONE | Fired after a federal-taxes save succeeds (from inside the dashboard's edit sub-flow) | None | | EMPLOYEE_STATE_TAXES_EDIT | Fired when the "Edit" CTA is clicked on a per-state State taxes card | { employeeId: string, state: string } | @@ -198,6 +200,133 @@ function MyPaymentPanel({ employeeId, onAddBankAccount, onSplitPaycheck }) { | EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_CARD_SPLIT_REQUESTED | Fired when the user clicks "Split paycheck" | None | | EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_CARD_BANK_ACCOUNT_DELETED | Fired after the user confirms a bank-account deletion from the card's row menu | Response from the Delete a bank account endpoint | +### EmployeeManagement.Deductions + +A self-contained block for viewing and managing an employee's post-tax deductions. Renders a read-only card listing each deduction with its frequency and withholding amount; clicking "Add deduction" or a row's "Edit" menu item swaps the card for the add/edit form, saving returns to the card view with a dismissible success alert ("Deduction successfully added/updated"), and confirming the delete dialog removes a deduction and returns with a "Deduction successfully deleted" alert. Cancelling the form returns to the card view without saving. Wraps everything in error and suspense boundaries via `BaseBoundaries`. + +```jsx +import { EmployeeManagement } from '@gusto/embedded-react-sdk' + +function MyComponent() { + return ( + {}} + /> + ) +} +``` + +#### Props + +| Name | Type | Description | +| ------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| employeeId Required | string | The associated employee identifier. | +| onEvent Required | function | See events table for available events. | +| dictionary | object | Optional translations for component text. Keys are namespaced under `Employee.Management.Deductions` — see the source JSON for the set. | +| FallbackComponent | React.ComponentType | Optional custom error fallback component used by the internal `BaseBoundaries` wrapper. | + +#### Events + +| Event type | Description | Data | +| -------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED | Fired when the "Add deduction" CTA is clicked on the card | { employeeId: string } | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED | Fired when a deduction's "Edit" menu item is chosen on the card | The `Garnishment` row being edited | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED | Fired after a new deduction is saved; the block returns to the card with the "added" alert | The created `Garnishment` | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED | Fired after an existing deduction is saved; the block returns to the card with the "updated" alert | The updated `Garnishment` | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED | Fired after the delete dialog is confirmed; the block returns to the card with the "deleted" alert | The now-inactive `Garnishment` | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED | Fired when the user clicks Cancel on the form; the block returns to the card view | None | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_ALERT_DISMISSED | Fired when the user dismisses the success alert above the card | null | + +#### Composing from EmployeeManagement.DeductionsCard and EmployeeManagement.DeductionsEditForm directly + +`EmployeeManagement.Deductions` above is the recommended entry point for the deductions experience — it bundles the card, the add/edit form, the swap between them, the delete dialog, and the success-alert wiring as a single drop-in. The card and edit form are also exported individually for cases where that orchestration is the wrong fit — for example, when the add/edit surface needs to render in a modal or drawer, when the card needs to appear read-only with no add/edit affordances, or when the swap is driven by a router. Using them directly means owning the swap, the alert, and any cross-component state yourself. + +`EmployeeManagement.DeductionsCard` renders the read-only deductions card, self-fetches its rows, and emits events when "Add deduction" is clicked, a row's "Edit" item is chosen, or a deduction is deleted via its built-in confirm dialog. `EmployeeManagement.DeductionsEditForm` renders the add/edit form: omit `editingDeductionId` to open in add mode, or pass a deduction's `uuid` (e.g. from the `EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED` payload) to open in edit mode pre-populated with that garnishment. It emits one event on a successful create, another on a successful update, and another on cancel. Each piece's `onEvent` receives the event type as its first argument and any associated payload as its second — branch on the event type to drive the swap. The per-piece events tables below list every event each piece emits. + +```jsx +import { useState } from 'react' +import { componentEvents, EmployeeManagement } from '@gusto/embedded-react-sdk' + +function MyDeductionsPanel({ employeeId }) { + const [isEditing, setIsEditing] = useState(false) + const [editingDeduction, setEditingDeduction] = useState(null) + + if (isEditing) { + return ( + { + if ( + eventType === componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED || + eventType === componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED || + eventType === componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED + ) { + setEditingDeduction(null) + setIsEditing(false) + } + }} + /> + ) + } + + return ( + { + if (eventType === componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED) { + setEditingDeduction(null) + setIsEditing(true) + } else if ( + eventType === componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED + ) { + setEditingDeduction(payload) + setIsEditing(true) + } + }} + /> + ) +} +``` + +##### EmployeeManagement.DeductionsCard + +**Props** + +| Name | Type | Description | +| ------------------- | -------- | -------------------------------------- | +| employeeId Required | string | The associated employee identifier. | +| onEvent Required | function | See events table for available events. | + +**Events** + +| Event type | Description | Data | +| -------------------------------------------------- | --------------------------------------------------- | ---------------------------------- | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED | Fired when the "Add deduction" CTA is clicked | { employeeId: string } | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED | Fired when a deduction's "Edit" menu item is chosen | The `Garnishment` row being edited | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED | Fired after the soft-delete dialog is confirmed | The now-inactive `Garnishment` | + +##### EmployeeManagement.DeductionsEditForm + +**Props** + +| Name | Type | Description | +| ------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| employeeId Required | string | The associated employee identifier. | +| editingDeductionId | string | Optional `uuid` of the deduction to edit. When omitted the form opens in add mode; when set the form pre-populates that row. | +| onEvent Required | function | See events table for available events. | +| dictionary | object | Optional translations for component text. Keys are namespaced under `Employee.Management.Deductions`. | +| FallbackComponent | React.ComponentType | Optional custom error fallback component used by the internal `BaseBoundaries` wrapper. | + +**Events** + +| Event type | Description | Data | +| -------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------- | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED | Fired after a new deduction is saved | The created `Garnishment` | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED | Fired after an existing deduction is saved | The updated `Garnishment` | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED | Fired when the user clicks Cancel and the orchestrator should swap back to the card | None | + ### EmployeeManagement.Profile A self-contained block for viewing and editing an employee's basic details — the same "Basic details" experience the dashboard surfaces, but as a drop-in component that doesn't require the surrounding dashboard chrome. Renders a read-only card showing the employee's legal name, start date, masked social security number, date of birth, and personal email. Clicking the card's "Edit" CTA swaps the card view for an inline edit form; saving the form returns to the card view with a dismissible "Profile updated" success alert; cancelling returns to the card view without saving. diff --git a/sdk-app/src/generated-registry-data.ts b/sdk-app/src/generated-registry-data.ts index 6f974bdfa..aa7d7266c 100644 --- a/sdk-app/src/generated-registry-data.ts +++ b/sdk-app/src/generated-registry-data.ts @@ -33,6 +33,9 @@ export const ENTITY_REQUIREMENTS: Record = { 'Contractor.PaymentSummary': ['companyId'], 'Contractor.PaymentsList': ['companyId'], 'EmployeeManagement.DashboardFlow': ['employeeId'], + 'EmployeeManagement.Deductions': ['employeeId'], + 'EmployeeManagement.DeductionsCard': ['employeeId'], + 'EmployeeManagement.DeductionsEditForm': ['employeeId'], 'EmployeeManagement.DocumentManager': ['employeeId'], 'EmployeeManagement.EmployeeDocuments': ['employeeId'], 'EmployeeManagement.EmployeeList': ['companyId'], diff --git a/src/components/Employee/Dashboard/Dashboard.test.tsx b/src/components/Employee/Dashboard/Dashboard.test.tsx index d92579fc1..13c73b6ba 100644 --- a/src/components/Employee/Dashboard/Dashboard.test.tsx +++ b/src/components/Employee/Dashboard/Dashboard.test.tsx @@ -376,7 +376,7 @@ describe('Dashboard', () => { expect(screen.queryByText('Old Deduction')).toBeNull() }) - it('emits EMPLOYEE_DEDUCTION_ADD when clicking the Add deduction button', async () => { + it('emits EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED when clicking the Add deduction button', async () => { stubGarnishmentsList([]) const user = userEvent.setup() @@ -385,12 +385,13 @@ describe('Dashboard', () => { await user.click(screen.getByRole('button', { name: 'Add deduction' })) - expect(onEvent).toHaveBeenCalledWith(componentEvents.EMPLOYEE_DEDUCTION_ADD, { - employeeId: 'employee-123', - }) + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED, + { employeeId: 'employee-123' }, + ) }) - it('emits EMPLOYEE_DEDUCTION_EDIT with the garnishment when clicking Edit', async () => { + it('emits EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED with the garnishment when clicking Edit', async () => { stubGarnishmentsList([ buildGarnishment({ uuid: 'd-1', description: 'Health Insurance', amount: '120' }), ]) @@ -403,12 +404,12 @@ describe('Dashboard', () => { await user.click(await screen.findByRole('menuitem', { name: 'Edit deduction' })) expect(onEvent).toHaveBeenCalledWith( - componentEvents.EMPLOYEE_DEDUCTION_EDIT, + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED, expect.objectContaining({ uuid: 'd-1', description: 'Health Insurance' }), ) }) - it('soft-deletes via PUT and emits EMPLOYEE_DEDUCTION_DELETED on confirm', async () => { + it('soft-deletes via PUT and emits EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED on confirm', async () => { const target = buildGarnishment({ uuid: 'd-1', description: 'Health Insurance' }) stubGarnishmentsList([target]) @@ -437,7 +438,7 @@ describe('Dashboard', () => { expect(updatePath).toBe('/v1/garnishments/d-1') expect(updateBody).toMatchObject({ active: false, version: 'version-d-1' }) expect(onEvent).toHaveBeenCalledWith( - componentEvents.EMPLOYEE_DEDUCTION_DELETED, + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED, expect.objectContaining({ uuid: 'd-1', active: false }), ) }) diff --git a/src/components/Employee/Dashboard/Dashboard.tsx b/src/components/Employee/Dashboard/Dashboard.tsx index 9fe95eedf..09e25284a 100644 --- a/src/components/Employee/Dashboard/Dashboard.tsx +++ b/src/components/Employee/Dashboard/Dashboard.tsx @@ -2,7 +2,6 @@ import { Suspense, useState, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useEmployeesGetSuspense } from '@gusto/embedded-api-v-2025-11-15/react-query/employeesGet' import type { Job } from '@gusto/embedded-api-v-2025-11-15/models/components/job' -import type { Garnishment } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' import type { GetV1EmployeesEmployeeIdFederalTaxesResponse } from '@gusto/embedded-api-v-2025-11-15/models/operations/getv1employeesemployeeidfederaltaxes' import { BasicDetailsView } from './BasicDetailsView' import { JobAndPayView } from './JobAndPayView' @@ -54,17 +53,6 @@ function DashboardRoot({ onEvent(componentEvents.EMPLOYEE_JOB_ADD_ANOTHER, { employeeId }) }, [onEvent, employeeId]) - const handleAddDeduction = useCallback(() => { - onEvent(componentEvents.EMPLOYEE_DEDUCTION_ADD, { employeeId }) - }, [onEvent, employeeId]) - - const handleEditDeduction = useCallback( - (deduction: Garnishment) => { - onEvent(componentEvents.EMPLOYEE_DEDUCTION_EDIT, deduction) - }, - [onEvent], - ) - const handleEditFederalTaxes = useCallback( (federalTaxes: EmployeeFederalTax | undefined) => { onEvent(componentEvents.EMPLOYEE_FEDERAL_TAXES_EDIT, { employeeId, federalTaxes }) @@ -139,8 +127,6 @@ function DashboardRoot({ onEditCompensation={handleEditCompensation} onAddJob={handleAddJob} onAddAnotherJob={handleAddAnotherJob} - onAddDeduction={handleAddDeduction} - onEditDeduction={handleEditDeduction} /> )} diff --git a/src/components/Employee/Dashboard/DashboardComponents.tsx b/src/components/Employee/Dashboard/DashboardComponents.tsx index 3f7f7a925..c1e3e653f 100644 --- a/src/components/Employee/Dashboard/DashboardComponents.tsx +++ b/src/components/Employee/Dashboard/DashboardComponents.tsx @@ -10,17 +10,15 @@ import { ProfileEditForm } from '@/components/Employee/Profile/management/Profil import { PaymentMethodBankForm } from '@/components/Employee/PaymentMethod/management/PaymentMethodBankForm' import { PaymentMethodSplitForm } from '@/components/Employee/PaymentMethod/management/PaymentMethodSplitForm' import { DocumentManager } from '@/components/Employee/Documents/management/DocumentManager' -import { DeductionsForm } from '@/components/Employee/Deductions/onboarding/DeductionsForm/DeductionsForm' +import { DeductionsEditForm } from '@/components/Employee/Deductions/management/DeductionsEditForm' import { ManagementEditCompensation, ManagementEditPendingCompensation, } from '@/components/Employee/Compensation/management' -import { useDeductionsList } from '@/components/Employee/Deductions/shared' import { AddAnotherJob } from '@/components/Employee/Compensation/management/AddAnotherJob/AddAnotherJob' import { EditCompensation } from '@/components/Employee/Compensation/onboarding/EditCompensation/EditCompensation' import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow' import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' -import { BaseLayout } from '@/components/Base' import { ensureRequired } from '@/helpers/ensureRequired' import { useI18n } from '@/i18n' import { componentEvents } from '@/shared/constants' @@ -41,8 +39,8 @@ export interface DashboardContextInterface extends FlowContextInterface { formId?: string currentJob?: Job | null successAlert?: DashboardSuccessAlert | null - /** Set by the EMPLOYEE_DEDUCTION_EDIT transition; consumed by - * DeductionFormContextual to pre-populate the form. */ + /** Set by the EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED transition; + * consumed by `DeductionsEditFormContextual` to pre-populate the form. */ editingDeductionId?: string /** Persists the active Dashboard tab across sub-flows so Cancel/Back * returns to the originating tab instead of resetting to basic details. */ @@ -134,38 +132,14 @@ export function DocumentManagerContextual() { ) } -export function DeductionFormContextual() { +export function DeductionsEditFormContextual() { const { employeeId, editingDeductionId, onEvent } = useFlow() - // The same list query the form hooks use internally — React Query dedupes, - // so this just looks up the loaded row to pre-populate edit mode. - const list = useDeductionsList({ employeeId: ensureRequired(employeeId) }) - - if (list.isLoading) { - return - } - - const deduction = editingDeductionId - ? (list.data.deductions.find(d => d.uuid === editingDeductionId) ?? null) - : null - return ( - - { - onEvent( - mode === 'create' - ? componentEvents.EMPLOYEE_DEDUCTION_CREATED - : componentEvents.EMPLOYEE_DEDUCTION_UPDATED, - saved, - ) - }} - onCancel={() => { - onEvent(componentEvents.CANCEL) - }} - /> - + ) } diff --git a/src/components/Employee/Dashboard/JobAndPayView.tsx b/src/components/Employee/Dashboard/JobAndPayView.tsx index a1e3cb30e..d7de5fffe 100644 --- a/src/components/Employee/Dashboard/JobAndPayView.tsx +++ b/src/components/Employee/Dashboard/JobAndPayView.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useJobsAndCompensationsDeleteMutation } from '@gusto/embedded-api-v-2025-11-15/react-query/jobsAndCompensationsDelete' import type { Job } from '@gusto/embedded-api-v-2025-11-15/models/components/job' -import type { Garnishment } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' import { useEmployeeCompensation } from './hooks' import type { PendingCompensationChange } from './getPendingCompensationChanges' import { usePendingChangeDetailRenderer } from './usePendingChangeDetailRenderer' @@ -16,15 +15,9 @@ import { BaseLayout } from '@/components/Base/Base' import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler' import { formatDateLongWithYear, formatDateToStringDate } from '@/helpers/dateFormatting' import { useFormatCompensationRate } from '@/helpers/formattedStrings' -import useNumberFormatter from '@/hooks/useNumberFormatter' import { useI18n } from '@/i18n' import { PaymentMethodCard } from '@/components/Employee/PaymentMethod/management' -import { - useDeductionsList, - useDeleteDeduction, - DeleteDeductionDialog, - formatDeductionAmount, -} from '@/components/Employee/Deductions/shared' +import { DeductionsCard } from '@/components/Employee/Deductions/management/DeductionsCard' import { PaystubsCard } from '@/components/Employee/Paystubs/management/PaystubsCard' import { componentEvents, FlsaStatus, type EventType } from '@/shared/constants' import type { OnEventType } from '@/components/Base/useBase' @@ -44,8 +37,6 @@ export interface JobAndPayViewProps { onEditCompensation?: (job: Job) => void onAddJob?: () => void onAddAnotherJob?: () => void - onAddDeduction?: () => void - onEditDeduction?: (deduction: Garnishment) => void } export function JobAndPayView({ @@ -54,18 +45,12 @@ export function JobAndPayView({ onEditCompensation, onAddJob, onAddAnotherJob, - onAddDeduction, - onEditDeduction, }: JobAndPayViewProps) { useI18n('Employee.Compensation') - useI18n('Employee.Deductions') const { t } = useTranslation('Employee.Dashboard') const { t: tCompensation } = useTranslation('Employee.Compensation') - const { t: tDeductions } = useTranslation('Employee.Deductions') const Components = useComponentContext() const formatCompensationRate = useFormatCompensationRate() - const formatCurrency = useNumberFormatter('currency') - const formatPercent = useNumberFormatter('percent') const compensation = useEmployeeCompensation({ employeeId }) const { @@ -171,29 +156,10 @@ export function JobAndPayView({ const showInlineAlert = hasPendingUpdates && !showSummaryAlert const nextChange = updatePendingChanges[0] - const deductionsList = useDeductionsList({ employeeId }) - const deductions = deductionsList.isLoading ? [] : deductionsList.data.deductions - const deletingGarnishmentUuid = deductionsList.isLoading - ? undefined - : deductionsList.status.deletingGarnishmentUuid - - const { - pendingDeleteDeduction, - setPendingDeleteDeduction, - handleConfirmDelete: handleConfirmDeleteDeduction, - } = useDeleteDeduction(async garnishment => { - if (deductionsList.isLoading) return - const result = await deductionsList.actions.onDelete(garnishment) - if (result) { - onEvent(componentEvents.EMPLOYEE_DEDUCTION_DELETED, result.data.garnishment) - } - }) - - // Compensation + Deductions own their own error state; merge into one - // error surface so the BaseLayout below shows whatever failed. The - // Payment card renders its own errors via its internal BaseLayout, so - // payment-method errors are excluded here to avoid duplicates. - const errorHandling = composeErrorHandler([compensation, deductionsList]) + // Compensation owns the only inline-rendered error state remaining. + // The Payment, Deductions, and Paystubs cards each render their own + // errors via their internal BaseLayouts, so they are not merged in here. + const errorHandling = composeErrorHandler([compensation]) const jobsColumns = [ { @@ -299,71 +265,6 @@ export function JobAndPayView({ }, }) - const garnishmentsColumns = [ - { - key: 'description', - title: t('jobAndPay.deductions.deduction'), - render: (garnishment: Garnishment) => garnishment.description || '-', - }, - { - key: 'frequency', - title: t('jobAndPay.deductions.frequency'), - render: (garnishment: Garnishment) => - garnishment.recurring - ? t('jobAndPay.deductions.recurring') - : t('jobAndPay.deductions.oneTime'), - }, - { - key: 'amount', - title: t('jobAndPay.deductions.withhold'), - render: (garnishment: Garnishment) => - formatDeductionAmount(garnishment, { - formatCurrency, - formatPercent, - formatPerPaycheck: (value: string) => - t('jobAndPay.deductions.amountPerPaycheck', { value }), - }), - }, - ] - - const garnishmentsDataView = useDataView({ - data: deductions, - columns: garnishmentsColumns, - itemMenu: (garnishment: Garnishment) => ( - onEditDeduction?.(garnishment), - icon: , - }, - { - label: tDeductions('deleteCta'), - onClick: () => { - setPendingDeleteDeduction(garnishment) - }, - icon: , - }, - ]} - triggerLabel={tDeductions('hamburgerTitle')} - /> - ), - emptyState: () => ( - - ), - }) - - // `useDeductionsList` still uses the older `HookLoadingResult | Ready` - // shape, which returns `isLoading: true` when the query has errored AND - // data is missing. Treat as "not loading" so the section doesn't show a - // perpetual skeleton while BaseLayout already renders the error alert above. - const isDeductionsLoading = - deductionsList.isLoading && deductionsList.errorHandling.errors.length === 0 - return ( @@ -534,33 +435,7 @@ export function JobAndPayView({ - } - > - {t('jobAndPay.deductions.addDeductionCta')} - - } - /> - } - > - {isDeductionsLoading ? ( - - ) : ( - - )} - + @@ -577,17 +452,6 @@ export function JobAndPayView({ }} /> - { - setPendingDeleteDeduction(null) - }} - onConfirm={() => { - void handleConfirmDeleteDeduction() - }} - /> - { diff --git a/src/components/Employee/Dashboard/dashboardStateMachine.ts b/src/components/Employee/Dashboard/dashboardStateMachine.ts index 3b1a17987..4a30f0da4 100644 --- a/src/components/Employee/Dashboard/dashboardStateMachine.ts +++ b/src/components/Employee/Dashboard/dashboardStateMachine.ts @@ -12,7 +12,7 @@ import { PaymentBankFormContextual, PaymentSplitViewContextual, DocumentManagerContextual, - DeductionFormContextual, + DeductionsEditFormContextual, AddJobContextual, EditCompensationContextual, AddAnotherJobContextual, @@ -23,7 +23,7 @@ import type { MachineEventType, MachineTransition } from '@/types/Helpers' type EventPayloads = { [componentEvents.EMPLOYEE_VIEW_FORM_TO_SIGN]: { employeeId: string; formId: string } - [componentEvents.EMPLOYEE_DEDUCTION_EDIT]: Garnishment + [componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED]: Garnishment [componentEvents.EMPLOYEE_COMPENSATION_CREATE]: { employeeId: string; job: Job } [componentEvents.EMPLOYEE_DASHBOARD_TAB_CHANGE]: { tab: DashboardTab } } @@ -193,34 +193,37 @@ export const dashboardStateMachine = { ), ), transition( - componentEvents.EMPLOYEE_DEDUCTION_ADD, - 'deductionForm', + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED, + 'editDeduction', reduce( (ctx: DashboardContextInterface): DashboardContextInterface => ({ ...ctx, - component: DeductionFormContextual, + component: DeductionsEditFormContextual, successAlert: null, editingDeductionId: undefined, }), ), ), transition( - componentEvents.EMPLOYEE_DEDUCTION_EDIT, - 'deductionForm', + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED, + 'editDeduction', reduce( ( ctx: DashboardContextInterface, - ev: MachineEventType, + ev: MachineEventType< + EventPayloads, + typeof componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED + >, ): DashboardContextInterface => ({ ...ctx, - component: DeductionFormContextual, + component: DeductionsEditFormContextual, successAlert: null, editingDeductionId: ev.payload.uuid, }), ), ), transition( - componentEvents.EMPLOYEE_DEDUCTION_DELETED, + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED, 'index', reduce( (ctx: DashboardContextInterface): DashboardContextInterface => ({ @@ -318,19 +321,22 @@ export const dashboardStateMachine = { ), transition(componentEvents.CANCEL, 'index', returnToIndex), ), - deductionForm: state( + editDeduction: state( transition( - componentEvents.EMPLOYEE_DEDUCTION_CREATED, + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED, 'index', returnToIndexWithAlert('deductionAdded'), ), transition( - componentEvents.EMPLOYEE_DEDUCTION_UPDATED, + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED, 'index', returnToIndexWithAlert('deductionUpdated'), ), - transition(componentEvents.EMPLOYEE_DEDUCTION_CANCEL, 'index', returnToIndex), - transition(componentEvents.CANCEL, 'index', returnToIndex), + transition( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED, + 'index', + returnToIndex, + ), ), addJob: state( transition( diff --git a/src/components/Employee/Deductions/management/Deductions.test.tsx b/src/components/Employee/Deductions/management/Deductions.test.tsx new file mode 100644 index 000000000..f891b9aed --- /dev/null +++ b/src/components/Employee/Deductions/management/Deductions.test.tsx @@ -0,0 +1,177 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { http, HttpResponse } from 'msw' +import { Deductions } from './Deductions' +import { renderWithProviders } from '@/test-utils/renderWithProviders' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { server } from '@/test/mocks/server' +import { API_BASE_URL } from '@/test/constants' +import { componentEvents } from '@/shared/constants' + +type GarnishmentFixture = { + uuid: string + active: boolean + amount: string + description: string + recurring: boolean + deduct_as_percentage: boolean + court_ordered: boolean + times: number | null + annual_maximum: string | null + pay_period_maximum: string | null + total_amount: string | null + version: string +} + +const buildGarnishment = ( + overrides: Partial & { uuid: string }, +): GarnishmentFixture => ({ + active: true, + amount: '50', + description: 'Health Insurance', + recurring: true, + deduct_as_percentage: false, + court_ordered: false, + times: null, + annual_maximum: null, + pay_period_maximum: null, + total_amount: null, + version: `version-${overrides.uuid}`, + ...overrides, +}) + +const stubGarnishmentsList = (garnishments: GarnishmentFixture[]) => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/garnishments`, () => + HttpResponse.json(garnishments), + ), + ) +} + +vi.mock('@/hooks/useContainerBreakpoints/useContainerBreakpoints', async () => { + const actual = await vi.importActual('@/hooks/useContainerBreakpoints/useContainerBreakpoints') + return { + ...actual, + default: () => ['base', 'small', 'medium', 'large'], + useContainerBreakpoints: () => ['base', 'small', 'medium', 'large'], + } +}) + +describe('Deductions (management block)', () => { + const onEvent = vi.fn() + + beforeEach(() => { + setupApiTestMocks() + onEvent.mockClear() + }) + + it('starts on the card surface and renders the deductions list', async () => { + stubGarnishmentsList([ + buildGarnishment({ uuid: 'd-1', description: 'Health Insurance', amount: '120' }), + ]) + renderWithProviders() + + expect(await screen.findByRole('heading', { name: 'Deductions' })).toBeInTheDocument() + expect(await screen.findByText('Health Insurance')).toBeInTheDocument() + }) + + it('transitions card → editDeduction when Add deduction is clicked', async () => { + stubGarnishmentsList([]) + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Add deduction' })).toBeEnabled() + }) + await user.click(screen.getByRole('button', { name: 'Add deduction' })) + + expect(await screen.findByRole('heading', { name: 'Add Deduction' })).toBeInTheDocument() + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED, + { employeeId: 'employee-123' }, + ) + }) + + it('transitions card → editDeduction when an existing deduction is edited', async () => { + stubGarnishmentsList([buildGarnishment({ uuid: 'd-1', description: 'Health Insurance' })]) + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Deduction actions menu' })).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: 'Deduction actions menu' })) + await user.click(await screen.findByRole('menuitem', { name: 'Edit deduction' })) + + expect(await screen.findByRole('heading', { name: 'Edit Deduction' })).toBeInTheDocument() + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED, + expect.objectContaining({ uuid: 'd-1' }), + ) + }) + + it('returns to card surface when the edit form is cancelled', async () => { + // Use edit mode so the variant is pre-selected and the Cancel button on + // the inline form renders immediately (add mode requires picking a + // variant first). + stubGarnishmentsList([buildGarnishment({ uuid: 'd-1', description: 'Health Insurance' })]) + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Deduction actions menu' })).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: 'Deduction actions menu' })) + await user.click(await screen.findByRole('menuitem', { name: 'Edit deduction' })) + + await user.click(await screen.findByRole('button', { name: 'Cancel' })) + + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Deductions' })).toBeInTheDocument() + }) + expect(screen.queryByRole('heading', { name: 'Edit Deduction' })).toBeNull() + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED, + undefined, + ) + }) + + it('renders the deductionDeleted alert after a soft-delete and dismisses on the X button', async () => { + const target = buildGarnishment({ uuid: 'd-1', description: 'Health Insurance' }) + stubGarnishmentsList([target]) + server.use( + http.put(`${API_BASE_URL}/v1/garnishments/:garnishment_id`, () => + HttpResponse.json({ ...target, active: false }), + ), + ) + + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Deduction actions menu' })).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: 'Deduction actions menu' })) + await user.click(await screen.findByRole('menuitem', { name: 'Delete deduction' })) + + const dialog = await screen.findByRole('dialog') + await user.click(within(dialog).getByRole('button', { name: 'Delete' })) + + const alert = await screen.findByText('Deduction successfully deleted.') + expect(alert).toBeInTheDocument() + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED, + expect.objectContaining({ uuid: 'd-1', active: false }), + ) + + await user.click(screen.getByRole('button', { name: /dismiss/i })) + await waitFor(() => { + expect(screen.queryByText('Deduction successfully deleted.')).toBeNull() + }) + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_ALERT_DISMISSED, + null, + ) + }) +}) diff --git a/src/components/Employee/Deductions/management/Deductions.tsx b/src/components/Employee/Deductions/management/Deductions.tsx new file mode 100644 index 000000000..5129af4ee --- /dev/null +++ b/src/components/Employee/Deductions/management/Deductions.tsx @@ -0,0 +1,52 @@ +import { createMachine } from 'robot3' +import { useMemo } from 'react' +import { DeductionsCardContextual, type DeductionsContextInterface } from './DeductionsComponents' +import { deductionsStateMachine } from './deductionsStateMachine' +import { Flow } from '@/components/Flow/Flow' +import { + BaseBoundaries, + type BaseComponentInterface, + type CommonComponentInterface, +} from '@/components/Base' +import { type EventType } from '@/shared/constants' +import { useComponentDictionary } from '@/i18n/I18n' +import { useI18n } from '@/i18n' +import type { OnEventType } from '@/components/Base/useBase' + +export interface DeductionsProps extends CommonComponentInterface<'Employee.Management.Deductions'> { + employeeId: string + onEvent: OnEventType +} + +function DeductionsFlow({ employeeId, onEvent }: DeductionsProps) { + useI18n('Employee.Management.Deductions') + + const machine = useMemo( + () => + createMachine('card', deductionsStateMachine, (ctx: DeductionsContextInterface) => ({ + ...ctx, + component: DeductionsCardContextual, + employeeId, + successAlert: null, + })), + [employeeId], + ) + + return +} + +export function Deductions({ + dictionary, + FallbackComponent, + ...props +}: DeductionsProps & BaseComponentInterface<'Employee.Management.Deductions'>) { + useComponentDictionary('Employee.Management.Deductions', dictionary) + return ( + + + + ) +} diff --git a/src/components/Employee/Deductions/management/DeductionsCard/DeductionsCard.test.tsx b/src/components/Employee/Deductions/management/DeductionsCard/DeductionsCard.test.tsx new file mode 100644 index 000000000..202fa157d --- /dev/null +++ b/src/components/Employee/Deductions/management/DeductionsCard/DeductionsCard.test.tsx @@ -0,0 +1,168 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { http, HttpResponse, type HttpResponseResolver } from 'msw' +import { DeductionsCard } from './DeductionsCard' +import { renderWithProviders } from '@/test-utils/renderWithProviders' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { server } from '@/test/mocks/server' +import { API_BASE_URL } from '@/test/constants' +import { componentEvents } from '@/shared/constants' + +type GarnishmentFixture = { + uuid: string + active: boolean + amount: string + description: string + recurring: boolean + deduct_as_percentage: boolean + court_ordered: boolean + times: number | null + annual_maximum: string | null + pay_period_maximum: string | null + total_amount: string | null + version: string +} + +const buildGarnishment = ( + overrides: Partial & { uuid: string }, +): GarnishmentFixture => ({ + active: true, + amount: '50', + description: 'Health Insurance', + recurring: true, + deduct_as_percentage: false, + court_ordered: false, + times: null, + annual_maximum: null, + pay_period_maximum: null, + total_amount: null, + version: `version-${overrides.uuid}`, + ...overrides, +}) + +const stubGarnishmentsList = (garnishments: GarnishmentFixture[]) => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/garnishments`, () => + HttpResponse.json(garnishments), + ), + ) +} + +vi.mock('@/hooks/useContainerBreakpoints/useContainerBreakpoints', async () => { + const actual = await vi.importActual('@/hooks/useContainerBreakpoints/useContainerBreakpoints') + return { + ...actual, + default: () => ['base', 'small', 'medium', 'large'], + useContainerBreakpoints: () => ['base', 'small', 'medium', 'large'], + } +}) + +describe('DeductionsCard (standalone)', () => { + const onEvent = vi.fn() + + beforeEach(() => { + setupApiTestMocks() + onEvent.mockClear() + }) + + it('renders the empty state when the employee has no active deductions', async () => { + stubGarnishmentsList([]) + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('No deductions')).toBeInTheDocument() + }) + expect(screen.getByRole('button', { name: 'Add deduction' })).toBeInTheDocument() + }) + + it('renders the deductions list with active rows only', async () => { + stubGarnishmentsList([ + buildGarnishment({ uuid: 'd-1', description: 'Health Insurance', amount: '120' }), + buildGarnishment({ uuid: 'd-2', description: 'Retirement', recurring: false }), + buildGarnishment({ + uuid: 'd-old', + description: 'Old Deduction', + active: false, + }), + ]) + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Health Insurance')).toBeInTheDocument() + }) + expect(screen.getByText('Retirement')).toBeInTheDocument() + expect(screen.queryByText('Old Deduction')).toBeNull() + }) + + it('fires EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED with the employeeId when Add deduction is clicked', async () => { + stubGarnishmentsList([]) + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Add deduction' })).toBeEnabled() + }) + await user.click(screen.getByRole('button', { name: 'Add deduction' })) + + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED, + { employeeId: 'employee-123' }, + ) + }) + + it('fires EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED with the garnishment when Edit deduction is clicked', async () => { + stubGarnishmentsList([ + buildGarnishment({ uuid: 'd-1', description: 'Health Insurance', amount: '120' }), + ]) + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Deduction actions menu' })).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: 'Deduction actions menu' })) + await user.click(await screen.findByRole('menuitem', { name: 'Edit deduction' })) + + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED, + expect.objectContaining({ uuid: 'd-1', description: 'Health Insurance' }), + ) + }) + + it('soft-deletes via PUT and fires EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED after the dialog is confirmed', async () => { + const target = buildGarnishment({ uuid: 'd-1', description: 'Health Insurance' }) + stubGarnishmentsList([target]) + + let updatePath: string | null = null + let updateBody: Record | null = null + const updateResolver = vi.fn(async ({ request }) => { + updatePath = new URL(request.url).pathname + updateBody = (await request.json()) as Record + return HttpResponse.json({ ...target, active: false }) + }) + server.use(http.put(`${API_BASE_URL}/v1/garnishments/:garnishment_id`, updateResolver)) + + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Deduction actions menu' })).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: 'Deduction actions menu' })) + await user.click(await screen.findByRole('menuitem', { name: 'Delete deduction' })) + + const dialog = await screen.findByRole('dialog') + await user.click(within(dialog).getByRole('button', { name: 'Delete' })) + + await waitFor(() => { + expect(updateResolver).toHaveBeenCalledTimes(1) + }) + expect(updatePath).toBe('/v1/garnishments/d-1') + expect(updateBody).toMatchObject({ active: false, version: 'version-d-1' }) + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED, + expect.objectContaining({ uuid: 'd-1', active: false }), + ) + }) +}) diff --git a/src/components/Employee/Deductions/management/DeductionsCard/DeductionsCard.tsx b/src/components/Employee/Deductions/management/DeductionsCard/DeductionsCard.tsx new file mode 100644 index 000000000..99653f8a0 --- /dev/null +++ b/src/components/Employee/Deductions/management/DeductionsCard/DeductionsCard.tsx @@ -0,0 +1,167 @@ +import { useTranslation } from 'react-i18next' +import type { Garnishment } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' +import { useDeductionsList } from '../../shared/useDeductionsList' +import { useDeleteDeduction } from '../../shared/useDeleteDeduction' +import { DeleteDeductionDialog } from '../../shared/DeleteDeductionDialog' +import { formatDeductionAmount } from '../../shared/formatDeductionAmount' +import { DataView, useDataView, EmptyData, Loading } from '@/components/Common' +import { HamburgerMenu } from '@/components/Common/HamburgerMenu' +import { BaseLayout } from '@/components/Base/Base' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import useNumberFormatter from '@/hooks/useNumberFormatter' +import { useI18n } from '@/i18n' +import { componentEvents, type EventType } from '@/shared/constants' +import type { OnEventType } from '@/components/Base/useBase' +import PlusCircleIcon from '@/assets/icons/plus-circle.svg?react' +import TrashCanSvg from '@/assets/icons/trashcan.svg?react' +import PencilSvg from '@/assets/icons/pencil.svg?react' + +export interface DeductionsCardProps { + employeeId: string + onEvent: OnEventType +} + +/** + * Standalone "Deductions" management card. Owns its own data fetch via + * `useDeductionsList`, plus the delete confirm dialog, and emits + * `EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED` / + * `EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED` / + * `EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED` events. The card has no + * alert API — alert rendering is the orchestrator's responsibility (block's + * `DeductionsCardContextual` for standalone consumption, dashboard chrome + * for dashboard consumption). + */ +export function DeductionsCard({ employeeId, onEvent }: DeductionsCardProps) { + useI18n('Employee.Management.Deductions') + const { t } = useTranslation('Employee.Management.Deductions') + const Components = useComponentContext() + const formatCurrency = useNumberFormatter('currency') + const formatPercent = useNumberFormatter('percent') + + const deductionsList = useDeductionsList({ employeeId }) + + const { + pendingDeleteDeduction, + setPendingDeleteDeduction, + handleConfirmDelete: handleConfirmDeleteDeduction, + } = useDeleteDeduction(async garnishment => { + if (deductionsList.isLoading) return + const result = await deductionsList.actions.onDelete(garnishment) + if (result) { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED, result.data.garnishment) + } + }) + + // `useDeductionsList` returns `isLoading: true` even when the query has + // errored and `data` is missing. Treat those rows as "not loading" so the + // section doesn't show a perpetual skeleton while BaseLayout already + // renders the error alert. + const isDeductionsLoading = + deductionsList.isLoading && deductionsList.errorHandling.errors.length === 0 + + const deductions = deductionsList.isLoading ? [] : deductionsList.data.deductions + const deletingGarnishmentUuid = deductionsList.isLoading + ? undefined + : deductionsList.status.deletingGarnishmentUuid + + const handleAdd = () => { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED, { employeeId }) + } + const handleEdit = (garnishment: Garnishment) => { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED, garnishment) + } + + const garnishmentsColumns = [ + { + key: 'description', + title: t('columns.deduction'), + render: (garnishment: Garnishment) => garnishment.description || '-', + }, + { + key: 'frequency', + title: t('columns.frequency'), + render: (garnishment: Garnishment) => (garnishment.recurring ? t('recurring') : t('oneTime')), + }, + { + key: 'amount', + title: t('columns.withhold'), + render: (garnishment: Garnishment) => + formatDeductionAmount(garnishment, { + formatCurrency, + formatPercent, + formatPerPaycheck: (value: string) => t('amountPerPaycheck', { value }), + }), + }, + ] + + const garnishmentsDataView = useDataView({ + data: deductions, + columns: garnishmentsColumns, + itemMenu: (garnishment: Garnishment) => ( + { + handleEdit(garnishment) + }, + icon: , + }, + { + label: t('deleteCta'), + onClick: () => { + setPendingDeleteDeduction(garnishment) + }, + icon: , + }, + ]} + triggerLabel={t('hamburgerTitle')} + /> + ), + emptyState: () => ( + + ), + }) + + return ( + + }> + {t('addDeductionCta')} + + } + /> + } + > + {isDeductionsLoading ? ( + + ) : ( + + )} + + + { + setPendingDeleteDeduction(null) + }} + onConfirm={() => { + void handleConfirmDeleteDeduction() + }} + title={t('deleteDeductionDialog.title')} + description={t('deleteDeductionDialog.description', { + deduction: pendingDeleteDeduction?.description ?? '', + })} + confirmLabel={t('deleteDeductionDialog.confirmCta')} + cancelLabel={t('deleteDeductionDialog.cancelCta')} + /> + + ) +} diff --git a/src/components/Employee/Deductions/management/DeductionsCard/index.ts b/src/components/Employee/Deductions/management/DeductionsCard/index.ts new file mode 100644 index 000000000..3fd68af43 --- /dev/null +++ b/src/components/Employee/Deductions/management/DeductionsCard/index.ts @@ -0,0 +1,2 @@ +export { DeductionsCard } from './DeductionsCard' +export type { DeductionsCardProps } from './DeductionsCard' diff --git a/src/components/Employee/Deductions/management/DeductionsComponents.tsx b/src/components/Employee/Deductions/management/DeductionsComponents.tsx new file mode 100644 index 000000000..b88f7171b --- /dev/null +++ b/src/components/Employee/Deductions/management/DeductionsComponents.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from 'react-i18next' +import { DeductionsCard } from './DeductionsCard' +import { DeductionsEditForm } from './DeductionsEditForm' +import { Flex } from '@/components/Common/Flex/Flex' +import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { ensureRequired } from '@/helpers/ensureRequired' +import { componentEvents } from '@/shared/constants' + +export type DeductionsSuccessAlertCode = 'deductionAdded' | 'deductionUpdated' | 'deductionDeleted' + +export interface DeductionsContextInterface extends FlowContextInterface { + employeeId?: string + /** Set when transitioning to `editDeduction` via the EDIT event; consumed + * by `DeductionsEditFormContextual` to pre-populate the form. Omit on + * ADD to open the form in create mode. */ + editingDeductionId?: string + successAlert?: DeductionsSuccessAlertCode | null +} + +export function DeductionsCardContextual() { + const { employeeId, onEvent, successAlert } = useFlow() + const { t } = useTranslation('Employee.Management.Deductions') + const Components = useComponentContext() + return ( + + {successAlert ? ( + { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_ALERT_DISMISSED, null) + }} + /> + ) : null} + + + ) +} + +export function DeductionsEditFormContextual() { + const { employeeId, editingDeductionId, onEvent } = useFlow() + return ( + + ) +} diff --git a/src/components/Employee/Deductions/management/DeductionsEditForm.tsx b/src/components/Employee/Deductions/management/DeductionsEditForm.tsx new file mode 100644 index 000000000..4da3f3f48 --- /dev/null +++ b/src/components/Employee/Deductions/management/DeductionsEditForm.tsx @@ -0,0 +1,82 @@ +import { DeductionsForm } from '../onboarding/DeductionsForm/DeductionsForm' +import { useDeductionsList } from '../shared/useDeductionsList' +import { + BaseBoundaries, + BaseLayout, + type BaseComponentInterface, + type CommonComponentInterface, +} from '@/components/Base' +import { useI18n, useComponentDictionary } from '@/i18n' +import { componentEvents } from '@/shared/constants' + +export interface DeductionsEditFormProps extends CommonComponentInterface<'Employee.Deductions'> { + employeeId: string + /** When provided, the form opens in edit mode pre-populated with the + * matching active deduction. Omit to open in add mode. */ + editingDeductionId?: string + onEvent: BaseComponentInterface['onEvent'] +} + +/** + * Standalone add/edit surface for a single deduction. Wraps the shared + * `DeductionsForm` (which is also used by the onboarding flow), looks up + * the row to edit by id, and translates the form's `onSaved` / `onCancel` + * callbacks into the management block's scoped events + * (`EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED` / `_UPDATED` / + * `_CANCELLED`). + */ +export function DeductionsEditForm({ + FallbackComponent, + ...props +}: DeductionsEditFormProps & Pick) { + return ( + + + + ) +} + +function DeductionsEditFormRoot({ + employeeId, + editingDeductionId, + dictionary, + onEvent, +}: DeductionsEditFormProps) { + useI18n('Employee.Deductions') + useComponentDictionary('Employee.Deductions', dictionary) + + // React Query dedupes against any sibling consumer of this list, so this + // is just a typed handle on the loaded row used to seed edit mode. + const list = useDeductionsList({ employeeId }) + + if (list.isLoading) { + return + } + + const deduction = editingDeductionId + ? (list.data.deductions.find(d => d.uuid === editingDeductionId) ?? null) + : null + + return ( + + { + onEvent( + mode === 'create' + ? componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED + : componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED, + saved, + ) + }} + onCancel={() => { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED) + }} + /> + + ) +} diff --git a/src/components/Employee/Deductions/management/deductionsStateMachine.ts b/src/components/Employee/Deductions/management/deductionsStateMachine.ts new file mode 100644 index 000000000..841210b79 --- /dev/null +++ b/src/components/Employee/Deductions/management/deductionsStateMachine.ts @@ -0,0 +1,92 @@ +import { reduce, state, transition } from 'robot3' +import type { ComponentType } from 'react' +import type { Garnishment } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' +import type { DeductionsContextInterface } from './DeductionsComponents' +import { DeductionsCardContextual, DeductionsEditFormContextual } from './DeductionsComponents' +import { componentEvents } from '@/shared/constants' +import type { MachineEventType, MachineTransition } from '@/types/Helpers' + +type EventPayloads = { + [componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED]: Garnishment +} + +const returnToCard = reduce( + (ctx: DeductionsContextInterface): DeductionsContextInterface => ({ + ...ctx, + component: DeductionsCardContextual as ComponentType, + successAlert: null, + editingDeductionId: undefined, + }), +) + +const returnToCardWithAlert = (alert: DeductionsContextInterface['successAlert']) => + reduce( + (ctx: DeductionsContextInterface): DeductionsContextInterface => ({ + ...ctx, + component: DeductionsCardContextual as ComponentType, + successAlert: alert, + editingDeductionId: undefined, + }), + ) + +export const deductionsStateMachine = { + card: state( + transition( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED, + 'editDeduction', + reduce( + (ctx: DeductionsContextInterface): DeductionsContextInterface => ({ + ...ctx, + component: DeductionsEditFormContextual as ComponentType, + successAlert: null, + editingDeductionId: undefined, + }), + ), + ), + transition( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED, + 'editDeduction', + reduce( + ( + ctx: DeductionsContextInterface, + ev: MachineEventType< + EventPayloads, + typeof componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED + >, + ): DeductionsContextInterface => ({ + ...ctx, + component: DeductionsEditFormContextual as ComponentType, + successAlert: null, + editingDeductionId: ev.payload.uuid, + }), + ), + ), + transition( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED, + 'card', + returnToCardWithAlert('deductionDeleted'), + ), + transition( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_ALERT_DISMISSED, + 'card', + returnToCard, + ), + ), + editDeduction: state( + transition( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED, + 'card', + returnToCardWithAlert('deductionAdded'), + ), + transition( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED, + 'card', + returnToCardWithAlert('deductionUpdated'), + ), + transition( + componentEvents.EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED, + 'card', + returnToCard, + ), + ), +} diff --git a/src/components/Employee/Deductions/management/index.ts b/src/components/Employee/Deductions/management/index.ts new file mode 100644 index 000000000..1038b8a3b --- /dev/null +++ b/src/components/Employee/Deductions/management/index.ts @@ -0,0 +1,6 @@ +export { Deductions } from './Deductions' +export type { DeductionsProps } from './Deductions' +export { DeductionsCard } from './DeductionsCard' +export type { DeductionsCardProps } from './DeductionsCard' +export { DeductionsEditForm } from './DeductionsEditForm' +export type { DeductionsEditFormProps } from './DeductionsEditForm' diff --git a/src/components/Employee/Deductions/shared/DeleteDeductionDialog.tsx b/src/components/Employee/Deductions/shared/DeleteDeductionDialog.tsx index bd313c7e7..b25fe57db 100644 --- a/src/components/Employee/Deductions/shared/DeleteDeductionDialog.tsx +++ b/src/components/Employee/Deductions/shared/DeleteDeductionDialog.tsx @@ -1,19 +1,31 @@ -import { useTranslation } from 'react-i18next' +import type { ReactNode } from 'react' import type { Garnishment } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +/** + * Presentational confirm dialog for deleting a deduction. All copy is + * passed in by the caller so each consumer owns its own translation strings + * — partner overrides on one flow's namespace won't leak into another. + */ export function DeleteDeductionDialog({ pendingDeleteDeduction, isPrimaryActionLoading, onClose, onConfirm, + title, + description, + confirmLabel, + cancelLabel, }: { pendingDeleteDeduction: Garnishment | null isPrimaryActionLoading: boolean onClose: () => void onConfirm: () => void + title: string + description: ReactNode + confirmLabel: string + cancelLabel: string }) { - const { t } = useTranslation('Employee.Deductions') const Components = useComponentContext() return ( - {pendingDeleteDeduction && - t('deleteDeductionDialog.description', { - deduction: pendingDeleteDeduction.description ?? '', - })} + {pendingDeleteDeduction && description} ) } diff --git a/src/components/Employee/exports/employeeManagement.ts b/src/components/Employee/exports/employeeManagement.ts index 99b3dbe7b..1be3c46bb 100644 --- a/src/components/Employee/exports/employeeManagement.ts +++ b/src/components/Employee/exports/employeeManagement.ts @@ -33,6 +33,12 @@ export { } from '../PaymentMethod/management' export { PaystubsCard } from '../Paystubs/management' export type { PaystubsCardProps } from '../Paystubs/management' +export { Deductions, DeductionsCard, DeductionsEditForm } from '../Deductions/management' +export type { + DeductionsProps, + DeductionsCardProps, + DeductionsEditFormProps, +} from '../Deductions/management' export { TerminateEmployee } from '../Terminations/TerminateEmployee/TerminateEmployee' export { TerminationSummary } from '../Terminations/TerminationSummary/TerminationSummary' export { TerminationFlow } from '../Terminations/TerminationFlow/TerminationFlow' diff --git a/src/i18n/en/Employee.Dashboard.json b/src/i18n/en/Employee.Dashboard.json index 417b58756..58fd67e3b 100644 --- a/src/i18n/en/Employee.Dashboard.json +++ b/src/i18n/en/Employee.Dashboard.json @@ -70,21 +70,6 @@ "minWageChangedNoRate": "Minimum wage adjustment will change" } } - }, - "deductions": { - "title": "Deductions", - "addDeductionCta": "Add deduction", - "listLabel": "List of deductions", - "deduction": "Deduction", - "frequency": "Frequency", - "withhold": "Withheld", - "recurring": "Recurring", - "oneTime": "One-time", - "amountPerPaycheck": "{{value}} per paycheck", - "emptyState": { - "title": "No deductions", - "description": "Employee deductions will appear here" - } } }, "taxes": { diff --git a/src/i18n/en/Employee.Management.Deductions.json b/src/i18n/en/Employee.Management.Deductions.json new file mode 100644 index 000000000..5042e8ec8 --- /dev/null +++ b/src/i18n/en/Employee.Management.Deductions.json @@ -0,0 +1,31 @@ +{ + "title": "Deductions", + "addDeductionCta": "Add deduction", + "listLabel": "List of deductions", + "columns": { + "deduction": "Deduction", + "frequency": "Frequency", + "withhold": "Withheld" + }, + "recurring": "Recurring", + "oneTime": "One-time", + "amountPerPaycheck": "{{value}} per paycheck", + "emptyState": { + "title": "No deductions", + "description": "Employee deductions will appear here" + }, + "hamburgerTitle": "Deduction actions menu", + "editCta": "Edit deduction", + "deleteCta": "Delete deduction", + "deleteDeductionDialog": { + "title": "Delete this deduction?", + "description": "{{deduction}} will no longer be deducted from this employee's paycheck.", + "confirmCta": "Delete", + "cancelCta": "Cancel" + }, + "alerts": { + "deductionAdded": "Deduction successfully added.", + "deductionUpdated": "Deduction successfully updated.", + "deductionDeleted": "Deduction successfully deleted." + } +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 22085191e..871691535 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -113,6 +113,18 @@ export const employeeEvents = { EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOAD_REQUESTED: 'employee/management/paystubs/card/downloadRequested', EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOADED: 'employee/management/paystubs/card/downloaded', + EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED: + 'employee/management/deductions/card/addRequested', + EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED: + 'employee/management/deductions/card/editRequested', + EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED: 'employee/management/deductions/card/deleted', + EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED: + 'employee/management/deductions/editForm/created', + EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED: + 'employee/management/deductions/editForm/updated', + EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED: + 'employee/management/deductions/editForm/cancelled', + EMPLOYEE_MANAGEMENT_DEDUCTIONS_ALERT_DISMISSED: 'employee/management/deductions/alertDismissed', } as const /** diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index b0a202dc5..8d75b9cb7 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -1500,21 +1500,6 @@ export interface EmployeeDashboard{ }; }; }; -"deductions":{ -"title":string; -"addDeductionCta":string; -"listLabel":string; -"deduction":string; -"frequency":string; -"withhold":string; -"recurring":string; -"oneTime":string; -"amountPerPaycheck":string; -"emptyState":{ -"title":string; -"description":string; -}; -}; }; "taxes":{ "federal":{ @@ -2008,6 +1993,37 @@ export interface EmployeeLanding{ }; "getStartedCta":string; }; +export interface EmployeeManagementDeductions{ +"title":string; +"addDeductionCta":string; +"listLabel":string; +"columns":{ +"deduction":string; +"frequency":string; +"withhold":string; +}; +"recurring":string; +"oneTime":string; +"amountPerPaycheck":string; +"emptyState":{ +"title":string; +"description":string; +}; +"hamburgerTitle":string; +"editCta":string; +"deleteCta":string; +"deleteDeductionDialog":{ +"title":string; +"description":string; +"confirmCta":string; +"cancelCta":string; +}; +"alerts":{ +"deductionAdded":string; +"deductionUpdated":string; +"deductionDeleted":string; +}; +}; export interface EmployeeManagementPaymentMethod{ "title":string; "splitCta":string; @@ -3671,6 +3687,6 @@ export interface common{ interface CustomTypeOptions { defaultNS: 'common'; - resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Company.TimeOff.CreateTimeOffPolicy': CompanyTimeOffCreateTimeOffPolicy, 'Company.TimeOff.EmployeeTable': CompanyTimeOffEmployeeTable, 'Company.TimeOff.HolidayPolicy': CompanyTimeOffHolidayPolicy, 'Company.TimeOff.PolicyDetail': CompanyTimeOffPolicyDetail, 'Company.TimeOff.SelectEmployees': CompanyTimeOffSelectEmployees, 'Company.TimeOff.SelectPolicyType': CompanyTimeOffSelectPolicyType, 'Company.TimeOff.TimeOffPolicies': CompanyTimeOffTimeOffPolicies, 'Company.TimeOff.TimeOffPolicyDetails': CompanyTimeOffTimeOffPolicyDetails, 'Company.TimeOff.TimeOffRequests': CompanyTimeOffTimeOffRequests, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentSummary': ContractorPaymentsPaymentSummary, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Dashboard': EmployeeDashboard, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentManager': EmployeeDocumentManager, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeDocuments': EmployeeEmployeeDocuments, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.EmploymentEligibility': EmployeeEmploymentEligibility, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress.Management': EmployeeHomeAddressManagement, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.I9SignatureForm': EmployeeI9SignatureForm, 'Employee.Landing': EmployeeLanding, 'Employee.Management.PaymentMethod': EmployeeManagementPaymentMethod, 'Employee.Management.PaymentMethodBankForm': EmployeeManagementPaymentMethodBankForm, 'Employee.Management.PaymentMethodSplitForm': EmployeeManagementPaymentMethodSplitForm, 'Employee.Management.Paystubs': EmployeeManagementPaystubs, 'Employee.Management.WorkAddress': EmployeeManagementWorkAddress, 'Employee.ManagementEmployeeList': EmployeeManagementEmployeeList, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile.Management': EmployeeProfileManagement, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Employee.Terminations.TerminateEmployee': EmployeeTerminationsTerminateEmployee, 'Employee.Terminations.TerminationFlow': EmployeeTerminationsTerminationFlow, 'Employee.Terminations.TerminationSummary': EmployeeTerminationsTerminationSummary, 'InformationRequests.InformationRequestForm': InformationRequestsInformationRequestForm, 'InformationRequests.InformationRequestList': InformationRequestsInformationRequestList, 'InformationRequests': InformationRequests, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.Dismissal': PayrollDismissal, 'Payroll.EmployeeSelection': PayrollEmployeeSelection, 'Payroll.GrossUpModal': PayrollGrossUpModal, 'Payroll.OffCycle': PayrollOffCycle, 'Payroll.OffCycleCreation': PayrollOffCycleCreation, 'Payroll.OffCycleDeductionsSetting': PayrollOffCycleDeductionsSetting, 'Payroll.OffCyclePayPeriodDateForm': PayrollOffCyclePayPeriodDateForm, 'Payroll.OffCycleReasonSelection': PayrollOffCycleReasonSelection, 'Payroll.OffCycleTaxWithholding': PayrollOffCycleTaxWithholding, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.Transition': PayrollTransition, 'Payroll.TransitionCreation': PayrollTransitionCreation, 'Payroll.TransitionPayrollAlert': PayrollTransitionPayrollAlert, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } + resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Company.TimeOff.CreateTimeOffPolicy': CompanyTimeOffCreateTimeOffPolicy, 'Company.TimeOff.EmployeeTable': CompanyTimeOffEmployeeTable, 'Company.TimeOff.HolidayPolicy': CompanyTimeOffHolidayPolicy, 'Company.TimeOff.PolicyDetail': CompanyTimeOffPolicyDetail, 'Company.TimeOff.SelectEmployees': CompanyTimeOffSelectEmployees, 'Company.TimeOff.SelectPolicyType': CompanyTimeOffSelectPolicyType, 'Company.TimeOff.TimeOffPolicies': CompanyTimeOffTimeOffPolicies, 'Company.TimeOff.TimeOffPolicyDetails': CompanyTimeOffTimeOffPolicyDetails, 'Company.TimeOff.TimeOffRequests': CompanyTimeOffTimeOffRequests, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentSummary': ContractorPaymentsPaymentSummary, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Dashboard': EmployeeDashboard, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentManager': EmployeeDocumentManager, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeDocuments': EmployeeEmployeeDocuments, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.EmploymentEligibility': EmployeeEmploymentEligibility, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress.Management': EmployeeHomeAddressManagement, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.I9SignatureForm': EmployeeI9SignatureForm, 'Employee.Landing': EmployeeLanding, 'Employee.Management.Deductions': EmployeeManagementDeductions, 'Employee.Management.PaymentMethod': EmployeeManagementPaymentMethod, 'Employee.Management.PaymentMethodBankForm': EmployeeManagementPaymentMethodBankForm, 'Employee.Management.PaymentMethodSplitForm': EmployeeManagementPaymentMethodSplitForm, 'Employee.Management.Paystubs': EmployeeManagementPaystubs, 'Employee.Management.WorkAddress': EmployeeManagementWorkAddress, 'Employee.ManagementEmployeeList': EmployeeManagementEmployeeList, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile.Management': EmployeeProfileManagement, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Employee.Terminations.TerminateEmployee': EmployeeTerminationsTerminateEmployee, 'Employee.Terminations.TerminationFlow': EmployeeTerminationsTerminationFlow, 'Employee.Terminations.TerminationSummary': EmployeeTerminationsTerminationSummary, 'InformationRequests.InformationRequestForm': InformationRequestsInformationRequestForm, 'InformationRequests.InformationRequestList': InformationRequestsInformationRequestList, 'InformationRequests': InformationRequests, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.Dismissal': PayrollDismissal, 'Payroll.EmployeeSelection': PayrollEmployeeSelection, 'Payroll.GrossUpModal': PayrollGrossUpModal, 'Payroll.OffCycle': PayrollOffCycle, 'Payroll.OffCycleCreation': PayrollOffCycleCreation, 'Payroll.OffCycleDeductionsSetting': PayrollOffCycleDeductionsSetting, 'Payroll.OffCyclePayPeriodDateForm': PayrollOffCyclePayPeriodDateForm, 'Payroll.OffCycleReasonSelection': PayrollOffCycleReasonSelection, 'Payroll.OffCycleTaxWithholding': PayrollOffCycleTaxWithholding, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.Transition': PayrollTransition, 'Payroll.TransitionCreation': PayrollTransitionCreation, 'Payroll.TransitionPayrollAlert': PayrollTransitionPayrollAlert, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } }; } \ No newline at end of file From debbcce70823a2efda5b8de3faffb721ff8d98e2 Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Thu, 4 Jun 2026 14:20:24 -0600 Subject: [PATCH 2/4] feat: decouple Deductions form translations per surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared `DeductionsForm` now owns a dedicated `Employee.DeductionsForm` namespace internally and accepts a typed `formDictionary` prop. Each surface (onboarding + management) resolves the form text from its own namespace through a `useFormDictionary` hook and passes the resolved dictionary down. Partners can override the form text by overriding only the surface's namespace — no cross-namespace coupling — and the form JSX stays single-sourced under `Deductions/shared/DeductionsForm/`. Onboarding's existing `Employee.Deductions.json` keys are preserved verbatim; the new onboarding `useFormDictionary` maps the historical flat names onto the form's nested shape so existing partner overrides continue to render unchanged. Management gets a nested `form.*` subtree on `Employee.Management.Deductions.json` and stops reading from the onboarding namespace. Also updates the `migrate-dashboard-card-to-block` skill with guidance on when to apply this pattern. Co-authored-by: Cursor --- .../migrate-dashboard-card-to-block/SKILL.md | 45 ++++- .reports/embedded-react-sdk.api.md | 67 +++++++- .../employee-management.md | 14 +- .../DeductionsEditForm.tsx | 22 ++- .../management/DeductionsEditForm/index.ts | 1 + .../DeductionsEditForm/useFormDictionary.ts | 98 +++++++++++ .../deductionsContextualComponents.tsx | 5 +- .../onboarding/useFormDictionary.ts | 102 +++++++++++ .../DeductionsForm/ChildSupportFormView.tsx | 79 +++++---- .../DeductionsForm/DeductionsForm.test.tsx | 26 +++ .../DeductionsForm/DeductionsForm.tsx | 48 +++--- .../DeductionsForm/StandardDeductionForm.tsx | 52 +++--- .../Deductions/shared/DeductionsForm/index.ts | 2 + .../Deductions/shared/DeductionsForm/types.ts | 14 ++ .../Employee/Deductions/shared/index.ts | 5 + src/i18n/en/Employee.DeductionsForm.json | 78 +++++++++ .../en/Employee.Management.Deductions.json | 78 +++++++++ src/types/i18next.d.ts | 158 +++++++++++++++++- 18 files changed, 786 insertions(+), 108 deletions(-) rename src/components/Employee/Deductions/management/{ => DeductionsEditForm}/DeductionsEditForm.tsx (71%) create mode 100644 src/components/Employee/Deductions/management/DeductionsEditForm/index.ts create mode 100644 src/components/Employee/Deductions/management/DeductionsEditForm/useFormDictionary.ts create mode 100644 src/components/Employee/Deductions/onboarding/useFormDictionary.ts rename src/components/Employee/Deductions/{onboarding => shared}/DeductionsForm/ChildSupportFormView.tsx (60%) rename src/components/Employee/Deductions/{onboarding => shared}/DeductionsForm/DeductionsForm.test.tsx (65%) rename src/components/Employee/Deductions/{onboarding => shared}/DeductionsForm/DeductionsForm.tsx (78%) rename src/components/Employee/Deductions/{onboarding => shared}/DeductionsForm/StandardDeductionForm.tsx (78%) create mode 100644 src/components/Employee/Deductions/shared/DeductionsForm/index.ts create mode 100644 src/components/Employee/Deductions/shared/DeductionsForm/types.ts create mode 100644 src/i18n/en/Employee.DeductionsForm.json diff --git a/.claude/skills/migrate-dashboard-card-to-block/SKILL.md b/.claude/skills/migrate-dashboard-card-to-block/SKILL.md index ef28d4a61..1830448f7 100644 --- a/.claude/skills/migrate-dashboard-card-to-block/SKILL.md +++ b/.claude/skills/migrate-dashboard-card-to-block/SKILL.md @@ -667,11 +667,13 @@ useI18n('Employee.HomeAddress.Management') Don't reach for the base namespace just because a shared form **hook** is rendered in both places. Form hooks like `useHomeAddressForm` emit field _components_ (e.g. `Street1`, `City`, `Zip`); they don't render any text themselves. Each call site passes its own `label` and `validationMessages` props from whatever namespace is convenient. The onboarding consumer reads from `Employee.HomeAddress`, the management consumer reads from `Employee.HomeAddress.Management`, and the two never need to share a translation namespace just because they share field components. -Concrete heuristic — only dual-load the base namespace if **a runtime-shared piece of UI** (a shared presentational component that itself calls `useTranslation('Employee.')`) is rendered inside the management path. If the management-side consumer of those strings is only ever rendered in the management path (e.g. a `View` that exists solely under `management/`), copy the strings it needs into the feature's management namespace and read everything from one namespace. Don't reach across. +Concrete heuristic by scenario: -Concretely: the field labels, validation messages, and courtesy-withholding copy that `HomeAddress/management/HomeAddressView.tsx` renders are duplicated into `Employee.HomeAddress.Management.json` rather than dual-loading `Employee.HomeAddress`, because `HomeAddressView` is exclusive to the management path. Onboarding's `EmployeeProfile`/`AdminProfile` keep reading the same keys from `Employee.HomeAddress`. +- **Shared UI lives only in the management path** (e.g. a `View` that exists solely under `management/`) — copy the strings it needs into `Employee..Management.json` and read everything from one namespace. Don't reach across. The field labels, validation messages, and courtesy-withholding copy that `HomeAddress/management/HomeAddressView.tsx` renders are duplicated into `Employee.HomeAddress.Management.json` for exactly this reason; onboarding's `EmployeeProfile`/`AdminProfile` keep reading the same keys from `Employee.HomeAddress`. Duplication is the cost; it buys a fully self-contained block. A partner who only overrides `Employee.HomeAddress.Management` via `useComponentDictionary` gets a coherent result without having to discover that they also need to override `Employee.HomeAddress`. +- **Shared UI is a hook with no text of its own** — each call site passes its own `label` / `validationMessages` props (see paragraph above). No coordination needed. +- **Shared UI is a JSX component that renders text and is mounted in both surfaces** — don't dual-load and don't duplicate the JSX. Use the per-surface `dictionary`-prop pattern described in "When a shared component renders in multiple surfaces" below. The shared component gets its own dedicated namespace and each surface injects its strings through a typed dictionary prop, so neither surface needs to reach across to the other or to the shared component's namespace. -Duplication is the cost; it buys a fully self-contained block. A partner who only overrides the management namespace via `useComponentDictionary` gets a coherent result without having to discover that they also need to override `Employee.HomeAddress`. When the same copy needs to change in both contexts (rare for field labels; almost never for validation messages), the change is two edits instead of one — that's a worthwhile trade. +When the same copy needs to change in both contexts (rare for field labels; almost never for validation messages), the change is two edits instead of one — that's a worthwhile trade. ### Components shared across flows must be presentational (no i18n) @@ -756,6 +758,43 @@ Why this shape: The wrappers belong in the management block's checklist alongside the card and the block — exports, registry regen, and the per-component i18n file all follow the standard pattern (just with `` substituted for `` in the names). +### When a shared component renders in multiple surfaces + +Sometimes the same JSX renders in both an onboarding flow and a management block — `DeductionsForm` is the canonical case: identical variant picker, identical child-support and standard sub-forms, mounted by both `Employee/Deductions/onboarding/deductionsContextualComponents.tsx` and `Employee/Deductions/management/DeductionsEditForm/`. The "dual-load or duplicate" choices above don't apply, because the JSX itself is one source of truth and we want each surface to own its own translation surface. + +The pattern: give the shared component its **own** dedicated namespace internally, and let each surface inject the resolved strings it wants to render through a typed dictionary prop. Surfaces never reference the shared component's namespace; the shared component never reads from a surface's namespace. + +**Architecture** + +- The shared component lives at `/shared//`. Internally it calls `useI18n('Employee.')`, `useComponentDictionary('Employee.', formDictionary)`, and `useTranslation('Employee.')`. It never references either surface's namespace. +- The shared component accepts a `formDictionary?: Dictionary` prop. `Dictionary` is a thin type alias for `ResourceDictionary<'Employee.'>` exported from the shared component's folder — callers reference the structural alias, never the namespace string. +- An `Employee..json` defaults file under `src/i18n/en/` carries the full English copy in the nested shape the component renders. It serves two purposes: it's the type contract that `Dictionary` is keyed against (via `i18n:generate`), and it's the safety net for any caller that doesn't pass `formDictionary`. +- Each surface adds a `useFormDictionary` hook colocated with that surface's block. The hook calls `useTranslation` against the **surface's** namespace (`Employee.` for onboarding, `Employee..Management` for management), reads the keys that surface wants to expose to the form, and returns them packed into the shared component's `Dictionary` shape. +- The block passes `useFormDictionary()`'s result into the shared component as `formDictionary`. Because the hook calls `t()` against the surface's namespace at render time, partner overrides on the surface's namespace flow through naturally — no extra wiring. + +**Override chain (one direction)** + +`partner dictionary` → `useComponentDictionary('Employee.', dictionary)` → surface namespace → `useFormDictionary` resolves `t(...)` → `formDictionary` prop → `useComponentDictionary('Employee.', formDictionary)` → shared component's internal namespace → rendered string. + +The same chain works at the GustoProvider level — a `dictionary={{ en: { 'Employee.': { ... } } }}` global override hits the surface's namespace first, the hook re-resolves, and the result flows down to the shared component. No global override has to know about the shared component's namespace. + +**Reference implementation — `DeductionsForm`** + +- Shared component: [`src/components/Employee/Deductions/shared/DeductionsForm/`](../../../src/components/Employee/Deductions/shared/DeductionsForm/) — the component itself plus the exported `DeductionsFormDictionary` type and the barrel that re-exports both. +- Internal namespace + defaults: [`src/i18n/en/Employee.DeductionsForm.json`](../../../src/i18n/en/Employee.DeductionsForm.json) — full nested shape (`addTitle`, `standard.*`, `childSupport.*`, `actions.*`, etc.). +- Onboarding's per-surface hook: [`src/components/Employee/Deductions/onboarding/useFormDictionary.ts`](../../../src/components/Employee/Deductions/onboarding/useFormDictionary.ts) — resolves `t(...)` against onboarding's historical flat keys in `Employee.Deductions` (`addDeductionTitle`, `descriptionLabelV2`, `agency`, `per`, …) and packs them into `DeductionsFormDictionary`'s nested shape. +- Management's per-surface hook: [`src/components/Employee/Deductions/management/DeductionsEditForm/useFormDictionary.ts`](../../../src/components/Employee/Deductions/management/DeductionsEditForm/useFormDictionary.ts) — resolves `t('form.*')` against `Employee.Management.Deductions`, which carries a dedicated nested `form.*` subtree. + +**Backward compatibility when retrofitting an existing shared component** + +If the shared component existed previously and one surface (typically onboarding) had partners overriding flat key names on the original namespace, write that surface's `useFormDictionary` first as a pure key-rename mapping over the historical keys. Don't change the surface's JSON file — leave every existing key in place. The mapping `descriptionLabel: t('descriptionLabelV2')` etc. is what proves no partner override breaks. The second surface (management) can adopt the new nested shape directly in its own JSON file because it had no historical partner contract to preserve. + +**Why this pattern over duplication or dual-load** + +- Shared JSX stays shared — no copy-paste maintenance burden when the form structure changes. +- Each surface owns its full translation surface in one JSON. A partner overriding `Employee..Management` via `useComponentDictionary` gets coherent management-side text including form strings, without having to discover and override a separate shared namespace. +- The shared component's namespace is purely an implementation detail. Surfaces don't import it, partners don't see it, and the two surfaces' overrides are fully decoupled — neither surface's translations can leak into the other. + ### Strings to move out of `Employee.Dashboard` during migration The current [`Employee.Dashboard.json`](../../../src/i18n/en/Employee.Dashboard.json) bundles every card's copy under tab-scoped keys (`basicDetails.*`, `homeAddress.*`, `workAddress.*`, `jobAndPay.compensation.*`, `jobAndPay.payment.*`, `jobAndPay.deductions.*`, `jobAndPay.paystubs.*`, `taxes.federal.*`, `taxes.state.*`, `documents.*`, `alerts.*`). Each card's migration relocates its slice: diff --git a/.reports/embedded-react-sdk.api.md b/.reports/embedded-react-sdk.api.md index 7d77ff754..1738d6e6a 100644 --- a/.reports/embedded-react-sdk.api.md +++ b/.reports/embedded-react-sdk.api.md @@ -1055,6 +1055,13 @@ export const componentEvents: { readonly EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED: "employee/management/workAddress/updated"; readonly EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED: "employee/management/workAddress/deleted"; readonly EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED: "employee/management/workAddress/editCancelled"; + readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED: "employee/management/deductions/card/addRequested"; + readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED: "employee/management/deductions/card/editRequested"; + readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED: "employee/management/deductions/card/deleted"; + readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED: "employee/management/deductions/editForm/created"; + readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED: "employee/management/deductions/editForm/updated"; + readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED: "employee/management/deductions/editForm/cancelled"; + readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_ALERT_DISMISSED: "employee/management/deductions/alertDismissed"; readonly ROBOT_MACHINE_DONE: "done"; readonly ERROR: "ERROR"; readonly CANCEL: "CANCEL"; @@ -1718,11 +1725,61 @@ export type DeductionFormRequiredValidation = typeof DeductionFormErrorCodes.REQ // @public (undocumented) function Deductions(input: DeductionsProps): JSX_2.Element; +// Warning: (ae-missing-release-tag) "Deductions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function Deductions_2(input: DeductionsProps_2 & BaseComponentInterface<'Employee.Management.Deductions'>): JSX_2.Element; + +// Warning: (ae-missing-release-tag) "DeductionsCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function DeductionsCard(input: DeductionsCardProps): JSX_2.Element; + +// Warning: (ae-missing-release-tag) "DeductionsCardProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface DeductionsCardProps { + // (undocumented) + employeeId: string; + // Warning: (ae-forgotten-export) The symbol "OnEventType" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "EventType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + onEvent: OnEventType; +} + +// Warning: (ae-missing-release-tag) "DeductionsEditForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function DeductionsEditForm(input: DeductionsEditFormProps & Pick): JSX_2.Element; + +// Warning: (ae-forgotten-export) The symbol "CommonComponentInterface" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "DeductionsEditFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface DeductionsEditFormProps extends CommonComponentInterface<'Employee.Management.Deductions'> { + editingDeductionId?: string; + // (undocumented) + employeeId: string; + // (undocumented) + onEvent: BaseComponentInterface['onEvent']; +} + // Warning: (ae-missing-release-tag) "DeductionsFieldProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export type DeductionsFieldProps = HookFieldProps>; +// Warning: (ae-missing-release-tag) "DeductionsProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface DeductionsProps_2 extends CommonComponentInterface<'Employee.Management.Deductions'> { + // (undocumented) + employeeId: string; + // (undocumented) + onEvent: OnEventType; +} + // Warning: (ae-missing-release-tag) "DependentsAmountFieldProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1791,9 +1848,6 @@ interface DismissalFlowProps { companyId: string; // (undocumented) employeeId?: string; - // Warning: (ae-forgotten-export) The symbol "OnEventType" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "EventType" needs to be exported by the entry point index.d.ts - // // (undocumented) onEvent: OnEventType; // (undocumented) @@ -2003,6 +2057,12 @@ declare namespace EmployeeManagement { ProfileEditFormProps, PaymentMethod_3 as PaymentMethod, PaymentMethodProps_3 as PaymentMethodProps, + Deductions_2 as Deductions, + DeductionsCard, + DeductionsEditForm, + DeductionsProps_2 as DeductionsProps, + DeductionsCardProps, + DeductionsEditFormProps, TerminateEmployee, TerminationSummary, TerminationFlow @@ -2219,7 +2279,6 @@ export type FederalTaxesFormOutputs = FederalTaxesFormData; // @public (undocumented) export type FederalTaxesOptionalFieldsToRequire = OptionalFieldsToRequire; -// Warning: (ae-forgotten-export) The symbol "CommonComponentInterface" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "FederalTaxesProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/docs/workflows-overview/employee-management/employee-management.md b/docs/workflows-overview/employee-management/employee-management.md index 07fb8dcca..816d7cdfd 100644 --- a/docs/workflows-overview/employee-management/employee-management.md +++ b/docs/workflows-overview/employee-management/employee-management.md @@ -311,13 +311,13 @@ function MyDeductionsPanel({ employeeId }) { **Props** -| Name | Type | Description | -| ------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| employeeId Required | string | The associated employee identifier. | -| editingDeductionId | string | Optional `uuid` of the deduction to edit. When omitted the form opens in add mode; when set the form pre-populates that row. | -| onEvent Required | function | See events table for available events. | -| dictionary | object | Optional translations for component text. Keys are namespaced under `Employee.Management.Deductions`. | -| FallbackComponent | React.ComponentType | Optional custom error fallback component used by the internal `BaseBoundaries` wrapper. | +| Name | Type | Description | +| ------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| employeeId Required | string | The associated employee identifier. | +| onEvent Required | function | See events table for available events. | +| editingDeductionId | string | When provided, opens the form in edit mode pre-populated with the matching deduction. Omit to open in add mode. | +| dictionary | object | Optional translations for component text. Keys are namespaced under `Employee.Management.Deductions` — see the source JSON for the set. | +| FallbackComponent | React.ComponentType | Optional custom error fallback component used by the internal `BaseBoundaries` wrapper. | **Events** diff --git a/src/components/Employee/Deductions/management/DeductionsEditForm.tsx b/src/components/Employee/Deductions/management/DeductionsEditForm/DeductionsEditForm.tsx similarity index 71% rename from src/components/Employee/Deductions/management/DeductionsEditForm.tsx rename to src/components/Employee/Deductions/management/DeductionsEditForm/DeductionsEditForm.tsx index 4da3f3f48..bc56166ef 100644 --- a/src/components/Employee/Deductions/management/DeductionsEditForm.tsx +++ b/src/components/Employee/Deductions/management/DeductionsEditForm/DeductionsEditForm.tsx @@ -1,5 +1,6 @@ -import { DeductionsForm } from '../onboarding/DeductionsForm/DeductionsForm' -import { useDeductionsList } from '../shared/useDeductionsList' +import { DeductionsForm } from '../../shared/DeductionsForm' +import { useDeductionsList } from '../../shared/useDeductionsList' +import { useManagementDeductionsFormDictionary } from './useFormDictionary' import { BaseBoundaries, BaseLayout, @@ -9,7 +10,7 @@ import { import { useI18n, useComponentDictionary } from '@/i18n' import { componentEvents } from '@/shared/constants' -export interface DeductionsEditFormProps extends CommonComponentInterface<'Employee.Deductions'> { +export interface DeductionsEditFormProps extends CommonComponentInterface<'Employee.Management.Deductions'> { employeeId: string /** When provided, the form opens in edit mode pre-populated with the * matching active deduction. Omit to open in add mode. */ @@ -18,10 +19,11 @@ export interface DeductionsEditFormProps extends CommonComponentInterface<'Emplo } /** - * Standalone add/edit surface for a single deduction. Wraps the shared - * `DeductionsForm` (which is also used by the onboarding flow), looks up - * the row to edit by id, and translates the form's `onSaved` / `onCancel` - * callbacks into the management block's scoped events + * Standalone add/edit surface for a single deduction. Renders the shared + * `DeductionsForm` with management's own translation dictionary so partner + * overrides on `Employee.Management.Deductions` flow into the form text. + * Looks up the row to edit by id and translates the form's `onSaved` / + * `onCancel` callbacks into the management block's scoped events * (`EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED` / `_UPDATED` / * `_CANCELLED`). */ @@ -45,12 +47,13 @@ function DeductionsEditFormRoot({ dictionary, onEvent, }: DeductionsEditFormProps) { - useI18n('Employee.Deductions') - useComponentDictionary('Employee.Deductions', dictionary) + useI18n('Employee.Management.Deductions') + useComponentDictionary('Employee.Management.Deductions', dictionary) // React Query dedupes against any sibling consumer of this list, so this // is just a typed handle on the loaded row used to seed edit mode. const list = useDeductionsList({ employeeId }) + const formDictionary = useManagementDeductionsFormDictionary() if (list.isLoading) { return @@ -65,6 +68,7 @@ function DeductionsEditFormRoot({ { onEvent( mode === 'create' diff --git a/src/components/Employee/Deductions/management/DeductionsEditForm/index.ts b/src/components/Employee/Deductions/management/DeductionsEditForm/index.ts new file mode 100644 index 000000000..1744401c7 --- /dev/null +++ b/src/components/Employee/Deductions/management/DeductionsEditForm/index.ts @@ -0,0 +1 @@ +export { DeductionsEditForm, type DeductionsEditFormProps } from './DeductionsEditForm' diff --git a/src/components/Employee/Deductions/management/DeductionsEditForm/useFormDictionary.ts b/src/components/Employee/Deductions/management/DeductionsEditForm/useFormDictionary.ts new file mode 100644 index 000000000..30a535d5a --- /dev/null +++ b/src/components/Employee/Deductions/management/DeductionsEditForm/useFormDictionary.ts @@ -0,0 +1,98 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import type { DeductionsFormDictionary } from '../../shared/DeductionsForm' + +/** + * Resolves the shared `DeductionsForm`'s text against management's + * `Employee.Management.Deductions` namespace. All form copy is owned by + * management's translation file under the nested `form.*` shape, so partner + * overrides on `Employee.Management.Deductions` flow into the form text via + * `t(...)` resolution at render time. + */ +export function useManagementDeductionsFormDictionary(): DeductionsFormDictionary { + const { t } = useTranslation('Employee.Management.Deductions') + + return useMemo( + () => ({ + en: { + addTitle: t('form.addTitle'), + editTitle: t('form.editTitle'), + description: t('form.description'), + variantLabel: t('form.variantLabel'), + variantDescription: t('form.variantDescription'), + garnishmentOption: t('form.garnishmentOption'), + customOption: t('form.customOption'), + garnishmentTypeLabel: t('form.garnishmentTypeLabel'), + types: { + childSupport: t('form.types.childSupport'), + federalTaxLien: t('form.types.federalTaxLien'), + stateTaxLien: t('form.types.stateTaxLien'), + studentLoan: t('form.types.studentLoan'), + creditorGarnishment: t('form.types.creditorGarnishment'), + federalLoan: t('form.types.federalLoan'), + otherGarnishment: t('form.types.otherGarnishment'), + custom: t('form.types.custom'), + }, + standard: { + descriptionLabel: t('form.standard.descriptionLabel'), + descriptionRequired: t('form.standard.descriptionRequired'), + frequencyLabel: t('form.standard.frequencyLabel'), + frequencyRecurring: t('form.standard.frequencyRecurring'), + frequencyOneTime: t('form.standard.frequencyOneTime'), + frequencyRequired: t('form.standard.frequencyRequired'), + typeLabel: t('form.standard.typeLabel'), + typePercentage: t('form.standard.typePercentage'), + typeFixed: t('form.standard.typeFixed'), + typeRequired: t('form.standard.typeRequired'), + amountLabel: t('form.standard.amountLabel'), + amountPercentDescription: t('form.standard.amountPercentDescription'), + amountFixedDescription: t('form.standard.amountFixedDescription'), + amountRequired: t('form.standard.amountRequired'), + amountNonNegative: t('form.standard.amountNonNegative'), + totalAmountLabel: t('form.standard.totalAmountLabel'), + totalAmountDescription: t('form.standard.totalAmountDescription'), + annualMaxLabel: t('form.standard.annualMaxLabel'), + annualMaxDescription: t('form.standard.annualMaxDescription'), + }, + childSupport: { + agencyLabel: t('form.childSupport.agencyLabel'), + agencyDescription: t('form.childSupport.agencyDescription'), + agencyRequired: t('form.childSupport.agencyRequired'), + manualPaymentRequired: t('form.childSupport.manualPaymentRequired'), + countyLabel: t('form.childSupport.countyLabel'), + countyDescription: t('form.childSupport.countyDescription'), + allCounties: t('form.childSupport.allCounties'), + countyRequired: t('form.childSupport.countyRequired'), + caseNumberDescription: t('form.childSupport.caseNumberDescription'), + caseNumberRequired: t('form.childSupport.caseNumberRequired'), + orderNumberDescription: t('form.childSupport.orderNumberDescription'), + orderNumberRequired: t('form.childSupport.orderNumberRequired'), + remittanceNumberDescription: t('form.childSupport.remittanceNumberDescription'), + remittanceNumberRequired: t('form.childSupport.remittanceNumberRequired'), + totalAmountWithheld: t('form.childSupport.totalAmountWithheld'), + totalAmountWithheldDescription: t('form.childSupport.totalAmountWithheldDescription'), + payPeriodMaximumRequired: t('form.childSupport.payPeriodMaximumRequired'), + maxPaycheckPercentage: t('form.childSupport.maxPaycheckPercentage'), + maxPaycheckPercentageDescription: t('form.childSupport.maxPaycheckPercentageDescription'), + amountRequired: t('form.childSupport.amountRequired'), + amountNonNegative: t('form.childSupport.amountNonNegative'), + percentOutOfRange: t('form.childSupport.percentOutOfRange'), + paymentPeriodLabel: t('form.childSupport.paymentPeriodLabel'), + paymentPeriodDescription: t('form.childSupport.paymentPeriodDescription'), + paymentPeriodRequired: t('form.childSupport.paymentPeriodRequired'), + paymentPeriod: { + everyWeek: t('form.childSupport.paymentPeriod.everyWeek'), + everyOtherWeek: t('form.childSupport.paymentPeriod.everyOtherWeek'), + twicePerMonth: t('form.childSupport.paymentPeriod.twicePerMonth'), + monthly: t('form.childSupport.paymentPeriod.monthly'), + }, + }, + actions: { + save: t('form.actions.save'), + cancel: t('form.actions.cancel'), + }, + }, + }), + [t], + ) +} diff --git a/src/components/Employee/Deductions/onboarding/deductionsContextualComponents.tsx b/src/components/Employee/Deductions/onboarding/deductionsContextualComponents.tsx index f3c6d6b3a..400135b3c 100644 --- a/src/components/Employee/Deductions/onboarding/deductionsContextualComponents.tsx +++ b/src/components/Employee/Deductions/onboarding/deductionsContextualComponents.tsx @@ -1,8 +1,9 @@ import { type Garnishment } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' import { useDeductionsList } from '../shared/useDeductionsList' +import { DeductionsForm } from '../shared/DeductionsForm' import { IncludeDeductions } from './IncludeDeductions/IncludeDeductions' import { DeductionsList } from './DeductionsList/DeductionsList' -import { DeductionsForm } from './DeductionsForm/DeductionsForm' +import { useOnboardingDeductionsFormDictionary } from './useFormDictionary' import { BaseLayout } from '@/components/Base/Base' import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow' import { componentEvents } from '@/shared/constants' @@ -99,6 +100,7 @@ export function DeductionsFormContextual() { // The same list query the form hooks use internally — React Query dedupes // so this is the only outstanding request. const list = useDeductionsList({ employeeId: ensureRequired(employeeId) }) + const formDictionary = useOnboardingDeductionsFormDictionary() if (list.isLoading) { return @@ -130,6 +132,7 @@ export function DeductionsFormContextual() { diff --git a/src/components/Employee/Deductions/onboarding/useFormDictionary.ts b/src/components/Employee/Deductions/onboarding/useFormDictionary.ts new file mode 100644 index 000000000..b606e1e32 --- /dev/null +++ b/src/components/Employee/Deductions/onboarding/useFormDictionary.ts @@ -0,0 +1,102 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import type { DeductionsFormDictionary } from '../shared/DeductionsForm' + +/** + * Resolves the shared `DeductionsForm`'s text against onboarding's existing + * `Employee.Deductions` namespace. + * + * The mapping bridges onboarding's historical flat key names + * (`addDeductionTitle`, `descriptionLabelV2`, `agency`, `per`, …) onto the + * form's nested shape (`addTitle`, `standard.descriptionLabel`, + * `childSupport.agencyLabel`, `childSupport.paymentPeriodLabel`, …). Keeping + * the source keys unchanged preserves backward compatibility for partners + * overriding them through the onboarding block's `dictionary` prop. + */ +export function useOnboardingDeductionsFormDictionary(): DeductionsFormDictionary { + const { t } = useTranslation('Employee.Deductions') + + return useMemo( + () => ({ + en: { + addTitle: t('addDeductionTitle'), + editTitle: t('editDeductionTitle'), + description: t('externalPostTaxDeductionsDescription'), + variantLabel: t('deductionTypeLabel'), + variantDescription: t('deductionTypeRadioLabel'), + garnishmentOption: t('garnishmentOption'), + customOption: t('customDeductionOption'), + garnishmentTypeLabel: t('garnishmentType'), + types: { + childSupport: t('childSupportTitle'), + federalTaxLien: t('federalTaxLien'), + stateTaxLien: t('stateTaxLien'), + studentLoan: t('studentLoan'), + creditorGarnishment: t('creditorGarnishment'), + federalLoan: t('federalLoan'), + otherGarnishment: t('otherGarnishment'), + custom: t('customDeductionTitle'), + }, + standard: { + descriptionLabel: t('descriptionLabelV2'), + descriptionRequired: t('descriptionRequired'), + frequencyLabel: t('frequencyLabel'), + frequencyRecurring: t('frequencyRecurringOptionV2'), + frequencyOneTime: t('frequencyOneTimeOptionV2'), + frequencyRequired: t('frequencyRequired'), + typeLabel: t('deductionTypeLabelV2'), + typePercentage: t('deductionTypePercentageOptionV2'), + typeFixed: t('deductionTypeFixedAmountOption'), + typeRequired: t('deductionTypeRequired'), + amountLabel: t('deductionAmountLabel'), + amountPercentDescription: t('deductionAmountDescriptionPercentage'), + amountFixedDescription: t('deductionAmountDescriptionFixed'), + amountRequired: t('amountRequired'), + amountNonNegative: t('amountNonNegative'), + totalAmountLabel: t('totalAmountLabel'), + totalAmountDescription: t('totalAmountDescription'), + annualMaxLabel: t('annualMaxLabel'), + annualMaxDescription: t('annualMaxDescription'), + }, + childSupport: { + agencyLabel: t('agency'), + agencyDescription: t('agencyDescription'), + agencyRequired: t('agencyRequired'), + manualPaymentRequired: t('manualPaymentRequired'), + countyLabel: t('county'), + countyDescription: t('countyDescription'), + allCounties: t('allCounties'), + countyRequired: t('countyRequired'), + caseNumberDescription: t('caseNumberDescription'), + caseNumberRequired: t('caseNumberRequired'), + orderNumberDescription: t('orderNumberDescription'), + orderNumberRequired: t('orderNumberRequired'), + remittanceNumberDescription: t('remittanceNumberDescription'), + remittanceNumberRequired: t('remittanceNumberRequired'), + totalAmountWithheld: t('totalAmountWithheld'), + totalAmountWithheldDescription: t('totalAmountWithheldDescription'), + payPeriodMaximumRequired: t('payPeriodMaximumRequired'), + maxPaycheckPercentage: t('maxPaycheckPercentage'), + maxPaycheckPercentageDescription: t('maxPaycheckPercentageDescription'), + amountRequired: t('amountRequired'), + amountNonNegative: t('amountNonNegative'), + percentOutOfRange: t('percentOutOfRange'), + paymentPeriodLabel: t('per'), + paymentPeriodDescription: t('perDescription'), + paymentPeriodRequired: t('paymentPeriodRequired'), + paymentPeriod: { + everyWeek: t('everyWeek'), + everyOtherWeek: t('everyOtherWeek'), + twicePerMonth: t('twicePerMonth'), + monthly: t('monthly'), + }, + }, + actions: { + save: t('saveCta'), + cancel: t('cancelCta'), + }, + }, + }), + [t], + ) +} diff --git a/src/components/Employee/Deductions/onboarding/DeductionsForm/ChildSupportFormView.tsx b/src/components/Employee/Deductions/shared/DeductionsForm/ChildSupportFormView.tsx similarity index 60% rename from src/components/Employee/Deductions/onboarding/DeductionsForm/ChildSupportFormView.tsx rename to src/components/Employee/Deductions/shared/DeductionsForm/ChildSupportFormView.tsx index 28aeda5d7..3222c8122 100644 --- a/src/components/Employee/Deductions/onboarding/DeductionsForm/ChildSupportFormView.tsx +++ b/src/components/Employee/Deductions/shared/DeductionsForm/ChildSupportFormView.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next' import type { Garnishment } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' import type { PaymentPeriod } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishmentchildsupport' -import { useChildSupportGarnishmentForm } from '../../shared/useChildSupportGarnishmentForm' -import type { StateFieldEntry, CountyEntry } from '../../shared/useChildSupportGarnishmentForm' +import { useChildSupportGarnishmentForm } from '../useChildSupportGarnishmentForm' +import type { StateFieldEntry, CountyEntry } from '../useChildSupportGarnishmentForm' import { Form } from '@/components/Common/Form' import { ActionsLayout } from '@/components/Common' import { Flex } from '@/components/Common/Flex/Flex' @@ -23,7 +23,7 @@ export function ChildSupportFormView({ onSaved, onCancel, }: ChildSupportFormViewProps) { - const { t } = useTranslation('Employee.Deductions') + const { t } = useTranslation('Employee.DeductionsForm') const Components = useComponentContext() const form = useChildSupportGarnishmentForm({ @@ -48,88 +48,95 @@ export function ChildSupportFormView({
- {t('childSupportTitle')} + {t('types.childSupport')} entry.name} - validationMessages={{ REQUIRED: t('agencyRequired') }} + validationMessages={{ REQUIRED: t('childSupport.agencyRequired') }} /> {form.status.isManualPaymentRequired && ( - + )} {hasSelection && ( {Fields.FipsCode && ( entry.county ?? t('allCounties')} - validationMessages={{ REQUIRED: t('countyRequired') }} + label={t('childSupport.countyLabel')} + description={t('childSupport.countyDescription')} + getOptionLabel={(entry: CountyEntry) => + entry.county ?? t('childSupport.allCounties') + } + validationMessages={{ REQUIRED: t('childSupport.countyRequired') }} /> )} {Fields.CaseNumber && ( )} {Fields.OrderNumber && ( )} {Fields.RemittanceNumber && ( )} paymentPeriodLabel(t, value)} - validationMessages={{ REQUIRED: t('paymentPeriodRequired') }} + validationMessages={{ REQUIRED: t('childSupport.paymentPeriodRequired') }} /> )} - {t('cancelCta')} + {t('actions.cancel')} {hasSelection && ( - {t('saveCta')} + {t('actions.save')} )} @@ -149,17 +156,17 @@ function requiredAttrLabel( } function paymentPeriodLabel( - t: ReturnType>['t'], + t: ReturnType>['t'], value: PaymentPeriod, ): string { switch (value) { case 'Every week': - return t('everyWeek') + return t('childSupport.paymentPeriod.everyWeek') case 'Every other week': - return t('everyOtherWeek') + return t('childSupport.paymentPeriod.everyOtherWeek') case 'Twice per month': - return t('twicePerMonth') + return t('childSupport.paymentPeriod.twicePerMonth') case 'Monthly': - return t('monthly') + return t('childSupport.paymentPeriod.monthly') } } diff --git a/src/components/Employee/Deductions/onboarding/DeductionsForm/DeductionsForm.test.tsx b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx similarity index 65% rename from src/components/Employee/Deductions/onboarding/DeductionsForm/DeductionsForm.test.tsx rename to src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx index 28e695a55..967581a7b 100644 --- a/src/components/Employee/Deductions/onboarding/DeductionsForm/DeductionsForm.test.tsx +++ b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { DeductionsForm } from './DeductionsForm' +import type { DeductionsFormDictionary } from './types' import { renderWithProviders } from '@/test-utils/renderWithProviders' import { setupApiTestMocks } from '@/test/mocks/apiServer' @@ -45,4 +46,29 @@ describe('DeductionsForm', () => { ).toBeInTheDocument() }) }) + + it('renders strings from the formDictionary prop in place of the defaults', async () => { + const formDictionary: DeductionsFormDictionary = { + en: { + addTitle: 'Add a payroll deduction', + variantLabel: 'Choose deduction type', + customOption: 'Voluntary deduction', + }, + } + + renderWithProviders( + , + ) + + expect( + await screen.findByRole('heading', { name: 'Add a payroll deduction' }), + ).toBeInTheDocument() + expect(screen.getByText('Choose deduction type')).toBeInTheDocument() + expect(screen.getByRole('radio', { name: 'Voluntary deduction' })).toBeInTheDocument() + }) }) diff --git a/src/components/Employee/Deductions/onboarding/DeductionsForm/DeductionsForm.tsx b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx similarity index 78% rename from src/components/Employee/Deductions/onboarding/DeductionsForm/DeductionsForm.tsx rename to src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx index 258704212..468591e86 100644 --- a/src/components/Employee/Deductions/onboarding/DeductionsForm/DeductionsForm.tsx +++ b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx @@ -6,10 +6,11 @@ import type { } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' import { StandardDeductionForm } from './StandardDeductionForm' import { ChildSupportFormView } from './ChildSupportFormView' +import type { DeductionsFormDictionary } from './types' import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' import { Grid } from '@/components/Common/Grid/Grid' import { Flex } from '@/components/Common/Flex/Flex' -import { useI18n } from '@/i18n' +import { useComponentDictionary, useI18n } from '@/i18n' // Garnishment types the form supports (mirrors the legacy SUPPORTED_GARNISHMENT_TYPES). const SUPPORTED_GARNISHMENT_TYPES: readonly GarnishmentType[] = [ @@ -39,6 +40,13 @@ export interface DeductionsFormProps { /** When provided, the form is in edit mode and the deduction's existing * garnishment type selects the inline form variant. Omit for add mode. */ deduction?: Garnishment | null + /** + * Per-surface translation override. Each consuming block builds this from + * its own namespace (via a `useFormDictionary` hook) and passes the + * resolved dictionary so partner overrides on the block's namespace flow + * into the form text. + */ + formDictionary?: DeductionsFormDictionary onSaved: (deduction: Garnishment, mode: 'create' | 'update') => void onCancel: () => void } @@ -47,15 +55,17 @@ export function DeductionsForm({ className, employeeId, deduction, + formDictionary, onSaved, onCancel, }: DeductionsFormProps) { - useI18n('Employee.Deductions') - const { t } = useTranslation('Employee.Deductions') + useI18n('Employee.DeductionsForm') + useComponentDictionary('Employee.DeductionsForm', formDictionary) + const { t } = useTranslation('Employee.DeductionsForm') const Components = useComponentContext() const isEdit = !!deduction - const title = isEdit ? t('editDeductionTitle') : t('addDeductionTitle') + const title = isEdit ? t('editTitle') : t('addTitle') // Pre-select the variant in edit mode; let the user pick in add mode. const initialVariant = useMemo( @@ -90,20 +100,18 @@ export function DeductionsForm({ {title} - - {t('externalPostTaxDeductionsDescription')} - + {t('description')} {!isEdit && ( <> @@ -160,23 +168,23 @@ export function DeductionsForm({ } function garnishmentTypeLabel( - t: ReturnType>['t'], + t: ReturnType>['t'], value: GarnishmentType, ): string { switch (value) { case 'child_support': - return t('childSupportTitle') + return t('types.childSupport') case 'federal_tax_lien': - return t('federalTaxLien') + return t('types.federalTaxLien') case 'state_tax_lien': - return t('stateTaxLien') + return t('types.stateTaxLien') case 'student_loan': - return t('studentLoan') + return t('types.studentLoan') case 'creditor_garnishment': - return t('creditorGarnishment') + return t('types.creditorGarnishment') case 'federal_loan': - return t('federalLoan') + return t('types.federalLoan') case 'other_garnishment': - return t('otherGarnishment') + return t('types.otherGarnishment') } } diff --git a/src/components/Employee/Deductions/onboarding/DeductionsForm/StandardDeductionForm.tsx b/src/components/Employee/Deductions/shared/DeductionsForm/StandardDeductionForm.tsx similarity index 78% rename from src/components/Employee/Deductions/onboarding/DeductionsForm/StandardDeductionForm.tsx rename to src/components/Employee/Deductions/shared/DeductionsForm/StandardDeductionForm.tsx index 0e8bfef9f..fec977883 100644 --- a/src/components/Employee/Deductions/onboarding/DeductionsForm/StandardDeductionForm.tsx +++ b/src/components/Employee/Deductions/shared/DeductionsForm/StandardDeductionForm.tsx @@ -5,8 +5,8 @@ import type { GarnishmentType, } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' import type { Control } from 'react-hook-form' -import { useDeductionForm } from '../../shared/useDeductionForm' -import type { DeductionFormData } from '../../shared/useDeductionForm' +import { useDeductionForm } from '../useDeductionForm' +import type { DeductionFormData } from '../useDeductionForm' import { Form } from '@/components/Common/Form' import { ActionsLayout } from '@/components/Common' import { Flex } from '@/components/Common/Flex/Flex' @@ -39,7 +39,7 @@ export function StandardDeductionForm({ onSaved, onCancel, }: StandardDeductionFormProps) { - const { t } = useTranslation('Employee.Deductions') + const { t } = useTranslation('Employee.DeductionsForm') const Components = useComponentContext() const form = useDeductionForm({ @@ -81,7 +81,7 @@ function ReadyForm({ form: Extract, { isLoading: false }> Fields: Extract, { isLoading: false }>['form']['Fields'] Components: ReturnType - t: ReturnType>['t'] + t: ReturnType>['t'] title: string onSaved: (deduction: Garnishment, mode: 'create' | 'update') => void onCancel: () => void @@ -115,58 +115,56 @@ function ReadyForm({ - value ? t('frequencyRecurringOptionV2') : t('frequencyOneTimeOptionV2') + value ? t('standard.frequencyRecurring') : t('standard.frequencyOneTime') } - validationMessages={{ REQUIRED: t('frequencyRequired') }} + validationMessages={{ REQUIRED: t('standard.frequencyRequired') }} /> - value - ? t('deductionTypePercentageOptionV2') - : t('deductionTypeFixedAmountOption') + value ? t('standard.typePercentage') : t('standard.typeFixed') } - validationMessages={{ REQUIRED: t('deductionTypeRequired') }} + validationMessages={{ REQUIRED: t('standard.typeRequired') }} /> {Fields.TotalAmount && Fields.AnnualMaximum && ( @@ -174,10 +172,10 @@ function ReadyForm({ - {t('cancelCta')} + {t('actions.cancel')} - {t('saveCta')} + {t('actions.save')} diff --git a/src/components/Employee/Deductions/shared/DeductionsForm/index.ts b/src/components/Employee/Deductions/shared/DeductionsForm/index.ts new file mode 100644 index 000000000..7b984cda7 --- /dev/null +++ b/src/components/Employee/Deductions/shared/DeductionsForm/index.ts @@ -0,0 +1,2 @@ +export { DeductionsForm, type DeductionsFormProps } from './DeductionsForm' +export type { DeductionsFormDictionary } from './types' diff --git a/src/components/Employee/Deductions/shared/DeductionsForm/types.ts b/src/components/Employee/Deductions/shared/DeductionsForm/types.ts new file mode 100644 index 000000000..5e6992784 --- /dev/null +++ b/src/components/Employee/Deductions/shared/DeductionsForm/types.ts @@ -0,0 +1,14 @@ +import type { ResourceDictionary } from '@/types/Helpers' + +/** + * Override surface for {@link DeductionsForm}'s text. Surfaces (onboarding, + * management, etc.) pass a resolved dictionary that fully replaces the form's + * default English copy at render time. + * + * The underlying namespace (`Employee.DeductionsForm`) is an implementation + * detail of the shared form — callers shouldn't reference it directly. Build + * the dictionary inside a per-surface `useFormDictionary` hook that resolves + * `t(...)` against the surface's own namespace, so partner overrides on that + * namespace flow into the form text automatically. + */ +export type DeductionsFormDictionary = ResourceDictionary<'Employee.DeductionsForm'> diff --git a/src/components/Employee/Deductions/shared/index.ts b/src/components/Employee/Deductions/shared/index.ts index 2382c9007..529df71e5 100644 --- a/src/components/Employee/Deductions/shared/index.ts +++ b/src/components/Employee/Deductions/shared/index.ts @@ -10,3 +10,8 @@ export { DeleteDeductionDialog } from './DeleteDeductionDialog' export { formatDeductionAmount, type DeductionAmountFormatters } from './formatDeductionAmount' export * from './useDeductionForm' export * from './useChildSupportGarnishmentForm' +export { + DeductionsForm, + type DeductionsFormProps, + type DeductionsFormDictionary, +} from './DeductionsForm' diff --git a/src/i18n/en/Employee.DeductionsForm.json b/src/i18n/en/Employee.DeductionsForm.json new file mode 100644 index 000000000..5cea4af1d --- /dev/null +++ b/src/i18n/en/Employee.DeductionsForm.json @@ -0,0 +1,78 @@ +{ + "addTitle": "Add Deduction", + "editTitle": "Edit Deduction", + "description": "Amounts are withheld from the net pay of an employee and reported on the paystub and within payroll receipts. Note that these deductions are always post-tax and will start with the next pay period.", + "variantLabel": "Deduction type", + "variantDescription": "A garnishment is a court-ordered wage reduction—we’ll handle the tax calculations based what you select. For other post-tax deductions, choose Custom Deduction.", + "garnishmentOption": "Garnishment (a court-ordered deduction)", + "customOption": "Custom deduction (post-tax)", + "garnishmentTypeLabel": "Garnishment type", + "types": { + "childSupport": "Child Support", + "federalTaxLien": "Federal Tax Lien", + "stateTaxLien": "State Tax Lien", + "studentLoan": "Student Loan", + "creditorGarnishment": "Creditor Garnishment", + "federalLoan": "Federal Loan", + "otherGarnishment": "Other Garnishment", + "custom": "Custom deduction" + }, + "standard": { + "descriptionLabel": "Description", + "descriptionRequired": "Description is required", + "frequencyLabel": "Deduction frequency", + "frequencyRecurring": "Recurring (every payroll)", + "frequencyOneTime": "One-time (next payroll only)", + "frequencyRequired": "Frequency is required", + "typeLabel": "Percentage or fixed", + "typePercentage": "Percentage of pay", + "typeFixed": "Fixed dollar amount", + "typeRequired": "Deduction type is required", + "amountLabel": "Amount to withhold", + "amountPercentDescription": "Enter the percentage of your employee’s wages to withhold.", + "amountFixedDescription": "Enter the amount of money to withhold each pay period from your employee’s wages.", + "amountRequired": "Amount is required", + "amountNonNegative": "Amount must be 0 or greater", + "totalAmountLabel": "Total amount owed", + "totalAmountDescription": "We will adjust the amount of the last payment and stop collecting once the total amount is reached.", + "annualMaxLabel": "Annual maximum", + "annualMaxDescription": "The maximum annual amount you deduct from the employee's pay for this specific deduction. Leave this field blank if there is no maximum." + }, + "childSupport": { + "agencyLabel": "Agency", + "agencyDescription": "Select the appropriate state agency.", + "agencyRequired": "Agency is required", + "manualPaymentRequired": "This agency doesn't support electronic payments. You are responsible for paying the agency yourself.", + "countyLabel": "County", + "countyDescription": "Select the appropriate state county", + "allCounties": "All counties", + "countyRequired": "County is required", + "caseNumberDescription": "Carefully enter the Child Support Enforcement Case Number", + "caseNumberRequired": "Case number is required", + "orderNumberDescription": "Enter the unique Order Identifier or Order ID associated with this child support obligation.", + "orderNumberRequired": "Order number is required", + "remittanceNumberDescription": "Carefully enter the Child Support Enforcement Remittance ID", + "remittanceNumberRequired": "Remittance number is required", + "totalAmountWithheld": "Total amount withheld", + "totalAmountWithheldDescription": "Enter the amount indicated in the letter from the child support agency", + "payPeriodMaximumRequired": "Pay period maximum is required", + "maxPaycheckPercentage": "Max paycheck percentage", + "maxPaycheckPercentageDescription": "Enter the maximum percentage of your employee's paycheck that we should withhold. You can find this info in the letter from the child support agency.", + "amountRequired": "Amount is required", + "amountNonNegative": "Amount must be 0 or greater", + "percentOutOfRange": "Must be between 0 and 100", + "paymentPeriodLabel": "Per", + "paymentPeriodDescription": "Enter how often the agency collects the withholding amount", + "paymentPeriodRequired": "Payment period is required", + "paymentPeriod": { + "everyWeek": "Every week", + "everyOtherWeek": "Every other week", + "twicePerMonth": "Twice per month", + "monthly": "Monthly" + } + }, + "actions": { + "save": "Save", + "cancel": "Cancel" + } +} diff --git a/src/i18n/en/Employee.Management.Deductions.json b/src/i18n/en/Employee.Management.Deductions.json index 5042e8ec8..0fd2faff5 100644 --- a/src/i18n/en/Employee.Management.Deductions.json +++ b/src/i18n/en/Employee.Management.Deductions.json @@ -27,5 +27,83 @@ "deductionAdded": "Deduction successfully added.", "deductionUpdated": "Deduction successfully updated.", "deductionDeleted": "Deduction successfully deleted." + }, + "form": { + "addTitle": "Add Deduction", + "editTitle": "Edit Deduction", + "description": "Amounts are withheld from the net pay of an employee and reported on the paystub and within payroll receipts. Note that these deductions are always post-tax and will start with the next pay period.", + "variantLabel": "Deduction type", + "variantDescription": "A garnishment is a court-ordered wage reduction—we’ll handle the tax calculations based on what you select. For other post-tax deductions, choose Custom Deduction.", + "garnishmentOption": "Garnishment (a court-ordered deduction)", + "customOption": "Custom deduction (post-tax)", + "garnishmentTypeLabel": "Garnishment type", + "types": { + "childSupport": "Child Support", + "federalTaxLien": "Federal Tax Lien", + "stateTaxLien": "State Tax Lien", + "studentLoan": "Student Loan", + "creditorGarnishment": "Creditor Garnishment", + "federalLoan": "Federal Loan", + "otherGarnishment": "Other Garnishment", + "custom": "Custom deduction" + }, + "standard": { + "descriptionLabel": "Description", + "descriptionRequired": "Description is required", + "frequencyLabel": "Deduction frequency", + "frequencyRecurring": "Recurring (every payroll)", + "frequencyOneTime": "One-time (next payroll only)", + "frequencyRequired": "Frequency is required", + "typeLabel": "Percentage or fixed", + "typePercentage": "Percentage of pay", + "typeFixed": "Fixed dollar amount", + "typeRequired": "Deduction type is required", + "amountLabel": "Amount to withhold", + "amountPercentDescription": "Enter the percentage of the employee’s wages to withhold.", + "amountFixedDescription": "Enter the amount of money to withhold each pay period from the employee’s wages.", + "amountRequired": "Amount is required", + "amountNonNegative": "Amount must be 0 or greater", + "totalAmountLabel": "Total amount owed", + "totalAmountDescription": "We will adjust the amount of the last payment and stop collecting once the total amount is reached.", + "annualMaxLabel": "Annual maximum", + "annualMaxDescription": "The maximum annual amount you deduct from the employee's pay for this specific deduction. Leave this field blank if there is no maximum." + }, + "childSupport": { + "agencyLabel": "Agency", + "agencyDescription": "Select the appropriate state agency.", + "agencyRequired": "Agency is required", + "manualPaymentRequired": "This agency doesn't support electronic payments. You are responsible for paying the agency yourself.", + "countyLabel": "County", + "countyDescription": "Select the appropriate state county.", + "allCounties": "All counties", + "countyRequired": "County is required", + "caseNumberDescription": "Carefully enter the Child Support Enforcement Case Number.", + "caseNumberRequired": "Case number is required", + "orderNumberDescription": "Enter the unique Order Identifier or Order ID associated with this child support obligation.", + "orderNumberRequired": "Order number is required", + "remittanceNumberDescription": "Carefully enter the Child Support Enforcement Remittance ID.", + "remittanceNumberRequired": "Remittance number is required", + "totalAmountWithheld": "Total amount withheld", + "totalAmountWithheldDescription": "Enter the amount indicated in the letter from the child support agency.", + "payPeriodMaximumRequired": "Pay period maximum is required", + "maxPaycheckPercentage": "Max paycheck percentage", + "maxPaycheckPercentageDescription": "Enter the maximum percentage of the employee's paycheck that we should withhold. You can find this info in the letter from the child support agency.", + "amountRequired": "Amount is required", + "amountNonNegative": "Amount must be 0 or greater", + "percentOutOfRange": "Must be between 0 and 100", + "paymentPeriodLabel": "Per", + "paymentPeriodDescription": "Enter how often the agency collects the withholding amount.", + "paymentPeriodRequired": "Payment period is required", + "paymentPeriod": { + "everyWeek": "Every week", + "everyOtherWeek": "Every other week", + "twicePerMonth": "Twice per month", + "monthly": "Monthly" + } + }, + "actions": { + "save": "Save", + "cancel": "Cancel" + } } } diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index 8d75b9cb7..477c39f07 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -1673,6 +1673,84 @@ export interface EmployeeDeductions{ "percentOutOfRange":string; "paymentPeriodRequired":string; }; +export interface EmployeeDeductionsForm{ +"addTitle":string; +"editTitle":string; +"description":string; +"variantLabel":string; +"variantDescription":string; +"garnishmentOption":string; +"customOption":string; +"garnishmentTypeLabel":string; +"types":{ +"childSupport":string; +"federalTaxLien":string; +"stateTaxLien":string; +"studentLoan":string; +"creditorGarnishment":string; +"federalLoan":string; +"otherGarnishment":string; +"custom":string; +}; +"standard":{ +"descriptionLabel":string; +"descriptionRequired":string; +"frequencyLabel":string; +"frequencyRecurring":string; +"frequencyOneTime":string; +"frequencyRequired":string; +"typeLabel":string; +"typePercentage":string; +"typeFixed":string; +"typeRequired":string; +"amountLabel":string; +"amountPercentDescription":string; +"amountFixedDescription":string; +"amountRequired":string; +"amountNonNegative":string; +"totalAmountLabel":string; +"totalAmountDescription":string; +"annualMaxLabel":string; +"annualMaxDescription":string; +}; +"childSupport":{ +"agencyLabel":string; +"agencyDescription":string; +"agencyRequired":string; +"manualPaymentRequired":string; +"countyLabel":string; +"countyDescription":string; +"allCounties":string; +"countyRequired":string; +"caseNumberDescription":string; +"caseNumberRequired":string; +"orderNumberDescription":string; +"orderNumberRequired":string; +"remittanceNumberDescription":string; +"remittanceNumberRequired":string; +"totalAmountWithheld":string; +"totalAmountWithheldDescription":string; +"payPeriodMaximumRequired":string; +"maxPaycheckPercentage":string; +"maxPaycheckPercentageDescription":string; +"amountRequired":string; +"amountNonNegative":string; +"percentOutOfRange":string; +"paymentPeriodLabel":string; +"paymentPeriodDescription":string; +"paymentPeriodRequired":string; +"paymentPeriod":{ +"everyWeek":string; +"everyOtherWeek":string; +"twicePerMonth":string; +"monthly":string; +}; +}; +"actions":{ +"save":string; +"cancel":string; +}; +}; export interface EmployeeDocumentManager{ "viewDocumentCta":string; "downloadDocumentCta":string; @@ -2023,6 +2101,84 @@ export interface EmployeeManagementDeductions{ "deductionUpdated":string; "deductionDeleted":string; }; +"form":{ +"addTitle":string; +"editTitle":string; +"description":string; +"variantLabel":string; +"variantDescription":string; +"garnishmentOption":string; +"customOption":string; +"garnishmentTypeLabel":string; +"types":{ +"childSupport":string; +"federalTaxLien":string; +"stateTaxLien":string; +"studentLoan":string; +"creditorGarnishment":string; +"federalLoan":string; +"otherGarnishment":string; +"custom":string; +}; +"standard":{ +"descriptionLabel":string; +"descriptionRequired":string; +"frequencyLabel":string; +"frequencyRecurring":string; +"frequencyOneTime":string; +"frequencyRequired":string; +"typeLabel":string; +"typePercentage":string; +"typeFixed":string; +"typeRequired":string; +"amountLabel":string; +"amountPercentDescription":string; +"amountFixedDescription":string; +"amountRequired":string; +"amountNonNegative":string; +"totalAmountLabel":string; +"totalAmountDescription":string; +"annualMaxLabel":string; +"annualMaxDescription":string; +}; +"childSupport":{ +"agencyLabel":string; +"agencyDescription":string; +"agencyRequired":string; +"manualPaymentRequired":string; +"countyLabel":string; +"countyDescription":string; +"allCounties":string; +"countyRequired":string; +"caseNumberDescription":string; +"caseNumberRequired":string; +"orderNumberDescription":string; +"orderNumberRequired":string; +"remittanceNumberDescription":string; +"remittanceNumberRequired":string; +"totalAmountWithheld":string; +"totalAmountWithheldDescription":string; +"payPeriodMaximumRequired":string; +"maxPaycheckPercentage":string; +"maxPaycheckPercentageDescription":string; +"amountRequired":string; +"amountNonNegative":string; +"percentOutOfRange":string; +"paymentPeriodLabel":string; +"paymentPeriodDescription":string; +"paymentPeriodRequired":string; +"paymentPeriod":{ +"everyWeek":string; +"everyOtherWeek":string; +"twicePerMonth":string; +"monthly":string; +}; +}; +"actions":{ +"save":string; +"cancel":string; +}; +}; }; export interface EmployeeManagementPaymentMethod{ "title":string; @@ -3687,6 +3843,6 @@ export interface common{ interface CustomTypeOptions { defaultNS: 'common'; - resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Company.TimeOff.CreateTimeOffPolicy': CompanyTimeOffCreateTimeOffPolicy, 'Company.TimeOff.EmployeeTable': CompanyTimeOffEmployeeTable, 'Company.TimeOff.HolidayPolicy': CompanyTimeOffHolidayPolicy, 'Company.TimeOff.PolicyDetail': CompanyTimeOffPolicyDetail, 'Company.TimeOff.SelectEmployees': CompanyTimeOffSelectEmployees, 'Company.TimeOff.SelectPolicyType': CompanyTimeOffSelectPolicyType, 'Company.TimeOff.TimeOffPolicies': CompanyTimeOffTimeOffPolicies, 'Company.TimeOff.TimeOffPolicyDetails': CompanyTimeOffTimeOffPolicyDetails, 'Company.TimeOff.TimeOffRequests': CompanyTimeOffTimeOffRequests, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentSummary': ContractorPaymentsPaymentSummary, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Dashboard': EmployeeDashboard, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentManager': EmployeeDocumentManager, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeDocuments': EmployeeEmployeeDocuments, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.EmploymentEligibility': EmployeeEmploymentEligibility, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress.Management': EmployeeHomeAddressManagement, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.I9SignatureForm': EmployeeI9SignatureForm, 'Employee.Landing': EmployeeLanding, 'Employee.Management.Deductions': EmployeeManagementDeductions, 'Employee.Management.PaymentMethod': EmployeeManagementPaymentMethod, 'Employee.Management.PaymentMethodBankForm': EmployeeManagementPaymentMethodBankForm, 'Employee.Management.PaymentMethodSplitForm': EmployeeManagementPaymentMethodSplitForm, 'Employee.Management.Paystubs': EmployeeManagementPaystubs, 'Employee.Management.WorkAddress': EmployeeManagementWorkAddress, 'Employee.ManagementEmployeeList': EmployeeManagementEmployeeList, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile.Management': EmployeeProfileManagement, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Employee.Terminations.TerminateEmployee': EmployeeTerminationsTerminateEmployee, 'Employee.Terminations.TerminationFlow': EmployeeTerminationsTerminationFlow, 'Employee.Terminations.TerminationSummary': EmployeeTerminationsTerminationSummary, 'InformationRequests.InformationRequestForm': InformationRequestsInformationRequestForm, 'InformationRequests.InformationRequestList': InformationRequestsInformationRequestList, 'InformationRequests': InformationRequests, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.Dismissal': PayrollDismissal, 'Payroll.EmployeeSelection': PayrollEmployeeSelection, 'Payroll.GrossUpModal': PayrollGrossUpModal, 'Payroll.OffCycle': PayrollOffCycle, 'Payroll.OffCycleCreation': PayrollOffCycleCreation, 'Payroll.OffCycleDeductionsSetting': PayrollOffCycleDeductionsSetting, 'Payroll.OffCyclePayPeriodDateForm': PayrollOffCyclePayPeriodDateForm, 'Payroll.OffCycleReasonSelection': PayrollOffCycleReasonSelection, 'Payroll.OffCycleTaxWithholding': PayrollOffCycleTaxWithholding, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.Transition': PayrollTransition, 'Payroll.TransitionCreation': PayrollTransitionCreation, 'Payroll.TransitionPayrollAlert': PayrollTransitionPayrollAlert, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } + resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Company.TimeOff.CreateTimeOffPolicy': CompanyTimeOffCreateTimeOffPolicy, 'Company.TimeOff.EmployeeTable': CompanyTimeOffEmployeeTable, 'Company.TimeOff.HolidayPolicy': CompanyTimeOffHolidayPolicy, 'Company.TimeOff.PolicyDetail': CompanyTimeOffPolicyDetail, 'Company.TimeOff.SelectEmployees': CompanyTimeOffSelectEmployees, 'Company.TimeOff.SelectPolicyType': CompanyTimeOffSelectPolicyType, 'Company.TimeOff.TimeOffPolicies': CompanyTimeOffTimeOffPolicies, 'Company.TimeOff.TimeOffPolicyDetails': CompanyTimeOffTimeOffPolicyDetails, 'Company.TimeOff.TimeOffRequests': CompanyTimeOffTimeOffRequests, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentSummary': ContractorPaymentsPaymentSummary, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Dashboard': EmployeeDashboard, 'Employee.Deductions': EmployeeDeductions, 'Employee.DeductionsForm': EmployeeDeductionsForm, 'Employee.DocumentManager': EmployeeDocumentManager, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeDocuments': EmployeeEmployeeDocuments, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.EmploymentEligibility': EmployeeEmploymentEligibility, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress.Management': EmployeeHomeAddressManagement, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.I9SignatureForm': EmployeeI9SignatureForm, 'Employee.Landing': EmployeeLanding, 'Employee.Management.Deductions': EmployeeManagementDeductions, 'Employee.Management.PaymentMethod': EmployeeManagementPaymentMethod, 'Employee.Management.PaymentMethodBankForm': EmployeeManagementPaymentMethodBankForm, 'Employee.Management.PaymentMethodSplitForm': EmployeeManagementPaymentMethodSplitForm, 'Employee.Management.Paystubs': EmployeeManagementPaystubs, 'Employee.Management.WorkAddress': EmployeeManagementWorkAddress, 'Employee.ManagementEmployeeList': EmployeeManagementEmployeeList, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile.Management': EmployeeProfileManagement, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Employee.Terminations.TerminateEmployee': EmployeeTerminationsTerminateEmployee, 'Employee.Terminations.TerminationFlow': EmployeeTerminationsTerminationFlow, 'Employee.Terminations.TerminationSummary': EmployeeTerminationsTerminationSummary, 'InformationRequests.InformationRequestForm': InformationRequestsInformationRequestForm, 'InformationRequests.InformationRequestList': InformationRequestsInformationRequestList, 'InformationRequests': InformationRequests, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.Dismissal': PayrollDismissal, 'Payroll.EmployeeSelection': PayrollEmployeeSelection, 'Payroll.GrossUpModal': PayrollGrossUpModal, 'Payroll.OffCycle': PayrollOffCycle, 'Payroll.OffCycleCreation': PayrollOffCycleCreation, 'Payroll.OffCycleDeductionsSetting': PayrollOffCycleDeductionsSetting, 'Payroll.OffCyclePayPeriodDateForm': PayrollOffCyclePayPeriodDateForm, 'Payroll.OffCycleReasonSelection': PayrollOffCycleReasonSelection, 'Payroll.OffCycleTaxWithholding': PayrollOffCycleTaxWithholding, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.Transition': PayrollTransition, 'Payroll.TransitionCreation': PayrollTransitionCreation, 'Payroll.TransitionPayrollAlert': PayrollTransitionPayrollAlert, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } }; } \ No newline at end of file From a6cefd8b9101f3c31371438ec8f7718a3bf7eaed Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Thu, 4 Jun 2026 14:55:55 -0600 Subject: [PATCH 3/4] refactor: rename DeductionsForm prop formDictionary -> dictionary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared `DeductionsForm` now takes the standard `dictionary` prop name that every other SDK component uses for partner translation overrides. The underlying mechanism is unchanged — each consuming block (onboarding contextual / management `DeductionsEditForm`) still resolves the strings through its own `useFormDictionary` hook and passes the resolved dictionary down — only the prop name changes. Also updates the `migrate-dashboard-card-to-block` skill so the new "shared component renders in multiple surfaces" subsection prescribes the standard `dictionary` prop name (not `formDictionary`) when documenting the pattern. Co-authored-by: Cursor --- .../migrate-dashboard-card-to-block/SKILL.md | 18 +++++++++--------- .../DeductionsEditForm/DeductionsEditForm.tsx | 2 +- .../deductionsContextualComponents.tsx | 2 +- .../DeductionsForm/DeductionsForm.test.tsx | 6 +++--- .../shared/DeductionsForm/DeductionsForm.tsx | 13 ++++++------- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.claude/skills/migrate-dashboard-card-to-block/SKILL.md b/.claude/skills/migrate-dashboard-card-to-block/SKILL.md index 1830448f7..7500035da 100644 --- a/.claude/skills/migrate-dashboard-card-to-block/SKILL.md +++ b/.claude/skills/migrate-dashboard-card-to-block/SKILL.md @@ -766,33 +766,33 @@ The pattern: give the shared component its **own** dedicated namespace internall **Architecture** -- The shared component lives at `/shared//`. Internally it calls `useI18n('Employee.')`, `useComponentDictionary('Employee.', formDictionary)`, and `useTranslation('Employee.')`. It never references either surface's namespace. -- The shared component accepts a `formDictionary?: Dictionary` prop. `Dictionary` is a thin type alias for `ResourceDictionary<'Employee.'>` exported from the shared component's folder — callers reference the structural alias, never the namespace string. -- An `Employee..json` defaults file under `src/i18n/en/` carries the full English copy in the nested shape the component renders. It serves two purposes: it's the type contract that `Dictionary` is keyed against (via `i18n:generate`), and it's the safety net for any caller that doesn't pass `formDictionary`. -- Each surface adds a `useFormDictionary` hook colocated with that surface's block. The hook calls `useTranslation` against the **surface's** namespace (`Employee.` for onboarding, `Employee..Management` for management), reads the keys that surface wants to expose to the form, and returns them packed into the shared component's `Dictionary` shape. -- The block passes `useFormDictionary()`'s result into the shared component as `formDictionary`. Because the hook calls `t()` against the surface's namespace at render time, partner overrides on the surface's namespace flow through naturally — no extra wiring. +- The shared component lives at `/shared//`. Internally it calls `useI18n('Employee.')`, `useComponentDictionary('Employee.', dictionary)`, and `useTranslation('Employee.')`. It never references either surface's namespace. +- The shared component accepts a standard `dictionary?: Dictionary` prop (same prop name SDK components everywhere use). `Dictionary` is a thin type alias for `ResourceDictionary<'Employee.'>` exported from the shared component's folder — callers reference the structural alias, never the namespace string. +- An `Employee..json` defaults file under `src/i18n/en/` carries the full English copy in the nested shape the component renders. It serves two purposes: it's the type contract that `Dictionary` is keyed against (via `i18n:generate`), and it's the safety net for any caller that doesn't pass `dictionary`. +- Each surface adds a small per-surface hook colocated with that surface's block (the reference implementation calls it `useFormDictionary`, but the name is local — pick whatever reads naturally for the component). The hook calls `useTranslation` against the **surface's** namespace (`Employee.` for onboarding, `Employee..Management` for management), reads the keys that surface wants to expose to the shared component, and returns them packed into the `Dictionary` shape. +- The block passes the hook's result down as `dictionary={…}`. Because the hook calls `t()` against the surface's namespace at render time, partner overrides on the surface's namespace flow through naturally — no extra wiring. **Override chain (one direction)** -`partner dictionary` → `useComponentDictionary('Employee.', dictionary)` → surface namespace → `useFormDictionary` resolves `t(...)` → `formDictionary` prop → `useComponentDictionary('Employee.', formDictionary)` → shared component's internal namespace → rendered string. +`partner dictionary` → `useComponentDictionary('Employee.', dictionary)` → surface namespace → per-surface hook resolves `t(...)` → `dictionary` prop on the shared component → `useComponentDictionary('Employee.', dictionary)` → shared component's internal namespace → rendered string. The same chain works at the GustoProvider level — a `dictionary={{ en: { 'Employee.': { ... } } }}` global override hits the surface's namespace first, the hook re-resolves, and the result flows down to the shared component. No global override has to know about the shared component's namespace. **Reference implementation — `DeductionsForm`** -- Shared component: [`src/components/Employee/Deductions/shared/DeductionsForm/`](../../../src/components/Employee/Deductions/shared/DeductionsForm/) — the component itself plus the exported `DeductionsFormDictionary` type and the barrel that re-exports both. +- Shared component: [`src/components/Employee/Deductions/shared/DeductionsForm/`](../../../src/components/Employee/Deductions/shared/DeductionsForm/) — the component itself (`dictionary?: DeductionsFormDictionary` prop) plus the exported `DeductionsFormDictionary` type and the barrel that re-exports both. - Internal namespace + defaults: [`src/i18n/en/Employee.DeductionsForm.json`](../../../src/i18n/en/Employee.DeductionsForm.json) — full nested shape (`addTitle`, `standard.*`, `childSupport.*`, `actions.*`, etc.). - Onboarding's per-surface hook: [`src/components/Employee/Deductions/onboarding/useFormDictionary.ts`](../../../src/components/Employee/Deductions/onboarding/useFormDictionary.ts) — resolves `t(...)` against onboarding's historical flat keys in `Employee.Deductions` (`addDeductionTitle`, `descriptionLabelV2`, `agency`, `per`, …) and packs them into `DeductionsFormDictionary`'s nested shape. - Management's per-surface hook: [`src/components/Employee/Deductions/management/DeductionsEditForm/useFormDictionary.ts`](../../../src/components/Employee/Deductions/management/DeductionsEditForm/useFormDictionary.ts) — resolves `t('form.*')` against `Employee.Management.Deductions`, which carries a dedicated nested `form.*` subtree. **Backward compatibility when retrofitting an existing shared component** -If the shared component existed previously and one surface (typically onboarding) had partners overriding flat key names on the original namespace, write that surface's `useFormDictionary` first as a pure key-rename mapping over the historical keys. Don't change the surface's JSON file — leave every existing key in place. The mapping `descriptionLabel: t('descriptionLabelV2')` etc. is what proves no partner override breaks. The second surface (management) can adopt the new nested shape directly in its own JSON file because it had no historical partner contract to preserve. +If the shared component existed previously and one surface (typically onboarding) had partners overriding flat key names on the original namespace, write that surface's per-surface hook first as a pure key-rename mapping over the historical keys. Don't change the surface's JSON file — leave every existing key in place. The mapping `descriptionLabel: t('descriptionLabelV2')` etc. is what proves no partner override breaks. The second surface (management) can adopt the new nested shape directly in its own JSON file because it had no historical partner contract to preserve. **Why this pattern over duplication or dual-load** - Shared JSX stays shared — no copy-paste maintenance burden when the form structure changes. -- Each surface owns its full translation surface in one JSON. A partner overriding `Employee..Management` via `useComponentDictionary` gets coherent management-side text including form strings, without having to discover and override a separate shared namespace. +- Each surface owns its full translation surface in one JSON. A partner overriding `Employee..Management` via `useComponentDictionary` gets coherent management-side text including the shared component's strings, without having to discover and override a separate shared namespace. - The shared component's namespace is purely an implementation detail. Surfaces don't import it, partners don't see it, and the two surfaces' overrides are fully decoupled — neither surface's translations can leak into the other. ### Strings to move out of `Employee.Dashboard` during migration diff --git a/src/components/Employee/Deductions/management/DeductionsEditForm/DeductionsEditForm.tsx b/src/components/Employee/Deductions/management/DeductionsEditForm/DeductionsEditForm.tsx index bc56166ef..e6a16715c 100644 --- a/src/components/Employee/Deductions/management/DeductionsEditForm/DeductionsEditForm.tsx +++ b/src/components/Employee/Deductions/management/DeductionsEditForm/DeductionsEditForm.tsx @@ -68,7 +68,7 @@ function DeductionsEditFormRoot({ { onEvent( mode === 'create' diff --git a/src/components/Employee/Deductions/onboarding/deductionsContextualComponents.tsx b/src/components/Employee/Deductions/onboarding/deductionsContextualComponents.tsx index 400135b3c..77c0420c6 100644 --- a/src/components/Employee/Deductions/onboarding/deductionsContextualComponents.tsx +++ b/src/components/Employee/Deductions/onboarding/deductionsContextualComponents.tsx @@ -132,7 +132,7 @@ export function DeductionsFormContextual() { diff --git a/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx index 967581a7b..f4bea0d73 100644 --- a/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx +++ b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx @@ -47,8 +47,8 @@ describe('DeductionsForm', () => { }) }) - it('renders strings from the formDictionary prop in place of the defaults', async () => { - const formDictionary: DeductionsFormDictionary = { + it('renders strings from the dictionary prop in place of the defaults', async () => { + const dictionary: DeductionsFormDictionary = { en: { addTitle: 'Add a payroll deduction', variantLabel: 'Choose deduction type', @@ -59,7 +59,7 @@ describe('DeductionsForm', () => { renderWithProviders( , diff --git a/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx index 468591e86..feb7f164d 100644 --- a/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx +++ b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx @@ -41,12 +41,11 @@ export interface DeductionsFormProps { * garnishment type selects the inline form variant. Omit for add mode. */ deduction?: Garnishment | null /** - * Per-surface translation override. Each consuming block builds this from - * its own namespace (via a `useFormDictionary` hook) and passes the - * resolved dictionary so partner overrides on the block's namespace flow - * into the form text. + * Translation overrides for the form's strings. Each consuming block + * passes the dictionary it resolved from its own namespace so partner + * overrides on that namespace flow into the form text. */ - formDictionary?: DeductionsFormDictionary + dictionary?: DeductionsFormDictionary onSaved: (deduction: Garnishment, mode: 'create' | 'update') => void onCancel: () => void } @@ -55,12 +54,12 @@ export function DeductionsForm({ className, employeeId, deduction, - formDictionary, + dictionary, onSaved, onCancel, }: DeductionsFormProps) { useI18n('Employee.DeductionsForm') - useComponentDictionary('Employee.DeductionsForm', formDictionary) + useComponentDictionary('Employee.DeductionsForm', dictionary) const { t } = useTranslation('Employee.DeductionsForm') const Components = useComponentContext() From cb50fc26d7b63a5624ce17dfb8d445bcc777b76c Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Thu, 4 Jun 2026 15:02:37 -0600 Subject: [PATCH 4/4] refactor: inline DeductionsFormDictionary type, drop dedicated types.ts Folds the one-line `DeductionsFormDictionary = ResourceDictionary<...>` alias into `DeductionsForm.tsx` next to the prop it types and drops the separate `types.ts` file. The barrel and test import the type directly from `DeductionsForm` now. Co-authored-by: Cursor --- .reports/embedded-react-sdk.api.md | 299 +++++++++++------- docs/reference/endpoint-inventory.json | 6 +- docs/reference/endpoint-reference.md | 4 +- .../employee-management.md | 8 +- .../DeductionsForm/DeductionsForm.test.tsx | 3 +- .../shared/DeductionsForm/DeductionsForm.tsx | 4 +- .../Deductions/shared/DeductionsForm/index.ts | 7 +- .../Deductions/shared/DeductionsForm/types.ts | 14 - 8 files changed, 207 insertions(+), 138 deletions(-) delete mode 100644 src/components/Employee/Deductions/shared/DeductionsForm/types.ts diff --git a/.reports/embedded-react-sdk.api.md b/.reports/embedded-react-sdk.api.md index 1738d6e6a..75eb74402 100644 --- a/.reports/embedded-react-sdk.api.md +++ b/.reports/embedded-react-sdk.api.md @@ -44,7 +44,6 @@ import { HTMLAttributes } from 'react'; import { InputHTMLAttributes } from 'react'; import { Job } from '@gusto/embedded-api-v-2025-11-15/models/components/job'; import { JSX } from 'react'; -import { JSX as JSX_2 } from 'react/jsx-runtime'; import { JSXElementConstructor } from 'react'; import { Location as Location_2 } from '@gusto/embedded-api-v-2025-11-15/models/components/location'; import { MinimumWage } from '@gusto/embedded-api-v-2025-11-15/models/components/minimumwage'; @@ -97,7 +96,7 @@ export type AccountTypeFieldProps = HookFieldProps, 'class // Warning: (ae-missing-release-tag) "BankAccount" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function BankAccount(props: BankAccountProps): JSX_2.Element; +function BankAccount(props: BankAccountProps): JSX; // Warning: (ae-forgotten-export) The symbol "fieldValidators_8" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "BankFormData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -681,7 +680,7 @@ declare namespace CompanyOnboarding { // Warning: (ae-missing-release-tag) "Compensation" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function Compensation_2(props: CompensationProps): JSX_2.Element; +function Compensation_2(props: CompensationProps): JSX; // @public (undocumented) namespace Compensation_2 { @@ -1055,6 +1054,16 @@ export const componentEvents: { readonly EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED: "employee/management/workAddress/updated"; readonly EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED: "employee/management/workAddress/deleted"; readonly EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED: "employee/management/workAddress/editCancelled"; + readonly EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_CARD_ADD_REQUESTED: "employee/management/paymentMethod/card/addRequested"; + readonly EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_CARD_SPLIT_REQUESTED: "employee/management/paymentMethod/card/splitRequested"; + readonly EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_CARD_BANK_ACCOUNT_DELETED: "employee/management/paymentMethod/card/bankAccountDeleted"; + readonly EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_BANK_FORM_SUBMITTED: "employee/management/paymentMethod/bankForm/submitted"; + readonly EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_BANK_FORM_CANCELLED: "employee/management/paymentMethod/bankForm/cancelled"; + readonly EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_SPLIT_FORM_SUBMITTED: "employee/management/paymentMethod/splitForm/submitted"; + readonly EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_SPLIT_FORM_CANCELLED: "employee/management/paymentMethod/splitForm/cancelled"; + readonly EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_ALERT_DISMISSED: "employee/management/paymentMethod/alertDismissed"; + readonly EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOAD_REQUESTED: "employee/management/paystubs/card/downloadRequested"; + readonly EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOADED: "employee/management/paystubs/card/downloaded"; readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_ADD_REQUESTED: "employee/management/deductions/card/addRequested"; readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_EDIT_REQUESTED: "employee/management/deductions/card/editRequested"; readonly EMPLOYEE_MANAGEMENT_DEDUCTIONS_CARD_DELETED: "employee/management/deductions/card/deleted"; @@ -1177,7 +1186,7 @@ export type ConfirmSignatureFieldProps = HookFieldProps, z.ZodNumber>; // Warning: (ae-missing-release-tag) "CreateSignatory" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function CreateSignatory(props: CreateSignatoryProps & BaseComponentInterface): JSX_2.Element; +function CreateSignatory(props: CreateSignatoryProps & BaseComponentInterface): JSX; // Warning: (ae-forgotten-export) The symbol "SignCompanyFormSchemaOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "createSignCompanyFormSchema" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1555,7 +1564,7 @@ export type CustomTwicePerMonthFieldProps = HookFieldProps JSX_2.Element; +const DashboardFlow: (input: DashboardFlowProps) => JSX; // Warning: (ae-missing-release-tag) "DashboardFlowProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1723,17 +1732,17 @@ export type DeductionFormRequiredValidation = typeof DeductionFormErrorCodes.REQ // Warning: (ae-missing-release-tag) "Deductions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function Deductions(input: DeductionsProps): JSX_2.Element; +function Deductions(input: DeductionsProps): JSX; // Warning: (ae-missing-release-tag) "Deductions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function Deductions_2(input: DeductionsProps_2 & BaseComponentInterface<'Employee.Management.Deductions'>): JSX_2.Element; +function Deductions_2(input: DeductionsProps_2 & BaseComponentInterface<'Employee.Management.Deductions'>): JSX; // Warning: (ae-missing-release-tag) "DeductionsCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function DeductionsCard(input: DeductionsCardProps): JSX_2.Element; +function DeductionsCard(input: DeductionsCardProps): JSX; // Warning: (ae-missing-release-tag) "DeductionsCardProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1751,7 +1760,7 @@ interface DeductionsCardProps { // Warning: (ae-missing-release-tag) "DeductionsEditForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function DeductionsEditForm(input: DeductionsEditFormProps & Pick): JSX_2.Element; +function DeductionsEditForm(input: DeductionsEditFormProps & Pick): JSX; // Warning: (ae-forgotten-export) The symbol "CommonComponentInterface" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DeductionsEditFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1825,7 +1834,7 @@ export interface DialogProps { // Warning: (ae-missing-release-tag) "DismissalFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function DismissalFlow(input: DismissalFlowProps): JSX_2.Element; +function DismissalFlow(input: DismissalFlowProps): JSX; // Warning: (ae-forgotten-export) The symbol "FlowContextInterface" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DismissalFlowContextInterface" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1858,25 +1867,25 @@ interface DismissalFlowProps { // Warning: (ae-missing-release-tag) "DocumentList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function DocumentList(props: DocumentListProps): JSX_2.Element; +function DocumentList(props: DocumentListProps): JSX; // Warning: (ae-forgotten-export) The symbol "DocumentManagerProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DocumentManager" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function DocumentManager(props: DocumentManagerProps & BaseComponentInterface): JSX_2.Element; +function DocumentManager(props: DocumentManagerProps & BaseComponentInterface): JSX; // Warning: (ae-forgotten-export) The symbol "DocumentSignerProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DocumentSigner" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function DocumentSigner(props: DocumentSignerProps): JSX_2.Element; +function DocumentSigner(props: DocumentSignerProps): JSX; // Warning: (ae-forgotten-export) The symbol "DocumentSignerProps_2" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DocumentSigner" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function DocumentSigner_2(props: DocumentSignerProps_2): JSX_2.Element; +function DocumentSigner_2(props: DocumentSignerProps_2): JSX; // Warning: (ae-missing-release-tag) "EffectiveDateFieldProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2004,18 +2013,18 @@ export interface EmployeeDetailsSubmitCallbacks { // Warning: (ae-missing-release-tag) "EmployeeDocuments" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function EmployeeDocuments(props: EmployeeDocumentsProps): JSX_2.Element; +function EmployeeDocuments(props: EmployeeDocumentsProps): JSX; // Warning: (ae-forgotten-export) The symbol "EmployeeListProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EmployeeList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function EmployeeList(input: EmployeeListProps & BaseComponentInterface): JSX_2.Element; +function EmployeeList(input: EmployeeListProps & BaseComponentInterface): JSX; // Warning: (ae-missing-release-tag) "EmployeeListFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const EmployeeListFlow: (input: EmployeeListFlowProps) => JSX_2.Element; +const EmployeeListFlow: (input: EmployeeListFlowProps) => JSX; // Warning: (ae-missing-release-tag) "EmployeeListFlowProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2057,6 +2066,14 @@ declare namespace EmployeeManagement { ProfileEditFormProps, PaymentMethod_3 as PaymentMethod, PaymentMethodProps_3 as PaymentMethodProps, + PaymentMethodCard, + PaymentMethodCardProps, + PaymentMethodBankForm, + PaymentMethodBankFormProps, + PaymentMethodSplitForm, + PaymentMethodSplitFormProps, + PaystubsCard, + PaystubsCardProps, Deductions_2 as Deductions, DeductionsCard, DeductionsEditForm, @@ -2172,7 +2189,7 @@ export interface EmployeeStateTaxesSchemaOptions { // Warning: (ae-missing-release-tag) "EmploymentEligibility" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function EmploymentEligibility(props: EmploymentEligibilityProps): JSX_2.Element; +function EmploymentEligibility(props: EmploymentEligibilityProps): JSX; // Warning: (ae-missing-release-tag) "EmploymentEligibilityProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2191,17 +2208,17 @@ export type ExtraWithholdingFieldProps = HookFieldProps): JSX_2.Element; +function FederalTaxes_2(input: FederalTaxesProps_2 & Pick): JSX; // Warning: (ae-missing-release-tag) "FederalTaxes" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function FederalTaxes_3(input: FederalTaxesProps_3 & Pick): JSX_2.Element; +function FederalTaxes_3(input: FederalTaxesProps_3 & Pick): JSX; // Warning: (ae-missing-release-tag) "FederalTaxesErrorCode" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2392,7 +2409,7 @@ export interface FormFieldsMetadataContextValue { // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@gusto/embedded-react-sdk" does not have an export "FormFieldsMetadataContext" // // @internal -export function FormFieldsMetadataProvider(input: FormFieldsMetadataProviderProps): JSX_2.Element; +export function FormFieldsMetadataProvider(input: FormFieldsMetadataProviderProps): JSX; // @public export type FormHookResult = { @@ -2559,7 +2576,7 @@ interface HolidayPolicyDetailPresentationProps { // Warning: (ae-missing-release-tag) "HolidaySelectionForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function HolidaySelectionForm(props: HolidaySelectionFormProps): JSX_2.Element; +function HolidaySelectionForm(props: HolidaySelectionFormProps): JSX; // Warning: (ae-missing-release-tag) "HolidaySelectionFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2574,12 +2591,12 @@ interface HolidaySelectionFormProps extends BaseComponentInterface { // Warning: (ae-missing-release-tag) "HomeAddress" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function HomeAddress(input: HomeAddressProps & BaseComponentInterface<'Employee.HomeAddress.Management'>): JSX_2.Element; +function HomeAddress(input: HomeAddressProps & BaseComponentInterface<'Employee.HomeAddress.Management'>): JSX; // Warning: (ae-missing-release-tag) "HomeAddressCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function HomeAddressCard(input: HomeAddressCardProps): JSX_2.Element; +function HomeAddressCard(input: HomeAddressCardProps): JSX; // Warning: (ae-missing-release-tag) "HomeAddressCardProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2594,7 +2611,7 @@ interface HomeAddressCardProps { // Warning: (ae-missing-release-tag) "HomeAddressEditForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function HomeAddressEditForm(input: HomeAddressEditFormProps & BaseComponentInterface): JSX_2.Element; +function HomeAddressEditForm(input: HomeAddressEditFormProps & BaseComponentInterface): JSX; // Warning: (ae-missing-release-tag) "HomeAddressEditFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2717,28 +2734,28 @@ export interface HookSubmitResult { // Warning: (ae-missing-release-tag) "Industry" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function Industry(props: IndustryProps): JSX_2.Element; +function Industry(props: IndustryProps): JSX; // Warning: (ae-forgotten-export) The symbol "InformationRequestFormProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "InformationRequestForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "InformationRequestForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function InformationRequestForm(props: InformationRequestFormProps): JSX_2.Element; +function InformationRequestForm(props: InformationRequestFormProps): JSX; // @public (undocumented) namespace InformationRequestForm { var // (undocumented) Footer: (input: { onEvent: OnEventType; - }) => JSX_2.Element; + }) => JSX; } // Warning: (ae-forgotten-export) The symbol "InformationRequestListProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "InformationRequestList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function InformationRequestList(props: InformationRequestListProps): JSX_2.Element; +function InformationRequestList(props: InformationRequestListProps): JSX; declare namespace InformationRequests { export { @@ -2752,13 +2769,13 @@ declare namespace InformationRequests { // Warning: (ae-missing-release-tag) "InformationRequestsFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function InformationRequestsFlow(input: InformationRequestsFlowProps): JSX_2.Element; +function InformationRequestsFlow(input: InformationRequestsFlowProps): JSX; // Warning: (ae-forgotten-export) The symbol "InviteSignatoryProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "InviteSignatory" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function InviteSignatory(props: InviteSignatoryProps & BaseComponentInterface): JSX_2.Element; +function InviteSignatory(props: InviteSignatoryProps & BaseComponentInterface): JSX; // Warning: (ae-missing-release-tag) "JobErrorCode" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2844,7 +2861,7 @@ export type JobTitleFieldProps = HookFieldProps JSX_2.Element; +export const ObservabilityProvider: (input: ObservabilityProviderProps) => JSX; // Warning: (ae-internal-missing-underscore) The name "ObservabilityProviderProps" should be prefixed with an underscore because the declaration is marked as @internal // @@ -3110,7 +3127,7 @@ export interface ObservabilityProviderProps { // Warning: (ae-missing-release-tag) "OffCycleCreation" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function OffCycleCreation(props: OffCycleCreationProps): JSX_2.Element; +function OffCycleCreation(props: OffCycleCreationProps): JSX; // Warning: (ae-missing-release-tag) "OffCycleCreationFormData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3139,7 +3156,7 @@ interface OffCycleCreationProps extends BaseComponentInterface<'Payroll.OffCycle // Warning: (ae-missing-release-tag) "OffCycleDeductionsSetting" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function OffCycleDeductionsSetting(input: OffCycleDeductionsSettingProps): JSX_2.Element; +function OffCycleDeductionsSetting(input: OffCycleDeductionsSettingProps): JSX; // Warning: (ae-missing-release-tag) "OffCycleDeductionsSettingChangePayload" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3162,7 +3179,7 @@ interface OffCycleDeductionsSettingProps extends CommonComponentInterface<'Payro // Warning: (ae-missing-release-tag) "OffCycleFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function OffCycleFlow(input: OffCycleFlowProps): JSX_2.Element; +function OffCycleFlow(input: OffCycleFlowProps): JSX; // Warning: (ae-missing-release-tag) "OffCycleFlowContextInterface" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3231,7 +3248,7 @@ interface OffCycleReasonDefaults { // Warning: (ae-missing-release-tag) "OffCycleReasonSelection" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function OffCycleReasonSelection(props: OffCycleReasonSelectionProps): JSX_2.Element; +function OffCycleReasonSelection(props: OffCycleReasonSelectionProps): JSX; // Warning: (ae-missing-release-tag) "OffCycleReasonSelectionProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3244,7 +3261,7 @@ interface OffCycleReasonSelectionProps extends BaseComponentInterface<'Payroll.O // Warning: (ae-missing-release-tag) "OnboardingExecutionFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function OnboardingExecutionFlow(input: OnboardingExecutionFlowProps): JSX_2.Element; +function OnboardingExecutionFlow(input: OnboardingExecutionFlowProps): JSX; // Warning: (ae-missing-release-tag) "OnboardingExecutionFlowProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3284,31 +3301,31 @@ type OnboardingExecutionInitialState = keyof typeof INITIAL_COMPONENT_MAP; // Warning: (ae-missing-release-tag) "OnboardingFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const OnboardingFlow: (input: OnboardingFlowProps) => JSX_2.Element; +const OnboardingFlow: (input: OnboardingFlowProps) => JSX; // Warning: (ae-forgotten-export) The symbol "OnboardingFlowProps_2" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OnboardingFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const OnboardingFlow_2: (input: OnboardingFlowProps_2) => JSX_2.Element; +const OnboardingFlow_2: (input: OnboardingFlowProps_2) => JSX; // Warning: (ae-forgotten-export) The symbol "OnboardingFlowProps_3" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OnboardingFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const OnboardingFlow_3: (input: OnboardingFlowProps_3) => JSX_2.Element; +const OnboardingFlow_3: (input: OnboardingFlowProps_3) => JSX; // Warning: (ae-forgotten-export) The symbol "OnboardingOverviewProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OnboardingOverview" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function OnboardingOverview(props: OnboardingOverviewProps & BaseComponentInterface): JSX_2.Element; +function OnboardingOverview(props: OnboardingOverviewProps & BaseComponentInterface): JSX; // Warning: (ae-forgotten-export) The symbol "SummaryProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OnboardingSummary" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function OnboardingSummary(props: SummaryProps & BaseComponentInterface): JSX_2.Element; +function OnboardingSummary(props: SummaryProps & BaseComponentInterface): JSX; // Warning: (ae-forgotten-export) The symbol "BaseListProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OrderedListProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -3356,29 +3373,60 @@ export const PAYMENT_METHOD_TYPES: readonly ["Direct Deposit", "Check"]; // Warning: (ae-missing-release-tag) "PaymentFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const PaymentFlow: (input: PaymentFlowProps) => JSX_2.Element; +const PaymentFlow: (input: PaymentFlowProps) => JSX; // Warning: (ae-forgotten-export) The symbol "PaymentHistoryProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PaymentHistory" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PaymentHistory(props: PaymentHistoryProps): JSX_2.Element; +function PaymentHistory(props: PaymentHistoryProps): JSX; // Warning: (ae-forgotten-export) The symbol "PaymentMethodProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PaymentMethod" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PaymentMethod(props: PaymentMethodProps): JSX_2.Element; +function PaymentMethod(props: PaymentMethodProps): JSX; // Warning: (ae-missing-release-tag) "PaymentMethod" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PaymentMethod_2(input: PaymentMethodProps_2 & BaseComponentInterface): JSX_2.Element; +function PaymentMethod_2(input: PaymentMethodProps_2 & BaseComponentInterface): JSX; // Warning: (ae-missing-release-tag) "PaymentMethod" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PaymentMethod_3(input: PaymentMethodProps_3 & BaseComponentInterface): JSX_2.Element; +function PaymentMethod_3(input: PaymentMethodProps_3 & BaseComponentInterface<'Employee.Management.PaymentMethod'>): JSX; + +// Warning: (ae-missing-release-tag) "PaymentMethodBankForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function PaymentMethodBankForm(input: PaymentMethodBankFormProps): JSX; + +// Warning: (ae-missing-release-tag) "PaymentMethodBankFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PaymentMethodBankFormProps extends Omit { + // (undocumented) + employeeId: string; + // (undocumented) + onEvent: OnEventType; +} + +// Warning: (ae-missing-release-tag) "PaymentMethodCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@gusto/embedded-react-sdk" does not have an export "usePaymentMethodList" +// +// @public +function PaymentMethodCard(input: PaymentMethodCardProps): JSX; + +// Warning: (ae-missing-release-tag) "PaymentMethodCardProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PaymentMethodCardProps { + // (undocumented) + employeeId: string; + // (undocumented) + onEvent: OnEventType; +} // Warning: (ae-forgotten-export) The symbol "fieldValidators_9" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PaymentMethodFormData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -3453,7 +3501,7 @@ interface PaymentMethodProps_2 extends CommonComponentInterface<'Employee.Paymen // Warning: (ae-missing-release-tag) "PaymentMethodProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -interface PaymentMethodProps_3 extends CommonComponentInterface<'Employee.PaymentMethod'> { +interface PaymentMethodProps_3 extends CommonComponentInterface<'Employee.Management.PaymentMethod'> { // (undocumented) defaultValues?: never; // (undocumented) @@ -3466,6 +3514,21 @@ interface PaymentMethodProps_3 extends CommonComponentInterface<'Employee.Paymen onEvent: OnEventType; } +// Warning: (ae-missing-release-tag) "PaymentMethodSplitForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function PaymentMethodSplitForm(input: PaymentMethodSplitFormProps): JSX; + +// Warning: (ae-missing-release-tag) "PaymentMethodSplitFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PaymentMethodSplitFormProps extends Omit { + // (undocumented) + employeeId: string; + // (undocumented) + onEvent: OnEventType; +} + // Warning: (ae-missing-release-tag) "PaymentMethodType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -3480,19 +3543,19 @@ export type PaymentPeriodFieldProps = HookFieldProps JSX_2.Element | null; +const PaymentSummary: (input: PaymentSummaryProps) => JSX | null; // Warning: (ae-missing-release-tag) "PaymentUnitFieldProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3560,24 +3623,24 @@ declare namespace Payroll { // Warning: (ae-missing-release-tag) "PayrollBlockerList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function PayrollBlockerList(props: PayrollBlockerListProps): JSX_2.Element; +function PayrollBlockerList(props: PayrollBlockerListProps): JSX; // Warning: (ae-forgotten-export) The symbol "PayrollConfigurationProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PayrollConfiguration" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PayrollConfiguration(props: PayrollConfigurationProps & BaseComponentInterface): JSX_2.Element; +function PayrollConfiguration(props: PayrollConfigurationProps & BaseComponentInterface): JSX; // Warning: (ae-forgotten-export) The symbol "PayrollEditEmployeeProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PayrollEditEmployee" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PayrollEditEmployee(props: PayrollEditEmployeeProps & BaseComponentInterface): JSX_2.Element; +function PayrollEditEmployee(props: PayrollEditEmployeeProps & BaseComponentInterface): JSX; // Warning: (ae-missing-release-tag) "PayrollExecutionFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PayrollExecutionFlow(input: PayrollExecutionFlowProps): JSX_2.Element; +function PayrollExecutionFlow(input: PayrollExecutionFlowProps): JSX; // Warning: (ae-missing-release-tag) "PayrollExecutionFlowProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3614,25 +3677,25 @@ type PayrollExecutionInitialState = 'configuration' | 'overview'; // Warning: (ae-missing-release-tag) "PayrollFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const PayrollFlow: (input: PayrollFlowProps) => JSX_2.Element; +const PayrollFlow: (input: PayrollFlowProps) => JSX; // Warning: (ae-forgotten-export) The symbol "PayrollHistoryProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PayrollHistory" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PayrollHistory(props: PayrollHistoryProps): JSX_2.Element; +function PayrollHistory(props: PayrollHistoryProps): JSX; // Warning: (ae-forgotten-export) The symbol "PayrollLandingProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PayrollLanding" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PayrollLanding(props: PayrollLandingProps): JSX_2.Element; +function PayrollLanding(props: PayrollLandingProps): JSX; // Warning: (ae-forgotten-export) The symbol "PayrollListBlockProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PayrollList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function PayrollList(props: PayrollListBlockProps): JSX_2.Element; +function PayrollList(props: PayrollListBlockProps): JSX; // Warning: (ae-missing-release-tag) "PayrollLoadingProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3653,19 +3716,19 @@ type PayrollOption = 'dismissalPayroll' | 'regularPayroll' | 'anotherWay'; // Warning: (ae-missing-release-tag) "PayrollOverview" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PayrollOverview(props: PayrollOverviewProps): JSX_2.Element; +function PayrollOverview(props: PayrollOverviewProps): JSX; // Warning: (ae-forgotten-export) The symbol "PayrollReceiptsProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PayrollReceipts" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PayrollReceipts(props: PayrollReceiptsProps): JSX_2.Element; +function PayrollReceipts(props: PayrollReceiptsProps): JSX; // Warning: (ae-forgotten-export) The symbol "PayScheduleProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PaySchedule" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const PaySchedule: (input: PayScheduleProps & BaseComponentInterface) => JSX_2.Element; +const PaySchedule: (input: PayScheduleProps & BaseComponentInterface) => JSX; // Warning: (ae-missing-release-tag) "PayScheduleErrorCode" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3725,10 +3788,26 @@ export type PayScheduleOptionalFieldsToRequire = OptionalFieldsToRequire; +} + // Warning: (ae-missing-release-tag) "PolicyConfigurationForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PolicyConfigurationForm(props: PolicyConfigurationFormProps): JSX_2.Element; +function PolicyConfigurationForm(props: PolicyConfigurationFormProps): JSX; // Warning: (ae-missing-release-tag) "PolicyConfigurationFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3756,7 +3835,7 @@ type PolicyDetails = UnlimitedPolicyDetails | RateBasedPolicyDetails; // Warning: (ae-missing-release-tag) "PolicyList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PolicyList(input: PolicyListProps): JSX_2.Element; +function PolicyList(input: PolicyListProps): JSX; // Warning: (ae-missing-release-tag) "PolicyListProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3785,7 +3864,7 @@ interface PolicySettingsDisplay { // Warning: (ae-missing-release-tag) "PolicySettingsPresentation" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PolicySettingsPresentation(input: PolicySettingsPresentationProps): JSX_2.Element; +function PolicySettingsPresentation(input: PolicySettingsPresentationProps): JSX; // Warning: (ae-missing-release-tag) "PolicySettingsPresentationProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3814,7 +3893,7 @@ interface PolicySettingsPresentationProps { // Warning: (ae-missing-release-tag) "PolicyTypeSelector" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function PolicyTypeSelector(props: PolicyTypeSelectorProps): JSX_2.Element; +function PolicyTypeSelector(props: PolicyTypeSelectorProps): JSX; // Warning: (ae-missing-release-tag) "PolicyTypeSelectorProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3868,17 +3947,17 @@ export type PreparerTextFieldProps = HookFieldProps): JSX_2.Element; +function Profile_2(input: ProfileProps_2 & BaseComponentInterface<'Employee.Profile.Management'>): JSX; // Warning: (ae-missing-release-tag) "ProfileCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function ProfileCard(input: ProfileCardProps): JSX_2.Element; +function ProfileCard(input: ProfileCardProps): JSX; // Warning: (ae-missing-release-tag) "ProfileCardProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3893,7 +3972,7 @@ interface ProfileCardProps { // Warning: (ae-missing-release-tag) "ProfileEditForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function ProfileEditForm(input: ProfileEditFormProps & Pick): JSX_2.Element; +function ProfileEditForm(input: ProfileEditFormProps & Pick): JSX; // Warning: (ae-missing-release-tag) "ProfileEditFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3995,7 +4074,7 @@ export type RateValidation = (typeof CompensationErrorCodes)['REQUIRED' | 'RATE_ // Warning: (ae-missing-release-tag) "RecoveryCases" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function RecoveryCases(input: RecoveryCasesInternalProps): JSX_2.Element; +function RecoveryCases(input: RecoveryCasesInternalProps): JSX; // Warning: (ae-missing-release-tag) "RecurringFieldProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -4053,7 +4132,7 @@ export interface SDKFieldError { // @public export function SDKFormProvider>(input: SDKFormProviderProps): JSX_2.Element; +} = Record>(input: SDKFormProviderProps): JSX; // @public export interface SDKHooks { @@ -4139,7 +4218,7 @@ export type SelfOnboardingFieldProps = HookFieldProps; // Warning: (ae-missing-release-tag) "SelfOnboardingFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const SelfOnboardingFlow: (input: SelfOnboardingFlowProps) => JSX_2.Element; +const SelfOnboardingFlow: (input: SelfOnboardingFlowProps) => JSX; // Warning: (ae-missing-release-tag) "SignatureFieldProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -4150,7 +4229,7 @@ export type SignatureFieldProps = HookFieldProps): JSX_2.Element; +function StateTaxes_2(input: StateTaxesProps_2 & Pick): JSX; // Warning: (ae-missing-release-tag) "StateTaxes" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function StateTaxes_3(input: StateTaxesProps_3 & Pick): JSX_2.Element; +function StateTaxes_3(input: StateTaxesProps_3 & Pick): JSX; // Warning: (ae-forgotten-export) The symbol "StateTaxesFormProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "StateTaxesForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function StateTaxesForm(props: StateTaxesFormProps & BaseComponentInterface): JSX_2.Element; +function StateTaxesForm(props: StateTaxesFormProps & BaseComponentInterface): JSX; // Warning: (ae-forgotten-export) The symbol "StateTaxesListProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "StateTaxesList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function StateTaxesList(props: StateTaxesListProps): JSX_2.Element; +function StateTaxesList(props: StateTaxesListProps): JSX; // Warning: (ae-missing-release-tag) "StateTaxesProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -4634,12 +4713,12 @@ export interface TabsProps { // Warning: (ae-missing-release-tag) "Taxes" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) -function Taxes(props: TaxesProps & BaseComponentInterface): JSX_2.Element; +function Taxes(props: TaxesProps & BaseComponentInterface): JSX; // Warning: (ae-missing-release-tag) "TerminateEmployee" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function TerminateEmployee(props: TerminateEmployeeProps): JSX_2.Element; +function TerminateEmployee(props: TerminateEmployeeProps): JSX; // Warning: (ae-missing-release-tag) "TerminateEmployeeProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -4654,7 +4733,7 @@ interface TerminateEmployeeProps extends BaseComponentInterface<'Employee.Termin // Warning: (ae-missing-release-tag) "TerminationFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const TerminationFlow: (input: TerminationFlowProps) => JSX_2.Element; +const TerminationFlow: (input: TerminationFlowProps) => JSX; // Warning: (ae-missing-release-tag) "TerminationFlowProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -4669,7 +4748,7 @@ interface TerminationFlowProps extends BaseComponentInterface { // Warning: (ae-missing-release-tag) "TerminationSummary" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function TerminationSummary(props: TerminationSummaryProps): JSX_2.Element; +function TerminationSummary(props: TerminationSummaryProps): JSX; // Warning: (ae-missing-release-tag) "TerminationSummaryProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -4783,7 +4862,7 @@ declare namespace TimeOff { // Warning: (ae-missing-release-tag) "TimeOffFlow" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const TimeOffFlow: (input: TimeOffFlowProps) => JSX_2.Element; +const TimeOffFlow: (input: TimeOffFlowProps) => JSX; // Warning: (ae-missing-release-tag) "TimeOffFlowProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -4806,7 +4885,7 @@ interface TimeOffPolicyDetailEmployee extends EmployeeTableItem { // Warning: (ae-missing-release-tag) "TimeOffPolicyDetailPresentation" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function TimeOffPolicyDetailPresentation(input: TimeOffPolicyDetailPresentationProps): JSX_2.Element; +function TimeOffPolicyDetailPresentation(input: TimeOffPolicyDetailPresentationProps): JSX; // Warning: (ae-forgotten-export) The symbol "TimeOffPolicyDetailPresentationBaseProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "TimeOffPolicyDetailPresentationProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -4830,7 +4909,7 @@ export type TotalAmountFieldProps = HookFieldProps(base: FieldMetadata, options: Arra // Warning: (ae-missing-release-tag) "WorkAddress" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function WorkAddress(input: WorkAddressProps & BaseComponentInterface<'Employee.Management.WorkAddress'>): JSX_2.Element; +function WorkAddress(input: WorkAddressProps & BaseComponentInterface<'Employee.Management.WorkAddress'>): JSX; // Warning: (ae-missing-release-tag) "WorkAddressCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function WorkAddressCard(input: WorkAddressCardProps): JSX_2.Element; +function WorkAddressCard(input: WorkAddressCardProps): JSX; // Warning: (ae-missing-release-tag) "WorkAddressCardProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -5761,7 +5840,7 @@ interface WorkAddressCardProps { // Warning: (ae-missing-release-tag) "WorkAddressEditForm" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -function WorkAddressEditForm(input: WorkAddressEditFormProps & BaseComponentInterface): JSX_2.Element; +function WorkAddressEditForm(input: WorkAddressEditFormProps & BaseComponentInterface): JSX; // Warning: (ae-missing-release-tag) "WorkAddressEditFormProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -5882,7 +5961,7 @@ export type ZipValidation = (typeof HomeAddressErrorCodes)['REQUIRED' | 'INVALID // Warnings were encountered during analysis: // -// dist/partner-hook-utils/types.d.ts:267:13 - (ae-forgotten-export) The symbol "FieldElementRegistry" needs to be exported by the entry point index.d.ts +// dist/partner-hook-utils/types.d.ts:268:13 - (ae-forgotten-export) The symbol "FieldElementRegistry" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/docs/reference/endpoint-inventory.json b/docs/reference/endpoint-inventory.json index 6b4a9beaf..a9e976537 100644 --- a/docs/reference/endpoint-inventory.json +++ b/docs/reference/endpoint-inventory.json @@ -2608,9 +2608,9 @@ "Employee.DashboardFlow": { "blocks": [ "Employee.Compensation", - "Employee.Deductions", "Employee.HomeAddress", "Employee.WorkAddress", + "EmployeeManagement.Deductions", "EmployeeManagement.DocumentManager", "EmployeeManagement.FederalTaxes", "EmployeeManagement.PaymentMethod", @@ -2666,6 +2666,7 @@ }, "EmployeeManagement.DashboardFlow": { "blocks": [ + "EmployeeManagement.Deductions", "EmployeeManagement.DocumentManager", "EmployeeManagement.FederalTaxes", "EmployeeManagement.HomeAddress", @@ -2674,8 +2675,7 @@ "EmployeeManagement.Profile", "EmployeeManagement.StateTaxes", "EmployeeManagement.WorkAddress", - "EmployeeOnboarding.Compensation", - "EmployeeOnboarding.Deductions" + "EmployeeOnboarding.Compensation" ] }, "EmployeeManagement.EmployeeListFlow": { diff --git a/docs/reference/endpoint-reference.md b/docs/reference/endpoint-reference.md index 0e18f5787..3119f9a2d 100644 --- a/docs/reference/endpoint-reference.md +++ b/docs/reference/endpoint-reference.md @@ -483,13 +483,13 @@ Flows compose multiple blocks into a single workflow. The endpoint list for a fl | **Contractor.OnboardingFlow** | Contractor.Address, Contractor.ContractorList, Contractor.ContractorProfile, Contractor.ContractorSubmit, Contractor.NewHireReport, Contractor.PaymentMethod | | **Contractor.PaymentFlow** | Contractor.CreatePayment, Contractor.PaymentHistory, Contractor.PaymentStatement, Contractor.PaymentSummary, Contractor.PaymentsList, InformationRequests.InformationRequestsFlow | | **ContractorOnboarding.OnboardingFlow** | ContractorOnboarding.Address, ContractorOnboarding.ContractorList, ContractorOnboarding.ContractorProfile, ContractorOnboarding.ContractorSubmit, ContractorOnboarding.NewHireReport, ContractorOnboarding.PaymentMethod | -| **Employee.DashboardFlow** | Employee.Compensation, Employee.Deductions, Employee.HomeAddress, Employee.WorkAddress, EmployeeManagement.DocumentManager, EmployeeManagement.FederalTaxes, EmployeeManagement.PaymentMethod, EmployeeManagement.PaystubsCard, EmployeeManagement.Profile, EmployeeManagement.StateTaxes | +| **Employee.DashboardFlow** | Employee.Compensation, Employee.HomeAddress, Employee.WorkAddress, EmployeeManagement.Deductions, EmployeeManagement.DocumentManager, EmployeeManagement.FederalTaxes, EmployeeManagement.PaymentMethod, EmployeeManagement.PaystubsCard, EmployeeManagement.Profile, EmployeeManagement.StateTaxes | | **Employee.EmployeeListFlow** | Employee.DashboardFlow, Employee.OnboardingExecutionFlow, Employee.TerminationFlow, EmployeeManagement.EmployeeList | | **Employee.OnboardingExecutionFlow** | Employee.Compensation, Employee.Deductions, Employee.EmployeeDocuments, Employee.OnboardingSummary, Employee.PaymentMethod, Employee.Profile, EmployeeOnboarding.FederalTaxes, EmployeeOnboarding.StateTaxes | | **Employee.OnboardingFlow** | Employee.EmployeeList, Employee.OnboardingExecutionFlow | | **Employee.SelfOnboardingFlow** | Employee.DocumentSigner, Employee.Landing, Employee.OnboardingSummary, Employee.PaymentMethod, Employee.Profile, EmployeeOnboarding.FederalTaxes, EmployeeOnboarding.StateTaxes | | **Employee.TerminationFlow** | Employee.TerminateEmployee, Employee.TerminationSummary, Payroll.DismissalFlow, Payroll.PayrollLanding | -| **EmployeeManagement.DashboardFlow** | EmployeeManagement.DocumentManager, EmployeeManagement.FederalTaxes, EmployeeManagement.HomeAddress, EmployeeManagement.PaymentMethod, EmployeeManagement.PaystubsCard, EmployeeManagement.Profile, EmployeeManagement.StateTaxes, EmployeeManagement.WorkAddress, EmployeeOnboarding.Compensation, EmployeeOnboarding.Deductions | +| **EmployeeManagement.DashboardFlow** | EmployeeManagement.Deductions, EmployeeManagement.DocumentManager, EmployeeManagement.FederalTaxes, EmployeeManagement.HomeAddress, EmployeeManagement.PaymentMethod, EmployeeManagement.PaystubsCard, EmployeeManagement.Profile, EmployeeManagement.StateTaxes, EmployeeManagement.WorkAddress, EmployeeOnboarding.Compensation | | **EmployeeManagement.EmployeeListFlow** | EmployeeManagement.DashboardFlow, EmployeeManagement.EmployeeList, EmployeeManagement.TerminationFlow, EmployeeOnboarding.OnboardingExecutionFlow | | **EmployeeManagement.TerminationFlow** | EmployeeManagement.TerminateEmployee, EmployeeManagement.TerminationSummary, Payroll.DismissalFlow, Payroll.PayrollLanding | | **EmployeeOnboarding.OnboardingExecutionFlow** | Employee.EmployeeDocuments, Employee.PaymentMethod, EmployeeOnboarding.Compensation, EmployeeOnboarding.Deductions, EmployeeOnboarding.FederalTaxes, EmployeeOnboarding.OnboardingSummary, EmployeeOnboarding.Profile, EmployeeOnboarding.StateTaxes | diff --git a/docs/workflows-overview/employee-management/employee-management.md b/docs/workflows-overview/employee-management/employee-management.md index 816d7cdfd..635f7cd6e 100644 --- a/docs/workflows-overview/employee-management/employee-management.md +++ b/docs/workflows-overview/employee-management/employee-management.md @@ -321,10 +321,10 @@ function MyDeductionsPanel({ employeeId }) { **Events** -| Event type | Description | Data | -| -------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------- | -| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED | Fired after a new deduction is saved | The created `Garnishment` | -| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED | Fired after an existing deduction is saved | The updated `Garnishment` | +| Event type | Description | Data | +| -------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------- | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CREATED | Fired after a new deduction is saved | The created `Garnishment` | +| EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_UPDATED | Fired after an existing deduction is saved | The updated `Garnishment` | | EMPLOYEE_MANAGEMENT_DEDUCTIONS_EDIT_FORM_CANCELLED | Fired when the user clicks Cancel and the orchestrator should swap back to the card | None | ### EmployeeManagement.Profile diff --git a/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx index f4bea0d73..9e0aab78b 100644 --- a/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx +++ b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.test.tsx @@ -1,8 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { DeductionsForm } from './DeductionsForm' -import type { DeductionsFormDictionary } from './types' +import { DeductionsForm, type DeductionsFormDictionary } from './DeductionsForm' import { renderWithProviders } from '@/test-utils/renderWithProviders' import { setupApiTestMocks } from '@/test/mocks/apiServer' diff --git a/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx index feb7f164d..cefe070a9 100644 --- a/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx +++ b/src/components/Employee/Deductions/shared/DeductionsForm/DeductionsForm.tsx @@ -6,11 +6,13 @@ import type { } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment' import { StandardDeductionForm } from './StandardDeductionForm' import { ChildSupportFormView } from './ChildSupportFormView' -import type { DeductionsFormDictionary } from './types' import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' import { Grid } from '@/components/Common/Grid/Grid' import { Flex } from '@/components/Common/Flex/Flex' import { useComponentDictionary, useI18n } from '@/i18n' +import type { ResourceDictionary } from '@/types/Helpers' + +export type DeductionsFormDictionary = ResourceDictionary<'Employee.DeductionsForm'> // Garnishment types the form supports (mirrors the legacy SUPPORTED_GARNISHMENT_TYPES). const SUPPORTED_GARNISHMENT_TYPES: readonly GarnishmentType[] = [ diff --git a/src/components/Employee/Deductions/shared/DeductionsForm/index.ts b/src/components/Employee/Deductions/shared/DeductionsForm/index.ts index 7b984cda7..f8a845d58 100644 --- a/src/components/Employee/Deductions/shared/DeductionsForm/index.ts +++ b/src/components/Employee/Deductions/shared/DeductionsForm/index.ts @@ -1,2 +1,5 @@ -export { DeductionsForm, type DeductionsFormProps } from './DeductionsForm' -export type { DeductionsFormDictionary } from './types' +export { + DeductionsForm, + type DeductionsFormProps, + type DeductionsFormDictionary, +} from './DeductionsForm' diff --git a/src/components/Employee/Deductions/shared/DeductionsForm/types.ts b/src/components/Employee/Deductions/shared/DeductionsForm/types.ts deleted file mode 100644 index 5e6992784..000000000 --- a/src/components/Employee/Deductions/shared/DeductionsForm/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ResourceDictionary } from '@/types/Helpers' - -/** - * Override surface for {@link DeductionsForm}'s text. Surfaces (onboarding, - * management, etc.) pass a resolved dictionary that fully replaces the form's - * default English copy at render time. - * - * The underlying namespace (`Employee.DeductionsForm`) is an implementation - * detail of the shared form — callers shouldn't reference it directly. Build - * the dictionary inside a per-surface `useFormDictionary` hook that resolves - * `t(...)` against the surface's own namespace, so partner overrides on that - * namespace flow into the form text automatically. - */ -export type DeductionsFormDictionary = ResourceDictionary<'Employee.DeductionsForm'>