diff --git a/docs/reference/endpoint-inventory.json b/docs/reference/endpoint-inventory.json
index e4c00d3c0..6b4a9beaf 100644
--- a/docs/reference/endpoint-inventory.json
+++ b/docs/reference/endpoint-inventory.json
@@ -837,10 +837,6 @@
"method": "GET",
"path": "/v1/employees/:employeeId/jobs"
},
- {
- "method": "GET",
- "path": "/v1/employees/:employeeId/pay_stubs"
- },
{
"method": "GET",
"path": "/v1/employees/:employeeUuid/federal_taxes"
@@ -852,18 +848,13 @@
{
"method": "DELETE",
"path": "/v1/jobs/:jobId"
- },
- {
- "method": "GET",
- "path": "/v1/payrolls/:payrollId/employees/:employeeId/pay_stub"
}
],
"variables": [
"compensationId",
"employeeId",
"employeeUuid",
- "jobId",
- "payrollId"
+ "jobId"
]
},
"Employee.HomeAddress": {
@@ -1982,10 +1973,6 @@
"method": "GET",
"path": "/v1/employees/:employeeId/jobs"
},
- {
- "method": "GET",
- "path": "/v1/employees/:employeeId/pay_stubs"
- },
{
"method": "GET",
"path": "/v1/employees/:employeeUuid/federal_taxes"
@@ -1997,18 +1984,13 @@
{
"method": "DELETE",
"path": "/v1/jobs/:jobId"
- },
- {
- "method": "GET",
- "path": "/v1/payrolls/:payrollId/employees/:employeeId/pay_stub"
}
],
"variables": [
"compensationId",
"employeeId",
"employeeUuid",
- "jobId",
- "payrollId"
+ "jobId"
]
},
"EmployeeManagement.HomeAddress": {
@@ -2075,6 +2057,22 @@
"employeeId"
]
},
+ "EmployeeManagement.PaystubsCard": {
+ "endpoints": [
+ {
+ "method": "GET",
+ "path": "/v1/employees/:employeeId/pay_stubs"
+ },
+ {
+ "method": "GET",
+ "path": "/v1/payrolls/:payrollId/employees/:employeeId/pay_stub"
+ }
+ ],
+ "variables": [
+ "employeeId",
+ "payrollId"
+ ]
+ },
"EmployeeManagement.TerminateEmployee": {
"endpoints": [
{
@@ -2612,11 +2610,11 @@
"Employee.Compensation",
"Employee.Deductions",
"Employee.HomeAddress",
- "Employee.PaymentMethod",
"Employee.WorkAddress",
"EmployeeManagement.DocumentManager",
"EmployeeManagement.FederalTaxes",
"EmployeeManagement.PaymentMethod",
+ "EmployeeManagement.PaystubsCard",
"EmployeeManagement.Profile",
"EmployeeManagement.StateTaxes"
]
@@ -2668,11 +2666,11 @@
},
"EmployeeManagement.DashboardFlow": {
"blocks": [
- "Employee.PaymentMethod",
"EmployeeManagement.DocumentManager",
"EmployeeManagement.FederalTaxes",
"EmployeeManagement.HomeAddress",
"EmployeeManagement.PaymentMethod",
+ "EmployeeManagement.PaystubsCard",
"EmployeeManagement.Profile",
"EmployeeManagement.StateTaxes",
"EmployeeManagement.WorkAddress",
diff --git a/docs/reference/endpoint-reference.md b/docs/reference/endpoint-reference.md
index aa0f5c79b..0e18f5787 100644
--- a/docs/reference/endpoint-reference.md
+++ b/docs/reference/endpoint-reference.md
@@ -163,11 +163,9 @@ import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json'
| | GET | `/v1/employees/:employeeId` |
| | GET | `/v1/employees/:employeeId/forms` |
| | GET | `/v1/employees/:employeeId/jobs` |
-| | GET | `/v1/employees/:employeeId/pay_stubs` |
| | GET | `/v1/employees/:employeeUuid/federal_taxes` |
| | GET | `/v1/employees/:employeeUuid/state_taxes` |
| | DELETE | `/v1/jobs/:jobId` |
-| | GET | `/v1/payrolls/:payrollId/employees/:employeeId/pay_stub` |
| **Employee.HomeAddress** | GET | `/v1/employees/:employeeId` |
| | GET | `/v1/employees/:employeeId/home_addresses` |
| | DELETE | `/v1/home_addresses/:homeAddressUuid` |
@@ -374,11 +372,9 @@ import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json'
| | GET | `/v1/employees/:employeeId` |
| | GET | `/v1/employees/:employeeId/forms` |
| | GET | `/v1/employees/:employeeId/jobs` |
-| | GET | `/v1/employees/:employeeId/pay_stubs` |
| | GET | `/v1/employees/:employeeUuid/federal_taxes` |
| | GET | `/v1/employees/:employeeUuid/state_taxes` |
| | DELETE | `/v1/jobs/:jobId` |
-| | GET | `/v1/payrolls/:payrollId/employees/:employeeId/pay_stub` |
| **EmployeeManagement.HomeAddress** | GET | `/v1/employees/:employeeId` |
| | GET | `/v1/employees/:employeeId/home_addresses` |
| | DELETE | `/v1/home_addresses/:homeAddressUuid` |
@@ -389,6 +385,8 @@ import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json'
| | GET | `/v1/employees/:employeeId` |
| | PUT | `/v1/employees/:employeeId` |
| | PUT | `/v1/employees/:employeeId/onboarding_status` |
+| **EmployeeManagement.PaystubsCard** | GET | `/v1/employees/:employeeId/pay_stubs` |
+| | GET | `/v1/payrolls/:payrollId/employees/:employeeId/pay_stub` |
| **EmployeeManagement.TerminateEmployee** | GET | `/v1/companies/:companyId/pay_periods/unprocessed_termination_pay_periods` |
| | GET | `/v1/companies/:companyId/payrolls` |
| | POST | `/v1/companies/:companyId/payrolls` |
@@ -485,13 +483,13 @@ Flows compose multiple blocks into a single workflow. The endpoint list for a fl
| **Contractor.OnboardingFlow** | Contractor.Address, Contractor.ContractorList, Contractor.ContractorProfile, Contractor.ContractorSubmit, Contractor.NewHireReport, Contractor.PaymentMethod |
| **Contractor.PaymentFlow** | Contractor.CreatePayment, Contractor.PaymentHistory, Contractor.PaymentStatement, Contractor.PaymentSummary, Contractor.PaymentsList, InformationRequests.InformationRequestsFlow |
| **ContractorOnboarding.OnboardingFlow** | ContractorOnboarding.Address, ContractorOnboarding.ContractorList, ContractorOnboarding.ContractorProfile, ContractorOnboarding.ContractorSubmit, ContractorOnboarding.NewHireReport, ContractorOnboarding.PaymentMethod |
-| **Employee.DashboardFlow** | Employee.Compensation, Employee.Deductions, Employee.HomeAddress, Employee.PaymentMethod, Employee.WorkAddress, EmployeeManagement.DocumentManager, EmployeeManagement.FederalTaxes, EmployeeManagement.PaymentMethod, EmployeeManagement.Profile, EmployeeManagement.StateTaxes |
+| **Employee.DashboardFlow** | Employee.Compensation, Employee.Deductions, Employee.HomeAddress, Employee.WorkAddress, EmployeeManagement.DocumentManager, EmployeeManagement.FederalTaxes, EmployeeManagement.PaymentMethod, EmployeeManagement.PaystubsCard, EmployeeManagement.Profile, EmployeeManagement.StateTaxes |
| **Employee.EmployeeListFlow** | Employee.DashboardFlow, Employee.OnboardingExecutionFlow, Employee.TerminationFlow, EmployeeManagement.EmployeeList |
| **Employee.OnboardingExecutionFlow** | Employee.Compensation, Employee.Deductions, Employee.EmployeeDocuments, Employee.OnboardingSummary, Employee.PaymentMethod, Employee.Profile, EmployeeOnboarding.FederalTaxes, EmployeeOnboarding.StateTaxes |
| **Employee.OnboardingFlow** | Employee.EmployeeList, Employee.OnboardingExecutionFlow |
| **Employee.SelfOnboardingFlow** | Employee.DocumentSigner, Employee.Landing, Employee.OnboardingSummary, Employee.PaymentMethod, Employee.Profile, EmployeeOnboarding.FederalTaxes, EmployeeOnboarding.StateTaxes |
| **Employee.TerminationFlow** | Employee.TerminateEmployee, Employee.TerminationSummary, Payroll.DismissalFlow, Payroll.PayrollLanding |
-| **EmployeeManagement.DashboardFlow** | Employee.PaymentMethod, EmployeeManagement.DocumentManager, EmployeeManagement.FederalTaxes, EmployeeManagement.HomeAddress, EmployeeManagement.PaymentMethod, EmployeeManagement.Profile, EmployeeManagement.StateTaxes, EmployeeManagement.WorkAddress, EmployeeOnboarding.Compensation, EmployeeOnboarding.Deductions |
+| **EmployeeManagement.DashboardFlow** | EmployeeManagement.DocumentManager, EmployeeManagement.FederalTaxes, EmployeeManagement.HomeAddress, EmployeeManagement.PaymentMethod, EmployeeManagement.PaystubsCard, EmployeeManagement.Profile, EmployeeManagement.StateTaxes, EmployeeManagement.WorkAddress, EmployeeOnboarding.Compensation, EmployeeOnboarding.Deductions |
| **EmployeeManagement.EmployeeListFlow** | EmployeeManagement.DashboardFlow, EmployeeManagement.EmployeeList, EmployeeManagement.TerminationFlow, EmployeeOnboarding.OnboardingExecutionFlow |
| **EmployeeManagement.TerminationFlow** | EmployeeManagement.TerminateEmployee, EmployeeManagement.TerminationSummary, Payroll.DismissalFlow, Payroll.PayrollLanding |
| **EmployeeOnboarding.OnboardingExecutionFlow** | Employee.EmployeeDocuments, Employee.PaymentMethod, EmployeeOnboarding.Compensation, EmployeeOnboarding.Deductions, EmployeeOnboarding.FederalTaxes, EmployeeOnboarding.OnboardingSummary, EmployeeOnboarding.Profile, EmployeeOnboarding.StateTaxes |
diff --git a/docs/workflows-overview/employee-management/employee-management.md b/docs/workflows-overview/employee-management/employee-management.md
index 4951d7d52..b2d42fbe5 100644
--- a/docs/workflows-overview/employee-management/employee-management.md
+++ b/docs/workflows-overview/employee-management/employee-management.md
@@ -39,6 +39,7 @@ Employee management components can be used to compose your own workflow, or can
- [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.PaystubsCard](#employeemanagementpaystubscard)
### EmployeeManagement.DashboardFlow
@@ -536,3 +537,42 @@ function MyWorkAddressPanel({ employeeId }) {
| 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 |
+
+### EmployeeManagement.PaystubsCard
+
+A self-contained, read-only card for viewing an employee's paystubs — the same "Paystubs" surface the dashboard renders, drop-in usable anywhere. Renders a paginated table showing each payday, check amount, gross pay, and payment method, with a per-row download button. Clicking a row's download button fetches that paystub's PDF and opens it in a new browser tab — there is no edit surface or view to swap into; the card's only action is the download side effect.
+
+Unlike most other `EmployeeManagement.*` components, the paystubs surface is exported only as a card and not as a block: there is no edit form to orchestrate transitions with, so the card is the entire feature. Render it inline anywhere a `
` would go; wrap it in your own error and suspense boundaries if you want fallback UI for those scenarios.
+
+```jsx
+import { componentEvents, EmployeeManagement } from '@gusto/embedded-react-sdk'
+
+function MyPaystubsPanel({ employeeId }) {
+ return (
+
{
+ if (eventType === componentEvents.EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOADED) {
+ // payload is { employeeId, payrollUuid }
+ }
+ }}
+ />
+ )
+}
+```
+
+The card populates its "Payment method" column from the employee's payment method, which it fetches internally alongside the paystubs list — the per-paystub event payload does not carry the payment method itself.
+
+#### 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_PAYSTUBS_CARD_DOWNLOAD_REQUESTED | Fired when the user clicks a row's download button, before the PDF is fetched | { employeeId: string, payrollUuid: string } |
+| EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOADED | Fired after the paystub PDF is successfully fetched and opened in a new tab | { employeeId: string, payrollUuid: string } |
diff --git a/sdk-app/src/generated-registry-data.ts b/sdk-app/src/generated-registry-data.ts
index 3c6e37f2a..6f974bdfa 100644
--- a/sdk-app/src/generated-registry-data.ts
+++ b/sdk-app/src/generated-registry-data.ts
@@ -45,6 +45,7 @@ export const ENTITY_REQUIREMENTS: Record = {
'EmployeeManagement.PaymentMethodBankForm': ['employeeId'],
'EmployeeManagement.PaymentMethodCard': ['employeeId'],
'EmployeeManagement.PaymentMethodSplitForm': ['employeeId'],
+ 'EmployeeManagement.PaystubsCard': ['employeeId'],
'EmployeeManagement.Profile': ['employeeId'],
'EmployeeManagement.ProfileCard': ['employeeId'],
'EmployeeManagement.ProfileEditForm': ['employeeId'],
diff --git a/src/components/Employee/Dashboard/JobAndPayView.tsx b/src/components/Employee/Dashboard/JobAndPayView.tsx
index f792487a6..a1e3cb30e 100644
--- a/src/components/Employee/Dashboard/JobAndPayView.tsx
+++ b/src/components/Employee/Dashboard/JobAndPayView.tsx
@@ -1,12 +1,8 @@
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { useGustoEmbeddedContext } from '@gusto/embedded-api-v-2025-11-15/react-query/_context'
-import { payrollsGetPayStub } from '@gusto/embedded-api-v-2025-11-15/funcs/payrollsGetPayStub'
-import { useErrorBoundary } from 'react-error-boundary'
import { useJobsAndCompensationsDeleteMutation } from '@gusto/embedded-api-v-2025-11-15/react-query/jobsAndCompensationsDelete'
import type { Job } from '@gusto/embedded-api-v-2025-11-15/models/components/job'
import type { Garnishment } from '@gusto/embedded-api-v-2025-11-15/models/components/garnishment'
-import type { GetV1EmployeesEmployeeUuidPayStubsResponse } from '@gusto/embedded-api-v-2025-11-15/models/operations/getv1employeesemployeeuuidpaystubs'
import { useEmployeeCompensation } from './hooks'
import type { PendingCompensationChange } from './getPendingCompensationChanges'
import { usePendingChangeDetailRenderer } from './usePendingChangeDetailRenderer'
@@ -18,12 +14,10 @@ import { DataView, useDataView, EmptyData, Loading, VisuallyHidden } from '@/com
import { HamburgerMenu } from '@/components/Common/HamburgerMenu'
import { BaseLayout } from '@/components/Base/Base'
import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler'
-import { readableStreamToBlob } from '@/helpers/readableStreamToBlob'
import { formatDateLongWithYear, formatDateToStringDate } from '@/helpers/dateFormatting'
import { useFormatCompensationRate } from '@/helpers/formattedStrings'
import useNumberFormatter from '@/hooks/useNumberFormatter'
import { useI18n } from '@/i18n'
-import { usePaymentMethodList } from '@/components/Employee/PaymentMethod/shared'
import { PaymentMethodCard } from '@/components/Employee/PaymentMethod/management'
import {
useDeductionsList,
@@ -31,17 +25,13 @@ import {
DeleteDeductionDialog,
formatDeductionAmount,
} from '@/components/Employee/Deductions/shared'
+import { PaystubsCard } from '@/components/Employee/Paystubs/management/PaystubsCard'
import { componentEvents, FlsaStatus, type EventType } from '@/shared/constants'
import type { OnEventType } from '@/components/Base/useBase'
import PlusCircleIcon from '@/assets/icons/plus-circle.svg?react'
-import DownloadCloudIcon from '@/assets/icons/download-cloud.svg?react'
import TrashCanSvg from '@/assets/icons/trashcan.svg?react'
import PencilSvg from '@/assets/icons/pencil.svg?react'
-type EmployeePayStub = NonNullable<
- GetV1EmployeesEmployeeUuidPayStubsResponse['employeePayStubsList']
->[number]
-
function parseJobRate(rate: Job['rate']): number | null {
if (rate === undefined) return null
const numericRate = parseFloat(rate)
@@ -76,8 +66,6 @@ export function JobAndPayView({
const formatCompensationRate = useFormatCompensationRate()
const formatCurrency = useNumberFormatter('currency')
const formatPercent = useNumberFormatter('percent')
- const gustoEmbedded = useGustoEmbeddedContext()
- const { showBoundary } = useErrorBoundary()
const compensation = useEmployeeCompensation({ employeeId })
const {
@@ -85,14 +73,11 @@ export function JobAndPayView({
primaryFlsaStatus,
pendingChanges,
hasMultipleJobs: hasMultipleJobsFromHook,
- payStubs,
employeeFirstName,
} = compensation.data
- const payStubsPagination = compensation.pagination.payStubs
const cancellingCompensationUuid = compensation.status.cancellingCompensationUuid
const { cancelPendingChange } = compensation.actions
const isCompensationCardLoading = compensation.status.isCompensationLoading
- const isPayStubsLoading = compensation.status.isPayStubsLoading
const handleCancelChange = useCallback(
async (pendingChange: PendingCompensationChange) => {
@@ -107,77 +92,6 @@ export function JobAndPayView({
[cancelPendingChange, onEvent, employeeId],
)
- const [downloadingPayrollUuids, setDownloadingPayrollUuids] = useState>(
- () => new Set(),
- )
-
- const handlePaystubDownload = useCallback(
- async (payrollUuid: string) => {
- // Omit `noopener` — it makes window.open return null in modern browsers,
- // which would leave us unable to navigate the new tab to the blob URL.
- const newWindow = window.open('', '_blank')
- const loadingMessage = t('jobAndPay.paystubs.downloadLoadingMessage')
- if (newWindow) {
- // Avoid the user staring at about:blank while we fetch the PDF. The
- // navigation to the Blob URL below replaces this document.
- const doc = newWindow.document
- doc.title = loadingMessage
- const style = doc.createElement('style')
- style.textContent =
- 'body{font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;' +
- 'justify-content:center;height:100vh;margin:0;color:#444;gap:12px}' +
- '.spinner{width:20px;height:20px;border:2px solid #ccc;border-top-color:#444;' +
- 'border-radius:50%;animation:spin .8s linear infinite}' +
- '@keyframes spin{to{transform:rotate(360deg)}}'
- doc.head.appendChild(style)
- const spinner = doc.createElement('div')
- spinner.className = 'spinner'
- spinner.setAttribute('aria-hidden', 'true')
- const label = doc.createElement('span')
- label.textContent = loadingMessage
- doc.body.replaceChildren(spinner, label)
- }
- setDownloadingPayrollUuids(prev => {
- const next = new Set(prev)
- next.add(payrollUuid)
- return next
- })
- try {
- const response = await payrollsGetPayStub(gustoEmbedded, {
- payrollId: payrollUuid,
- employeeId,
- })
- if (!response.value?.responseStream) {
- throw new Error(t('jobAndPay.paystubs.downloadError'))
- }
- const pdfBlob = await readableStreamToBlob(response.value.responseStream, 'application/pdf')
- const url = URL.createObjectURL(pdfBlob)
- if (newWindow) {
- // Revoke after the new tab has loaded the blob; revoking synchronously
- // would race the navigation and leave the tab blank.
- newWindow.addEventListener('load', () => {
- URL.revokeObjectURL(url)
- })
- newWindow.location.href = url
- } else {
- URL.revokeObjectURL(url)
- }
- } catch (err) {
- if (newWindow) {
- newWindow.close()
- }
- showBoundary(err instanceof Error ? err : new Error(String(err)))
- } finally {
- setDownloadingPayrollUuids(prev => {
- const next = new Set(prev)
- next.delete(payrollUuid)
- return next
- })
- }
- },
- [gustoEmbedded, employeeId, t, showBoundary],
- )
-
const [pendingDeleteJob, setPendingDeleteJob] = useState<{
uuid: string
title: string
@@ -257,15 +171,6 @@ export function JobAndPayView({
const showInlineAlert = hasPendingUpdates && !showSummaryAlert
const nextChange = updatePendingChanges[0]
- // The Paystubs column reads `paymentMethod?.type` for one row of metadata.
- // The Payment card itself is rendered standalone by ``
- // below and owns its own data fetch + error handling — this call is just
- // for the Paystubs column. React Query dedupes the request.
- const paymentMethodList = usePaymentMethodList({ employeeId })
- const paymentMethod = paymentMethodList.isLoading
- ? undefined
- : paymentMethodList.data.paymentMethod
-
const deductionsList = useDeductionsList({ employeeId })
const deductions = deductionsList.isLoading ? [] : deductionsList.data.deductions
const deletingGarnishmentUuid = deductionsList.isLoading
@@ -421,37 +326,6 @@ export function JobAndPayView({
},
]
- const payStubsColumns = [
- {
- key: 'payday',
- title: t('jobAndPay.paystubs.payday'),
- render: (payStub: EmployeePayStub) => formatDateLongWithYear(payStub.checkDate) || '-',
- },
- {
- key: 'checkAmount',
- title: t('jobAndPay.paystubs.checkAmount'),
- render: (payStub: EmployeePayStub) => {
- if (!payStub.netPay) return '-'
- const amount = parseFloat(payStub.netPay)
- return isNaN(amount) ? '-' : formatCurrency(amount)
- },
- },
- {
- key: 'grossPay',
- title: t('jobAndPay.paystubs.grossPay'),
- render: (payStub: EmployeePayStub) => {
- if (!payStub.grossPay) return '-'
- const amount = parseFloat(payStub.grossPay)
- return isNaN(amount) ? '-' : formatCurrency(amount)
- },
- },
- {
- key: 'paymentMethod',
- title: t('jobAndPay.paystubs.paymentMethod'),
- render: () => paymentMethod?.type || t('jobAndPay.paystubs.noPaymentMethod'),
- },
- ]
-
const garnishmentsDataView = useDataView({
data: deductions,
columns: garnishmentsColumns,
@@ -483,37 +357,6 @@ export function JobAndPayView({
),
})
- const payStubsDataView = useDataView({
- data: payStubs,
- columns: payStubsColumns,
- pagination: payStubsPagination,
- itemMenu: payStub => {
- const isDownloading =
- !!payStub.payrollUuid && downloadingPayrollUuids.has(payStub.payrollUuid)
- return (
- {
- if (payStub.payrollUuid) {
- void handlePaystubDownload(payStub.payrollUuid)
- }
- }}
- >
-
-
- )
- },
- emptyState: () => (
-
- ),
- })
-
// `useDeductionsList` still uses the older `HookLoadingResult | Ready`
// shape, which returns `isLoading: true` when the query has errored AND
// data is missing. Treat as "not loading" so the section doesn't show a
@@ -719,16 +562,7 @@ export function JobAndPayView({
)}
- }
- >
- {isPayStubsLoading ? (
-
- ) : (
-
- )}
-
+
{
})
expect(result.current.status.isCompensationLoading).toBe(true)
- expect(result.current.status.isPayStubsLoading).toBe(true)
expect(result.current.data.jobs).toEqual([])
- expect(result.current.data.payStubs).toEqual([])
await waitFor(() => {
expect(result.current.status.isCompensationLoading).toBe(false)
- expect(result.current.status.isPayStubsLoading).toBe(false)
})
expect(result.current.data.jobs.length).toBeGreaterThan(0)
@@ -274,24 +270,4 @@ describe('useEmployeeCompensation', () => {
expect(jobsRequestUrl).toContain('/v1/employees/employee-123/jobs')
expect(jobsRequestUrl).toContain('include=all_compensations')
})
-
- it('surfaces paystub pagination control props once the paystubs query resolves', async () => {
- server.use(
- http.get(`${API_BASE_URL}/v1/employees/:employee_uuid/pay_stubs`, () =>
- HttpResponse.json([], {
- headers: { 'x-total-count': '0', 'x-total-pages': '1', 'x-page': '1' },
- }),
- ),
- )
-
- const { result } = renderHook(() => useEmployeeCompensation({ employeeId: 'employee-123' }), {
- wrapper: GustoTestProvider,
- })
-
- await waitFor(() => {
- expect(result.current.status.isPayStubsLoading).toBe(false)
- })
-
- expect(result.current.pagination.payStubs).toBeDefined()
- })
})
diff --git a/src/components/Employee/Dashboard/hooks/useEmployeeCompensation.tsx b/src/components/Employee/Dashboard/hooks/useEmployeeCompensation.tsx
index 58914f3b1..6becfbef9 100644
--- a/src/components/Employee/Dashboard/hooks/useEmployeeCompensation.tsx
+++ b/src/components/Employee/Dashboard/hooks/useEmployeeCompensation.tsx
@@ -3,9 +3,7 @@ import { useEmployeesGet } from '@gusto/embedded-api-v-2025-11-15/react-query/em
import { useJobsAndCompensationsGetJobs } from '@gusto/embedded-api-v-2025-11-15/react-query/jobsAndCompensationsGetJobs'
import { GetV1EmployeesEmployeeIdJobsQueryParamInclude } from '@gusto/embedded-api-v-2025-11-15/models/operations/getv1employeesemployeeidjobs'
import { useJobsAndCompensationsDeleteCompensationMutation } from '@gusto/embedded-api-v-2025-11-15/react-query/jobsAndCompensationsDeleteCompensation'
-import { usePayrollsGetPayStubs } from '@gusto/embedded-api-v-2025-11-15/react-query/payrollsGetPayStubs'
import type { Job } from '@gusto/embedded-api-v-2025-11-15/models/components/job'
-import type { GetV1EmployeesEmployeeUuidPayStubsResponse } from '@gusto/embedded-api-v-2025-11-15/models/operations/getv1employeesemployeeuuidpaystubs'
import {
getPendingCompensationChanges,
type PendingCompensationChange,
@@ -13,13 +11,7 @@ import {
import { derivePrimaryFlsaStatus } from '@/components/Employee/Compensation/shared/derivePrimaryFlsaStatus'
import { useBaseSubmit } from '@/components/Base/useBaseSubmit'
import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler'
-import { usePagination } from '@/hooks/usePagination/usePagination'
import type { BaseHookReady, HookSubmitResult } from '@/partner-hook-utils/types'
-import type { PaginationControlProps } from '@/components/Common/PaginationControl/PaginationControlTypes'
-
-type EmployeePayStub = NonNullable<
- GetV1EmployeesEmployeeUuidPayStubsResponse['employeePayStubsList']
->[number]
export interface UseEmployeeCompensationProps {
employeeId: string
@@ -32,7 +24,6 @@ export interface UseEmployeeCompensationResult extends BaseHookReady<
primaryFlsaStatus?: string
hasMultipleJobs: boolean
pendingChanges: PendingCompensationChange[]
- payStubs: EmployeePayStub[]
/** First name from the shared employee fetch; useful for cosmetic copy
* in alerts (e.g. "Heads up, Jane has pending changes"). Optional
* because the employee record can omit it. */
@@ -44,13 +35,8 @@ export interface UseEmployeeCompensationResult extends BaseHookReady<
/** Compensation card depends on the jobs fetch (jobs, pending
* changes, FLSA status). */
isCompensationLoading: boolean
- /** Paystubs card depends on a separate paginated endpoint. */
- isPayStubsLoading: boolean
}
> {
- pagination: {
- payStubs?: PaginationControlProps
- }
actions: {
cancelPendingChange: (
pendingChange: PendingCompensationChange,
@@ -59,18 +45,15 @@ export interface UseEmployeeCompensationResult extends BaseHookReady<
}
/**
- * Phase B: non-Suspense queries so the Compensation and Paystubs cards
- * can paint independently within the Job and pay tab. JobAndPayView
- * already gets the Payment and Deductions cards as separate non-Suspense
- * hooks, so this completes the four-section incremental render.
+ * Non-Suspense queries for the Compensation card on the Job and pay tab.
+ * Returns jobs + pending-changes + the employee's first name (for cosmetic
+ * alert copy) along with a cancel-pending-change action. Paystubs data
+ * moved into `@/components/Employee/Paystubs/shared/usePaystubsList` when
+ * the Paystubs card became its own management block.
*/
export function useEmployeeCompensation({
employeeId,
}: UseEmployeeCompensationProps): UseEmployeeCompensationResult {
- const { currentPage, itemsPerPage, getPaginationProps } = usePagination({
- defaultItemsPerPage: 10,
- })
-
// staleTime: Infinity on dashboard reads — the SDK QueryClient already
// invalidates all queries on any mutation success, so post-write
// freshness is preserved. Without this, every subscriber re-mount
@@ -87,10 +70,6 @@ export function useEmployeeCompensation({
// Employee query is a lightweight secondary fetch for cosmetic data only
// (first name used in alert copy). Jobs / compensation data comes from jobsQuery.
const employeeQuery = useEmployeesGet({ employeeId }, { staleTime: Infinity })
- const payStubsQuery = usePayrollsGetPayStubs(
- { employeeId, page: currentPage, per: itemsPerPage },
- { staleTime: Infinity },
- )
const cancelCompensationMutation = useJobsAndCompensationsDeleteCompensationMutation()
const {
baseSubmitHandler,
@@ -107,14 +86,6 @@ export function useEmployeeCompensation({
const pendingChanges = useMemo(() => getPendingCompensationChanges(jobs), [jobs])
- const payStubs = payStubsQuery.data?.employeePayStubsList ?? []
-
- const payStubsPagination = useMemo(() => {
- const headers = payStubsQuery.data?.httpMeta.response.headers
- if (!headers) return undefined
- return getPaginationProps(headers, payStubsQuery.isFetching)
- }, [payStubsQuery.data?.httpMeta.response.headers, payStubsQuery.isFetching, getPaginationProps])
-
const cancellingCompensationUuid = cancelCompensationMutation.isPending
? cancelCompensationMutation.variables.request.compensationId
: null
@@ -136,12 +107,9 @@ export function useEmployeeCompensation({
)
const isPending =
- jobsQuery.isFetching ||
- employeeQuery.isFetching ||
- payStubsQuery.isFetching ||
- cancelCompensationMutation.isPending
+ jobsQuery.isFetching || employeeQuery.isFetching || cancelCompensationMutation.isPending
- const errorHandling = composeErrorHandler([jobsQuery, employeeQuery, payStubsQuery], {
+ const errorHandling = composeErrorHandler([jobsQuery, employeeQuery], {
submitError,
setSubmitError,
})
@@ -154,17 +122,12 @@ export function useEmployeeCompensation({
primaryFlsaStatus,
hasMultipleJobs,
pendingChanges,
- payStubs,
employeeFirstName: employee?.firstName ?? undefined,
},
status: {
isPending,
cancellingCompensationUuid,
isCompensationLoading: jobsQuery.isLoading,
- isPayStubsLoading: payStubsQuery.isLoading,
- },
- pagination: {
- payStubs: payStubsPagination,
},
actions: {
cancelPendingChange,
diff --git a/src/components/Employee/Paystubs/management/PaystubsCard/PaystubsCard.test.tsx b/src/components/Employee/Paystubs/management/PaystubsCard/PaystubsCard.test.tsx
new file mode 100644
index 000000000..7f8d938b6
--- /dev/null
+++ b/src/components/Employee/Paystubs/management/PaystubsCard/PaystubsCard.test.tsx
@@ -0,0 +1,134 @@
+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, type HttpResponseResolver } from 'msw'
+import { PaystubsCard } from './PaystubsCard'
+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'
+
+const stubPayStubs = (
+ payStubs: Array>,
+ headers: Record = { 'x-total-count': '0', 'x-total-pages': '1', 'x-page': '1' },
+) => {
+ server.use(
+ http.get(`${API_BASE_URL}/v1/employees/:employee_uuid/pay_stubs`, () =>
+ HttpResponse.json(payStubs, { headers }),
+ ),
+ )
+}
+
+const TWO_STUBS = [
+ {
+ uuid: 'stub-1',
+ payroll_uuid: 'payroll-1',
+ check_date: '2025-01-15',
+ gross_pay: '2000.00',
+ net_pay: '1500.00',
+ },
+ {
+ uuid: 'stub-2',
+ payroll_uuid: 'payroll-2',
+ check_date: '2024-12-31',
+ gross_pay: '1800.00',
+ net_pay: '1300.00',
+ },
+]
+
+describe('PaystubsCard', () => {
+ const onEvent = vi.fn()
+
+ beforeEach(() => {
+ setupApiTestMocks()
+ onEvent.mockClear()
+ })
+
+ it('renders the title and a download action per row once paystubs load', async () => {
+ stubPayStubs(TWO_STUBS, { 'x-total-count': '2', 'x-total-pages': '1', 'x-page': '1' })
+
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByText('Paystubs')).toBeInTheDocument()
+ })
+
+ const downloadButtons = await screen.findAllByRole('button', { name: 'Download paystub' })
+ expect(downloadButtons).toHaveLength(2)
+ })
+
+ it('renders the empty state when the employee has no paystubs', async () => {
+ stubPayStubs([])
+
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByText('No paystubs')).toBeInTheDocument()
+ })
+
+ expect(screen.getByText('Paystubs will appear here after payroll is run')).toBeInTheDocument()
+ })
+
+ it('fires EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOAD_REQUESTED and _DOWNLOADED when a row download succeeds', async () => {
+ stubPayStubs(TWO_STUBS, { 'x-total-count': '2', 'x-total-pages': '1', 'x-page': '1' })
+ const downloadResolver = vi.fn(() => {
+ return new HttpResponse(new Uint8Array([0x25, 0x50, 0x44, 0x46]), {
+ headers: { 'content-type': 'application/pdf' },
+ })
+ })
+ server.use(
+ http.get(
+ `${API_BASE_URL}/v1/payrolls/:payroll_id/employees/:employee_id/pay_stub`,
+ downloadResolver,
+ ),
+ )
+
+ // window.open returns null in jsdom — handleDownload tolerates this and skips
+ // the new-tab loading UI. URL.createObjectURL needs a stub.
+ const openSpy = vi.spyOn(window, 'open').mockReturnValue(null)
+ const createObjectURLSpy = vi
+ .spyOn(URL, 'createObjectURL')
+ .mockReturnValue('blob:mock-blob-url')
+ const revokeObjectURLSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})
+
+ try {
+ const user = userEvent.setup()
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getAllByRole('button', { name: 'Download paystub' })).toHaveLength(2)
+ })
+
+ const [firstDownload] = screen.getAllByRole('button', { name: 'Download paystub' })
+ await user.click(firstDownload!)
+
+ await waitFor(() => {
+ expect(downloadResolver).toHaveBeenCalledTimes(1)
+ })
+
+ expect(onEvent).toHaveBeenCalledWith(
+ componentEvents.EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOAD_REQUESTED,
+ { employeeId: 'employee-123', payrollUuid: 'payroll-1' },
+ )
+ await waitFor(() => {
+ expect(onEvent).toHaveBeenCalledWith(
+ componentEvents.EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOADED,
+ { employeeId: 'employee-123', payrollUuid: 'payroll-1' },
+ )
+ })
+
+ // Regression: the per-row loading spinner must clear once the download
+ // finishes. Previously the UUID was added to `downloadingPayrollUuids`
+ // but never removed, so the button stayed in its loading state forever.
+ await waitFor(() => {
+ const [firstAfter] = screen.getAllByRole('button', { name: 'Download paystub' })
+ expect(firstAfter).not.toHaveAttribute('data-loading')
+ })
+ } finally {
+ openSpy.mockRestore()
+ createObjectURLSpy.mockRestore()
+ revokeObjectURLSpy.mockRestore()
+ }
+ })
+})
diff --git a/src/components/Employee/Paystubs/management/PaystubsCard/PaystubsCard.tsx b/src/components/Employee/Paystubs/management/PaystubsCard/PaystubsCard.tsx
new file mode 100644
index 000000000..8cff3d181
--- /dev/null
+++ b/src/components/Employee/Paystubs/management/PaystubsCard/PaystubsCard.tsx
@@ -0,0 +1,235 @@
+import { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useErrorBoundary } from 'react-error-boundary'
+import {
+ usePaystubsList,
+ type EmployeePayStub,
+ type UsePaystubsListReady,
+} from '../../shared/usePaystubsList'
+import { DataView, EmptyData, useDataView } from '@/components/Common'
+import { BaseLayout } from '@/components/Base/Base'
+import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
+import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler'
+import {
+ usePaymentMethodList,
+ type UsePaymentMethodListResult,
+} from '@/components/Employee/PaymentMethod/shared'
+import { formatDateLongWithYear } from '@/helpers/dateFormatting'
+import useNumberFormatter from '@/hooks/useNumberFormatter'
+import { useI18n } from '@/i18n'
+import { componentEvents, type EventType } from '@/shared/constants'
+import type { OnEventType } from '@/components/Base/useBase'
+import DownloadCloudIcon from '@/assets/icons/download-cloud.svg?react'
+
+export interface PaystubsCardProps {
+ employeeId: string
+ onEvent: OnEventType
+}
+
+/**
+ * Standalone "Paystubs" card. Owns its own data fetch via
+ * {@link usePaystubsList} and renders the paginated paystubs table with a
+ * per-row PDF download action. Emits the management block's scoped events
+ * (`EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_*`) on download request and on
+ * download success. The card has no edit transitions and no alert API —
+ * paystubs is a read-only surface whose only action is a download side
+ * effect that opens the PDF in a new tab.
+ */
+export function PaystubsCard({ employeeId, onEvent }: PaystubsCardProps) {
+ useI18n('Employee.Management.Paystubs')
+ const paystubsList = usePaystubsList({ employeeId })
+ const paymentMethodList = usePaymentMethodList({ employeeId })
+
+ const errorHandling = composeErrorHandler([paystubsList, paymentMethodList])
+
+ if (paystubsList.isLoading) {
+ return
+ }
+
+ return (
+
+ )
+}
+
+interface PaystubsCardReadyProps extends PaystubsCardProps {
+ paystubsList: UsePaystubsListReady
+ paymentMethodList: UsePaymentMethodListResult
+ errorHandling: ReturnType
+}
+
+function PaystubsCardReady({
+ employeeId,
+ onEvent,
+ paystubsList,
+ paymentMethodList,
+ errorHandling,
+}: PaystubsCardReadyProps) {
+ const { t } = useTranslation('Employee.Management.Paystubs')
+ const Components = useComponentContext()
+ const formatCurrency = useNumberFormatter('currency')
+ const { showBoundary } = useErrorBoundary()
+
+ const [downloadingPayrollUuids, setDownloadingPayrollUuids] = useState>(
+ () => new Set(),
+ )
+
+ const { payStubs } = paystubsList.data
+ const payStubsPagination = paystubsList.pagination.payStubs
+ const paymentMethod = paymentMethodList.isLoading
+ ? undefined
+ : paymentMethodList.data.paymentMethod
+
+ const handleDownload = useCallback(
+ async (payrollUuid: string) => {
+ onEvent(componentEvents.EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOAD_REQUESTED, {
+ employeeId,
+ payrollUuid,
+ })
+
+ // Omit `noopener` — it makes window.open return null in modern browsers,
+ // which would leave us unable to navigate the new tab to the blob URL.
+ const newWindow = window.open('', '_blank')
+ const loadingMessage = t('downloadLoadingMessage')
+ if (newWindow) {
+ // Avoid the user staring at about:blank while we fetch the PDF. The
+ // navigation to the Blob URL below replaces this document.
+ const doc = newWindow.document
+ doc.title = loadingMessage
+ const style = doc.createElement('style')
+ style.textContent =
+ 'body{font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;' +
+ 'justify-content:center;height:100vh;margin:0;color:#444;gap:12px}' +
+ '.spinner{width:20px;height:20px;border:2px solid #ccc;border-top-color:#444;' +
+ 'border-radius:50%;animation:spin .8s linear infinite}' +
+ '@keyframes spin{to{transform:rotate(360deg)}}'
+ doc.head.appendChild(style)
+ const spinner = doc.createElement('div')
+ spinner.className = 'spinner'
+ spinner.setAttribute('aria-hidden', 'true')
+ const label = doc.createElement('span')
+ label.textContent = loadingMessage
+ doc.body.replaceChildren(spinner, label)
+ }
+ setDownloadingPayrollUuids(prev => {
+ const next = new Set(prev)
+ next.add(payrollUuid)
+ return next
+ })
+ try {
+ const result = await paystubsList.actions.downloadPayStub(payrollUuid)
+ if (!result) {
+ if (newWindow) newWindow.close()
+ return
+ }
+ const url = URL.createObjectURL(result.data)
+ if (newWindow) {
+ // Revoke after the new tab has loaded the blob; revoking synchronously
+ // would race the navigation and leave the tab blank.
+ newWindow.addEventListener('load', () => {
+ URL.revokeObjectURL(url)
+ })
+ newWindow.location.href = url
+ } else {
+ URL.revokeObjectURL(url)
+ }
+ onEvent(componentEvents.EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOADED, {
+ employeeId,
+ payrollUuid,
+ })
+ } catch (err) {
+ if (newWindow) newWindow.close()
+ showBoundary(err instanceof Error ? err : new Error(String(err)))
+ } finally {
+ setDownloadingPayrollUuids(prev => {
+ if (!prev.has(payrollUuid)) return prev
+ const next = new Set(prev)
+ next.delete(payrollUuid)
+ return next
+ })
+ }
+ },
+ [paystubsList.actions, onEvent, employeeId, t, showBoundary],
+ )
+
+ const payStubsColumns = [
+ {
+ key: 'payday',
+ title: t('payday'),
+ render: (payStub: EmployeePayStub) => formatDateLongWithYear(payStub.checkDate) || '-',
+ },
+ {
+ key: 'checkAmount',
+ title: t('checkAmount'),
+ render: (payStub: EmployeePayStub) => {
+ if (!payStub.netPay) return '-'
+ const amount = parseFloat(payStub.netPay)
+ return isNaN(amount) ? '-' : formatCurrency(amount)
+ },
+ },
+ {
+ key: 'grossPay',
+ title: t('grossPay'),
+ render: (payStub: EmployeePayStub) => {
+ if (!payStub.grossPay) return '-'
+ const amount = parseFloat(payStub.grossPay)
+ return isNaN(amount) ? '-' : formatCurrency(amount)
+ },
+ },
+ {
+ key: 'paymentMethod',
+ title: t('paymentMethod'),
+ render: () => paymentMethod?.type || t('noPaymentMethod'),
+ },
+ ]
+
+ const payStubsDataView = useDataView({
+ data: payStubs,
+ columns: payStubsColumns,
+ pagination: payStubsPagination,
+ itemMenu: payStub => {
+ const isDownloading =
+ !!payStub.payrollUuid && downloadingPayrollUuids.has(payStub.payrollUuid)
+ return (
+ {
+ if (payStub.payrollUuid) {
+ void handleDownload(payStub.payrollUuid)
+ }
+ }}
+ >
+
+
+ )
+ },
+ emptyState: () => (
+
+ ),
+ })
+
+ const isShowingTable = payStubs.length > 0
+
+ return (
+
+ }
+ >
+ {isShowingTable ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/src/components/Employee/Paystubs/management/PaystubsCard/index.ts b/src/components/Employee/Paystubs/management/PaystubsCard/index.ts
new file mode 100644
index 000000000..3f5bcbb46
--- /dev/null
+++ b/src/components/Employee/Paystubs/management/PaystubsCard/index.ts
@@ -0,0 +1,2 @@
+export { PaystubsCard } from './PaystubsCard'
+export type { PaystubsCardProps } from './PaystubsCard'
diff --git a/src/components/Employee/Paystubs/management/index.ts b/src/components/Employee/Paystubs/management/index.ts
new file mode 100644
index 000000000..3f5bcbb46
--- /dev/null
+++ b/src/components/Employee/Paystubs/management/index.ts
@@ -0,0 +1,2 @@
+export { PaystubsCard } from './PaystubsCard'
+export type { PaystubsCardProps } from './PaystubsCard'
diff --git a/src/components/Employee/Paystubs/shared/index.ts b/src/components/Employee/Paystubs/shared/index.ts
new file mode 100644
index 000000000..2499de057
--- /dev/null
+++ b/src/components/Employee/Paystubs/shared/index.ts
@@ -0,0 +1,7 @@
+export { usePaystubsList } from './usePaystubsList'
+export type {
+ UsePaystubsListParams,
+ UsePaystubsListResult,
+ UsePaystubsListReady,
+ EmployeePayStub,
+} from './usePaystubsList'
diff --git a/src/components/Employee/Paystubs/shared/usePaystubsList/index.ts b/src/components/Employee/Paystubs/shared/usePaystubsList/index.ts
new file mode 100644
index 000000000..2499de057
--- /dev/null
+++ b/src/components/Employee/Paystubs/shared/usePaystubsList/index.ts
@@ -0,0 +1,7 @@
+export { usePaystubsList } from './usePaystubsList'
+export type {
+ UsePaystubsListParams,
+ UsePaystubsListResult,
+ UsePaystubsListReady,
+ EmployeePayStub,
+} from './usePaystubsList'
diff --git a/src/components/Employee/Paystubs/shared/usePaystubsList/usePaystubsList.test.tsx b/src/components/Employee/Paystubs/shared/usePaystubsList/usePaystubsList.test.tsx
new file mode 100644
index 000000000..8f673e84b
--- /dev/null
+++ b/src/components/Employee/Paystubs/shared/usePaystubsList/usePaystubsList.test.tsx
@@ -0,0 +1,148 @@
+import { renderHook, act, waitFor } from '@testing-library/react'
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { http, HttpResponse, type HttpResponseResolver } from 'msw'
+import { usePaystubsList, type UsePaystubsListResult } from './usePaystubsList'
+import { server } from '@/test/mocks/server'
+import { setupApiTestMocks } from '@/test/mocks/apiServer'
+import { GustoTestProvider } from '@/test/GustoTestApiProvider'
+import { API_BASE_URL } from '@/test/constants'
+
+type ReadyResult = Extract
+
+function assertReady(hookResult: UsePaystubsListResult): asserts hookResult is ReadyResult {
+ if (hookResult.isLoading) {
+ throw new Error('Expected hook to be ready but it is still loading')
+ }
+}
+
+type PayStubApiFixture = {
+ uuid: string
+ payroll_uuid: string
+ check_date: string
+ gross_pay: string
+ net_pay: string
+}
+
+const buildPayStubFixture = (overrides: Partial & { uuid: string }) => ({
+ payroll_uuid: `payroll-${overrides.uuid}`,
+ check_date: '2025-01-15',
+ gross_pay: '2000.00',
+ net_pay: '1500.00',
+ ...overrides,
+})
+
+const stubList = (
+ payStubs: ReturnType[],
+ headers: Record = { 'x-total-count': '0', 'x-total-pages': '1', 'x-page': '1' },
+) => {
+ server.use(
+ http.get(`${API_BASE_URL}/v1/employees/:employee_uuid/pay_stubs`, () =>
+ HttpResponse.json(payStubs, { headers }),
+ ),
+ )
+}
+
+describe('usePaystubsList', () => {
+ beforeEach(() => {
+ setupApiTestMocks()
+ })
+
+ it('starts loading then resolves to a ready state with paystubs and pagination', async () => {
+ stubList(
+ [
+ buildPayStubFixture({ uuid: 'stub-1', check_date: '2025-01-15' }),
+ buildPayStubFixture({ uuid: 'stub-2', check_date: '2024-12-31' }),
+ ],
+ { 'x-total-count': '2', 'x-total-pages': '1', 'x-page': '1' },
+ )
+
+ const { result } = renderHook(() => usePaystubsList({ employeeId: 'employee-123' }), {
+ wrapper: GustoTestProvider,
+ })
+
+ expect(result.current.isLoading).toBe(true)
+ expect(result.current.errorHandling.errors).toEqual([])
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false)
+ })
+
+ assertReady(result.current)
+ expect(result.current.data.payStubs).toHaveLength(2)
+ expect(result.current.data.payStubs[0]?.uuid).toBe('stub-1')
+ expect(result.current.status.isFetching).toBe(false)
+ expect(result.current.status.isPending).toBe(false)
+ expect(result.current.pagination.payStubs).toBeDefined()
+ expect(result.current.actions.downloadPayStub).toBeDefined()
+ })
+
+ it('actions.downloadPayStub GETs the paystub PDF and returns a Blob HookSubmitResult', async () => {
+ stubList([buildPayStubFixture({ uuid: 'stub-1' })])
+
+ let downloadPath: string | null = null
+ const downloadResolver = vi.fn(({ request }) => {
+ downloadPath = new URL(request.url).pathname
+ // The funcs/payrollsGetPayStub helper expects a binary PDF response;
+ // MSW serves it as a Blob with the right content-type so readableStreamToBlob
+ // succeeds. The exact bytes don't matter — only that a Blob comes back.
+ return new HttpResponse(new Uint8Array([0x25, 0x50, 0x44, 0x46]), {
+ headers: { 'content-type': 'application/pdf' },
+ })
+ })
+ server.use(
+ http.get(
+ `${API_BASE_URL}/v1/payrolls/:payroll_id/employees/:employee_id/pay_stub`,
+ downloadResolver,
+ ),
+ )
+
+ const { result } = renderHook(() => usePaystubsList({ employeeId: 'employee-123' }), {
+ wrapper: GustoTestProvider,
+ })
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false)
+ })
+
+ let submitResult: Awaited>
+ await act(async () => {
+ assertReady(result.current)
+ submitResult = await result.current.actions.downloadPayStub('payroll-stub-1')
+ })
+
+ expect(downloadResolver).toHaveBeenCalledTimes(1)
+ expect(downloadPath).toBe('/v1/payrolls/payroll-stub-1/employees/employee-123/pay_stub')
+ expect(submitResult).toMatchObject({ mode: 'update' })
+ expect(submitResult?.data).toBeInstanceOf(Blob)
+ expect(submitResult?.data.type).toBe('application/pdf')
+ })
+
+ it('surfaces a paystub download failure through errorHandling.errors', async () => {
+ stubList([buildPayStubFixture({ uuid: 'stub-1' })])
+ server.use(
+ http.get(`${API_BASE_URL}/v1/payrolls/:payroll_id/employees/:employee_id/pay_stub`, () =>
+ HttpResponse.json(
+ { error_key: 'server_error', errors: [{ message: 'Boom' }] },
+ { status: 500 },
+ ),
+ ),
+ )
+
+ const { result } = renderHook(() => usePaystubsList({ employeeId: 'employee-123' }), {
+ wrapper: GustoTestProvider,
+ })
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false)
+ })
+
+ await act(async () => {
+ assertReady(result.current)
+ await result.current.actions.downloadPayStub('payroll-stub-1')
+ })
+
+ await waitFor(() => {
+ expect(result.current.errorHandling.errors.length).toBeGreaterThan(0)
+ })
+ })
+})
diff --git a/src/components/Employee/Paystubs/shared/usePaystubsList/usePaystubsList.ts b/src/components/Employee/Paystubs/shared/usePaystubsList/usePaystubsList.ts
new file mode 100644
index 000000000..1b430dce4
--- /dev/null
+++ b/src/components/Employee/Paystubs/shared/usePaystubsList/usePaystubsList.ts
@@ -0,0 +1,115 @@
+import { useGustoEmbeddedContext } from '@gusto/embedded-api-v-2025-11-15/react-query/_context'
+import { usePayrollsGetPayStubs } from '@gusto/embedded-api-v-2025-11-15/react-query/payrollsGetPayStubs'
+import { payrollsGetPayStub } from '@gusto/embedded-api-v-2025-11-15/funcs/payrollsGetPayStub'
+import { useCallback, useMemo } from 'react'
+import type { GetV1EmployeesEmployeeUuidPayStubsResponse } from '@gusto/embedded-api-v-2025-11-15/models/operations/getv1employeesemployeeuuidpaystubs'
+import { useBaseSubmit } from '@/components/Base/useBaseSubmit'
+import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler'
+import { readableStreamToBlob } from '@/helpers/readableStreamToBlob'
+import { usePagination } from '@/hooks/usePagination/usePagination'
+import type { BaseHookReady, HookLoadingResult, HookSubmitResult } from '@/partner-hook-utils/types'
+import type {
+ PaginationControlProps,
+ PaginationItemsPerPage,
+} from '@/components/Common/PaginationControl/PaginationControlTypes'
+
+export type EmployeePayStub = NonNullable<
+ GetV1EmployeesEmployeeUuidPayStubsResponse['employeePayStubsList']
+>[number]
+
+export interface UsePaystubsListParams {
+ employeeId: string
+ /** Items per page for the paystubs list. Defaults to 10. */
+ defaultItemsPerPage?: PaginationItemsPerPage
+}
+
+export interface UsePaystubsListReady extends BaseHookReady<
+ { payStubs: EmployeePayStub[] },
+ { isFetching: boolean; isPending: boolean }
+> {
+ pagination: { payStubs?: PaginationControlProps }
+ actions: {
+ /** Fetch the paystub PDF for the given payroll. Returns a Blob on success
+ * so the caller decides how to surface it (open in a new tab, save to
+ * disk, embed, etc.). */
+ downloadPayStub: (payrollId: string) => Promise | undefined>
+ }
+}
+
+export type UsePaystubsListResult = HookLoadingResult | UsePaystubsListReady
+
+/**
+ * Data hook for the employee paystubs list. Returns the paginated list of
+ * paystubs and an action to download an individual paystub PDF. Used by the
+ * standalone `PaystubsCard` component and consumable directly by partners
+ * building a fully custom paystubs UI.
+ */
+export function usePaystubsList({
+ employeeId,
+ defaultItemsPerPage = 10,
+}: UsePaystubsListParams): UsePaystubsListResult {
+ const { currentPage, itemsPerPage, getPaginationProps } = usePagination({
+ defaultItemsPerPage,
+ })
+
+ const gustoEmbedded = useGustoEmbeddedContext()
+ const payStubsQuery = usePayrollsGetPayStubs(
+ { employeeId, page: currentPage, per: itemsPerPage },
+ { staleTime: Infinity },
+ )
+
+ const {
+ baseSubmitHandler,
+ error: submitError,
+ setError: setSubmitError,
+ } = useBaseSubmit('PaystubsList')
+
+ const payStubs = payStubsQuery.data?.employeePayStubsList ?? []
+
+ const payStubsPagination = useMemo(() => {
+ const headers = payStubsQuery.data?.httpMeta.response.headers
+ if (!headers) return undefined
+ return getPaginationProps(headers, payStubsQuery.isFetching)
+ }, [payStubsQuery.data?.httpMeta.response.headers, payStubsQuery.isFetching, getPaginationProps])
+
+ const errorHandling = composeErrorHandler([payStubsQuery], { submitError, setSubmitError })
+
+ const downloadPayStub = useCallback(
+ async (payrollId: string): Promise | undefined> => {
+ let submitResult: HookSubmitResult | undefined
+ await baseSubmitHandler(payrollId, async id => {
+ const response = await payrollsGetPayStub(gustoEmbedded, { payrollId: id, employeeId })
+ // `funcs/payrollsGetPayStub` returns a Result discriminated union;
+ // surface API/validation errors so `baseSubmitHandler` can route them
+ // through the standard SDK error pipeline instead of bubbling to the
+ // error boundary as an unknown throw.
+ if (!response.ok) {
+ throw response.error
+ }
+ if (!response.value.responseStream) {
+ throw new Error('Pay stub response missing PDF stream')
+ }
+ const pdfBlob = await readableStreamToBlob(response.value.responseStream, 'application/pdf')
+ submitResult = { mode: 'update', data: pdfBlob }
+ })
+ return submitResult
+ },
+ [baseSubmitHandler, gustoEmbedded, employeeId],
+ )
+
+ if (payStubsQuery.isLoading) {
+ return { isLoading: true, errorHandling }
+ }
+
+ return {
+ isLoading: false,
+ data: { payStubs },
+ status: {
+ isFetching: payStubsQuery.isFetching,
+ isPending: false,
+ },
+ pagination: { payStubs: payStubsPagination },
+ actions: { downloadPayStub },
+ errorHandling,
+ }
+}
diff --git a/src/components/Employee/exports/employeeManagement.ts b/src/components/Employee/exports/employeeManagement.ts
index caacb41e3..99b3dbe7b 100644
--- a/src/components/Employee/exports/employeeManagement.ts
+++ b/src/components/Employee/exports/employeeManagement.ts
@@ -31,6 +31,8 @@ export {
PaymentMethodSplitForm,
type PaymentMethodSplitFormProps,
} from '../PaymentMethod/management'
+export { PaystubsCard } from '../Paystubs/management'
+export type { PaystubsCardProps } from '../Paystubs/management'
export { TerminateEmployee } from '../Terminations/TerminateEmployee/TerminateEmployee'
export { TerminationSummary } from '../Terminations/TerminationSummary/TerminationSummary'
export { TerminationFlow } from '../Terminations/TerminationFlow/TerminationFlow'
diff --git a/src/i18n/en/Employee.Dashboard.json b/src/i18n/en/Employee.Dashboard.json
index 3ce557a7f..0410c03e6 100644
--- a/src/i18n/en/Employee.Dashboard.json
+++ b/src/i18n/en/Employee.Dashboard.json
@@ -85,22 +85,6 @@
"title": "No deductions",
"description": "Employee deductions will appear here"
}
- },
- "paystubs": {
- "title": "Paystubs",
- "listLabel": "List of paystubs",
- "payday": "Payday",
- "checkAmount": "Check amount",
- "grossPay": "Gross pay",
- "paymentMethod": "Payment method",
- "noPaymentMethod": "Not available",
- "downloadCta": "Download paystub",
- "downloadError": "Unable to download paystub",
- "downloadLoadingMessage": "Generating paystub…",
- "emptyState": {
- "title": "No paystubs",
- "description": "Paystubs will appear here after payroll is run"
- }
}
},
"taxes": {
diff --git a/src/i18n/en/Employee.Management.Paystubs.json b/src/i18n/en/Employee.Management.Paystubs.json
new file mode 100644
index 000000000..7f94ba6fe
--- /dev/null
+++ b/src/i18n/en/Employee.Management.Paystubs.json
@@ -0,0 +1,17 @@
+{
+ "title": "Paystubs",
+ "listLabel": "List of paystubs",
+ "payday": "Payday",
+ "checkAmount": "Check amount",
+ "grossPay": "Gross pay",
+ "paymentMethod": "Payment method",
+ "noPaymentMethod": "Not available",
+ "downloadCta": "Download paystub",
+ "downloadError": "Unable to download paystub",
+ "downloadLoadingMessage": "Generating paystub…",
+ "listEmptyPlaceholder": "No value",
+ "emptyState": {
+ "title": "No paystubs",
+ "description": "Paystubs will appear here after payroll is run"
+ }
+}
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index 8e6c34328..ec1ad3f8b 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -100,6 +100,9 @@ export const employeeEvents = {
'employee/management/paymentMethod/splitForm/cancelled',
EMPLOYEE_MANAGEMENT_PAYMENT_METHOD_ALERT_DISMISSED:
'employee/management/paymentMethod/alertDismissed',
+ EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOAD_REQUESTED:
+ 'employee/management/paystubs/card/downloadRequested',
+ EMPLOYEE_MANAGEMENT_PAYSTUBS_CARD_DOWNLOADED: 'employee/management/paystubs/card/downloaded',
} as const
export const companyEvents = {
diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts
index c897f1fb9..ee691ccda 100644
--- a/src/types/i18next.d.ts
+++ b/src/types/i18next.d.ts
@@ -1515,22 +1515,6 @@ export interface EmployeeDashboard{
"description":string;
};
};
-"paystubs":{
-"title":string;
-"listLabel":string;
-"payday":string;
-"checkAmount":string;
-"grossPay":string;
-"paymentMethod":string;
-"noPaymentMethod":string;
-"downloadCta":string;
-"downloadError":string;
-"downloadLoadingMessage":string;
-"emptyState":{
-"title":string;
-"description":string;
-};
-};
};
"taxes":{
"federal":{
@@ -2083,6 +2067,23 @@ export interface EmployeeManagementPaymentMethodSplitForm{
"percentageAmountError":string;
};
};
+export interface EmployeeManagementPaystubs{
+"title":string;
+"listLabel":string;
+"payday":string;
+"checkAmount":string;
+"grossPay":string;
+"paymentMethod":string;
+"noPaymentMethod":string;
+"downloadCta":string;
+"downloadError":string;
+"downloadLoadingMessage":string;
+"listEmptyPlaceholder":string;
+"emptyState":{
+"title":string;
+"description":string;
+};
+};
export interface EmployeeManagementWorkAddress{
"cardTitle":string;
"cardManageCta":string;
@@ -3669,6 +3670,6 @@ export interface common{
interface CustomTypeOptions {
defaultNS: 'common';
- resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Company.TimeOff.CreateTimeOffPolicy': CompanyTimeOffCreateTimeOffPolicy, 'Company.TimeOff.EmployeeTable': CompanyTimeOffEmployeeTable, 'Company.TimeOff.HolidayPolicy': CompanyTimeOffHolidayPolicy, 'Company.TimeOff.PolicyDetail': CompanyTimeOffPolicyDetail, 'Company.TimeOff.SelectEmployees': CompanyTimeOffSelectEmployees, 'Company.TimeOff.SelectPolicyType': CompanyTimeOffSelectPolicyType, 'Company.TimeOff.TimeOffPolicies': CompanyTimeOffTimeOffPolicies, 'Company.TimeOff.TimeOffPolicyDetails': CompanyTimeOffTimeOffPolicyDetails, 'Company.TimeOff.TimeOffRequests': CompanyTimeOffTimeOffRequests, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentSummary': ContractorPaymentsPaymentSummary, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Dashboard': EmployeeDashboard, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentManager': EmployeeDocumentManager, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeDocuments': EmployeeEmployeeDocuments, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.EmploymentEligibility': EmployeeEmploymentEligibility, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress.Management': EmployeeHomeAddressManagement, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.I9SignatureForm': EmployeeI9SignatureForm, 'Employee.Landing': EmployeeLanding, 'Employee.Management.PaymentMethod': EmployeeManagementPaymentMethod, 'Employee.Management.PaymentMethodBankForm': EmployeeManagementPaymentMethodBankForm, 'Employee.Management.PaymentMethodSplitForm': EmployeeManagementPaymentMethodSplitForm, 'Employee.Management.WorkAddress': EmployeeManagementWorkAddress, 'Employee.ManagementEmployeeList': EmployeeManagementEmployeeList, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile.Management': EmployeeProfileManagement, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Employee.Terminations.TerminateEmployee': EmployeeTerminationsTerminateEmployee, 'Employee.Terminations.TerminationFlow': EmployeeTerminationsTerminationFlow, 'Employee.Terminations.TerminationSummary': EmployeeTerminationsTerminationSummary, 'InformationRequests.InformationRequestForm': InformationRequestsInformationRequestForm, 'InformationRequests.InformationRequestList': InformationRequestsInformationRequestList, 'InformationRequests': InformationRequests, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.Dismissal': PayrollDismissal, 'Payroll.EmployeeSelection': PayrollEmployeeSelection, 'Payroll.GrossUpModal': PayrollGrossUpModal, 'Payroll.OffCycle': PayrollOffCycle, 'Payroll.OffCycleCreation': PayrollOffCycleCreation, 'Payroll.OffCycleDeductionsSetting': PayrollOffCycleDeductionsSetting, 'Payroll.OffCyclePayPeriodDateForm': PayrollOffCyclePayPeriodDateForm, 'Payroll.OffCycleReasonSelection': PayrollOffCycleReasonSelection, 'Payroll.OffCycleTaxWithholding': PayrollOffCycleTaxWithholding, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.Transition': PayrollTransition, 'Payroll.TransitionCreation': PayrollTransitionCreation, 'Payroll.TransitionPayrollAlert': PayrollTransitionPayrollAlert, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, }
+ resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Company.TimeOff.CreateTimeOffPolicy': CompanyTimeOffCreateTimeOffPolicy, 'Company.TimeOff.EmployeeTable': CompanyTimeOffEmployeeTable, 'Company.TimeOff.HolidayPolicy': CompanyTimeOffHolidayPolicy, 'Company.TimeOff.PolicyDetail': CompanyTimeOffPolicyDetail, 'Company.TimeOff.SelectEmployees': CompanyTimeOffSelectEmployees, 'Company.TimeOff.SelectPolicyType': CompanyTimeOffSelectPolicyType, 'Company.TimeOff.TimeOffPolicies': CompanyTimeOffTimeOffPolicies, 'Company.TimeOff.TimeOffPolicyDetails': CompanyTimeOffTimeOffPolicyDetails, 'Company.TimeOff.TimeOffRequests': CompanyTimeOffTimeOffRequests, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentSummary': ContractorPaymentsPaymentSummary, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Dashboard': EmployeeDashboard, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentManager': EmployeeDocumentManager, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeDocuments': EmployeeEmployeeDocuments, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.EmploymentEligibility': EmployeeEmploymentEligibility, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress.Management': EmployeeHomeAddressManagement, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.I9SignatureForm': EmployeeI9SignatureForm, 'Employee.Landing': EmployeeLanding, 'Employee.Management.PaymentMethod': EmployeeManagementPaymentMethod, 'Employee.Management.PaymentMethodBankForm': EmployeeManagementPaymentMethodBankForm, 'Employee.Management.PaymentMethodSplitForm': EmployeeManagementPaymentMethodSplitForm, 'Employee.Management.Paystubs': EmployeeManagementPaystubs, 'Employee.Management.WorkAddress': EmployeeManagementWorkAddress, 'Employee.ManagementEmployeeList': EmployeeManagementEmployeeList, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile.Management': EmployeeProfileManagement, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Employee.Terminations.TerminateEmployee': EmployeeTerminationsTerminateEmployee, 'Employee.Terminations.TerminationFlow': EmployeeTerminationsTerminationFlow, 'Employee.Terminations.TerminationSummary': EmployeeTerminationsTerminationSummary, 'InformationRequests.InformationRequestForm': InformationRequestsInformationRequestForm, 'InformationRequests.InformationRequestList': InformationRequestsInformationRequestList, 'InformationRequests': InformationRequests, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.Dismissal': PayrollDismissal, 'Payroll.EmployeeSelection': PayrollEmployeeSelection, 'Payroll.GrossUpModal': PayrollGrossUpModal, 'Payroll.OffCycle': PayrollOffCycle, 'Payroll.OffCycleCreation': PayrollOffCycleCreation, 'Payroll.OffCycleDeductionsSetting': PayrollOffCycleDeductionsSetting, 'Payroll.OffCyclePayPeriodDateForm': PayrollOffCyclePayPeriodDateForm, 'Payroll.OffCycleReasonSelection': PayrollOffCycleReasonSelection, 'Payroll.OffCycleTaxWithholding': PayrollOffCycleTaxWithholding, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.Transition': PayrollTransition, 'Payroll.TransitionCreation': PayrollTransitionCreation, 'Payroll.TransitionPayrollAlert': PayrollTransitionPayrollAlert, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, }
};
}
\ No newline at end of file