From 4420de453b4903025e02b50949eb6f6a20edb12a Mon Sep 17 00:00:00 2001 From: Jeff Johnson Date: Wed, 29 Oct 2025 10:56:59 -0700 Subject: [PATCH 1/2] feat: add contractor payment overview component --- .../Payments/Overview/Overview.stories.tsx | 72 +++++++ .../Overview/OverviewPresentation.tsx | 199 ++++++++++++++++++ src/components/Contractor/Payments/types.ts | 7 + ...ctorPayment.ContractorPaymentOverview.json | 37 ++++ src/types/i18next.d.ts | 39 +++- 5 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 src/components/Contractor/Payments/Overview/Overview.stories.tsx create mode 100644 src/components/Contractor/Payments/Overview/OverviewPresentation.tsx create mode 100644 src/components/Contractor/Payments/types.ts create mode 100644 src/i18n/en/ContractorPayment.ContractorPaymentOverview.json diff --git a/src/components/Contractor/Payments/Overview/Overview.stories.tsx b/src/components/Contractor/Payments/Overview/Overview.stories.tsx new file mode 100644 index 00000000..3a20d9eb --- /dev/null +++ b/src/components/Contractor/Payments/Overview/Overview.stories.tsx @@ -0,0 +1,72 @@ +import type { StoryDefault, Story } from '@ladle/react' +import { action } from '@ladle/react' +import { OverviewPresentation } from './OverviewPresentation' + +export default { + title: 'Domain/ContractorPayment/Payment Summary', +} satisfies StoryDefault + +export const ReviewAndSubmitDefault: Story = () => { + const mockPaymentSummary = { + totalAmount: '1180', + debitAmount: '1180', + debitAccount: 'Checking Account ending in 4567', + debitDate: '2025-09-15', + contractorPayDate: '2025-09-17', + checkDate: '2025-09-17', + submitByDate: '2025-09-14', + } + + const mockContractorPaymentGroup = { + uuid: 'group-1', + companyUuid: 'company-1', + checkDate: '2025-09-17', + debitDate: '2025-09-15', + status: 'Unfunded' as const, + totals: { + amount: '1180', + debitAmount: '1180', + wageAmount: '1000', + reimbursementAmount: '0', + }, + contractorPayments: [ + { + uuid: '1', + contractorUuid: 'armstrong-louis', + wageType: 'Fixed' as const, + paymentMethod: 'Direct Deposit' as const, + hours: undefined, + wage: '1000', + bonus: '0', + reimbursement: '0', + wageTotal: '1000', + }, + { + uuid: '2', + contractorUuid: 'fitzgerald-ella', + wageType: 'Hourly' as const, + hourlyRate: '18', + paymentMethod: 'Direct Deposit' as const, + hours: '10', + wage: undefined, + bonus: '0', + reimbursement: '0', + wageTotal: '180', + }, + ], + } + + return ( + + ) +} + +ReviewAndSubmitDefault.meta = { + description: + 'Payment Summary page - final confirmation with payment summary details and all contractor payment information ready for submission', +} diff --git a/src/components/Contractor/Payments/Overview/OverviewPresentation.tsx b/src/components/Contractor/Payments/Overview/OverviewPresentation.tsx new file mode 100644 index 00000000..eac9431d --- /dev/null +++ b/src/components/Contractor/Payments/Overview/OverviewPresentation.tsx @@ -0,0 +1,199 @@ +import { useTranslation } from 'react-i18next' +import type { ContractorPaymentForGroup, ContractorPaymentGroup } from '../types' +import { DataView, Flex } from '@/components/Common' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useI18n } from '@/i18n' +import { formatNumberAsCurrency } from '@/helpers/formattedStrings' +import { useLocale } from '@/contexts/LocaleProvider/useLocale' +import { formatHoursDisplay } from '@/components/Payroll/helpers' + +const ZERO_HOURS_DISPLAY = '0.000' + +interface PaymentSummary { + totalAmount: string + debitAmount: string + debitAccount: string + debitDate: string + contractorPayDate: string + checkDate: string + submitByDate: string +} + +interface ContractorPaymentOverviewPresentationProps { + paymentSummary: PaymentSummary + contractorPaymentGroup: ContractorPaymentGroup + onEdit: () => void + onSubmit: () => void +} + +export const OverviewPresentation = ({ + paymentSummary, + contractorPaymentGroup, + onEdit, + onSubmit, +}: ContractorPaymentOverviewPresentationProps) => { + const { Button, Text, Heading } = useComponentContext() + useI18n('ContractorPayment.ContractorPaymentOverview') + const { t } = useTranslation('ContractorPayment.ContractorPaymentOverview') + const { locale } = useLocale() + + const formatWageType = (contractor: ContractorPaymentForGroup) => { + if (contractor.wageType === 'Hourly' && contractor.hourlyRate) { + return `${t('wageTypes.hourly')} ${formatNumberAsCurrency(parseFloat(contractor.hourlyRate), locale)}${t('perHour')}` + } + return contractor.wageType + } + + const contractors: ContractorPaymentForGroup[] = contractorPaymentGroup.contractorPayments || [] + + return ( + + + {t('title')} + {t('reviewAndSubmitTitle')} + {t('reviewSubtitle', { submitByDate: paymentSummary.submitByDate })} + + + {/* Payment Summary */} + + ( + + {formatNumberAsCurrency( + parseFloat(contractorPaymentGroup.totals?.amount || '0'), + locale, + )} + + ), + }, + { + title: t('summaryTableHeaders.debitAmount'), + render: () => ( + + {formatNumberAsCurrency( + parseFloat(contractorPaymentGroup.totals?.debitAmount || '0'), + locale, + )} + + ), + }, + { + title: t('summaryTableHeaders.debitAccount'), + render: () => {paymentSummary.debitAccount}, + }, + { + title: t('summaryTableHeaders.debitDate'), + render: () => ( + {contractorPaymentGroup.debitDate || paymentSummary.debitDate} + ), + }, + { + title: t('summaryTableHeaders.contractorPayDate'), + render: () => ( + {contractorPaymentGroup.checkDate || paymentSummary.contractorPayDate} + ), + }, + ]} + data={[contractorPaymentGroup]} + label="Payment Summary" + /> + + + + + {t('whatYourCompanyPays')} + + {t('dateLabel')}: {contractorPaymentGroup.checkDate} + + + + {/* Contractor Payments Table */} +
+ {contractor.contractorUuid || 'N/A'}, + }, + { + title: t('contractorTableHeaders.wageType'), + render: contractor => {formatWageType(contractor)}, + }, + { + title: t('contractorTableHeaders.paymentMethod'), + render: contractor => {contractor.paymentMethod || 'N/A'}, + }, + { + title: t('contractorTableHeaders.hours'), + render: contractor => ( + + {contractor.wageType === 'Hourly' && contractor.hours + ? formatHoursDisplay(parseFloat(contractor.hours)) + : ZERO_HOURS_DISPLAY} + + ), + }, + { + title: t('contractorTableHeaders.wage'), + render: contractor => ( + + {contractor.wageType === 'Fixed' && contractor.wage + ? formatNumberAsCurrency(parseFloat(contractor.wage), locale) + : formatNumberAsCurrency(0, locale)} + + ), + }, + { + title: t('contractorTableHeaders.bonus'), + render: contractor => ( + + {formatNumberAsCurrency( + contractor.bonus ? parseFloat(contractor.bonus) : 0, + locale, + )} + + ), + }, + { + title: t('contractorTableHeaders.reimbursement'), + render: contractor => ( + + {formatNumberAsCurrency( + contractor.reimbursement ? parseFloat(contractor.reimbursement) : 0, + locale, + )} + + ), + }, + { + title: t('contractorTableHeaders.total'), + render: contractor => ( + + {formatNumberAsCurrency( + contractor.wageTotal ? parseFloat(contractor.wageTotal) : 0, + locale, + )} + + ), + }, + ]} + data={contractors} + label={t('whatYourCompanyPays')} + /> +
+
+ + + + + +
+ ) +} diff --git a/src/components/Contractor/Payments/types.ts b/src/components/Contractor/Payments/types.ts new file mode 100644 index 00000000..0001212a --- /dev/null +++ b/src/components/Contractor/Payments/types.ts @@ -0,0 +1,7 @@ +import type { ContractorPaymentGroup } from '@gusto/embedded-api/models/components/contractorpaymentgroup' +import type { ContractorPaymentForGroup } from '@gusto/embedded-api/models/components/contractorpaymentforgroup' +import type { ContractorPaymentGroupTotals } from '@gusto/embedded-api/models/components/contractorpaymentgroup' + +export type { ContractorPaymentGroup, ContractorPaymentForGroup, ContractorPaymentGroupTotals } + +export type ContractorPaymentGroupMinimal = Omit diff --git a/src/i18n/en/ContractorPayment.ContractorPaymentOverview.json b/src/i18n/en/ContractorPayment.ContractorPaymentOverview.json new file mode 100644 index 00000000..f2e7cf7a --- /dev/null +++ b/src/i18n/en/ContractorPayment.ContractorPaymentOverview.json @@ -0,0 +1,37 @@ +{ + "title": "Pay contractors", + "reviewAndSubmitTitle": "Review and submit", + "reviewSubtitle": "Here's a quick summary to review - we'll debit funds on the debit date listed below. To pay your contractors on the date below, submit payments by {{submitByDate}}", + "summaryTableHeaders": { + "totalAmount": "Total amount", + "debitAmount": "Debit amount", + "debitAccount": "Debit account", + "debitDate": "Debit date", + "contractorPayDate": "Contractor pay date" + }, + "dateLabel": "Date", + "whatYourCompanyPays": "What your company pays", + "contractorTableHeaders": { + "contractor": "Contractor", + "wageType": "Wage type", + "paymentMethod": "Payment method", + "hours": "Hours", + "wage": "Wage", + "bonus": "Bonus", + "reimbursement": "Reimbursement", + "total": "Total" + }, + "totalsLabel": "Totals", + "editButton": "Edit", + "submitButton": "Submit", + "perHour": "/hr", + "wageTypes": { + "fixed": "Fixed", + "hourly": "Hourly" + }, + "paymentMethods": { + "directDeposit": "Direct Deposit", + "check": "Check", + "historicalPayment": "Historical Payment" + } +} diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index 71b78285..4a725622 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -513,6 +513,43 @@ export interface ContractorSubmit{ "successMessage":string; }; }; +export interface ContractorPaymentContractorPaymentOverview{ +"title":string; +"reviewAndSubmitTitle":string; +"reviewSubtitle":string; +"summaryTableHeaders":{ +"totalAmount":string; +"debitAmount":string; +"debitAccount":string; +"debitDate":string; +"contractorPayDate":string; +}; +"dateLabel":string; +"whatYourCompanyPays":string; +"contractorTableHeaders":{ +"contractor":string; +"wageType":string; +"paymentMethod":string; +"hours":string; +"wage":string; +"bonus":string; +"reimbursement":string; +"total":string; +}; +"totalsLabel":string; +"editButton":string; +"submitButton":string; +"perHour":string; +"wageTypes":{ +"fixed":string; +"hourly":string; +}; +"paymentMethods":{ +"directDeposit":string; +"check":string; +"historicalPayment":string; +}; +}; export interface EmployeeBankAccount{ "accountNumberLabel":string; "accountTypeChecking":string; @@ -1608,6 +1645,6 @@ export interface common{ interface CustomTypeOptions { defaultNS: 'common'; - resources: { 'Company.AddBank': CompanyAddBank, 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.Landing': EmployeeLanding, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.PayrollSchedule': PayrollPayrollSchedule, 'common': common, } + resources: { 'Company.AddBank': CompanyAddBank, 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'ContractorPayment.ContractorPaymentOverview': ContractorPaymentContractorPaymentOverview, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.Landing': EmployeeLanding, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.PayrollSchedule': PayrollPayrollSchedule, 'common': common, } }; } \ No newline at end of file From aef3b63545e580b69ea28cfc6db820bc92f69aff Mon Sep 17 00:00:00 2001 From: Jeff Johnson Date: Wed, 29 Oct 2025 13:00:18 -0700 Subject: [PATCH 2/2] chore: pr feedback --- .../Contractor/Payments/Overview/Overview.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Contractor/Payments/Overview/Overview.stories.tsx b/src/components/Contractor/Payments/Overview/Overview.stories.tsx index 3a20d9eb..c9f4d227 100644 --- a/src/components/Contractor/Payments/Overview/Overview.stories.tsx +++ b/src/components/Contractor/Payments/Overview/Overview.stories.tsx @@ -3,7 +3,7 @@ import { action } from '@ladle/react' import { OverviewPresentation } from './OverviewPresentation' export default { - title: 'Domain/ContractorPayment/Payment Summary', + title: 'Domain/Contractor/Payments', } satisfies StoryDefault export const ReviewAndSubmitDefault: Story = () => {