Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions src/assets/icons/slash-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ export function CreatePaymentContextual() {
}

export function PaymentHistoryContextual() {
const { companyId, currentPaymentId, onEvent } = useFlow<PaymentFlowContextInterface>()
return (
<PaymentHistory
onEvent={onEvent}
companyId={ensureRequired(companyId)}
paymentId={ensureRequired(currentPaymentId)}
/>
)
const { currentPaymentId, onEvent } = useFlow<PaymentFlowContextInterface>()
return <PaymentHistory onEvent={onEvent} paymentId={ensureRequired(currentPaymentId)} />
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useContractorPaymentGroupsGetSuspense } from '@gusto/embedded-api/react-query/contractorPaymentGroupsGet'
import { useContractorsListSuspense } from '@gusto/embedded-api/react-query/contractorsList'
import { useTranslation } from 'react-i18next'
import { PaymentHistoryPresentation } from './PaymentHistoryPresentation'
import { useComponentDictionary } from '@/i18n'
import { BaseComponent, type BaseComponentInterface } from '@/components/Base'
import { componentEvents, ContractorOnboardingStatus } from '@/shared/constants'

interface PaymentHistoryProps extends BaseComponentInterface<'Contractor.Payments.PaymentHistory'> {
companyId: string
paymentId: string
}

Expand All @@ -15,17 +18,41 @@ export function PaymentHistory(props: PaymentHistoryProps) {
)
}

