Skip to content

Conversation

@serikjensen
Copy link
Member

This adds responsive breadcrumb behavior. We do this by creating a prop on the component which indicates when the container size is small. Adapter consumers can choose to ignore this in favor of their own solutions. In our implementation, we respect the isSmallContainer prop and opt to render the next breadcrumb up to keep things more consolidated.

See figma here: https://www.figma.com/design/6dxOSiONDiJoa9zY1wOwbs/Frontend-SDK-Partner-Design-File?node-id=12047-15247&t=mPca0wqLqmxffKja-1Connect your Figma account

Proof of functionality

Screen.Recording.2025-10-23.at.10.57.36.AM.mov

Comment on lines 77 to 564
alerts = [],
}: PayrollOverviewProps) => {
const { Alert, Button, ButtonIcon, Dialog, Heading, Text, Tabs, LoadingSpinner } =
useComponentContext()
useI18n('Payroll.PayrollOverview')
const { locale } = useLocale()
const { t } = useTranslation('Payroll.PayrollOverview')
const formatCurrency = useNumberFormatter('currency')
const [selectedTab, setSelectedTab] = useState('companyPays')
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false)
const { LoadingIndicator } = useLoadingIndicator()

const totalPayroll = calculateTotalPayroll(payrollData)

const expectedDebitDate = payrollData.payrollStatusMeta?.expectedDebitTime
? parseDateStringToLocal(payrollData.payrollStatusMeta.expectedDebitTime)
: payrollData.payrollDeadline!

const getCompanyTaxes = (employeeCompensation: EmployeeCompensations) => {
return (
employeeCompensation.taxes?.reduce(
(acc, tax) => (tax.employer ? acc + tax.amount : acc),
0,
) ?? 0
)
}
const getCompanyBenefits = (employeeCompensation: EmployeeCompensations) => {
return (
employeeCompensation.benefits?.reduce(
(acc, benefit) => (benefit.companyContribution ? acc + benefit.companyContribution : acc),
0,
) ?? 0
)
}
const getReimbursements = (employeeCompensation: EmployeeCompensations) => {
return employeeCompensation.fixedCompensations?.length
? Number(
employeeCompensation.fixedCompensations.find(
c => c.name?.toLowerCase() === compensationTypeLabels.REIMBURSEMENT_NAME.toLowerCase(),
)?.amount || 0,
)
: 0
}

const getCompanyCost = (employeeCompensation: EmployeeCompensations) => {
return (
employeeCompensation.grossPay! +
getReimbursements(employeeCompensation) +
getCompanyTaxes(employeeCompensation) +
getCompanyBenefits(employeeCompensation)
)
}

const employeeMap = new Map(employeeDetails.map(employee => [employee.uuid, employee]))

const getEmployeeHours = (
employeeCompensations: EmployeeCompensations,
): Record<string, number> => {
return (
employeeCompensations.hourlyCompensations?.reduce(
(acc, hourlyCompensation) => {
if (typeof hourlyCompensation.name === 'undefined') {
return acc
}
const name = hourlyCompensation.name.toLowerCase()
const currentHours = acc[name] ?? 0
acc[name] = currentHours + Number(hourlyCompensation.hours || 0)
return acc
},
{} as Record<string, number>,
) || {}
)
}
const getEmployeePtoHours = (employeeCompensations: EmployeeCompensations) => {
return (
employeeCompensations.paidTimeOff?.reduce((acc, paidTimeOff) => {
return acc + Number(paidTimeOff.hours || 0)
}, 0) ?? 0
)
}

