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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PaymentsList } from '../PaymentsList/PaymentsList'
import { CreatePayment } from '../CreatePayment/CreatePayment'
import { PaymentHistory } from '../PaymentHistory/PaymentHistory'
import { PaymentStatement } from '../PaymentStatement/PaymentStatement'
import type { InternalAlert } from '../types'
import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow'
import type { BaseComponentInterface } from '@/components/Base'
Expand All @@ -15,6 +16,7 @@ export interface PaymentFlowContextInterface extends FlowContextInterface {
companyId: string
breadcrumbs?: BreadcrumbTrail
currentPaymentId?: string
currentContractorUuid?: string
alerts?: InternalAlert[]
}

Expand All @@ -32,3 +34,15 @@ export function PaymentHistoryContextual() {
const { currentPaymentId, onEvent } = useFlow<PaymentFlowContextInterface>()
return <PaymentHistory onEvent={onEvent} paymentId={ensureRequired(currentPaymentId)} />
}

export function PaymentStatementContextual() {
const { currentPaymentId, currentContractorUuid, onEvent } =
useFlow<PaymentFlowContextInterface>()
return (
<PaymentStatement
onEvent={onEvent}
paymentGroupId={ensureRequired(currentPaymentId)}
contractorUuid={ensureRequired(currentContractorUuid)}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type PaymentFlowContextInterface,
PaymentHistoryContextual,
PaymentListContextual,
PaymentStatementContextual,
} from './PaymentFlowComponents'
import { componentEvents } from '@/shared/constants'
import type { MachineEventType, MachineTransition } from '@/types/Helpers'
Expand All @@ -20,6 +21,10 @@ type EventPayloads = {
[componentEvents.CONTRACTOR_PAYMENT_SUBMIT]: undefined
[componentEvents.CONTRACTOR_PAYMENT_CREATED]: ContractorPaymentGroup
[componentEvents.CONTRACTOR_PAYMENT_VIEW]: { paymentId: string }
[componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS]: {
contractorUuid: string
paymentGroupId: string
}
[componentEvents.BREADCRUMB_NAVIGATE]: {
key: string
onNavigate: (ctx: PaymentFlowContextInterface) => PaymentFlowContextInterface
Expand Down Expand Up @@ -78,9 +83,21 @@ export const paymentFlowBreadcrumbsNodes: BreadcrumbNodes = {
history: {
parent: 'landing',
item: {
id: 'receipts',
id: 'history',
label: 'breadcrumbLabel',
namespace: 'Contractor.Payments.PaymentHistory',
onNavigate: ((ctx: PaymentFlowContextInterface) => ({
...updateBreadcrumbs('history', ctx),
component: PaymentHistoryContextual,
})) as (context: unknown) => unknown,
},
},
statement: {
parent: 'history',
item: {
id: 'statement',
label: 'breadcrumbLabel',
namespace: 'Contractor.Payments.PaymentStatement',
},
},
} as const
Expand Down Expand Up @@ -163,5 +180,33 @@ export const paymentMachine = {
),
breadcrumbNavigateTransition('landing'),
),
history: state(),
history: state<MachineTransition>(
transition(
componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS,
'statement',
reduce(
(
ctx: PaymentFlowContextInterface,
ev: MachineEventType<
EventPayloads,
typeof componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS
>,
): PaymentFlowContextInterface => {
return {
...updateBreadcrumbs('statement', ctx),
component: PaymentStatementContextual,
currentContractorUuid: ev.payload.contractorUuid,
currentPaymentId: ev.payload.paymentGroupId,
progressBarType: 'breadcrumbs',
alerts: undefined,
}
},
),
),
breadcrumbNavigateTransition('landing'),
),
statement: state<MachineTransition>(
breadcrumbNavigateTransition('landing'),
breadcrumbNavigateTransition('history'),
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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'
import { componentEvents } from '@/shared/constants'

interface PaymentHistoryProps extends BaseComponentInterface<'Contractor.Payments.PaymentHistory'> {
paymentId: string
Expand Down Expand Up @@ -32,14 +32,13 @@ export const Root = ({ paymentId, dictionary, onEvent }: PaymentHistoryProps) =>
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 contractors = contractorList.contractorList || []

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

const handleCancelPayment = (paymentId: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { formatHoursDisplay } from '@/components/Payroll/helpers'
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'
// import CancelIcon from '@/assets/icons/slash-circle.svg?react'

interface PaymentHistoryPresentationProps {
paymentGroup: ContractorPaymentGroup
Expand All @@ -37,7 +37,7 @@ export const PaymentHistoryPresentation = ({
return (
<Flex flexDirection="column" gap={32}>
<Flex flexDirection="column" gap={8}>
<Heading as="h1">{t('title')}</Heading>
<Heading as="h2">{t('title')}</Heading>
<Text>
<Trans
i18nKey={'subtitle'}
Expand Down Expand Up @@ -131,20 +131,20 @@ export const PaymentHistoryPresentation = ({
),
},
]

if (mayCancel) {
items.push({
label: t('actions.cancel'),
onClick: () => {
onCancelPayment(contractorUuid!)
},
icon: (
<span className={styles.icon}>
<CancelIcon aria-hidden />
</span>
),
})
}
// TODO: Waiting for new UX for cancelling payments
// 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}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useContractorPaymentGroupsGetSuspense } from '@gusto/embedded-api/react-query/contractorPaymentGroupsGet'
import { useContractorsListSuspense } from '@gusto/embedded-api/react-query/contractorsList'
import { useTranslation } from 'react-i18next'
import { PaymentStatementPresentation } from './PaymentStatementPresentation'
import { useComponentDictionary } from '@/i18n'
import { BaseComponent, type BaseComponentInterface } from '@/components/Base'

interface PaymentStatementProps extends BaseComponentInterface<'Contractor.Payments.PaymentStatement'> {
paymentGroupId: string
contractorUuid: string
}

export function PaymentStatement(props: PaymentStatementProps) {
return (
<BaseComponent {...props}>
<Root {...props}>{props.children}</Root>
</BaseComponent>
)
}

export const Root = ({ paymentGroupId, contractorUuid, dictionary }: PaymentStatementProps) => {
useComponentDictionary('Contractor.Payments.PaymentStatement', dictionary)
const { t } = useTranslation('Contractor.Payments.PaymentStatement')

const { data: paymentGroupResponse } = useContractorPaymentGroupsGetSuspense({
contractorPaymentGroupUuid: paymentGroupId,
})

if (!paymentGroupResponse.contractorPaymentGroup) {
throw new Error(t('errors.paymentGroupNotFound'))
}

const companyId = paymentGroupResponse.contractorPaymentGroup.companyUuid!

const { data: contractorList } = useContractorsListSuspense({ companyUuid: companyId })
const contractors = contractorList.contractorList || []

const payment = paymentGroupResponse.contractorPaymentGroup.contractorPayments?.find(
p => p.contractorUuid === contractorUuid,
)

if (!payment) {
throw new Error(t('errors.paymentNotFound'))
}

const contractor = contractors.find(c => c.uuid === contractorUuid)
if (!contractor) {
throw new Error(t('errors.contractorNotFound'))
}

return (
<PaymentStatementPresentation
payment={payment}
contractor={contractor}
checkDate={paymentGroupResponse.contractorPaymentGroup.checkDate || ''}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useTranslation } from 'react-i18next'
import type { ContractorPaymentForGroup } from '@gusto/embedded-api/models/components/contractorpaymentforgroup'
import type { Contractor } from '@gusto/embedded-api/models/components/contractor'
import { useMemo } from 'react'
import { getContractorDisplayName } from '../CreatePayment/helpers'
import { DataView, Flex } from '@/components/Common'
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
import { useI18n } from '@/i18n'
import { formatHoursDisplay } from '@/components/Payroll/helpers'
import useNumberFormatter from '@/hooks/useNumberFormatter'
import { useDateFormatter } from '@/hooks/useDateFormatter'

interface PaymentStatementPresentationProps {
payment: ContractorPaymentForGroup
contractor: Contractor
checkDate: string
}

type PaymentStatementRow = {
label: string
amount: string | null
}

export const PaymentStatementPresentation = ({
payment,
contractor,
checkDate,
}: PaymentStatementPresentationProps) => {
const { Text, Heading } = useComponentContext()
useI18n('Contractor.Payments.PaymentStatement')
const { t } = useTranslation('Contractor.Payments.PaymentStatement')
const currencyFormatter = useNumberFormatter('currency')
const { formatLongWithYear } = useDateFormatter()

const contractorName = getContractorDisplayName(contractor)
const isHourly = payment.wageType === 'Hourly'
const hours = Number(payment.hours || 0)
const hourlyRate = Number(payment.hourlyRate || 0)
const bonus = Number(payment.bonus || 0)
const reimbursement = Number(payment.reimbursement || 0)
const wageTotal = Number(payment.wageTotal || 0)

const statementRows = useMemo<PaymentStatementRow[]>(() => {
const rows: PaymentStatementRow[] = [
{
label: payment.paymentMethod || '',
amount: currencyFormatter(wageTotal),
},
]

if (isHourly && hours > 0) {
rows.push({
label: t('hoursLabel'),
amount: t('hoursAmount', {
hours: formatHoursDisplay(hours),
rate: currencyFormatter(hourlyRate),
}),
})
} else {
rows.push({
label: t('wageLabel'),
amount: currencyFormatter(Number(payment.wage || 0)),
})
}

rows.push({
label: t('bonus'),
amount: currencyFormatter(bonus),
})

rows.push({
label: t('reimbursement'),
amount: currencyFormatter(reimbursement),
})

return rows
}, [
payment.paymentMethod,
wageTotal,
isHourly,
hours,
hourlyRate,
bonus,
reimbursement,
t,
currencyFormatter,
])

return (
<Flex flexDirection="column" gap={32}>
<Flex flexDirection="column" gap={16}>
<Flex flexDirection="column" gap={8}>
<Heading as="h2">{t('title', { contractorName })}</Heading>
<Text>{formatLongWithYear(checkDate)}</Text>
</Flex>
</Flex>

<DataView
columns={[
{
title: t('debitedColumn'),
render: ({ label }) => <Text>{label}</Text>,
},
{
title: t('amountColumn'),
render: ({ amount }) => <Text>{amount || ''}</Text>,
},
]}
data={statementRows}
label={t('title', { contractorName })}
/>
</Flex>
)
}
16 changes: 16 additions & 0 deletions src/i18n/en/Contractor.Payments.PaymentStatement.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"title": "Payment statement for {{contractorName}}",
"breadcrumbLabel": "Payment statement",
"debitedColumn": "Debited",
"amountColumn": "Amount",
"hoursLabel": "Hours",
"hoursAmount": "{{hours}} hours at {{rate}}/hr",
"wageLabel": "Wage",
"bonus": "Bonus",
"reimbursement": "Reimbursement",
"errors": {
"paymentGroupNotFound": "Contractor payment group not found",
"paymentNotFound": "Payment not found",
"contractorNotFound": "Contractor not found"
}
}
Loading