From d4566d6af96184f033db12d1194344d92ba433d8 Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Wed, 3 Jun 2026 16:31:54 -0600 Subject: [PATCH] feat: extract Work address card from DashboardFlow into standalone management block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates the Work address card off the monolithic DashboardFlow into a self-contained `EmployeeManagement.WorkAddress` block, matching the shape of the existing PaymentMethod block and the just-landed Profile block (#1976). The dashboard now composes the card and edit-form pieces directly; the block is the convenient pre-wired composition for partners who want a single drop-in. Four standalone partner-consumable surfaces under `Employee/WorkAddress/`: - `useEmployeeWorkAddressSummary` — `BaseHookReady`-shaped data hook in `shared/`, wrapping `useEmployeeAddressesGetWorkAddresses`. - `WorkAddressCard` — self-fetching card with `{ employeeId, onEvent }` shape, fires `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED`. - `WorkAddressEditForm` — the existing edit screen, renamed from the old top-level `WorkAddress.tsx` to free the block name. - `WorkAddress` — orchestrated block with a local robot3 state machine wiring card ↔ edit form. Scoped event surface (added to `src/shared/constants.ts`, all new strings prefixed `employee/management/workAddress/...`): - `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED` - `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_CREATED` - `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED` - `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED` - `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED` No success-alert wiring — Work address is in the skill's "don't introduce one" list. The edit screen is modal-style: create/update/ delete keep the user on the edit screen so additional addresses can be managed; only the explicit Back action transitions back to the card. Dashboard integration (this PR is the last touch for the Work address card; other cards keep the legacy path until their own migrations): - `dashboardStateMachine` retargeted to the scoped events. - `DashboardComponents.WorkAddressContextual` now renders `WorkAddressEditForm`. - `BasicDetailsView` inline work-address markup replaced with ``. - `useEmployeeBasicDetails` slimmed — work address fetch removed (the card self-fetches via the new hook). - Card-internal coverage moved out of `Dashboard.test.tsx` into the new card and block tests. i18n namespace introduced as `Employee.Management.WorkAddress` (using the prefix-style convention from PR #2006 of the migrate skill, since this card's migration is the touch-event for the file). Strings moved out of `Employee.Dashboard:workAddress.*` into the block's own namespace; partner override supported via `useComponentDictionary`. Generated artifacts refreshed: `i18next.d.ts`, `sdk-app/src/generated-registry-data.ts`, and `.reports/embedded-react-sdk.api.md`. Co-authored-by: Cursor --- .reports/embedded-react-sdk.api.md | 47 ++++- docs/reference/endpoint-inventory.json | 8 - docs/reference/endpoint-reference.md | 2 - docs/workflows-overview/employee-dashboard.md | 42 ++-- .../employee-management.md | 185 +++++++++++++++--- sdk-app/src/generated-registry-data.ts | 2 + .../Dashboard/BasicDetailsView.stories.tsx | 33 +--- .../Employee/Dashboard/BasicDetailsView.tsx | 94 +-------- .../Employee/Dashboard/Dashboard.test.tsx | 15 +- .../Employee/Dashboard/Dashboard.tsx | 12 +- .../Dashboard/DashboardComponents.tsx | 4 +- .../Dashboard/dashboardStateMachine.ts | 10 +- .../Employee/Dashboard/hooks/index.ts | 6 - .../hooks/useEmployeeBasicDetails.test.tsx | 106 ---------- .../hooks/useEmployeeBasicDetails.tsx | 54 ----- .../management/WorkAddress.test.tsx | 72 +++++++ .../WorkAddress/management/WorkAddress.tsx | 86 +++----- .../WorkAddressCard/WorkAddressCard.test.tsx | 63 ++++++ .../WorkAddressCard/WorkAddressCard.tsx | 72 +++++++ .../management/WorkAddressCard/index.ts | 2 + .../management/WorkAddressComponents.tsx | 18 ++ .../management/WorkAddressEditForm.tsx | 76 +++++++ .../management/WorkAddressView.tsx | 2 +- .../Employee/WorkAddress/management/index.ts | 6 + .../management/useWorkAddressManagement.tsx | 4 +- .../management/workAddressStateMachine.ts | 38 ++++ .../Employee/WorkAddress/shared/index.ts | 6 + .../useEmployeeWorkAddressSummary/index.ts | 6 + .../useEmployeeWorkAddressSummary.test.tsx | 93 +++++++++ .../useEmployeeWorkAddressSummary.tsx | 51 +++++ .../Employee/exports/employeeManagement.ts | 8 +- src/i18n/en/Employee.Dashboard.json | 6 - ...n => Employee.Management.WorkAddress.json} | 3 + src/shared/constants.ts | 5 + src/types/i18next.d.ts | 105 +++++----- 35 files changed, 855 insertions(+), 487 deletions(-) delete mode 100644 src/components/Employee/Dashboard/hooks/useEmployeeBasicDetails.test.tsx delete mode 100644 src/components/Employee/Dashboard/hooks/useEmployeeBasicDetails.tsx create mode 100644 src/components/Employee/WorkAddress/management/WorkAddress.test.tsx create mode 100644 src/components/Employee/WorkAddress/management/WorkAddressCard/WorkAddressCard.test.tsx create mode 100644 src/components/Employee/WorkAddress/management/WorkAddressCard/WorkAddressCard.tsx create mode 100644 src/components/Employee/WorkAddress/management/WorkAddressCard/index.ts create mode 100644 src/components/Employee/WorkAddress/management/WorkAddressComponents.tsx create mode 100644 src/components/Employee/WorkAddress/management/WorkAddressEditForm.tsx create mode 100644 src/components/Employee/WorkAddress/management/index.ts create mode 100644 src/components/Employee/WorkAddress/management/workAddressStateMachine.ts create mode 100644 src/components/Employee/WorkAddress/shared/index.ts create mode 100644 src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/index.ts create mode 100644 src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/useEmployeeWorkAddressSummary.test.tsx create mode 100644 src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/useEmployeeWorkAddressSummary.tsx rename src/i18n/en/{Employee.WorkAddress.Management.json => Employee.Management.WorkAddress.json} (96%) diff --git a/.reports/embedded-react-sdk.api.md b/.reports/embedded-react-sdk.api.md index b2e69168a..12397a70d 100644 --- a/.reports/embedded-react-sdk.api.md +++ b/.reports/embedded-react-sdk.api.md @@ -1076,6 +1076,11 @@ export const componentEvents: { readonly EMPLOYEE_PROFILE_MANAGEMENT_UPDATED: "employee/profile/management/updated"; readonly EMPLOYEE_PROFILE_MANAGEMENT_EDIT_CANCELLED: "employee/profile/management/editCancelled"; readonly EMPLOYEE_PROFILE_MANAGEMENT_ALERT_DISMISSED: "employee/profile/management/alertDismissed"; + readonly EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED: "employee/management/workAddress/editRequested"; + readonly EMPLOYEE_MANAGEMENT_WORK_ADDRESS_CREATED: "employee/management/workAddress/created"; + 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 ROBOT_MACHINE_DONE: "done"; readonly ERROR: "ERROR"; readonly CANCEL: "CANCEL"; @@ -2015,7 +2020,11 @@ declare namespace EmployeeManagement { HomeAddressCardProps, HomeAddressEditFormProps, WorkAddress, + WorkAddressCard, + WorkAddressEditForm, WorkAddressProps, + WorkAddressCardProps, + WorkAddressEditFormProps, FederalTaxes_2 as FederalTaxes, FederalTaxesProps_2 as FederalTaxesProps, StateTaxes_2 as StateTaxes, @@ -3641,7 +3650,7 @@ function PayrollLanding(props: PayrollLandingProps): JSX_2.Element; // 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 (undocumented) +// @public function PayrollList(props: PayrollListBlockProps): JSX_2.Element; // 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) @@ -5792,7 +5801,37 @@ export function withOptions(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): JSX_2.Element; +function WorkAddress(input: WorkAddressProps & BaseComponentInterface<'Employee.Management.WorkAddress'>): JSX_2.Element; + +// 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; + +// 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) +// +// @public (undocumented) +interface WorkAddressCardProps { + // (undocumented) + employeeId: string; + // (undocumented) + onEvent: OnEventType; +} + +// 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; + +// 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) +// +// @public (undocumented) +interface WorkAddressEditFormProps extends CommonComponentInterface<'Employee.Management.WorkAddress'> { + // (undocumented) + employeeId: string; + // (undocumented) + onEvent: BaseComponentInterface['onEvent']; +} // Warning: (ae-missing-release-tag) "WorkAddressErrorCode" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -5843,11 +5882,11 @@ export type WorkAddressOptionalFieldsToRequire = OptionalFieldsToRequire { +interface WorkAddressProps extends CommonComponentInterface<'Employee.Management.WorkAddress'> { // (undocumented) employeeId: string; // (undocumented) - onEvent: BaseComponentInterface['onEvent']; + onEvent: OnEventType; } // Warning: (ae-missing-release-tag) "RequiredValidation" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/docs/reference/endpoint-inventory.json b/docs/reference/endpoint-inventory.json index b90aa124b..51c15b84a 100644 --- a/docs/reference/endpoint-inventory.json +++ b/docs/reference/endpoint-inventory.json @@ -841,10 +841,6 @@ "method": "GET", "path": "/v1/employees/:employeeId/pay_stubs" }, - { - "method": "GET", - "path": "/v1/employees/:employeeId/work_addresses" - }, { "method": "GET", "path": "/v1/employees/:employeeUuid/federal_taxes" @@ -1990,10 +1986,6 @@ "method": "GET", "path": "/v1/employees/:employeeId/pay_stubs" }, - { - "method": "GET", - "path": "/v1/employees/:employeeId/work_addresses" - }, { "method": "GET", "path": "/v1/employees/:employeeUuid/federal_taxes" diff --git a/docs/reference/endpoint-reference.md b/docs/reference/endpoint-reference.md index cf21a7e76..616c620fb 100644 --- a/docs/reference/endpoint-reference.md +++ b/docs/reference/endpoint-reference.md @@ -164,7 +164,6 @@ import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json' | | GET | `/v1/employees/:employeeId/forms` | | | GET | `/v1/employees/:employeeId/jobs` | | | GET | `/v1/employees/:employeeId/pay_stubs` | -| | GET | `/v1/employees/:employeeId/work_addresses` | | | GET | `/v1/employees/:employeeUuid/federal_taxes` | | | GET | `/v1/employees/:employeeUuid/state_taxes` | | | DELETE | `/v1/jobs/:jobId` | @@ -376,7 +375,6 @@ import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json' | | GET | `/v1/employees/:employeeId/forms` | | | GET | `/v1/employees/:employeeId/jobs` | | | GET | `/v1/employees/:employeeId/pay_stubs` | -| | GET | `/v1/employees/:employeeId/work_addresses` | | | GET | `/v1/employees/:employeeUuid/federal_taxes` | | | GET | `/v1/employees/:employeeUuid/state_taxes` | | | DELETE | `/v1/jobs/:jobId` | diff --git a/docs/workflows-overview/employee-dashboard.md b/docs/workflows-overview/employee-dashboard.md index 0735e9789..8af393292 100644 --- a/docs/workflows-overview/employee-dashboard.md +++ b/docs/workflows-overview/employee-dashboard.md @@ -30,23 +30,23 @@ function MyApp() { #### Events -| Event type | Description | Data | -| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------- | -| EMPLOYEE_UPDATE | Fired when editing basic details | { employeeId: string } | -| EMPLOYEE_HOME_ADDRESS | Fired when managing home address | { employeeId: string } | -| EMPLOYEE_WORK_ADDRESS | Fired when managing work address | { employeeId: string } | -| EMPLOYEE_COMPENSATION_CREATE | Fired when editing compensation | { employeeId: string, job: Job } | -| EMPLOYEE_JOB_ADD | Fired when adding a job (empty state or multi-job view) | { employeeId: string } | -| EMPLOYEE_JOB_DELETED | Fired after a non-primary job is deleted from the table | { employeeId: string, jobId: string } | -| EMPLOYEE_BANK_ACCOUNT_CREATE | Fired when adding a bank account | { employeeId: string } | -| EMPLOYEE_DEDUCTION_ADD | Fired when adding a deduction | { employeeId: string } | -| EMPLOYEE_FEDERAL_TAXES_EDIT | Fired when editing federal taxes | { employeeId: string, federalTaxes: EmployeeFederalTax } | -| EMPLOYEE_STATE_TAXES_EDIT | Fired when editing state taxes | { employeeId: string, state: string } | -| EMPLOYEE_VIEW_FORM_TO_SIGN | Fired when viewing a form | { employeeId: string, formUuid: string } | +| Event type | Description | Data | +| ----------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------- | +| EMPLOYEE_UPDATE | Fired when editing basic details | { employeeId: string } | +| EMPLOYEE_HOME_ADDRESS | Fired when managing home address | { employeeId: string } | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED | Fired when the "Manage" CTA is clicked on the Work address card | { employeeId: string } | +| EMPLOYEE_COMPENSATION_CREATE | Fired when editing compensation | { employeeId: string, job: Job } | +| EMPLOYEE_JOB_ADD | Fired when adding a job (empty state or multi-job view) | { employeeId: string } | +| EMPLOYEE_JOB_DELETED | Fired after a non-primary job is deleted from the table | { employeeId: string, jobId: string } | +| EMPLOYEE_BANK_ACCOUNT_CREATE | Fired when adding a bank account | { employeeId: string } | +| EMPLOYEE_DEDUCTION_ADD | Fired when adding a deduction | { employeeId: string } | +| EMPLOYEE_FEDERAL_TAXES_EDIT | Fired when editing federal taxes | { employeeId: string, federalTaxes: EmployeeFederalTax } | +| EMPLOYEE_STATE_TAXES_EDIT | Fired when editing state taxes | { employeeId: string, state: string } | +| EMPLOYEE_VIEW_FORM_TO_SIGN | Fired when viewing a form | { employeeId: string, formUuid: string } | ## Using Dashboard Subcomponents -The Dashboard workflow can be used through the wrapping flow component or rendered directly without the flow wrapper. The `EmployeeManagement` namespace also exports related steady-state components that are typically rendered in response to events emitted from the Dashboard (for example, an "Edit work address" CTA emits `EMPLOYEE_WORK_ADDRESS` and your application should render `EmployeeManagement.WorkAddress` in response). For guidance on creating a custom workflow, see [docs on composition](../integration-guide/composition.md). +The Dashboard workflow can be used through the wrapping flow component or rendered directly without the flow wrapper. The `EmployeeManagement` namespace also exports related steady-state components that are typically rendered in response to events emitted from the Dashboard (for example, an "Edit work address" CTA emits `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED` and your application should render `EmployeeManagement.WorkAddress` in response). For guidance on creating a custom workflow, see [docs on composition](../integration-guide/composition.md). ### Available Subcomponents @@ -120,7 +120,7 @@ function MyComponent() { ### EmployeeManagement.WorkAddress -A standalone management screen for viewing and editing an employee's work addresses. Supports adding, switching the active address (with an effective date), editing existing addresses, and deleting addresses. Typically rendered in response to the `EMPLOYEE_WORK_ADDRESS` event emitted from the Dashboard. +A self-contained block for viewing and managing an employee's work addresses. Renders a read-only card; clicking the card's "Manage" CTA swaps the card view for an edit screen that supports adding, switching the active address (with an effective date), editing existing addresses, and deleting addresses. Typically rendered in response to the `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED` event emitted from the Dashboard. See [employee-management.md](./employee-management/employee-management.md#employeemanagementworkaddress) for the full props and events surface. ```jsx import { EmployeeManagement } from '@gusto/embedded-react-sdk' @@ -141,11 +141,13 @@ function MyComponent() { #### Events -| Event type | Description | Data | -| ----------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| EMPLOYEE_WORK_ADDRESS_CREATED | Fired when a new work address is created | [Response from the Create a work address endpoint](https://docs.gusto.com/embedded-payroll/reference/post-v1-employees-employee_id-work_addresses) | -| EMPLOYEE_WORK_ADDRESS_UPDATED | Fired when an existing work address is updated | [Response from the Update a work address endpoint](https://docs.gusto.com/embedded-payroll/reference/put-v1-work_addresses-work_address_uuid) | -| EMPLOYEE_WORK_ADDRESS_DELETED | Fired when a work address is deleted | The deleted `EmployeeWorkAddress` snapshot | +| Event type | Description | Data | +| ----------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED | Fired when the "Manage" CTA is clicked on the card; the block swaps to the edit screen | { employeeId: string } | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_CREATED | Fired when a new work address is created on the edit screen | [Response from the Create a work address endpoint](https://docs.gusto.com/embedded-payroll/reference/post-v1-employees-employee_id-work_addresses) | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED | Fired when an existing work address is updated on the edit screen | [Response from the Update a work address endpoint](https://docs.gusto.com/embedded-payroll/reference/put-v1-work_addresses-work_address_uuid) | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED | Fired when a work address is deleted on the edit screen | The deleted `EmployeeWorkAddress` snapshot | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED | Fired when the user clicks Back on the edit screen; the block returns to the card view | None | ### EmployeeManagement.StateTaxes diff --git a/docs/workflows-overview/employee-management/employee-management.md b/docs/workflows-overview/employee-management/employee-management.md index ebfe2e88f..ba861ac88 100644 --- a/docs/workflows-overview/employee-management/employee-management.md +++ b/docs/workflows-overview/employee-management/employee-management.md @@ -35,6 +35,8 @@ Employee management components can be used to compose your own workflow, or can - [Composing from EmployeeManagement.ProfileCard and EmployeeManagement.ProfileEditForm directly](#composing-from-employeemanagementprofilecard-and-employeemanagementprofileeditform-directly) - [EmployeeManagement.HomeAddress](#employeemanagementhomeaddress) - [Composing from EmployeeManagement.HomeAddressCard and EmployeeManagement.HomeAddressEditForm directly](#composing-from-employeemanagementhomeaddresscard-and-employeemanagementhomeaddresseditform-directly) +- [EmployeeManagement.WorkAddress](#employeemanagementworkaddress) + - [Composing from EmployeeManagement.WorkAddressCard and EmployeeManagement.WorkAddressEditForm directly](#composing-from-employeemanagementworkaddresscard-and-employeemanagementworkaddresseditform-directly) ### EmployeeManagement.DashboardFlow @@ -67,37 +69,40 @@ function MyApp() { The dashboard forwards every event emitted by its card surfaces and edit screens to the partner via `onEvent`. The events below are the partner-visible surface; the dashboard's internal state machine also reacts to them to swap between card and edit views. -| Event type | Description | Data | -| ------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| EMPLOYEE_PROFILE_MANAGEMENT_EDIT_REQUESTED | Fired when the "Edit" CTA is clicked on the Basic details card | { employeeId: string } | -| EMPLOYEE_PROFILE_MANAGEMENT_UPDATED | Fired after the basic-details edit form is successfully saved | Updated `Employee` entity | -| EMPLOYEE_PROFILE_MANAGEMENT_EDIT_CANCELLED | Fired when the user clicks Cancel on the basic-details edit form | None | -| EMPLOYEE_PROFILE_MANAGEMENT_ALERT_DISMISSED | Fired when the user dismisses the in-card "Profile updated" alert (standalone block path) | None | -| EMPLOYEE_HOME_ADDRESS | Fired when the "Manage" CTA is clicked on the Home address card | { employeeId: string } | -| EMPLOYEE_WORK_ADDRESS | Fired when the "Manage" CTA is clicked on the Work address card | { employeeId: string } | -| EMPLOYEE_COMPENSATION_CREATE | Fired when the "Edit" CTA is clicked for an existing job (single- or multi-job views) | { employeeId: string, job: Job } | -| EMPLOYEE_JOB_ADD | Fired when the "Add job" / "Add another job" CTA is clicked | { employeeId: string } | -| EMPLOYEE_JOB_ADD_ANOTHER | Fired when "Add another job" is selected in the multi-job view | { employeeId: string } | -| EMPLOYEE_JOB_DELETED | Fired after a non-primary job is deleted from the multi-job table | Response from the Delete a job endpoint | -| EMPLOYEE_COMPENSATION_UPDATED | Fired after an Add / Add-another-job submission succeeds; surfaces the "Job added" alert | Response from the Update a compensation endpoint | -| EMPLOYEE_COMPENSATION_DONE | Fired after an Edit-compensation submission succeeds | None | -| EMPLOYEE_BANK_ACCOUNT_CREATE | Fired when the "Add bank account" CTA is clicked | { employeeId: string } | -| EMPLOYEE_BANK_ACCOUNT_CREATED | Fired after a bank account is successfully created; surfaces the "Bank account added" alert | Response from the Create a bank account endpoint | -| EMPLOYEE_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_SPLIT_PAYCHECK | Fired when the "Split paycheck" CTA is clicked | { employeeId: string } | -| EMPLOYEE_PAYMENT_METHOD_UPDATED | 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_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 } | -| EMPLOYEE_VIEW_FORM_TO_SIGN | Fired when a document row's "View" CTA is clicked | { employeeId: string, formId: string } | -| EMPLOYEE_DASHBOARD_TAB_CHANGE | Fired when the user switches dashboard tabs | { tab: 'basicDetails' \| 'jobAndPay' \| 'taxes' \| 'documents' } | -| EMPLOYEE_DISMISS | Fired when the user dismisses the top-of-dashboard success alert | None | -| CANCEL | Fired when the user cancels an in-flight edit and returns to the card view | None | +| Event type | Description | Data | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| EMPLOYEE_PROFILE_MANAGEMENT_EDIT_REQUESTED | Fired when the "Edit" CTA is clicked on the Basic details card | { employeeId: string } | +| EMPLOYEE_PROFILE_MANAGEMENT_UPDATED | Fired after the basic-details edit form is successfully saved | Updated `Employee` entity | +| EMPLOYEE_PROFILE_MANAGEMENT_EDIT_CANCELLED | Fired when the user clicks Cancel on the basic-details edit form | None | +| EMPLOYEE_PROFILE_MANAGEMENT_ALERT_DISMISSED | Fired when the user dismisses the in-card "Profile updated" alert (standalone block path) | None | +| EMPLOYEE_HOME_ADDRESS | Fired when the "Manage" CTA is clicked on the Home address card | { employeeId: string } | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED | Fired when the "Manage" CTA is clicked on the Work address card | { employeeId: string } | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_CREATED | Fired after a new work address is added on the work-address edit screen | Created `EmployeeWorkAddress` entity | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED | Fired after a work address is updated on the work-address edit screen | Updated `EmployeeWorkAddress` entity | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED | Fired after a work address is deleted on the work-address edit screen | Deleted `EmployeeWorkAddress` entity | +| EMPLOYEE_COMPENSATION_CREATE | Fired when the "Edit" CTA is clicked for an existing job (single- or multi-job views) | { employeeId: string, job: Job } | +| EMPLOYEE_JOB_ADD | Fired when the "Add job" / "Add another job" CTA is clicked | { employeeId: string } | +| EMPLOYEE_JOB_ADD_ANOTHER | Fired when "Add another job" is selected in the multi-job view | { employeeId: string } | +| EMPLOYEE_JOB_DELETED | Fired after a non-primary job is deleted from the multi-job table | Response from the Delete a job endpoint | +| EMPLOYEE_COMPENSATION_UPDATED | Fired after an Add / Add-another-job submission succeeds; surfaces the "Job added" alert | Response from the Update a compensation endpoint | +| EMPLOYEE_COMPENSATION_DONE | Fired after an Edit-compensation submission succeeds | None | +| EMPLOYEE_BANK_ACCOUNT_CREATE | Fired when the "Add bank account" CTA is clicked | { employeeId: string } | +| EMPLOYEE_BANK_ACCOUNT_CREATED | Fired after a bank account is successfully created; surfaces the "Bank account added" alert | Response from the Create a bank account endpoint | +| EMPLOYEE_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_SPLIT_PAYCHECK | Fired when the "Split paycheck" CTA is clicked | { employeeId: string } | +| EMPLOYEE_PAYMENT_METHOD_UPDATED | 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_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 } | +| EMPLOYEE_VIEW_FORM_TO_SIGN | Fired when a document row's "View" CTA is clicked | { employeeId: string, formId: string } | +| EMPLOYEE_DASHBOARD_TAB_CHANGE | Fired when the user switches dashboard tabs | { tab: 'basicDetails' \| 'jobAndPay' \| 'taxes' \| 'documents' } | +| EMPLOYEE_DISMISS | Fired when the user dismisses the top-of-dashboard success alert | None | +| CANCEL | Fired when the user cancels an in-flight edit and returns to the card view | None | ### EmployeeManagement.Profile @@ -320,3 +325,121 @@ function MyHomeAddressPanel({ employeeId }) { | EMPLOYEE_HOME_ADDRESS_MANAGEMENT_UPDATED | Fired after an existing home address is successfully updated | Updated `EmployeeAddress` entity | | EMPLOYEE_HOME_ADDRESS_MANAGEMENT_DELETED | Fired after a non-active home address is successfully deleted | Deleted `EmployeeAddress` entity | | EMPLOYEE_HOME_ADDRESS_MANAGEMENT_EDIT_CANCELLED | Fired when the user clicks Back on the manage screen | None | + +### EmployeeManagement.WorkAddress + +A self-contained block for viewing and managing an employee's work addresses — the same "Work address" 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 current work address. Clicking the card's "Manage" CTA swaps the card view for an edit screen where the current address can be edited, a new address added, or an existing address deleted. The edit screen is modal-style: editing, changing, or deleting a row closes the modal but keeps the user on the edit screen so additional addresses can be managed in one sitting. Clicking Back returns to the card view. + +```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.WorkAddress` — 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_WORK_ADDRESS_EDIT_REQUESTED | Fired when the user clicks the "Manage" CTA on the card; the block swaps to the edit screen | { employeeId: string } | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_CREATED | Fired after a new work address is added; the block stays on the edit screen | Created `EmployeeWorkAddress` entity | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED | Fired after a work address is updated; the block stays on the edit screen | Updated `EmployeeWorkAddress` entity | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED | Fired after a work address is deleted; the block stays on the edit screen | Deleted `EmployeeWorkAddress` entity | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED | Fired when the user clicks Back on the edit screen; the block returns to the card view | None | + +#### Composing from EmployeeManagement.WorkAddressCard and EmployeeManagement.WorkAddressEditForm directly + +`EmployeeManagement.WorkAddress` above is the recommended entry point for the work-address experience — it bundles the card, the edit screen, and the swap between them 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 edit surface needs to render in a modal or drawer, when the card needs to appear read-only with no manage affordance, or when the swap is driven by a router. Using them directly means owning the swap and any cross-component state yourself. + +`EmployeeManagement.WorkAddressCard` renders the read-only work-address card and emits a single event when its "Manage" CTA is clicked. `EmployeeManagement.WorkAddressEditForm` renders the corresponding edit screen and emits an event on each create, update, and delete, plus one on Back. Only the Back event (`EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED`) is meant to return to the card — the create/update/delete events keep the user on the edit screen so additional addresses can be managed, and are surfaced for your own use (e.g. a confirmation message). 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 MyWorkAddressPanel({ employeeId }) { + const [isEditing, setIsEditing] = useState(false) + + if (isEditing) { + return ( + { + if (eventType === componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED) { + setIsEditing(false) + } else if ( + eventType === componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_CREATED || + eventType === componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED || + eventType === componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED + ) { + // These keep the user on the edit screen so additional addresses + // can be managed; handle them here to surface your own confirmation. + } + }} + /> + ) + } + + return ( + { + if (eventType === componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED) { + setIsEditing(true) + } + }} + /> + ) +} +``` + +##### EmployeeManagement.WorkAddressCard + +**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_WORK_ADDRESS_EDIT_REQUESTED | Fired when the user clicks the "Manage" CTA on the card | { employeeId: string } | + +##### EmployeeManagement.WorkAddressEditForm + +**Props** + +| Name | Type | Description | +| ------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| employeeId Required | string | The associated employee identifier. | +| onEvent Required | function | See events table for available events. | +| className | string | Optional class applied to the form's root section element. | +| dictionary | object | Optional translations for component text. Keys are namespaced under `Employee.Management.WorkAddress` — 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_WORK_ADDRESS_CREATED | Fired after a new work address is added | Created `EmployeeWorkAddress` entity | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED | Fired after a work address is updated | Updated `EmployeeWorkAddress` entity | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED | Fired after a work address is deleted | Deleted `EmployeeWorkAddress` entity | +| EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED | Fired when the user clicks Back on the edit screen | None | diff --git a/sdk-app/src/generated-registry-data.ts b/sdk-app/src/generated-registry-data.ts index 586138a0e..db130f528 100644 --- a/sdk-app/src/generated-registry-data.ts +++ b/sdk-app/src/generated-registry-data.ts @@ -50,6 +50,8 @@ export const ENTITY_REQUIREMENTS: Record = { 'EmployeeManagement.TerminationFlow': ['companyId', 'employeeId'], 'EmployeeManagement.TerminationSummary': ['employeeId', 'companyId'], 'EmployeeManagement.WorkAddress': ['employeeId'], + 'EmployeeManagement.WorkAddressCard': ['employeeId'], + 'EmployeeManagement.WorkAddressEditForm': ['employeeId'], 'EmployeeOnboarding.Compensation': ['employeeId'], 'EmployeeOnboarding.Deductions': ['employeeId'], 'EmployeeOnboarding.DocumentSigner': ['employeeId'], diff --git a/src/components/Employee/Dashboard/BasicDetailsView.stories.tsx b/src/components/Employee/Dashboard/BasicDetailsView.stories.tsx index 3e48be02f..2d86cc173 100644 --- a/src/components/Employee/Dashboard/BasicDetailsView.stories.tsx +++ b/src/components/Employee/Dashboard/BasicDetailsView.stories.tsx @@ -1,5 +1,4 @@ import { fn } from 'storybook/test' -import type { EmployeeWorkAddress } from '@gusto/embedded-api-v-2025-11-15/models/components/employeeworkaddress' import { BasicDetailsView } from './BasicDetailsView' import { BaseComponent } from '@/components/Base' @@ -15,37 +14,7 @@ export default { } const onEvent = fn().mockName('onEvent') -const onManageWorkAddress = fn().mockName('onManageWorkAddress') const EMPLOYEE_ID = 'employee-123' -const workAddress: EmployeeWorkAddress = { - uuid: 'work-address-1', - version: '1', - country: 'USA', - street1: '2216 Icie Villages', - city: 'Big Delta', - state: 'AK', - zip: '99737', -} - -export const Loading = () => ( - -) - -export const WithAllDetails = () => ( - -) - -export const WithoutAddresses = () => ( - -) +export const Default = () => diff --git a/src/components/Employee/Dashboard/BasicDetailsView.tsx b/src/components/Employee/Dashboard/BasicDetailsView.tsx index 9a0fa5c27..2ae523414 100644 --- a/src/components/Employee/Dashboard/BasicDetailsView.tsx +++ b/src/components/Employee/Dashboard/BasicDetailsView.tsx @@ -1,109 +1,29 @@ -import { useTranslation } from 'react-i18next' -import type { EmployeeWorkAddress } from '@gusto/embedded-api-v-2025-11-15/models/components/employeeworkaddress' -import { useEmployeeBasicDetails } from './hooks' import { ProfileCard } from '@/components/Employee/Profile/management/ProfileCard' import { HomeAddressCard } from '@/components/Employee/HomeAddress/management/HomeAddressCard' +import { WorkAddressCard } from '@/components/Employee/WorkAddress/management/WorkAddressCard' import { Flex } from '@/components/Common/Flex/Flex' -import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' -import { Loading } from '@/components/Common' -import { BaseLayout } from '@/components/Base/Base' import type { OnEventType } from '@/components/Base/useBase' import type { EventType } from '@/shared/constants' export interface BasicDetailsViewProps { employeeId: string onEvent: OnEventType - currentWorkAddress?: EmployeeWorkAddress - /** Loads the work address card. */ - isLoading?: boolean - isWorkAddressLoading?: boolean - onManageWorkAddress?: () => void -} - -export interface BasicDetailsViewWithDataProps { - employeeId: string - onEvent: OnEventType - onManageWorkAddress?: () => void } /** - * Tab-mounted container for the Basic details tab. Owns the - * `useEmployeeBasicDetails` fetch for the work address card. The - * basic-details card is self-fetching via ``, and the - * home address card is self-fetching via ``, so - * this container only threads work-address data through. + * Tab-mounted container for the Basic details tab. After the Profile, + * Home address, and Work address cards moved to standalone + * self-fetching surfaces, this container is just a layout wrapper — + * each card owns its own data fetch and event emission. */ -export function BasicDetailsViewWithData({ - employeeId, - onEvent, - onManageWorkAddress, -}: BasicDetailsViewWithDataProps) { - const basicDetails = useEmployeeBasicDetails({ employeeId }) - - return ( - - - - ) -} - -export function BasicDetailsView({ - employeeId, - onEvent, - currentWorkAddress, - isLoading = false, - isWorkAddressLoading = isLoading, - onManageWorkAddress, -}: BasicDetailsViewProps) { - const { t } = useTranslation('Employee.Dashboard') - const Components = useComponentContext() - +export function BasicDetailsView({ employeeId, onEvent }: BasicDetailsViewProps) { return ( - - {t('workAddress.manageCta')} - - } - /> - } - > - - {isWorkAddressLoading ? ( - - ) : currentWorkAddress ? ( - - - {currentWorkAddress.street1} - {currentWorkAddress.street2 ? `, ${currentWorkAddress.street2}` : ''} - - - {currentWorkAddress.city}, {currentWorkAddress.state} {currentWorkAddress.zip} - - - ) : ( - {t('workAddress.noAddress')} - )} - - + ) } diff --git a/src/components/Employee/Dashboard/Dashboard.test.tsx b/src/components/Employee/Dashboard/Dashboard.test.tsx index b9659eef4..d92579fc1 100644 --- a/src/components/Employee/Dashboard/Dashboard.test.tsx +++ b/src/components/Employee/Dashboard/Dashboard.test.tsx @@ -298,7 +298,7 @@ describe('Dashboard', () => { ) }) - it('emits EMPLOYEE_WORK_ADDRESS event when clicking manage work address', async () => { + it('emits the scoped EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED event when clicking manage work address', async () => { const user = userEvent.setup() renderWithProviders() @@ -309,11 +309,15 @@ describe('Dashboard', () => { .getByRole('heading', { name: 'Work address' }) .closest('[data-testid="data-box"]') assertDefined(workAddressBox) + await waitFor(() => { + expect(within(workAddressBox).getByRole('button', { name: 'Manage' })).toBeEnabled() + }) await user.click(within(workAddressBox).getByRole('button', { name: 'Manage' })) - expect(onEvent).toHaveBeenCalledWith(componentEvents.EMPLOYEE_WORK_ADDRESS, { - employeeId: 'employee-123', - }) + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED, + { employeeId: 'employee-123' }, + ) }) it('shows an empty Compensation card with Add job CTA when the employee has no jobs', async () => { @@ -1831,6 +1835,9 @@ describe('Dashboard', () => { .getByRole('heading', { name: 'State taxes' }) .closest('[data-testid="data-box"]') assertDefined(stateTaxesBox) + await waitFor(() => { + expect(within(stateTaxesBox).getByRole('button', { name: 'Edit' })).toBeEnabled() + }) await user.click(within(stateTaxesBox).getByRole('button', { name: 'Edit' })) expect(onEvent).toHaveBeenCalledWith(componentEvents.EMPLOYEE_STATE_TAXES_EDIT, { diff --git a/src/components/Employee/Dashboard/Dashboard.tsx b/src/components/Employee/Dashboard/Dashboard.tsx index e619ed7f6..9fe95eedf 100644 --- a/src/components/Employee/Dashboard/Dashboard.tsx +++ b/src/components/Employee/Dashboard/Dashboard.tsx @@ -4,7 +4,7 @@ import { useEmployeesGetSuspense } from '@gusto/embedded-api-v-2025-11-15/react- 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 { BasicDetailsViewWithData } from './BasicDetailsView' +import { BasicDetailsView } from './BasicDetailsView' import { JobAndPayView } from './JobAndPayView' import { TaxesViewWithData } from './TaxesView' import { DocumentsViewWithData } from './DocumentsView' @@ -39,10 +39,6 @@ function DashboardRoot({ const [internalTab, setInternalTab] = useState('basicDetails') const selectedTab = controlledTab ?? internalTab - const handleManageWorkAddress = useCallback(() => { - onEvent(componentEvents.EMPLOYEE_WORK_ADDRESS, { employeeId }) - }, [onEvent, employeeId]) - const handleEditCompensation = useCallback( (job: Job) => { onEvent(componentEvents.EMPLOYEE_COMPENSATION_CREATE, { employeeId, job }) @@ -131,11 +127,7 @@ function DashboardRoot({ {selectedTab === 'basicDetails' && ( }> - + )} diff --git a/src/components/Employee/Dashboard/DashboardComponents.tsx b/src/components/Employee/Dashboard/DashboardComponents.tsx index 7b8eba5c5..b37733720 100644 --- a/src/components/Employee/Dashboard/DashboardComponents.tsx +++ b/src/components/Employee/Dashboard/DashboardComponents.tsx @@ -3,7 +3,7 @@ import type { Job } from '@gusto/embedded-api-v-2025-11-15/models/components/job import { Dashboard, type DashboardTab } from './Dashboard' import { getPendingCompensationChanges } from './getPendingCompensationChanges' import { HomeAddressEditForm } from '@/components/Employee/HomeAddress/management/HomeAddressEditForm' -import { WorkAddress } from '@/components/Employee/WorkAddress/management/WorkAddress' +import { WorkAddressEditForm } from '@/components/Employee/WorkAddress/management/WorkAddressEditForm' import { FederalTaxes } from '@/components/Employee/FederalTaxes/management/FederalTaxes' import { StateTaxes } from '@/components/Employee/StateTaxes/management/StateTaxes' import { ProfileEditForm } from '@/components/Employee/Profile/management/ProfileEditForm' @@ -93,7 +93,7 @@ export function HomeAddressContextual() { export function WorkAddressContextual() { const { employeeId, onEvent } = useFlow() - return + return } export function FederalTaxesContextual() { diff --git a/src/components/Employee/Dashboard/dashboardStateMachine.ts b/src/components/Employee/Dashboard/dashboardStateMachine.ts index 95f05ae10..0d5b1e9e4 100644 --- a/src/components/Employee/Dashboard/dashboardStateMachine.ts +++ b/src/components/Employee/Dashboard/dashboardStateMachine.ts @@ -74,7 +74,7 @@ export const dashboardStateMachine = { ), ), transition( - componentEvents.EMPLOYEE_WORK_ADDRESS, + componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED, 'workAddress', reduce( (ctx: DashboardContextInterface): DashboardContextInterface => ({ @@ -260,7 +260,13 @@ export const dashboardStateMachine = { returnToIndex, ), ), - workAddress: state(transition(componentEvents.CANCEL, 'index', returnToIndex)), + workAddress: state( + transition( + componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED, + 'index', + returnToIndex, + ), + ), federalTaxes: state( transition(componentEvents.CANCEL, 'index', returnToIndex), transition(componentEvents.EMPLOYEE_FEDERAL_TAXES_DONE, 'index', returnToIndex), diff --git a/src/components/Employee/Dashboard/hooks/index.ts b/src/components/Employee/Dashboard/hooks/index.ts index 455e3096c..728298736 100644 --- a/src/components/Employee/Dashboard/hooks/index.ts +++ b/src/components/Employee/Dashboard/hooks/index.ts @@ -1,9 +1,3 @@ -export { useEmployeeBasicDetails } from './useEmployeeBasicDetails' -export type { - UseEmployeeBasicDetailsProps, - UseEmployeeBasicDetailsResult, -} from './useEmployeeBasicDetails' - export { useEmployeeCompensation } from './useEmployeeCompensation' export type { UseEmployeeCompensationProps, diff --git a/src/components/Employee/Dashboard/hooks/useEmployeeBasicDetails.test.tsx b/src/components/Employee/Dashboard/hooks/useEmployeeBasicDetails.test.tsx deleted file mode 100644 index 127b94ca6..000000000 --- a/src/components/Employee/Dashboard/hooks/useEmployeeBasicDetails.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { renderHook, waitFor } from '@testing-library/react' -import { describe, it, expect, beforeEach } from 'vitest' -import { http, HttpResponse } from 'msw' -import { useEmployeeBasicDetails } from './useEmployeeBasicDetails' -import { GustoTestProvider } from '@/test/GustoTestApiProvider' -import { server } from '@/test/mocks/server' -import { setupApiTestMocks } from '@/test/mocks/apiServer' -import { API_BASE_URL } from '@/test/constants' - -describe('useEmployeeBasicDetails', () => { - beforeEach(() => { - setupApiTestMocks() - }) - - it('starts loading with the work-address flag true and resolves to populated data', async () => { - const { result } = renderHook(() => useEmployeeBasicDetails({ employeeId: 'employee-123' }), { - wrapper: GustoTestProvider, - }) - - expect(result.current.status.isWorkAddressLoading).toBe(true) - expect(result.current.data.currentWorkAddress).toBeUndefined() - - await waitFor(() => { - expect(result.current.status.isWorkAddressLoading).toBe(false) - }) - - expect(result.current.data.currentWorkAddress).toBeDefined() - }) - - it('picks the active work address out of the returned list', async () => { - server.use( - http.get(`${API_BASE_URL}/v1/employees/:employee_id/work_addresses`, () => - HttpResponse.json([ - { - uuid: 'work-inactive', - version: '1', - street_1: '1 Old Way', - city: 'Oldville', - state: 'CA', - zip: '90100', - active: false, - }, - { - uuid: 'work-active', - version: '1', - street_1: '500 New Ln', - city: 'Newville', - state: 'CA', - zip: '90200', - active: true, - }, - ]), - ), - ) - - const { result } = renderHook(() => useEmployeeBasicDetails({ employeeId: 'employee-123' }), { - wrapper: GustoTestProvider, - }) - - await waitFor(() => { - expect(result.current.status.isWorkAddressLoading).toBe(false) - }) - - expect(result.current.data.currentWorkAddress).toMatchObject({ - uuid: 'work-active', - active: true, - }) - }) - - it('returns undefined currentWorkAddress when none in the list are active', async () => { - server.use( - http.get(`${API_BASE_URL}/v1/employees/:employee_id/work_addresses`, () => - HttpResponse.json([]), - ), - ) - - const { result } = renderHook(() => useEmployeeBasicDetails({ employeeId: 'employee-123' }), { - wrapper: GustoTestProvider, - }) - - await waitFor(() => { - expect(result.current.status.isWorkAddressLoading).toBe(false) - }) - - expect(result.current.data.currentWorkAddress).toBeUndefined() - }) - - it('surfaces a work-address query failure through errorHandling.errors', async () => { - server.use( - http.get(`${API_BASE_URL}/v1/employees/:employee_id/work_addresses`, () => - HttpResponse.json( - { errors: [{ category: 'server_error', message: 'Boom' }] }, - { status: 500 }, - ), - ), - ) - - const { result } = renderHook(() => useEmployeeBasicDetails({ employeeId: 'employee-123' }), { - wrapper: GustoTestProvider, - }) - - await waitFor(() => { - expect(result.current.errorHandling.errors.length).toBeGreaterThan(0) - }) - }) -}) diff --git a/src/components/Employee/Dashboard/hooks/useEmployeeBasicDetails.tsx b/src/components/Employee/Dashboard/hooks/useEmployeeBasicDetails.tsx deleted file mode 100644 index cd97ac61f..000000000 --- a/src/components/Employee/Dashboard/hooks/useEmployeeBasicDetails.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useMemo } from 'react' -import { useEmployeeAddressesGetWorkAddresses } from '@gusto/embedded-api-v-2025-11-15/react-query/employeeAddressesGetWorkAddresses' -import type { EmployeeWorkAddress } from '@gusto/embedded-api-v-2025-11-15/models/components/employeeworkaddress' -import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler' -import type { BaseHookReady } from '@/partner-hook-utils/types' - -export interface UseEmployeeBasicDetailsProps { - employeeId: string -} - -export type UseEmployeeBasicDetailsResult = BaseHookReady< - { - currentWorkAddress?: EmployeeWorkAddress - }, - { - isPending: boolean - isWorkAddressLoading: boolean - } -> - -/** - * Tab-mounted hook for the Basic details tab. After the Profile and - * Home address cards moved to standalone self-fetching surfaces, this - * hook only feeds the inline Work address card. It will be deleted - * once the Work address card migrates to its own self-fetching block. - */ -export function useEmployeeBasicDetails({ - employeeId, -}: UseEmployeeBasicDetailsProps): UseEmployeeBasicDetailsResult { - // staleTime: Infinity — the SDK QueryClient invalidates on any mutation - // success, so individual hooks don't need their own refetch policy. - const workAddressesQuery = useEmployeeAddressesGetWorkAddresses( - { employeeId }, - { staleTime: Infinity }, - ) - - const employeeWorkAddressesList = workAddressesQuery.data?.employeeWorkAddressesList - - const currentWorkAddress = useMemo(() => { - return employeeWorkAddressesList?.find(address => address.active) - }, [employeeWorkAddressesList]) - - return { - isLoading: false, - data: { - currentWorkAddress, - }, - status: { - isPending: workAddressesQuery.isFetching, - isWorkAddressLoading: workAddressesQuery.isLoading, - }, - errorHandling: composeErrorHandler([workAddressesQuery]), - } -} diff --git a/src/components/Employee/WorkAddress/management/WorkAddress.test.tsx b/src/components/Employee/WorkAddress/management/WorkAddress.test.tsx new file mode 100644 index 000000000..1e2c92475 --- /dev/null +++ b/src/components/Employee/WorkAddress/management/WorkAddress.test.tsx @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { WorkAddress } from './WorkAddress' +import { renderWithProviders } from '@/test-utils/renderWithProviders' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { componentEvents } from '@/shared/constants' + +describe('WorkAddress (management block)', () => { + const onEvent = vi.fn() + + beforeEach(() => { + setupApiTestMocks() + onEvent.mockClear() + }) + + it('renders the card initially with the work-address title and Manage button', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Manage' })).toBeEnabled() + }) + + expect(screen.getByText('Work address')).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Back' })).toBeNull() + }) + + it('transitions card → editWorkAddress when the Manage button is clicked', async () => { + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Manage' })).toBeEnabled() + }) + + await user.click(screen.getByRole('button', { name: 'Manage' })) + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument() + }) + + expect(screen.getByRole('heading', { level: 1, name: 'Work address' })).toBeInTheDocument() + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED, + { employeeId: 'employee-123' }, + ) + }) + + it('returns to the card when the Back button is clicked from the edit screen', async () => { + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Manage' })).toBeEnabled() + }) + await user.click(screen.getByRole('button', { name: 'Manage' })) + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: 'Back' })) + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Manage' })).toBeInTheDocument() + }) + + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED, + undefined, + ) + }) +}) diff --git a/src/components/Employee/WorkAddress/management/WorkAddress.tsx b/src/components/Employee/WorkAddress/management/WorkAddress.tsx index 2d66ae0ea..55ef62757 100644 --- a/src/components/Employee/WorkAddress/management/WorkAddress.tsx +++ b/src/components/Employee/WorkAddress/management/WorkAddress.tsx @@ -1,76 +1,54 @@ -import type { EmployeeWorkAddress } from '@gusto/embedded-api-v-2025-11-15/models/components/employeeworkaddress' -import { WorkAddressView } from './WorkAddressView' +import { createMachine } from 'robot3' +import { useMemo } from 'react' import { - isUseWorkAddressManagementSuccess, - useWorkAddressManagement, -} from './useWorkAddressManagement' + WorkAddressCardContextual, + type WorkAddressContextInterface, +} from './WorkAddressComponents' +import { workAddressStateMachine } from './workAddressStateMachine' +import { Flow } from '@/components/Flow/Flow' import { BaseBoundaries, - BaseLayout, type BaseComponentInterface, type CommonComponentInterface, -} from '@/components/Base/Base' -import { useI18n, useComponentDictionary } from '@/i18n' -import type { HookSubmitResult } from '@/partner-hook-utils/types' -import { componentEvents } from '@/shared/constants' +} 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 WorkAddressProps extends CommonComponentInterface<'Employee.WorkAddress.Management'> { +export interface WorkAddressProps extends CommonComponentInterface<'Employee.Management.WorkAddress'> { employeeId: string - onEvent: BaseComponentInterface['onEvent'] + onEvent: OnEventType } -function WorkAddressRoot({ employeeId, dictionary, onEvent }: WorkAddressProps) { - useI18n(['Employee.WorkAddress.Management']) - useComponentDictionary('Employee.WorkAddress.Management', dictionary) - - const management = useWorkAddressManagement({ employeeId, onEvent }) - - if (management.isLoading) { - return - } - - if (!isUseWorkAddressManagementSuccess(management)) { - return - } - - const handleWorkAddressSaved = (result: HookSubmitResult) => { - if (result.mode === 'create') { - onEvent(componentEvents.EMPLOYEE_WORK_ADDRESS_CREATED, result.data) - } else { - onEvent(componentEvents.EMPLOYEE_WORK_ADDRESS_UPDATED, result.data) - } - } - - return ( - - { - onEvent(componentEvents.CANCEL) - }} - isDeletePending={management.status.isDeletePending} - /> - +function WorkAddressFlow({ employeeId, onEvent }: WorkAddressProps) { + useI18n('Employee.Management.WorkAddress') + + const machine = useMemo( + () => + createMachine('card', workAddressStateMachine, (ctx: WorkAddressContextInterface) => ({ + ...ctx, + component: WorkAddressCardContextual, + employeeId, + })), + [employeeId], ) + + return } export function WorkAddress({ + dictionary, FallbackComponent, ...props -}: WorkAddressProps & BaseComponentInterface) { +}: WorkAddressProps & BaseComponentInterface<'Employee.Management.WorkAddress'>) { + useComponentDictionary('Employee.Management.WorkAddress', dictionary) return ( - + ) } diff --git a/src/components/Employee/WorkAddress/management/WorkAddressCard/WorkAddressCard.test.tsx b/src/components/Employee/WorkAddress/management/WorkAddressCard/WorkAddressCard.test.tsx new file mode 100644 index 000000000..e0713132f --- /dev/null +++ b/src/components/Employee/WorkAddress/management/WorkAddressCard/WorkAddressCard.test.tsx @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { http, HttpResponse } from 'msw' +import { WorkAddressCard } from './WorkAddressCard' +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' + +describe('WorkAddressCard', () => { + const onEvent = vi.fn() + + beforeEach(() => { + setupApiTestMocks() + onEvent.mockClear() + }) + + it('renders the active work address and an enabled Manage button once the data loads', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Manage' })).toBeEnabled() + }) + + expect(screen.getByText('Work address')).toBeInTheDocument() + expect(screen.getByText(/2216 Icie Villages, Apt\. 798/)).toBeInTheDocument() + expect(screen.getByText(/Big Delta, AK 99737/)).toBeInTheDocument() + }) + + it('shows the empty-state copy when no active work address is on file', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/work_addresses`, () => + HttpResponse.json([]), + ), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Manage' })).toBeEnabled() + }) + + expect(screen.getByText('No work address on file')).toBeInTheDocument() + }) + + it('fires EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED with { employeeId } when Manage is clicked', async () => { + const user = userEvent.setup() + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Manage' })).toBeEnabled() + }) + + await user.click(screen.getByRole('button', { name: 'Manage' })) + + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED, + { employeeId: 'employee-123' }, + ) + }) +}) diff --git a/src/components/Employee/WorkAddress/management/WorkAddressCard/WorkAddressCard.tsx b/src/components/Employee/WorkAddress/management/WorkAddressCard/WorkAddressCard.tsx new file mode 100644 index 000000000..86b0c7ca5 --- /dev/null +++ b/src/components/Employee/WorkAddress/management/WorkAddressCard/WorkAddressCard.tsx @@ -0,0 +1,72 @@ +import { useTranslation } from 'react-i18next' +import { useEmployeeWorkAddressSummary } from '../../shared/useEmployeeWorkAddressSummary' +import { Loading } from '@/components/Common' +import { Flex } from '@/components/Common/Flex/Flex' +import { BaseLayout } from '@/components/Base/Base' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useI18n } from '@/i18n' +import { componentEvents, type EventType } from '@/shared/constants' +import type { OnEventType } from '@/components/Base/useBase' + +export interface WorkAddressCardProps { + employeeId: string + onEvent: OnEventType +} + +/** + * Standalone "Work address" card. Owns its own data fetch via + * `useEmployeeWorkAddressSummary` and emits + * `EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED` when the Manage button + * is clicked. The card has no alert API — alert rendering is the + * orchestrator's responsibility (block's `WorkAddressCardContextual` for + * standalone consumption, dashboard chrome for dashboard consumption). + */ +export function WorkAddressCard({ employeeId, onEvent }: WorkAddressCardProps) { + useI18n('Employee.Management.WorkAddress') + const { t } = useTranslation('Employee.Management.WorkAddress') + const Components = useComponentContext() + + const summary = useEmployeeWorkAddressSummary({ employeeId }) + + const isLoading = summary.isLoading + const currentWorkAddress = summary.isLoading ? undefined : summary.data.currentWorkAddress + + const handleEdit = () => { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED, { employeeId }) + } + + return ( + + + {t('cardManageCta')} + + } + /> + } + > + + {isLoading ? ( + + ) : currentWorkAddress ? ( + + + {currentWorkAddress.street1} + {currentWorkAddress.street2 ? `, ${currentWorkAddress.street2}` : ''} + + + {currentWorkAddress.city}, {currentWorkAddress.state} {currentWorkAddress.zip} + + + ) : ( + {t('cardNoAddress')} + )} + + + + ) +} diff --git a/src/components/Employee/WorkAddress/management/WorkAddressCard/index.ts b/src/components/Employee/WorkAddress/management/WorkAddressCard/index.ts new file mode 100644 index 000000000..7c563eeaa --- /dev/null +++ b/src/components/Employee/WorkAddress/management/WorkAddressCard/index.ts @@ -0,0 +1,2 @@ +export { WorkAddressCard } from './WorkAddressCard' +export type { WorkAddressCardProps } from './WorkAddressCard' diff --git a/src/components/Employee/WorkAddress/management/WorkAddressComponents.tsx b/src/components/Employee/WorkAddress/management/WorkAddressComponents.tsx new file mode 100644 index 000000000..dfb959fb6 --- /dev/null +++ b/src/components/Employee/WorkAddress/management/WorkAddressComponents.tsx @@ -0,0 +1,18 @@ +import { WorkAddressCard } from './WorkAddressCard' +import { WorkAddressEditForm } from './WorkAddressEditForm' +import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow' +import { ensureRequired } from '@/helpers/ensureRequired' + +export interface WorkAddressContextInterface extends FlowContextInterface { + employeeId?: string +} + +export function WorkAddressCardContextual() { + const { employeeId, onEvent } = useFlow() + return +} + +export function WorkAddressEditFormContextual() { + const { employeeId, onEvent } = useFlow() + return +} diff --git a/src/components/Employee/WorkAddress/management/WorkAddressEditForm.tsx b/src/components/Employee/WorkAddress/management/WorkAddressEditForm.tsx new file mode 100644 index 000000000..906851d72 --- /dev/null +++ b/src/components/Employee/WorkAddress/management/WorkAddressEditForm.tsx @@ -0,0 +1,76 @@ +import type { EmployeeWorkAddress } from '@gusto/embedded-api-v-2025-11-15/models/components/employeeworkaddress' +import { WorkAddressView } from './WorkAddressView' +import { + isUseWorkAddressManagementSuccess, + useWorkAddressManagement, +} from './useWorkAddressManagement' +import { + BaseBoundaries, + BaseLayout, + type BaseComponentInterface, + type CommonComponentInterface, +} from '@/components/Base/Base' +import { useI18n, useComponentDictionary } from '@/i18n' +import type { HookSubmitResult } from '@/partner-hook-utils/types' +import { componentEvents } from '@/shared/constants' + +export interface WorkAddressEditFormProps extends CommonComponentInterface<'Employee.Management.WorkAddress'> { + employeeId: string + onEvent: BaseComponentInterface['onEvent'] +} + +function WorkAddressEditFormRoot({ employeeId, dictionary, onEvent }: WorkAddressEditFormProps) { + useI18n(['Employee.Management.WorkAddress']) + useComponentDictionary('Employee.Management.WorkAddress', dictionary) + + const management = useWorkAddressManagement({ employeeId, onEvent }) + + if (management.isLoading) { + return + } + + if (!isUseWorkAddressManagementSuccess(management)) { + return + } + + const handleWorkAddressSaved = (result: HookSubmitResult) => { + if (result.mode === 'create') { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_CREATED, result.data) + } else { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED, result.data) + } + } + + return ( + + { + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED) + }} + isDeletePending={management.status.isDeletePending} + /> + + ) +} + +export function WorkAddressEditForm({ + FallbackComponent, + ...props +}: WorkAddressEditFormProps & BaseComponentInterface) { + return ( + + + + ) +} diff --git a/src/components/Employee/WorkAddress/management/WorkAddressView.tsx b/src/components/Employee/WorkAddress/management/WorkAddressView.tsx index fbc6dc590..b9e3ded92 100644 --- a/src/components/Employee/WorkAddress/management/WorkAddressView.tsx +++ b/src/components/Employee/WorkAddress/management/WorkAddressView.tsx @@ -81,7 +81,7 @@ export function WorkAddressView({ onBack, isDeletePending = false, }: WorkAddressViewProps) { - const { t } = useTranslation('Employee.WorkAddress.Management') + const { t } = useTranslation('Employee.Management.WorkAddress') const Components = useComponentContext() const [addressModal, setAddressModal] = useState<'edit' | 'create' | null>(null) const [deleteConfirmUuid, setDeleteConfirmUuid] = useState(null) diff --git a/src/components/Employee/WorkAddress/management/index.ts b/src/components/Employee/WorkAddress/management/index.ts new file mode 100644 index 000000000..f5f83d761 --- /dev/null +++ b/src/components/Employee/WorkAddress/management/index.ts @@ -0,0 +1,6 @@ +export { WorkAddress } from './WorkAddress' +export type { WorkAddressProps } from './WorkAddress' +export { WorkAddressCard } from './WorkAddressCard' +export type { WorkAddressCardProps } from './WorkAddressCard' +export { WorkAddressEditForm } from './WorkAddressEditForm' +export type { WorkAddressEditFormProps } from './WorkAddressEditForm' diff --git a/src/components/Employee/WorkAddress/management/useWorkAddressManagement.tsx b/src/components/Employee/WorkAddress/management/useWorkAddressManagement.tsx index 18cbce7ef..b7f54356f 100644 --- a/src/components/Employee/WorkAddress/management/useWorkAddressManagement.tsx +++ b/src/components/Employee/WorkAddress/management/useWorkAddressManagement.tsx @@ -109,7 +109,7 @@ export function useWorkAddressManagement({ baseSubmitHandler, error: rootSubmitError, setError: setRootSubmitError, - } = useBaseSubmit('Employee.WorkAddress.Management') + } = useBaseSubmit('Employee.Management.WorkAddress') const deleteWorkAddressMutation = useEmployeeAddressesDeleteWorkAddressMutation() const [editTargetUuid, setEditTargetUuid] = useState(undefined) @@ -219,7 +219,7 @@ export function useWorkAddressManagement({ }) succeeded = true if (snap) { - onEvent(componentEvents.EMPLOYEE_WORK_ADDRESS_DELETED, snap) + onEvent(componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED, snap) } }, ) diff --git a/src/components/Employee/WorkAddress/management/workAddressStateMachine.ts b/src/components/Employee/WorkAddress/management/workAddressStateMachine.ts new file mode 100644 index 000000000..936ab50b6 --- /dev/null +++ b/src/components/Employee/WorkAddress/management/workAddressStateMachine.ts @@ -0,0 +1,38 @@ +import { reduce, state, transition } from 'robot3' +import type { ComponentType } from 'react' +import type { WorkAddressContextInterface } from './WorkAddressComponents' +import { WorkAddressCardContextual, WorkAddressEditFormContextual } from './WorkAddressComponents' +import { componentEvents } from '@/shared/constants' +import type { MachineTransition } from '@/types/Helpers' + +const returnToCard = reduce( + (ctx: WorkAddressContextInterface): WorkAddressContextInterface => ({ + ...ctx, + component: WorkAddressCardContextual as ComponentType, + }), +) + +export const workAddressStateMachine = { + card: state( + transition( + componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED, + 'editWorkAddress', + reduce( + (ctx: WorkAddressContextInterface): WorkAddressContextInterface => ({ + ...ctx, + component: WorkAddressEditFormContextual as ComponentType, + }), + ), + ), + ), + // The edit surface is modal-style: editing/changing/deleting a row closes + // the modal but keeps the user on the edit screen so they can manage + // additional rows. Only the explicit Back action returns to the card. + editWorkAddress: state( + transition( + componentEvents.EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED, + 'card', + returnToCard, + ), + ), +} diff --git a/src/components/Employee/WorkAddress/shared/index.ts b/src/components/Employee/WorkAddress/shared/index.ts new file mode 100644 index 000000000..eee2dcf67 --- /dev/null +++ b/src/components/Employee/WorkAddress/shared/index.ts @@ -0,0 +1,6 @@ +export { useEmployeeWorkAddressSummary } from './useEmployeeWorkAddressSummary' +export type { + UseEmployeeWorkAddressSummaryParams, + UseEmployeeWorkAddressSummaryReady, + UseEmployeeWorkAddressSummaryResult, +} from './useEmployeeWorkAddressSummary' diff --git a/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/index.ts b/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/index.ts new file mode 100644 index 000000000..eee2dcf67 --- /dev/null +++ b/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/index.ts @@ -0,0 +1,6 @@ +export { useEmployeeWorkAddressSummary } from './useEmployeeWorkAddressSummary' +export type { + UseEmployeeWorkAddressSummaryParams, + UseEmployeeWorkAddressSummaryReady, + UseEmployeeWorkAddressSummaryResult, +} from './useEmployeeWorkAddressSummary' diff --git a/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/useEmployeeWorkAddressSummary.test.tsx b/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/useEmployeeWorkAddressSummary.test.tsx new file mode 100644 index 000000000..52e719110 --- /dev/null +++ b/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/useEmployeeWorkAddressSummary.test.tsx @@ -0,0 +1,93 @@ +import { renderHook, waitFor } from '@testing-library/react' +import { describe, it, expect, beforeEach } from 'vitest' +import { http, HttpResponse } from 'msw' +import { useEmployeeWorkAddressSummary } from './useEmployeeWorkAddressSummary' +import { GustoTestProvider } from '@/test/GustoTestApiProvider' +import { server } from '@/test/mocks/server' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { API_BASE_URL } from '@/test/constants' + +describe('useEmployeeWorkAddressSummary', () => { + beforeEach(() => { + setupApiTestMocks() + }) + + it('starts in the loading branch and resolves into the ready branch with the active work address', async () => { + const { result } = renderHook( + () => useEmployeeWorkAddressSummary({ employeeId: 'employee-123' }), + { wrapper: GustoTestProvider }, + ) + + expect(result.current.isLoading).toBe(true) + + await waitFor(() => { + expect(result.current.isLoading).toBe(false) + }) + + if (result.current.isLoading) return + + expect(result.current.data.currentWorkAddress).toMatchObject({ + active: true, + street1: '2216 Icie Villages', + city: 'Big Delta', + state: 'AK', + zip: '99737', + }) + expect(result.current.status).toMatchObject({ + isFetching: false, + isPending: false, + }) + }) + + it('returns currentWorkAddress = undefined when no row is active', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/work_addresses`, () => + HttpResponse.json([ + { + uuid: 'work-1', + version: '1', + street_1: '100 Old Way', + city: 'Oldville', + state: 'CA', + zip: '90100', + active: false, + }, + ]), + ), + ) + + const { result } = renderHook( + () => useEmployeeWorkAddressSummary({ employeeId: 'employee-123' }), + { wrapper: GustoTestProvider }, + ) + + await waitFor(() => { + expect(result.current.isLoading).toBe(false) + }) + + if (result.current.isLoading) return + expect(result.current.data.currentWorkAddress).toBeUndefined() + }) + + it('surfaces a query failure through errorHandling.errors', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/work_addresses`, () => + HttpResponse.json( + { errors: [{ category: 'server_error', message: 'Boom' }] }, + { status: 500 }, + ), + ), + ) + + const { result } = renderHook( + () => useEmployeeWorkAddressSummary({ employeeId: 'employee-123' }), + { wrapper: GustoTestProvider }, + ) + + await waitFor(() => { + expect(result.current.errorHandling.errors.length).toBeGreaterThan(0) + }) + + expect(result.current.isLoading).toBe(true) + }) +}) diff --git a/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/useEmployeeWorkAddressSummary.tsx b/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/useEmployeeWorkAddressSummary.tsx new file mode 100644 index 000000000..051371186 --- /dev/null +++ b/src/components/Employee/WorkAddress/shared/useEmployeeWorkAddressSummary/useEmployeeWorkAddressSummary.tsx @@ -0,0 +1,51 @@ +import { useEmployeeAddressesGetWorkAddresses } from '@gusto/embedded-api-v-2025-11-15/react-query/employeeAddressesGetWorkAddresses' +import type { EmployeeWorkAddress } from '@gusto/embedded-api-v-2025-11-15/models/components/employeeworkaddress' +import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler' +import type { BaseHookReady, HookLoadingResult } from '@/partner-hook-utils/types' + +export interface UseEmployeeWorkAddressSummaryParams { + employeeId: string +} + +export type UseEmployeeWorkAddressSummaryReady = BaseHookReady< + { currentWorkAddress: EmployeeWorkAddress | undefined }, + { isFetching: boolean; isPending: boolean } +> + +export type UseEmployeeWorkAddressSummaryResult = + | HookLoadingResult + | UseEmployeeWorkAddressSummaryReady + +/** + * Read-only data hook for the Work address management card. Wraps + * `useEmployeeAddressesGetWorkAddresses` and selects the active row, returning + * `BaseHookReady`-shaped data. Mutations live in the work-address forms hook. + */ +export function useEmployeeWorkAddressSummary({ + employeeId, +}: UseEmployeeWorkAddressSummaryParams): UseEmployeeWorkAddressSummaryResult { + const workAddressesQuery = useEmployeeAddressesGetWorkAddresses( + { employeeId }, + { staleTime: Infinity }, + ) + + const errorHandling = composeErrorHandler([workAddressesQuery]) + + const workAddressesList = workAddressesQuery.data?.employeeWorkAddressesList + + if (workAddressesQuery.isLoading || !workAddressesList) { + return { isLoading: true, errorHandling } + } + + const currentWorkAddress = workAddressesList.find(address => address.active) + + return { + isLoading: false, + data: { currentWorkAddress }, + status: { + isFetching: workAddressesQuery.isFetching, + isPending: false, + }, + errorHandling, + } +} diff --git a/src/components/Employee/exports/employeeManagement.ts b/src/components/Employee/exports/employeeManagement.ts index 29c975036..d300cb8fd 100644 --- a/src/components/Employee/exports/employeeManagement.ts +++ b/src/components/Employee/exports/employeeManagement.ts @@ -11,8 +11,12 @@ export type { HomeAddressCardProps, HomeAddressEditFormProps, } from '../HomeAddress/management' -export { WorkAddress } from '../WorkAddress/management/WorkAddress' -export type { WorkAddressProps } from '../WorkAddress/management/WorkAddress' +export { WorkAddress, WorkAddressCard, WorkAddressEditForm } from '../WorkAddress/management' +export type { + WorkAddressProps, + WorkAddressCardProps, + WorkAddressEditFormProps, +} from '../WorkAddress/management' export { FederalTaxes, type FederalTaxesProps } from '../FederalTaxes/management/FederalTaxes' export { StateTaxes, type StateTaxesProps } from '../StateTaxes/management/StateTaxes' export { Profile, ProfileCard, ProfileEditForm } from '../Profile/management' diff --git a/src/i18n/en/Employee.Dashboard.json b/src/i18n/en/Employee.Dashboard.json index 93821d98a..95839a350 100644 --- a/src/i18n/en/Employee.Dashboard.json +++ b/src/i18n/en/Employee.Dashboard.json @@ -9,12 +9,6 @@ "taxes": "Taxes", "documents": "Documents" }, - "workAddress": { - "title": "Work address", - "manageCta": "Manage", - "currentAddress": "Current address", - "noAddress": "No work address on file" - }, "jobAndPay": { "compensation": { "title": "Compensation", diff --git a/src/i18n/en/Employee.WorkAddress.Management.json b/src/i18n/en/Employee.Management.WorkAddress.json similarity index 96% rename from src/i18n/en/Employee.WorkAddress.Management.json rename to src/i18n/en/Employee.Management.WorkAddress.json index 58ce67340..55ce39fd1 100644 --- a/src/i18n/en/Employee.WorkAddress.Management.json +++ b/src/i18n/en/Employee.Management.WorkAddress.json @@ -1,4 +1,7 @@ { + "cardTitle": "Work address", + "cardManageCta": "Manage", + "cardNoAddress": "No work address on file", "title": "Work address", "description": "An employee's work address is the primary location where they perform their job. Keep it accurate for payroll and tax purposes.", "rowMenuAriaLabel": "Open work address row actions", diff --git a/src/shared/constants.ts b/src/shared/constants.ts index d8ad850ab..856f6d861 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -79,6 +79,11 @@ export const employeeEvents = { EMPLOYEE_PROFILE_MANAGEMENT_UPDATED: 'employee/profile/management/updated', EMPLOYEE_PROFILE_MANAGEMENT_EDIT_CANCELLED: 'employee/profile/management/editCancelled', EMPLOYEE_PROFILE_MANAGEMENT_ALERT_DISMISSED: 'employee/profile/management/alertDismissed', + EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_REQUESTED: 'employee/management/workAddress/editRequested', + EMPLOYEE_MANAGEMENT_WORK_ADDRESS_CREATED: 'employee/management/workAddress/created', + EMPLOYEE_MANAGEMENT_WORK_ADDRESS_UPDATED: 'employee/management/workAddress/updated', + EMPLOYEE_MANAGEMENT_WORK_ADDRESS_DELETED: 'employee/management/workAddress/deleted', + EMPLOYEE_MANAGEMENT_WORK_ADDRESS_EDIT_CANCELLED: 'employee/management/workAddress/editCancelled', } as const export const companyEvents = { diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index 7f7a351bb..724aae6cd 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -1438,12 +1438,6 @@ export interface EmployeeDashboard{ "taxes":string; "documents":string; }; -"workAddress":{ -"title":string; -"manageCta":string; -"currentAddress":string; -"noAddress":string; -}; "jobAndPay":{ "compensation":{ "title":string; @@ -2042,6 +2036,56 @@ export interface EmployeeLanding{ }; "getStartedCta":string; }; +export interface EmployeeManagementWorkAddress{ +"cardTitle":string; +"cardManageCta":string; +"cardNoAddress":string; +"title":string; +"description":string; +"rowMenuAriaLabel":string; +"rowEdit":string; +"rowDelete":string; +"currentSectionTitle":string; +"currentSince":string; +"currentEmpty":string; +"editCta":string; +"changeCta":string; +"changePendingTitle":string; +"changePendingPossessiveFallback":string; +"changePendingDescription":string; +"historySectionTitle":string; +"historyEmptyTitle":string; +"historyEmptyDescription":string; +"columns":{ +"location":string; +"startDate":string; +"endDate":string; +}; +"editModalDescription":string; +"changeModalDescription":string; +"editPastAddressAlertTitle":string; +"editModalTitle":string; +"changeModalTitle":string; +"form":{ +"editLocationLabel":string; +"editInactiveLocationLabel":string; +"editLocationDescription":string; +"newWorkAddressLabel":string; +"newWorkAddressDescription":string; +"selectPlaceholder":string; +"locationRequired":string; +"startDateLabel":string; +"startDateDescription":string; +"editInactiveStartDateDescription":string; +"startDateRequired":string; +}; +"submitCta":string; +"cancelCta":string; +"backCta":string; +"deleteModalTitle":string; +"deleteModalDescription":string; +"deleteModalConfirmCta":string; +}; export interface EmployeeManagementEmployeeList{ "title":string; "addEmployeeCta":string; @@ -2413,53 +2457,6 @@ export interface EmployeeTerminationsTerminationSummary{ "cancel":string; }; }; -export interface EmployeeWorkAddressManagement{ -"title":string; -"description":string; -"rowMenuAriaLabel":string; -"rowEdit":string; -"rowDelete":string; -"currentSectionTitle":string; -"currentSince":string; -"currentEmpty":string; -"editCta":string; -"changeCta":string; -"changePendingTitle":string; -"changePendingPossessiveFallback":string; -"changePendingDescription":string; -"historySectionTitle":string; -"historyEmptyTitle":string; -"historyEmptyDescription":string; -"columns":{ -"location":string; -"startDate":string; -"endDate":string; -}; -"editModalDescription":string; -"changeModalDescription":string; -"editPastAddressAlertTitle":string; -"editModalTitle":string; -"changeModalTitle":string; -"form":{ -"editLocationLabel":string; -"editInactiveLocationLabel":string; -"editLocationDescription":string; -"newWorkAddressLabel":string; -"newWorkAddressDescription":string; -"selectPlaceholder":string; -"locationRequired":string; -"startDateLabel":string; -"startDateDescription":string; -"editInactiveStartDateDescription":string; -"startDateRequired":string; -}; -"submitCta":string; -"cancelCta":string; -"backCta":string; -"deleteModalTitle":string; -"deleteModalDescription":string; -"deleteModalConfirmCta":string; -}; export interface InformationRequestsInformationRequestForm{ "title":string; "blockingAlert":{ @@ -3625,6 +3622,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.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, 'Employee.WorkAddress.Management': EmployeeWorkAddressManagement, '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.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