const checkPaymentsCount =
payrollData.employeeCompensations?.reduce(
(acc, comp) =>
!comp.excluded && comp.paymentMethod === PAYMENT_METHODS.check ? acc + 1 : acc,
0,
) ?? 0
const companyPaysColumns = [
{
key: 'employeeName',
title: t('tableHeaders.employees'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{firstLastName({
first_name: employeeMap.get(employeeCompensations.employeeUuid!)?.firstName,
last_name: employeeMap.get(employeeCompensations.employeeUuid!)?.lastName,
})}
</Text>
),
},
{
key: 'grossPay',
title: t('tableHeaders.grossPay'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>{formatCurrency(employeeCompensations.grossPay!)}</Text>
),
},
{
key: 'reimbursements',
title: t('tableHeaders.reimbursements'),
render: (employeeCompensation: EmployeeCompensations) => (
<Text>{formatCurrency(getReimbursements(employeeCompensation))}</Text>
),
},
{
key: 'companyTaxes',
title: t('tableHeaders.companyTaxes'),
render: (employeeCompensation: EmployeeCompensations) => (
<Text>{formatCurrency(getCompanyTaxes(employeeCompensation))}</Text>
),
},
{
key: 'companyBenefits',
title: t('tableHeaders.companyBenefits'),
render: (employeeCompensation: EmployeeCompensations) => (
<Text>{formatCurrency(getCompanyBenefits(employeeCompensation))}</Text>
),
},
{
key: 'companyPays',
title: t('tableHeaders.companyPays'),
render: (employeeCompensation: EmployeeCompensations) => (
<Text>{formatCurrency(getCompanyCost(employeeCompensation))}</Text>
),
},
]
if (isProcessed) {
companyPaysColumns.push({
key: 'paystubs',
title: t('tableHeaders.paystub'),
render: (employeeCompensations: EmployeeCompensations) => (
<ButtonIcon
aria-label={t('downloadPaystubLabel')}
variant="tertiary"
onClick={() => {
if (employeeCompensations.employeeUuid) {
onPaystubDownload(employeeCompensations.employeeUuid)
}
}}
>
<DownloadIcon />
</ButtonIcon>
),
})
}
const tabs = [
{
id: 'companyPays',
label: t('dataViews.companyPaysTab'),
content: (
<DataView
label={t('dataViews.companyPaysTable')}
columns={companyPaysColumns}
data={payrollData.employeeCompensations!}
footer={() => ({
employeeName: (
<>
<Text>{t('tableHeaders.footerTotalsLabel')}</Text>
<Text>{t('tableHeaders.footerTotalsDescription')}</Text>
</>
),
grossPay: <Text>{formatCurrency(Number(payrollData.totals?.grossPay ?? 0))}</Text>,
reimbursements: (
<Text>{formatCurrency(Number(payrollData.totals?.reimbursements ?? 0))}</Text>
),
companyTaxes: (
<Text>{formatCurrency(Number(payrollData.totals?.employerTaxes ?? 0))}</Text>
),
companyBenefits: (
<Text>{formatCurrency(Number(payrollData.totals?.benefits ?? 0))}</Text>
),
companyPays: <Text>{formatCurrency(totalPayroll)}</Text>,
})}
/>
),
},
{
id: 'hoursWorked',
label: t('dataViews.hoursWorkedTab'),
content: (
<DataView
label={t('dataViews.hoursWorkedTable')}
columns={[
{
title: t('tableHeaders.employees'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{firstLastName({
first_name: employeeMap.get(employeeCompensations.employeeUuid!)?.firstName,
last_name: employeeMap.get(employeeCompensations.employeeUuid!)?.lastName,
})}
</Text>
),
},
{
title: t('tableHeaders.compensationType'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{employeeMap
.get(employeeCompensations.employeeUuid!)
?.jobs?.reduce((acc, job) => {
if (job.primary) {
const flsaStatus = job.compensations?.find(
comp => comp.uuid === job.currentCompensationUuid,
)?.flsaStatus

switch (flsaStatus) {
case FlsaStatus.EXEMPT:
return t('compensationTypeLabels.exempt')
case FlsaStatus.NONEXEMPT:
return t('compensationTypeLabels.nonexempt')
default:
return flsaStatus ?? ''
}
}
return acc
}, '')}
</Text>
),
},
{
title: t('tableHeaders.regular'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{getEmployeeHours(employeeCompensations)[
compensationTypeLabels.REGULAR_HOURS_NAME
] || 0}
</Text>
),
},
{
title: t('tableHeaders.overtime'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{getEmployeeHours(employeeCompensations)[compensationTypeLabels.OVERTIME_NAME] ||
0}
</Text>
),
},
{
title: t('tableHeaders.doubleOT'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{getEmployeeHours(employeeCompensations)[
compensationTypeLabels.DOUBLE_OVERTIME_NAME
] || 0}
</Text>
),
},
{
title: t('tableHeaders.timeOff'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>{getEmployeePtoHours(employeeCompensations)}</Text>
),
},
{
title: t('tableHeaders.totalHours'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{Object.values(getEmployeeHours(employeeCompensations)).reduce(
(acc, hours) => acc + hours,
0,
) + getEmployeePtoHours(employeeCompensations)}
</Text>
),
},
]}
data={payrollData.employeeCompensations!}
/>
),
},
{
id: 'employeeTakeHome',
label: t('dataViews.employeeTakeHomeTab'),
content: (
<DataView
label={t('dataViews.employeeTakeHomeTable')}
columns={[
{
title: t('tableHeaders.employees'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{firstLastName({
first_name: employeeMap.get(employeeCompensations.employeeUuid!)?.firstName,
last_name: employeeMap.get(employeeCompensations.employeeUuid!)?.lastName,
})}
</Text>
),
},
{
title: t('tableHeaders.paymentType'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>{employeeCompensations.paymentMethod ?? ''}</Text>
),
},
{
title: t('tableHeaders.grossPay'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>{formatCurrency(employeeCompensations.grossPay ?? 0)}</Text>
),
},
{
title: t('tableHeaders.deductions'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{formatCurrency(
employeeCompensations.deductions?.reduce(
(acc, deduction) => acc + deduction.amount!,
0,
) ?? 0,
)}
</Text>
),
},
{
title: t('tableHeaders.reimbursements'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>{formatCurrency(getReimbursements(employeeCompensations))}</Text>
),
},
{
title: t('tableHeaders.employeeTaxes'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{formatCurrency(
employeeCompensations.taxes?.reduce(
(acc, tax) => (tax.employer ? acc : acc + tax.amount),
0,
) ?? 0,
)}
</Text>
),
},
{
title: t('tableHeaders.employeeBenefits'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>
{formatCurrency(
employeeCompensations.benefits?.reduce(
(acc, benefit) => acc + (benefit.employeeDeduction ?? 0),
0,
) ?? 0,
)}
</Text>
),
},
{
title: t('tableHeaders.payment'),
render: (employeeCompensations: EmployeeCompensations) => (
<Text>{formatCurrency(employeeCompensations.netPay ?? 0)}</Text>
),
},
]}
data={payrollData.employeeCompensations!}
/>
),
},
{
id: 'taxes',
label: t('dataViews.taxesTab'),
content: (
<Flex flexDirection="column" gap={32}>
<DataView
label={t('dataViews.taxesTable')}
columns={[
{
key: 'taxDescription',
title: t('tableHeaders.taxDescription'),
render: taxKey => <Text>{taxKey}</Text>,
},
{
key: 'byYourEmployees',
title: t('tableHeaders.byYourEmployees'),
render: taxKey => <Text>{formatCurrency(taxes[taxKey]?.employee ?? 0)}</Text>,
},
{
key: 'byYourCompany',
title: t('tableHeaders.byYourCompany'),
render: taxKey => <Text>{formatCurrency(taxes[taxKey]?.employer ?? 0)}</Text>,
},
]}
footer={() => ({
taxDescription: <Text>{t('totalsLabel')}</Text>,
byYourEmployees: (
<Text>{formatCurrency(Number(payrollData.totals?.employeeTaxes ?? 0))}</Text>
),
byYourCompany: (
<Text>{formatCurrency(Number(payrollData.totals?.employerTaxes ?? 0))}</Text>
),
})}
data={Object.keys(taxes)}
/>

<DataView
label={t('dataViews.debitedTable')}
columns={[
{
title: t('tableHeaders.debitedByGusto'),
render: ({ label }) => <Text>{label}</Text>,
},
{
title: t('tableHeaders.taxesTotal'),
render: ({ value }) => <Text>{formatCurrency(Number(value))}</Text>,
},
]}
data={[
{ label: t('directDepositLabel'), value: payrollData.totals?.netPayDebit || '0' },
{
label: t('reimbursementLabel'),
value: payrollData.totals?.reimbursementDebit || '0',
},
{
label: t('garnishmentsLabel'),
value: payrollData.totals?.childSupportDebit || '0',
},
{ label: t('taxesLabel'), value: payrollData.totals?.taxDebit || '0' },
]}
/>
</Flex>
),
},
]

