Skip to content

Commit 125c229

Browse files
authored
Da/contractor payment (#869)
* chore: removing card from modal * feat: pre-preview UX * chore: stashing to unblock others * chore: stashing to unblock others * chore: remove hardcoded data * Delete CHANGELOG-HMR-FIX.md * Delete HMR-FIX-QUICKSTART.md * Delete SOLUTION-SUMMARY.md * chore: creating payment
1 parent 1b28d06 commit 125c229

19 files changed

+2897
-2899
lines changed

package-lock.json

Lines changed: 2361 additions & 2282 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Contractor/Payments/CreatePayment/CreatePayment.tsx

Lines changed: 139 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
import { useContractorsListSuspense } from '@gusto/embedded-api/react-query/contractorsList'
22
import { useContractorPaymentGroupsCreateMutation } from '@gusto/embedded-api/react-query/contractorPaymentGroupsCreate'
33
import type { ContractorPayments } from '@gusto/embedded-api/models/operations/postv1companiescompanyidcontractorpaymentgroups'
4+
import { useContractorPaymentGroupsPreviewMutation } from '@gusto/embedded-api/react-query/contractorPaymentGroupsPreview'
45
import { useMemo, useState } from 'react'
56
import { RFCDate } from '@gusto/embedded-api/types/rfcdate'
67
import { FormProvider, useForm } from 'react-hook-form'
78
import { zodResolver } from '@hookform/resolvers/zod'
9+
import { useTranslation } from 'react-i18next'
10+
import type { ContractorPaymentGroupPreview } from '@gusto/embedded-api/models/components/contractorpaymentgrouppreview'
11+
import type { InternalAlert } from '../types'
812
import { CreatePaymentPresentation } from './CreatePaymentPresentation'
913
import {
1014
EditContractorPaymentPresentation,
1115
EditContractorPaymentFormSchema,
1216
type EditContractorPaymentFormValues,
1317
} from './EditContractorPaymentPresentation'
18+
import { PreviewPresentation } from './PreviewPresentation'
1419
import { useComponentDictionary } from '@/i18n'
15-
import { BaseComponent, type BaseComponentInterface } from '@/components/Base'
20+
import { BaseComponent, useBase, type BaseComponentInterface } from '@/components/Base'
1621
import { componentEvents, ContractorOnboardingStatus } from '@/shared/constants'
1722
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
23+
import { firstLastName } from '@/helpers/formattedStrings'
1824

1925
interface CreatePaymentProps extends BaseComponentInterface<'Contractor.Payments.CreatePayment'> {
2026
companyId: string
@@ -30,21 +36,60 @@ export function CreatePayment(props: CreatePaymentProps) {
3036

3137
export const Root = ({ companyId, dictionary, onEvent }: CreatePaymentProps) => {
3238
useComponentDictionary('Contractor.Payments.CreatePayment', dictionary)
39+
const { t } = useTranslation('Contractor.Payments.CreatePayment')
3340
const { Modal } = useComponentContext()
3441
const [isModalOpen, setIsModalOpen] = useState(false)
3542
const [paymentDate, setPaymentDate] = useState<string>(
3643
new Date().toISOString().split('T')[0] || '',
3744
)
45+
const { baseSubmitHandler } = useBase()
46+
const [alerts, setAlerts] = useState<Record<string, InternalAlert>>({})
47+
const [previewData, setPreviewData] = useState<ContractorPaymentGroupPreview | null>(null)
48+
/**{
49+
uuid: null,
50+
companyUuid: '948d671f-0301-4f84-9cf1-a9ef0911d195',
51+
checkDate: '2025-12-24',
52+
debitDate: '2025-12-22',
53+
status: 'Unfunded',
54+
creationToken: 'f3f2706a-3ee1-4334-b30f-8f1d72a9967f',
55+
partnerOwnedDisbursement: null,
56+
submissionBlockers: [],
57+
creditBlockers: [],
58+
contractorPayments: [
59+
{
60+
uuid: null,
61+
contractorUuid: '69ff2cd6-3bd8-48e3-acfe-cfe45b3d5d88',
62+
bonus: '0.0',
63+
hours: '12.0',
64+
hourlyRate: '18.0',
65+
mayCancel: true,
66+
paymentMethod: 'Direct Deposit',
67+
reimbursement: '0.0',
68+
status: 'Unfunded',
69+
wage: '0.0',
70+
wageType: 'Hourly',
71+
wageTotal: '216.0',
72+
},
73+
],
74+
totals: {
75+
amount: '216.00',
76+
debitAmount: '216.00',
77+
wageAmount: '216.00',
78+
reimbursementAmount: '0.00',
79+
checkAmount: '0.00',
80+
},
81+
} */
3882

3983
const { mutateAsync: createContractorPaymentGroup } = useContractorPaymentGroupsCreateMutation()
84+
const { mutateAsync: previewContractorPaymentGroup } = useContractorPaymentGroupsPreviewMutation()
4085

4186
const { data: contractorList } = useContractorsListSuspense({ companyUuid: companyId })
4287
const contractors = (contractorList.contractorList || []).filter(
4388
contractor =>
4489
contractor.isActive &&
4590
contractor.onboardingStatus === ContractorOnboardingStatus.ONBOARDING_COMPLETED,
4691
)
47-
const initialContractorPayments: ContractorPayments[] = useMemo(
92+
const initialContractorPayments: (ContractorPayments & { isTouched: boolean })[] = useMemo(
4893
() =>
4994
contractors.map(contractor => ({
5095
contractorUuid: contractor.uuid,
@@ -53,11 +98,12 @@ export const Root = ({ companyId, dictionary, onEvent }: CreatePaymentProps) =>
5398
hours: 0,
5499
bonus: 0,
55100
reimbursement: 0,
101+
isTouched: false,
56102
})),
57103
[contractors],
58104
)
59105
const [virtualContractorPayments, setVirtualContractorPayments] =
60-
useState<ContractorPayments[]>(initialContractorPayments)
106+
useState<(ContractorPayments & { isTouched: boolean })[]>(initialContractorPayments)
61107
//TODO: fix totals - they are not correct
62108
const totals = useMemo(
63109
() =>
@@ -94,18 +140,26 @@ export const Root = ({ companyId, dictionary, onEvent }: CreatePaymentProps) =>
94140
},
95141
})
96142

97-
const onSaveAndContinue = async () => {
98-
const response = await createContractorPaymentGroup({
99-
request: {
100-
companyId,
101-
requestBody: {
102-
checkDate: new RFCDate(paymentDate),
103-
contractorPayments: virtualContractorPayments,
104-
creationToken: crypto.randomUUID(),
143+
const onCreatePaymentGroup = async () => {
144+
await baseSubmitHandler(null, async () => {
145+
const contractorPayments = virtualContractorPayments.filter(payment => payment.isTouched)
146+
if (contractorPayments.length === 0 || !previewData?.creationToken) {
147+
return
148+
}
149+
const creationToken = previewData.creationToken
150+
//TODO: add empty set error alert
151+
const response = await createContractorPaymentGroup({
152+
request: {
153+
companyId,
154+
requestBody: {
155+
checkDate: new RFCDate(paymentDate),
156+
contractorPayments: contractorPayments,
157+
creationToken,
158+
},
105159
},
106-
},
160+
})
161+
onEvent(componentEvents.CONTRACTOR_PAYMENT_CREATED, response.contractorPaymentGroup)
107162
})
108-
onEvent(componentEvents.CONTRACTOR_PAYMENT_REVIEW, response)
109163
}
110164
const onEditContractor = (contractorUuid: string) => {
111165
const contractor = contractors.find(contractor => contractor.uuid === contractorUuid)
@@ -125,6 +179,7 @@ export const Root = ({ companyId, dictionary, onEvent }: CreatePaymentProps) =>
125179
},
126180
{ keepDirty: false, keepValues: false },
127181
)
182+
setAlerts({})
128183
setIsModalOpen(true)
129184
onEvent(componentEvents.CONTRACTOR_PAYMENT_EDIT)
130185
}
@@ -140,27 +195,88 @@ export const Root = ({ companyId, dictionary, onEvent }: CreatePaymentProps) =>
140195
bonus: data.bonus,
141196
reimbursement: data.reimbursement,
142197
paymentMethod: data.paymentMethod,
198+
isTouched: true,
143199
}
144200
: payment,
145201
),
146202
)
203+
const contractor = contractors.find(contractor => contractor.uuid === data.contractorUuid)
204+
const displayName =
205+
contractor?.type === 'Individual'
206+
? firstLastName({ first_name: contractor.firstName, last_name: contractor.lastName })
207+
: contractor?.businessName
208+
setAlerts(prevAlerts => ({
209+
...prevAlerts,
210+
[data.contractorUuid]: {
211+
type: 'success',
212+
title: t('alerts.contractorPaymentUpdated', {
213+
contractorName: displayName,
214+
}),
215+
onDismiss: () => {
216+
setAlerts(prevAlerts => {
217+
const { [data.contractorUuid]: _, ...rest } = prevAlerts
218+
return rest
219+
})
220+
},
221+
},
222+
}))
147223
setIsModalOpen(false)
148224
onEvent(componentEvents.CONTRACTOR_PAYMENT_UPDATE, data)
149225
}
150226

151-
//TODO: submit should not attemt to push virtualContractorPayments to API if they have not been changed
227+
//TODO: historical payment - check date should be in the past
228+
const onContinueToPreview = async () => {
229+
await baseSubmitHandler(null, async () => {
230+
const contractorPayments = virtualContractorPayments.filter(payment => payment.isTouched)
231+
if (contractorPayments.length === 0) {
232+
return
233+
}
234+
//TODO: clear alerts
235+
//TODO: handle errors
236+
const response = await previewContractorPaymentGroup({
237+
request: {
238+
companyId,
239+
requestBody: {
240+
contractorPayments: contractorPayments.map(payment => {
241+
const { isTouched, ...rest } = payment
242+
return rest
243+
}),
244+
checkDate: new RFCDate(paymentDate),
245+
},
246+
},
247+
})
248+
setAlerts({})
249+
setPreviewData(response.contractorPaymentGroupPreview || null)
250+
onEvent(componentEvents.CONTRACTOR_PAYMENT_PREVIEW, response.contractorPaymentGroupPreview)
251+
})
252+
}
253+
const onBackToEdit = () => {
254+
setPreviewData(null)
255+
onEvent(componentEvents.CONTRACTOR_PAYMENT_BACK_TO_EDIT)
256+
}
152257

153258
return (
154259
<>
155-
<CreatePaymentPresentation
156-
contractors={contractors}
157-
contractorPayments={virtualContractorPayments}
158-
paymentDate={paymentDate}
159-
onPaymentDateChange={setPaymentDate}
160-
onSaveAndContinue={onSaveAndContinue}
161-
onEditContractor={onEditContractor}
162-
totals={totals}
163-
/>
260+
{previewData && (
261+
<PreviewPresentation
262+
contractorPaymentGroup={previewData}
263+
contractors={contractors}
264+
onBackToEdit={onBackToEdit}
265+
onSubmit={onCreatePaymentGroup}
266+
/>
267+
)}
268+
{!previewData && (
269+
<CreatePaymentPresentation
270+
contractors={contractors}
271+
contractorPayments={virtualContractorPayments}
272+
paymentDate={paymentDate}
273+
onPaymentDateChange={setPaymentDate}
274+
onSaveAndContinue={onContinueToPreview}
275+
onEditContractor={onEditContractor}
276+
totals={totals}
277+
alerts={alerts}
278+
/>
279+
)}
164280
{/* TODO: see if moving actions to modal footer is possible */}
165281
<Modal
166282
isOpen={isModalOpen}

src/components/Contractor/Payments/CreatePayment/CreatePaymentPresentation.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { useTranslation } from 'react-i18next'
22
import type { Contractor } from '@gusto/embedded-api/models/components/contractor'
33
import type { ContractorPayments } from '@gusto/embedded-api/models/operations/postv1companiescompanyidcontractorpaymentgroups'
44
import { useMemo } from 'react'
5+
import type { InternalAlert } from '../types'
6+
import { getContractorDisplayName } from './helpers'
57
import { DataView, Flex, FlexItem } from '@/components/Common'
68
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
79
import { HamburgerMenu } from '@/components/Common/HamburgerMenu'
810
import { useI18n } from '@/i18n'
9-
import { firstLastName } from '@/helpers/formattedStrings'
1011
import { formatHoursDisplay } from '@/components/Payroll/helpers'
1112
import useNumberFormatter from '@/hooks/useNumberFormatter'
1213

@@ -25,6 +26,7 @@ interface ContractorPaymentCreatePaymentPresentationProps {
2526
reimbursement: number
2627
total: number
2728
}
29+
alerts: Record<string, InternalAlert>
2830
}
2931

3032
export const CreatePaymentPresentation = ({
@@ -35,8 +37,9 @@ export const CreatePaymentPresentation = ({
3537
onSaveAndContinue,
3638
onEditContractor,
3739
totals,
40+
alerts,
3841
}: ContractorPaymentCreatePaymentPresentationProps) => {
39-
const { Button, Text, Heading, TextInput } = useComponentContext()
42+
const { Button, Text, Heading, TextInput, Alert } = useComponentContext()
4043
useI18n('Contractor.Payments.CreatePayment')
4144
const { t } = useTranslation('Contractor.Payments.CreatePayment')
4245
const currencyFormatter = useNumberFormatter('currency')
@@ -51,17 +54,6 @@ export const CreatePaymentPresentation = ({
5154
return contractor.wageType
5255
}
5356

54-
function getDisplayName(contractor?: Contractor): string {
55-
if (!contractor) {
56-
return ''
57-
}
58-
if (contractor.type === 'Individual') {
59-
return firstLastName({ first_name: contractor.firstName, last_name: contractor.lastName })
60-
} else {
61-
return contractor.businessName || ''
62-
}
63-
}
64-
6557
const tableData = useMemo(
6658
() =>
6759
contractorPayments.map(payment => {
@@ -89,6 +81,17 @@ export const CreatePaymentPresentation = ({
8981
</FlexItem>
9082
</Flex>
9183

84+
{Object.values(alerts).map(alert => (
85+
<Alert
86+
key={alert.title}
87+
label={alert.title}
88+
onDismiss={alert.onDismiss}
89+
status={alert.type}
90+
>
91+
{alert.content ?? null}
92+
</Alert>
93+
))}
94+
9295
<Flex flexDirection="column" gap={8}>
9396
<TextInput
9497
type="date"
@@ -101,12 +104,14 @@ export const CreatePaymentPresentation = ({
101104

102105
<Flex flexDirection="column" gap={16}>
103106
<Heading as="h3">{t('hoursAndPaymentsLabel')}</Heading>
104-
107+
{/* //TODO: add empty state */}
105108
<DataView
106109
columns={[
107110
{
108111
title: t('contractorTableHeaders.contractor'),
109-
render: paymentData => <Text>{getDisplayName(paymentData.contractorDetails)}</Text>,
112+
render: paymentData => (
113+
<Text>{getContractorDisplayName(paymentData.contractorDetails)}</Text>
114+
),
110115
},
111116
{
112117
title: t('contractorTableHeaders.wageType'),

0 commit comments

Comments
 (0)