From 1936c228e795bc479d037567a451cb08c8e3ef25 Mon Sep 17 00:00:00 2001 From: Jeff Johnson Date: Wed, 29 Oct 2025 10:54:18 -0700 Subject: [PATCH 1/2] feat: add contractor payment detail component --- .../Payments/Detail/Detail.stories.tsx | 64 ++++++++ .../Payments/Detail/DetailPresentation.tsx | 148 ++++++++++++++++++ src/components/Contractor/Payments/types.ts | 7 + ...ractorPayment.ContractorPaymentDetail.json | 27 ++++ src/types/i18next.d.ts | 29 +++- 5 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/components/Contractor/Payments/Detail/Detail.stories.tsx create mode 100644 src/components/Contractor/Payments/Detail/DetailPresentation.tsx create mode 100644 src/components/Contractor/Payments/types.ts create mode 100644 src/i18n/en/ContractorPayment.ContractorPaymentDetail.json diff --git a/src/components/Contractor/Payments/Detail/Detail.stories.tsx b/src/components/Contractor/Payments/Detail/Detail.stories.tsx new file mode 100644 index 00000000..6317d02e --- /dev/null +++ b/src/components/Contractor/Payments/Detail/Detail.stories.tsx @@ -0,0 +1,64 @@ +import type { StoryDefault, Story } from '@ladle/react' +import { action } from '@ladle/react' +import { DetailPresentation } from './DetailPresentation' + +export default { + title: 'Domain/ContractorPayment/Payment Statement Detail', +} satisfies StoryDefault + +export const PaymentDetailDefault: Story = () => { + const mockPayments = [ + { + id: '1', + contractorName: 'Fitzgerald, Ella', + hours: 10.0, + wage: 0, + bonus: 0, + reimbursement: 0, + paymentMethod: 'Direct Deposit', + total: 180, + }, + { + id: '2', + contractorName: 'Armstrong, Louis', + hours: 0, + wage: 1000, + bonus: 0, + reimbursement: 0, + paymentMethod: 'Direct Deposit', + total: 1000, + }, + ] + + return ( + + ) +} + +PaymentDetailDefault.meta = { + description: + 'Payment Statement Detail showing detailed payment breakdown for a specific date with all contractors and payment components', +} + +export const PaymentDetailEmpty: Story = () => { + return ( + + ) +} + +PaymentDetailEmpty.meta = { + description: + 'Payment Statement Detail with no payments on the selected date - displays empty state', +} diff --git a/src/components/Contractor/Payments/Detail/DetailPresentation.tsx b/src/components/Contractor/Payments/Detail/DetailPresentation.tsx new file mode 100644 index 00000000..bd94ada4 --- /dev/null +++ b/src/components/Contractor/Payments/Detail/DetailPresentation.tsx @@ -0,0 +1,148 @@ +import { useTranslation } from 'react-i18next' +import { DataView, Flex, EmptyData, ActionsLayout } 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 + contractorName: string + hours: number + wage: number + bonus: number + reimbursement: number + paymentMethod: string + total: number +} + +interface ContractorPaymentDetailPresentationProps { + date: string + payments: PaymentData[] + onBack: () => void + onViewPayment: (paymentId: string) => void + onCancelPayment: (paymentId: string) => void +} + +export const DetailPresentation = ({ + date, + payments, + onBack, + onViewPayment, + onCancelPayment, +}: ContractorPaymentDetailPresentationProps) => { + const { Button, Text, Heading } = useComponentContext() + useI18n('ContractorPayment.ContractorPaymentDetail') + const { t } = useTranslation('ContractorPayment.ContractorPaymentDetail') + const { locale } = useLocale() + + // TODO: Replace window.confirm with Dialog component + const handleCancelPayment = (paymentId: string) => { + if (typeof window !== 'undefined') { + const confirmed = window.confirm(t('cancelConfirmation')) + if (confirmed) { + onCancelPayment(paymentId) + } + } else { + onCancelPayment(paymentId) + } + } + + return ( + + {t('title')} + + + {t('paymentsOnDateTitle', { date })} + + {payments.length === 0 ? ( + + + + + + ) : ( + <> + ( + + ), + }, + { + title: t('tableHeaders.hours'), + render: ({ hours }) => {formatHoursDisplay(hours)}, + }, + { + title: t('tableHeaders.wage'), + render: ({ wage }) => {formatNumberAsCurrency(wage, locale)}, + }, + { + title: t('tableHeaders.bonus'), + render: ({ bonus }) => {formatNumberAsCurrency(bonus, locale)}, + }, + { + title: t('tableHeaders.reimbursement'), + render: ({ reimbursement }) => ( + {formatNumberAsCurrency(reimbursement, locale)} + ), + }, + { + title: t('tableHeaders.paymentMethod'), + render: ({ paymentMethod }) => {paymentMethod}, + }, + { + title: t('tableHeaders.total'), + render: ({ total }) => {formatNumberAsCurrency(total, locale)}, + }, + { + title: t('tableHeaders.action'), + render: ({ id, contractorName }) => ( + { + onViewPayment(id) + }, + }, + { + label: t('actions.cancel'), + onClick: () => { + handleCancelPayment(id) + }, + }, + ]} + triggerLabel={t('tableHeaders.action')} + /> + ), + }, + ]} + data={payments} + label={t('title')} + /> + + + + + + )} + + + ) +} 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.ContractorPaymentDetail.json b/src/i18n/en/ContractorPayment.ContractorPaymentDetail.json new file mode 100644 index 00000000..3fd4dd9e --- /dev/null +++ b/src/i18n/en/ContractorPayment.ContractorPaymentDetail.json @@ -0,0 +1,27 @@ +{ + "title": "Contractor payment history", + "paymentsOnDateTitle": "Payments done on {{date}}", + "noPaymentsFound": "No payments found", + "noPaymentsDescription": "No payments were found for this date", + "tableHeaders": { + "contractor": "Contractor", + "hours": "Hours", + "wage": "Wage", + "bonus": "Bonus", + "reimbursement": "Reimbursement", + "paymentMethod": "Payment method", + "total": "Total", + "action": "Action" + }, + "actions": { + "view": "View", + "cancel": "Cancel" + }, + "cancelConfirmation": "Canceling a contractor payment cannot be undone. A new payment will have to be created if you want to pay this contractor. Are you sure?", + "backButton": "Back", + "paymentMethods": { + "directDeposit": "Direct Deposit", + "check": "Check", + "historicalPayment": "Historical Payment" + } +} diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index 1977cf05..dcb075cf 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -513,6 +513,33 @@ export interface ContractorSubmit{ "successMessage":string; }; }; +export interface ContractorPaymentContractorPaymentDetail{ +"title":string; +"paymentsOnDateTitle":string; +"noPaymentsFound":string; +"noPaymentsDescription":string; +"tableHeaders":{ +"contractor":string; +"hours":string; +"wage":string; +"bonus":string; +"reimbursement":string; +"paymentMethod":string; +"total":string; +"action":string; +}; +"actions":{ +"view":string; +"cancel":string; +}; +"cancelConfirmation":string; +"backButton":string; +"paymentMethods":{ +"directDeposit":string; +"check":string; +"historicalPayment":string; +}; +}; export interface EmployeeBankAccount{ "accountNumberLabel":string; "accountTypeChecking":string; @@ -1595,6 +1622,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.ContractorPaymentDetail': ContractorPaymentContractorPaymentDetail, '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 586cdee6a59323d1e5e1a48976e8a33bd06b32cf Mon Sep 17 00:00:00 2001 From: Jeff Johnson Date: Wed, 29 Oct 2025 12:46:26 -0700 Subject: [PATCH 2/2] chore: pr feedback --- .../Payments/Detail/Detail.stories.tsx | 6 ++--- .../Payments/Detail/DetailPresentation.tsx | 22 +++++-------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/components/Contractor/Payments/Detail/Detail.stories.tsx b/src/components/Contractor/Payments/Detail/Detail.stories.tsx index 6317d02e..bd2c26e0 100644 --- a/src/components/Contractor/Payments/Detail/Detail.stories.tsx +++ b/src/components/Contractor/Payments/Detail/Detail.stories.tsx @@ -3,14 +3,14 @@ import { action } from '@ladle/react' import { DetailPresentation } from './DetailPresentation' export default { - title: 'Domain/ContractorPayment/Payment Statement Detail', + title: 'Domain/Contractor/Payments', } satisfies StoryDefault export const PaymentDetailDefault: Story = () => { const mockPayments = [ { id: '1', - contractorName: 'Fitzgerald, Ella', + name: 'Fitzgerald, Ella', hours: 10.0, wage: 0, bonus: 0, @@ -20,7 +20,7 @@ export const PaymentDetailDefault: Story = () => { }, { id: '2', - contractorName: 'Armstrong, Louis', + name: 'Armstrong, Louis', hours: 0, wage: 1000, bonus: 0, diff --git a/src/components/Contractor/Payments/Detail/DetailPresentation.tsx b/src/components/Contractor/Payments/Detail/DetailPresentation.tsx index bd94ada4..8864f924 100644 --- a/src/components/Contractor/Payments/Detail/DetailPresentation.tsx +++ b/src/components/Contractor/Payments/Detail/DetailPresentation.tsx @@ -9,7 +9,7 @@ import { formatHoursDisplay } from '@/components/Payroll/helpers' interface PaymentData { id: string - contractorName: string + name: string hours: number wage: number bonus: number @@ -38,18 +38,6 @@ export const DetailPresentation = ({ const { t } = useTranslation('ContractorPayment.ContractorPaymentDetail') const { locale } = useLocale() - // TODO: Replace window.confirm with Dialog component - const handleCancelPayment = (paymentId: string) => { - if (typeof window !== 'undefined') { - const confirmed = window.confirm(t('cancelConfirmation')) - if (confirmed) { - onCancelPayment(paymentId) - } - } else { - onCancelPayment(paymentId) - } - } - return ( {t('title')} @@ -71,14 +59,14 @@ export const DetailPresentation = ({ columns={[ { title: t('tableHeaders.contractor'), - render: ({ contractorName, id }) => ( + render: ({ name, id }) => ( ), }, @@ -110,7 +98,7 @@ export const DetailPresentation = ({ }, { title: t('tableHeaders.action'), - render: ({ id, contractorName }) => ( + render: ({ id, name }) => ( { - handleCancelPayment(id) + onCancelPayment(id) }, }, ]}