diff --git a/src/components/Contractor/Payments/PaymentFlow/PaymentFlowComponents.tsx b/src/components/Contractor/Payments/PaymentFlow/PaymentFlowComponents.tsx index bd73fed8..8ff6bfb9 100644 --- a/src/components/Contractor/Payments/PaymentFlow/PaymentFlowComponents.tsx +++ b/src/components/Contractor/Payments/PaymentFlow/PaymentFlowComponents.tsx @@ -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' @@ -15,6 +16,7 @@ export interface PaymentFlowContextInterface extends FlowContextInterface { companyId: string breadcrumbs?: BreadcrumbTrail currentPaymentId?: string + currentContractorUuid?: string alerts?: InternalAlert[] } @@ -32,3 +34,15 @@ export function PaymentHistoryContextual() { const { currentPaymentId, onEvent } = useFlow() return } + +export function PaymentStatementContextual() { + const { currentPaymentId, currentContractorUuid, onEvent } = + useFlow() + return ( + + ) +} diff --git a/src/components/Contractor/Payments/PaymentFlow/paymentStateMachine.ts b/src/components/Contractor/Payments/PaymentFlow/paymentStateMachine.ts index ed483f75..2ce23235 100644 --- a/src/components/Contractor/Payments/PaymentFlow/paymentStateMachine.ts +++ b/src/components/Contractor/Payments/PaymentFlow/paymentStateMachine.ts @@ -5,6 +5,7 @@ import { type PaymentFlowContextInterface, PaymentHistoryContextual, PaymentListContextual, + PaymentStatementContextual, } from './PaymentFlowComponents' import { componentEvents } from '@/shared/constants' import type { MachineEventType, MachineTransition } from '@/types/Helpers' @@ -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 @@ -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 @@ -163,5 +180,33 @@ export const paymentMachine = { ), breadcrumbNavigateTransition('landing'), ), - history: state(), + history: state( + 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( + breadcrumbNavigateTransition('landing'), + breadcrumbNavigateTransition('history'), + ), } diff --git a/src/components/Contractor/Payments/PaymentHistory/PaymentHistory.tsx b/src/components/Contractor/Payments/PaymentHistory/PaymentHistory.tsx index 47c03db5..8d79e34e 100644 --- a/src/components/Contractor/Payments/PaymentHistory/PaymentHistory.tsx +++ b/src/components/Contractor/Payments/PaymentHistory/PaymentHistory.tsx @@ -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 @@ -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) => { diff --git a/src/components/Contractor/Payments/PaymentHistory/PaymentHistoryPresentation.tsx b/src/components/Contractor/Payments/PaymentHistory/PaymentHistoryPresentation.tsx index 67bccc0f..7cdf012a 100644 --- a/src/components/Contractor/Payments/PaymentHistory/PaymentHistoryPresentation.tsx +++ b/src/components/Contractor/Payments/PaymentHistory/PaymentHistoryPresentation.tsx @@ -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 @@ -37,7 +37,7 @@ export const PaymentHistoryPresentation = ({ return ( - {t('title')} + {t('title')} { - onCancelPayment(contractorUuid!) - }, - icon: ( - - - - ), - }) - } + // TODO: Waiting for new UX for cancelling payments + // if (mayCancel) { + // items.push({ + // label: t('actions.cancel'), + // onClick: () => { + // onCancelPayment(contractorUuid!) + // }, + // icon: ( + // + // + // + // ), + // }) + // } return }} data={payments} diff --git a/src/components/Contractor/Payments/PaymentStatement/PaymentStatement.tsx b/src/components/Contractor/Payments/PaymentStatement/PaymentStatement.tsx new file mode 100644 index 00000000..2562af7c --- /dev/null +++ b/src/components/Contractor/Payments/PaymentStatement/PaymentStatement.tsx @@ -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 ( + + {props.children} + + ) +} + +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 ( + + ) +} diff --git a/src/components/Contractor/Payments/PaymentStatement/PaymentStatementPresentation.tsx b/src/components/Contractor/Payments/PaymentStatement/PaymentStatementPresentation.tsx new file mode 100644 index 00000000..9d784a33 --- /dev/null +++ b/src/components/Contractor/Payments/PaymentStatement/PaymentStatementPresentation.tsx @@ -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(() => { + 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 ( + + + + {t('title', { contractorName })} + {formatLongWithYear(checkDate)} + + + + {label}, + }, + { + title: t('amountColumn'), + render: ({ amount }) => {amount || ''}, + }, + ]} + data={statementRows} + label={t('title', { contractorName })} + /> + + ) +} diff --git a/src/i18n/en/Contractor.Payments.PaymentStatement.json b/src/i18n/en/Contractor.Payments.PaymentStatement.json new file mode 100644 index 00000000..8d7e192c --- /dev/null +++ b/src/i18n/en/Contractor.Payments.PaymentStatement.json @@ -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" + } +} diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index b9783d9e..75147be5 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -540,6 +540,22 @@ export interface ContractorPaymentsPaymentHistory{ "paymentGroupNotFound":string; }; }; +export interface ContractorPaymentsPaymentStatement{ +"title":string; +"breadcrumbLabel":string; +"debitedColumn":string; +"amountColumn":string; +"hoursLabel":string; +"hoursAmount":string; +"wageLabel":string; +"bonus":string; +"reimbursement":string; +"errors":{ +"paymentGroupNotFound":string; +"paymentNotFound":string; +"contractorNotFound":string; +}; +}; export interface ContractorPaymentsPaymentsList{ "title":string; "subtitle":string; @@ -1897,6 +1913,6 @@ export interface common{ interface CustomTypeOptions { defaultNS: 'common'; - resources: { '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.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, '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.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.InformationRequestForm': PayrollInformationRequestForm, 'Payroll.InformationRequestList': PayrollInformationRequestList, '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.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } + resources: { '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.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, '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.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.InformationRequestForm': PayrollInformationRequestForm, 'Payroll.InformationRequestList': PayrollInformationRequestList, '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.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } }; } \ No newline at end of file