export const Root = ({ companyId, paymentId, dictionary, onEvent }: PaymentHistoryProps) => {
export const Root = ({ paymentId, dictionary, onEvent }: PaymentHistoryProps) => {
useComponentDictionary('Contractor.Payments.PaymentHistory', dictionary)
const { t } = useTranslation('Contractor.Payments.PaymentHistory')

const { data: paymentGroupResponse } = useContractorPaymentGroupsGetSuspense({
contractorPaymentGroupUuid: paymentId,
})
if (!paymentGroupResponse.contractorPaymentGroup) {
throw new Error(t('errors.paymentGroupNotFound'))
}

const companyId = paymentGroupResponse.contractorPaymentGroup.companyUuid!

const { data: contractorList } = useContractorsListSuspense({ companyUuid: companyId })
const contractors = (contractorList.contractorList || []).filter(
contractor =>
contractor.isActive &&
contractor.onboardingStatus === ContractorOnboardingStatus.ONBOARDING_COMPLETED,
)

const handleViewPayment = (paymentId: string) => {
onEvent(componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS, { paymentId })
}

const handleCancelPayment = (paymentId: string) => {
onEvent(componentEvents.CONTRACTOR_PAYMENT_CANCEL, { paymentId })
}

return (
<>
<PaymentHistoryPresentation
date={'2025-12-17'}
payments={[]}
onBack={() => {}}
onViewPayment={() => {}}
onCancelPayment={() => {}}
paymentGroup={paymentGroupResponse.contractorPaymentGroup}
contractors={contractors}
onViewPayment={handleViewPayment}
onCancelPayment={handleCancelPayment}
/>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.icon {
color: var(--g-colorBodySubContent);
height: toRem(16);
width: toRem(16);
display: flex;
align-items: center;
justify-content: center;
}
Original file line number Diff line number Diff line change
@@ -1,76 +1,76 @@
import { useTranslation } from 'react-i18next'
import { DataView, Flex, EmptyData, ActionsLayout } from '@/components/Common'
import { Trans, useTranslation } from 'react-i18next'
import type { ContractorPaymentGroup } from '@gusto/embedded-api/models/components/contractorpaymentgroup'
import type { Contractor } from '@gusto/embedded-api/models/components/contractor'
import { getContractorDisplayName } from '../CreatePayment/helpers'
import styles from './PaymentHistoryPresentation.module.scss'
import { DataView, Flex, EmptyData } from '@/components/Common'
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
import { HamburgerMenu } from '@/components/Common/HamburgerMenu'
import { useI18n } from '@/i18n'
import { formatNumberAsCurrency } from '@/helpers/formattedStrings'
import { useLocale } from '@/contexts/LocaleProvider/useLocale'
import { formatHoursDisplay } from '@/components/Payroll/helpers'

interface PaymentData {
id: string
name: string
wageType: string
paymentMethod: string
hours: number
wage: string
bonus: number
reimbursement: number
total: number
}
import useNumberFormatter from '@/hooks/useNumberFormatter'
import { useDateFormatter } from '@/hooks/useDateFormatter'
import EyeIcon from '@/assets/icons/eye.svg?react'
import CancelIcon from '@/assets/icons/slash-circle.svg?react'

interface PaymentHistoryPresentationProps {
date: string
payments: PaymentData[]
onBack: () => void
paymentGroup: ContractorPaymentGroup
contractors: Contractor[]
onViewPayment: (paymentId: string) => void
onCancelPayment: (paymentId: string) => void
}

export const PaymentHistoryPresentation = ({
date,
payments,
onBack,
paymentGroup,
contractors,
onViewPayment,
onCancelPayment,
}: PaymentHistoryPresentationProps) => {
const { Button, Text, Heading } = useComponentContext()
useI18n('Contractor.Payments.PaymentHistory')
const { t } = useTranslation('Contractor.Payments.PaymentHistory')
const { locale } = useLocale()
const currencyFormatter = useNumberFormatter('currency')
const { formatLongWithYear } = useDateFormatter()

const payments = paymentGroup.contractorPayments || []

return (
<Flex flexDirection="column" gap={32}>
<Flex flexDirection="column" gap={8}>
<Heading as="h1">{t('title')}</Heading>
<Text>{t('subtitle', { date })}</Text>
<Text>
<Trans
i18nKey={'subtitle'}
t={t}
values={{ date: formatLongWithYear(paymentGroup.debitDate) }}
components={{
strong: <Text weight="bold" as="span" />,
}}
/>
</Text>
</Flex>

<Flex flexDirection="column" gap={16}>
<Heading as="h2">{t('paymentsSection')}</Heading>

{payments.length === 0 ? (
<EmptyData title={t('noPaymentsFound')} description={t('noPaymentsDescription')}>
<ActionsLayout justifyContent="center">
<Button variant="primary" onClick={onBack}>
{t('backButton')}
</Button>
</ActionsLayout>
</EmptyData>
<EmptyData title={t('noPaymentsFound')} description={t('noPaymentsDescription')} />
) : (
<>
<DataView
columns={[
{
title: t('tableHeaders.contractor'),
render: ({ name, id }) => (
render: ({ contractorUuid }) => (
<Button
variant="tertiary"
onClick={() => {
onViewPayment(id)
onViewPayment(contractorUuid!)
}}
>
{name}
{getContractorDisplayName(
contractors.find(contractor => contractor.uuid === contractorUuid),
)}
</Button>
),
},
Expand All @@ -84,7 +84,9 @@ export const PaymentHistoryPresentation = ({
},
{
title: t('tableHeaders.hours'),
render: ({ hours }) => <Text>{hours ? formatHoursDisplay(hours) : '–'}</Text>,
render: ({ hours }) => (
<Text>{hours ? formatHoursDisplay(Number(hours)) : '–'}</Text>
),
},
{
title: t('tableHeaders.wage'),
Expand All @@ -93,51 +95,61 @@ export const PaymentHistoryPresentation = ({
{
title: t('tableHeaders.bonus'),
render: ({ bonus }) => (
<Text>{bonus ? formatNumberAsCurrency(bonus, locale) : '–'}</Text>
<Text>{bonus ? currencyFormatter(Number(bonus)) : '–'}</Text>
),
},
{
title: t('tableHeaders.reimbursements'),
render: ({ reimbursement }) => (
<Text>{formatNumberAsCurrency(reimbursement, locale)}</Text>
<Text>{reimbursement ? currencyFormatter(Number(reimbursement)) : '–'}</Text>
),
},
{
title: t('tableHeaders.total'),
render: ({ total }) => <Text>{formatNumberAsCurrency(total, locale)}</Text>,
},
{
title: t('tableHeaders.action'),
render: ({ id, name }) => (
<HamburgerMenu
items={[
{
label: t('actions.view'),
onClick: () => {
onViewPayment(id)
},
},
{
label: t('actions.cancel'),
onClick: () => {
onCancelPayment(id)
},
},
]}
triggerLabel={t('tableHeaders.action')}
/>
render: ({ wageTotal, reimbursement, bonus }) => (
<Text>
{wageTotal
? currencyFormatter(
Number(wageTotal) + Number(reimbursement) + Number(bonus),
)
: '–'}
</Text>
),
Comment on lines +109 to 117
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The total calculation assumes reimbursement and bonus are always defined. If either is undefined or null, Number() will convert them to NaN, resulting in an incorrect total. These values should be coerced to 0 when falsy before addition.

Copilot uses AI. Check for mistakes.
},
]}
itemMenu={({ contractorUuid, mayCancel }) => {
const items = [
{
label: t('actions.view'),
onClick: () => {
onViewPayment(contractorUuid!)
},
icon: (
<span className={styles.icon}>
<EyeIcon aria-hidden />
</span>
),
},
]

if (mayCancel) {
items.push({
label: t('actions.cancel'),
onClick: () => {
onCancelPayment(contractorUuid!)
},
icon: (
<span className={styles.icon}>
<CancelIcon aria-hidden />
</span>
),
})
}
return <HamburgerMenu items={items} triggerLabel={t('tableHeaders.action')} />
}}
data={payments}
label={t('title')}
/>

<Flex>
<Button onClick={onBack} variant="secondary">
{t('backButton')}
</Button>
</Flex>
</>
)}
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,20 @@ export const PaymentsListPresentation = ({
title: t('wageTotalColumnLabel'),
render: ({ totals }) => <Text>{currencyFormatter(Number(totals?.wageAmount || 0))}</Text>,
},
{
title: t('actionColumnLabel'),
render: ({ uuid }) => (
<Text>
<ButtonIcon
aria-label={t('viewPaymentCta')}
variant="tertiary"
onClick={() => {
onViewPayment(uuid || '')
}}
>
<EyeIcon aria-hidden />
</ButtonIcon>
</Text>
),
},
],
itemMenu: ({ uuid }) => (
<Text>
<ButtonIcon
aria-label={t('viewPaymentCta')}
variant="tertiary"
onClick={() => {
onViewPayment(uuid || '')
}}
>
<EyeIcon aria-hidden />
</ButtonIcon>
</Text>
),
emptyState: () => (
<EmptyData title={t('noPaymentsFound')} description={t('noPaymentsDescription')}>
<ActionsLayout justifyContent="center">
Expand Down
8 changes: 5 additions & 3 deletions src/i18n/en/Contractor.Payments.PaymentHistory.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
{
"title": "Contractor payment history",
"subtitle": "Payments created on {{date}}",
"subtitle": "Payments debited on <strong>{{date}}</strong>",
"paymentsSection": "Payments",
"breadcrumbLabel": "Payment history",
"noPaymentsFound": "No payments found",
"noPaymentsDescription": "There are no payments for this date.",
"backButton": "Back",
"tableHeaders": {
"contractor": "Contractor",
"wageType": "Wage type",
Expand All @@ -19,6 +18,9 @@
},
"actions": {
"view": "View",
"cancel": "Cancel"
"cancel": "Cancel payment"
},
"errors": {
"paymentGroupNotFound": "Contractor payment group not found"
}
}
2 changes: 2 additions & 0 deletions src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export const contractorPaymentEvents = {
CONTRACTOR_PAYMENT_CREATED: 'contractor/payments/created',
CONTRACTOR_PAYMENT_SUBMIT: 'contractor/payments/submit',
CONTRACTOR_PAYMENT_VIEW: 'contractor/payments/view',
CONTRACTOR_PAYMENT_VIEW_DETAILS: 'contractor/payments/view/details',
CONTRACTOR_PAYMENT_CANCEL: 'contractor/payments/cancel',
} as const

export const payScheduleEvents = {
Expand Down
Loading