return (
<Flex flexDirection="column" alignItems="stretch">
<Flex justifyContent="space-between">
<FlexItem flexGrow={1}>
<Heading as="h1">{isProcessed ? t('summaryTitle') : t('overviewTitle')}</Heading>
<Text>
<Trans
i18nKey="pageSubtitle"
t={t}
components={{ dateWrapper: <Text weight="bold" as="span" /> }}
values={getPayrollOverviewTitle({ payPeriod: payrollData.payPeriod, locale })}
/>
</Text>
</FlexItem>
<FlexItem flexGrow={1}>
<Flex justifyContent="flex-end">
{isProcessed ? (
<>
<Button onClick={onPayrollReceipt} variant="secondary" isDisabled={isSubmitting}>
{t('payrollReceiptCta')}
</Button>
<Button
onClick={() => {
setIsCancelDialogOpen(true)
}}
variant="error"
isDisabled={isSubmitting}
>
{t('cancelCta')}
</Button>
</>
) : (
<>
<Button onClick={onEdit} variant="secondary" isDisabled={isSubmitting}>
{t('editCta')}
</Button>
<Button onClick={onSubmit} isDisabled={isSubmitting}>
{t('submitCta')}
</Button>
</>
)}
</Flex>
</FlexItem>
</Flex>
{isSubmitting ? (
<LoadingIndicator>
<Flex flexDirection="column" alignItems="center" gap={4}>
<LoadingSpinner size="lg" />
<Heading as="h4">{t('loadingTitle')}</Heading>
<Text>{t('loadingDescription')}</Text>
</Flex>
</LoadingIndicator>
) : (
<>
{alerts?.length && alerts.length > 0 && (
{alerts.length > 0 && (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small non related fix to prevent a "0" from rendering unintentionally here

@serikjensen serikjensen merged commit bb252d4 into main Oct 23, 2025
3 checks passed
@serikjensen serikjensen deleted the feat/GWS-5762 branch October 23, 2025 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants