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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/assets/icons/spinner_large.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/components/Common/Loading/Loading.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
.skeletonBox {
height: 200px;
width: 100%;
padding: toReam(10);
display: flex;
align-items: center;
justify-content: center;
}

@keyframes pulse {
Expand Down
4 changes: 4 additions & 0 deletions src/components/Common/Loading/Loading.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export default {
export const Default = () => {
return <Loading />
}

export const WithChildren = () => {
return <Loading>Child content</Loading>
}
9 changes: 7 additions & 2 deletions src/components/Common/Loading/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import type { ReactNode } from 'react'
import styles from './Loading.module.scss'
import { FadeIn } from '@/components/Common/FadeIn/FadeIn'

export const Loading = () => {
export interface LoadingProps {
children?: ReactNode
}

export const Loading = ({ children }: LoadingProps) => {
const { t } = useTranslation('common')
return (
<FadeIn>
Expand All @@ -13,7 +18,7 @@ export const Loading = () => {
aria-live="polite"
aria-busy
>
<div className={cn(styles.skeleton, styles.skeletonBox)}></div>
<div className={cn(styles.skeleton, styles.skeletonBox)}>{children}</div>
</section>
</FadeIn>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ export const PayrollConfigurationStory = () => {
endDate: '2025-08-13',
payScheduleUuid: 'test-pay-schedule-uuid',
}}
onBack={action('on_back')}
onCalculatePayroll={action('on_calculate')}
onEdit={action('on_edit')}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { useEffect, type ReactNode } from 'react'
import { useEffect, useState, type ReactNode } from 'react'
import { useEmployeesListSuspense } from '@gusto/embedded-api/react-query/employeesList'
import { usePayrollsGetSuspense } from '@gusto/embedded-api/react-query/payrollsGet'
import { usePayrollsCalculateMutation } from '@gusto/embedded-api/react-query/payrollsCalculate'
import type { Employee } from '@gusto/embedded-api/models/components/employee'
import type { PayrollProcessingRequest } from '@gusto/embedded-api/models/components/payrollprocessingrequest'
import { PayrollProcessingRequestStatus } from '@gusto/embedded-api/models/components/payrollprocessingrequest'
import type { GetV1CompaniesCompanyIdPayrollsPayrollIdResponse } from '@gusto/embedded-api/models/operations/getv1companiescompanyidpayrollspayrollid'
import { useTranslation } from 'react-i18next'
import { usePreparedPayrollData } from '../usePreparedPayrollData'
import { PayrollConfigurationPresentation } from './PayrollConfigurationPresentation'
import type { BaseComponentInterface } from '@/components/Base/Base'
import { BaseComponent } from '@/components/Base/Base'
import { useBase } from '@/components/Base/useBase'
import { componentEvents } from '@/shared/constants'
import { useComponentDictionary, useI18n } from '@/i18n'
import { useBase } from '@/components/Base'

const isCalculating = (payrollData?: GetV1CompaniesCompanyIdPayrollsPayrollIdResponse) =>
payrollData?.payrollShow?.processingRequest?.status === PayrollProcessingRequestStatus.Calculating
const isCalculated = (payrollData?: GetV1CompaniesCompanyIdPayrollsPayrollIdResponse) =>
payrollData?.payrollShow?.processingRequest?.status ===
PayrollProcessingRequestStatus.CalculateSuccess
const isCalculating = (processingRequest?: PayrollProcessingRequest | null) =>
processingRequest?.status === PayrollProcessingRequestStatus.Calculating
const isCalculated = (processingRequest?: PayrollProcessingRequest | null) =>
processingRequest?.status === PayrollProcessingRequestStatus.CalculateSuccess

interface PayrollConfigurationProps extends BaseComponentInterface<'Payroll.PayrollConfiguration'> {
companyId: string
Expand All @@ -44,66 +43,81 @@ export const Root = ({
useComponentDictionary('Payroll.PayrollConfiguration', dictionary)
useI18n('Payroll.PayrollConfiguration')
const { t } = useTranslation('Payroll.PayrollConfiguration')
const { baseSubmitHandler } = useBase()

const { LoadingIndicator } = useBase()
const [isPolling, setIsPolling] = useState(false)
const { data: employeeData } = useEmployeesListSuspense({
companyId,
})

const { data: payrollData } = usePayrollsGetSuspense(
{
companyId,
payrollId,
include: ['taxes', 'benefits', 'deductions'],
},
{
refetchInterval: query => (isCalculating(query.state.data) ? 5_000 : false),
},
{ refetchInterval: isPolling ? 5_000 : false },
)

const { data: employeeData } = useEmployeesListSuspense({
companyId,
})

const { mutateAsync: calculatePayroll } = usePayrollsCalculateMutation()

const {
preparedPayroll,
paySchedule,
isLoading: isPreparedPayrollDataLoading,
isLoading: isPrepareLoading,
} = usePreparedPayrollData({
companyId,
payrollId,
})

const onBack = () => {
onEvent(componentEvents.RUN_PAYROLL_BACK)
}
const onCalculatePayroll = async () => {
await calculatePayroll({
request: {
companyId,
payrollId,
},
await baseSubmitHandler(null, async () => {
await calculatePayroll({
request: {
companyId,
payrollId,
},
})
setIsPolling(true)
})
}
const onEdit = (employee: Employee) => {
onEvent(componentEvents.RUN_PAYROLL_EMPLOYEE_EDIT, { employeeId: employee.uuid })
}

useEffect(() => {
if (isCalculated(payrollData)) {
// Start polling when payroll is calculating and not already polling
if (isCalculating(payrollData.payrollShow?.processingRequest) && !isPolling) {
setIsPolling(true)
}
// Stop polling and emit event when payroll is calculated successfully
if (isPolling && isCalculated(payrollData.payrollShow?.processingRequest)) {
onEvent(componentEvents.RUN_PAYROLL_CALCULATED, {
payrollId,
alert: { type: 'success', title: t('alerts.progressSaved') },
})
setIsPolling(false)
}
}, [payrollData, onEvent, payrollId, t])

if (isPreparedPayrollDataLoading || isCalculating(payrollData)) {
return <LoadingIndicator />
}
// If we are polling and payroll is in failed state, stop polling, and emit failure event
if (
isPolling &&
payrollData.payrollShow?.processingRequest?.status ===
PayrollProcessingRequestStatus.ProcessingFailed
) {
onEvent(componentEvents.RUN_PAYROLL_PROCESSING_FAILED)
setIsPolling(false)
}
}, [
payrollData.payrollShow?.processingRequest,
isPolling,
onEvent,
t,
payrollId,
payrollData.payrollShow?.calculatedAt,
])

return (
<PayrollConfigurationPresentation
onBack={onBack}
onCalculatePayroll={onCalculatePayroll}
onEdit={onEdit}
employeeCompensations={preparedPayroll?.employeeCompensations || []}
Expand All @@ -112,6 +126,7 @@ export const Root = ({
paySchedule={paySchedule}
isOffCycle={preparedPayroll?.offCycle}
alerts={alerts}
isPending={isPolling || isPrepareLoading}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('PayrollConfigurationPresentation', () => {
await waitFor(() => {
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument()
})
expect(screen.getByText(/Run payroll for/)).toBeInTheDocument()
expect(screen.getByText(/Regular payroll for/)).toBeInTheDocument()
})

it('displays employee information correctly', async () => {
Expand Down Expand Up @@ -150,17 +150,6 @@ describe('PayrollConfigurationPresentation', () => {
expect(onCalculatePayroll).toHaveBeenCalled()
})

it('calls onBack when back button is clicked', async () => {
const onBack = vi.fn()
const user = userEvent.setup()
renderWithProviders(<PayrollConfigurationPresentation {...defaultProps} onBack={onBack} />)

const backButton = await waitFor(() => screen.getByText('Back'))
await user.click(backButton)

expect(onBack).toHaveBeenCalled()
})

it('configures onEdit callback correctly for DataView interaction', async () => {
const onEdit = vi.fn()
const user = userEvent.setup()
Expand Down
Loading