Skip to content

Conversation

@jeffredodd
Copy link
Contributor

Contractor Payments: Presentation Layer Refactor

Overview

Refactored the Contractor Payments component structure to focus on presentation components and clean up the component hierarchy. Container logic has been moved to flow wrappers, enabling stories to showcase pure React views.

Changes

  • Presentation components now handle all UI rendering and state passed via props
  • State machine used to manage flow navigation
  • Updated all stories to use Presentation components directly with mock data props

Components

  • CreatePaymentPresentation: Contractor hours/earnings editor with edit modal
  • DetailPresentation: Payment statement breakdown for a specific date
  • OverviewPresentation: Payment summary review before submission
  • PaymentHistoryPresentation: Historical payment list view
  • EditModalPresentation: Individual contractor payment editor

Screenshots

Screenshot 2025-10-21 at 2 07 44 PM Screenshot 2025-10-21 at 2 07 39 PM Screenshot 2025-10-21 at 2 07 35 PM Screenshot 2025-10-21 at 2 07 30 PM Screenshot 2025-10-21 at 2 07 22 PM Screenshot 2025-10-21 at 2 07 18 PM Screenshot 2025-10-21 at 2 07 13 PM

@jeffredodd jeffredodd changed the title Jdj/contractor payments refactor phase2 Contractor Payments Presentational Views Oct 21, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the Contractor Payments feature to separate presentation components from container logic, enabling better testability and cleaner component structure. The state machine now manages flow navigation, with all UI rendering handled by presentation components that receive data via props.

Key changes:

  • Introduced presentation components for PaymentHistory, CreatePayment, Overview, Detail, and EditModal views
  • Created a state machine-based flow to manage navigation between contractor payment screens
  • Added comprehensive Storybook stories for all presentation components with mock data

Reviewed Changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/shared/constants.ts Added contractor payment event constants for state machine transitions
src/i18n/en/*.json Added translation files for all contractor payment screens
src/components/ContractorPayment/types.ts Defined TypeScript interfaces for contractor data structures
src/components/ContractorPayment/PaymentHistory/* Created payment history list view with date filtering
src/components/ContractorPayment/Overview/* Created payment summary review screen before submission
src/components/ContractorPayment/Detail/* Created payment statement detail view for specific dates
src/components/ContractorPayment/CreatePayment/* Created contractor hours/earnings editor with totals
src/components/ContractorPayment/EditModal/* Created individual contractor payment editor modal
src/components/ContractorPayment/Flow/* Implemented state machine and flow components to orchestrate navigation

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


const formatWageType = (contractor: ContractorData) => {
if (contractor.wageType === 'Hourly' && contractor.hourlyRate) {
return `Hourly ${formatNumberAsCurrency(contractor.hourlyRate, locale)}/hr`
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded string 'Hourly' and '/hr' should use the translation keys from the i18n file. Use t('wageTypes.hourly') and t('perHour') instead to maintain consistency with the rest of the component and support internationalization.

Suggested change
return `Hourly ${formatNumberAsCurrency(contractor.hourlyRate, locale)}/hr`
return `${t('wageTypes.hourly')} ${formatNumberAsCurrency(contractor.hourlyRate, locale)}${t('perHour')}`

Copilot uses AI. Check for mistakes.

const formatWageType = (contractor: ContractorData) => {
if (contractor.wageType === 'Hourly' && contractor.hourlyRate) {
return `Hourly ${formatNumberAsCurrency(contractor.hourlyRate, locale)}/hr`
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded string 'Hourly' and '/hr' should use the translation keys from the i18n file. Use t('wageTypes.hourly') and t('perHour') instead to maintain consistency with the rest of the component and support internationalization.

Suggested change
return `Hourly ${formatNumberAsCurrency(contractor.hourlyRate, locale)}/hr`
return `${t('wageTypes.hourly')} ${formatNumberAsCurrency(contractor.hourlyRate, locale)}${t('perHour')}`

Copilot uses AI. Check for mistakes.
render: ({ hours, wageType, isTotalRow }) => (
<div style={{ textAlign: 'right' }}>
<Text weight={isTotalRow ? 'bold' : 'regular'}>
{isTotalRow ? '' : wageType === 'Hourly' ? (hours as number).toFixed(3) : '0.0'}
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 3 for decimal places and the hardcoded '0.0' string should be extracted as named constants or configuration values to improve maintainability and make it easier to adjust precision requirements across the application.

Copilot uses AI. Check for mistakes.
},
{
title: t('tableHeaders.hours'),
render: ({ hours }) => <Text>{hours.toFixed(1)}</Text>,
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decimal precision (1) for hours differs from CreatePaymentPresentation which uses toFixed(3). This inconsistency could confuse users. Consider standardizing the hours display format across all components or documenting why different precision is needed in different views.

Suggested change
render: ({ hours }) => <Text>{hours.toFixed(1)}</Text>,
render: ({ hours }) => <Text>{hours.toFixed(3)}</Text>,

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this will currently just truncate. We could probably make fomatHoursDisplay a more general shared utility and leverage it here

export const formatHoursDisplay = (hours: number): string => {
const rounded = roundToTwoDecimals(hours)
if (rounded % 1 === 0) {
return `${rounded}.0`
}
return rounded.toString()
}

Comment on lines 158 to 159
? contractor.hours.toFixed(3)
: '0.000'}
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 3 for decimal places and the hardcoded '0.000' string should be extracted as named constants to maintain consistency with other components and improve maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines 40 to 41
const handleCancelPayment = (paymentId: string, contractorName: string) => {
const confirmed = window.confirm(t('cancelConfirmation'))
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using window.confirm is not ideal for accessibility and user experience. Consider using a proper confirmation dialog component from the component context that provides better styling, keyboard navigation, and screen reader support.

Copilot uses AI. Check for mistakes.
@dmortal
Copy link
Contributor

dmortal commented Oct 21, 2025

should this be placed under Contractors domain?

Copy link
Member

@serikjensen serikjensen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! a few things

  • Noted that we may need to do a deeper dive on contractor groups, as it seems that was the recommended way of running contractor payments, that may impact the types being used here
  • My feeling is that we need to do a pass here and remove any of the logic and calculations that AI put in place in favor of hard coded values that are clearly marked as placeholders. My concern here is that it'll be hard for engineers to identify what is hallucinated or not once this has been merged
  • Overarching concern around if we need to pause on this while we are investigating the infrastructure work currently being discussed as that has the potential to impact the component APIs in here depending on where that lands

Comment on lines 17 to 21
export type ContractorDataStrict = Omit<ContractorData, 'hours' | 'wageType' | 'paymentMethod'> & {
wageType: 'Fixed' | 'Hourly'
paymentMethod: PostV1CompaniesCompanyIdContractorPaymentsRequestBody['paymentMethod']
hours: number
}
Copy link
Member

@serikjensen serikjensen Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know why this strict version is needed?

@@ -0,0 +1,21 @@
import type { PostV1CompaniesCompanyIdContractorPaymentsRequestBody } from '@gusto/embedded-api/models/operations/postv1companiescompanyidcontractorpayments'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know if this is the correct entity for contractor types? Did we also figure out how contractor groups fit into this? i believe there was some discussion around groups being the more up to date direction? ex. see https://docs.gusto.com/embedded-payroll/reference/post-v1-companies-company_id-contractor_payment_groups

Thinking we should get a solid direction there as these types will broadly impact how all of this is organized

onCancel: () => void
}

export const ContractorPaymentEditModal = ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we update this naming? Seems like this is actually just being used as a screen not a modal? Flows may do some of these as modals but I don't think we need to follow that

Comment on lines 62 to 70
if (editingContractor) {
return (
<ContractorPaymentEditModal
contractor={editingContractor}
onSave={onSaveContractor}
onCancel={onCancelEdit}
/>
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should rename this to just be ContractorPaymentEdit and then include it in the state machine navigation rather than nesting it inside CreatePaymentPresentation. That will allow ContractorPaymentEdit to be used in isolation as a block

Comment on lines 40 to 41
const handleCancelPayment = (paymentId: string, contractorName: string) => {
const confirmed = window.confirm(t('cancelConfirmation'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be stubbed out for now and we can ticket separately to add our actual Dialog component

},
{
title: t('tableHeaders.hours'),
render: ({ hours }) => <Text>{hours.toFixed(1)}</Text>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this will currently just truncate. We could probably make fomatHoursDisplay a more general shared utility and leverage it here

export const formatHoursDisplay = (hours: number): string => {
const rounded = roundToTwoDecimals(hours)
if (rounded % 1 === 0) {
return `${rounded}.0`
}
return rounded.toString()
}

Comment on lines 36 to 42
const handleFieldChange = (field: keyof ContractorData, value: number | string) => {
setEditedContractor(prev => ({
...prev,
[field]: value,
total: calculateTotal(),
}))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking this could be omitted as forms can be wired up in a subsequent commit?

Form here will also need to be updated to use Field components and hook form. not sure if that's within scope here, might be preferable to just keep the current form components as placeholders and hook form can be wired up in a follow up?

Comment on lines 28 to 34
const calculateTotal = () => {
const wageAmount =
editedContractor.wageType === 'Hourly'
? editedContractor.hours * (editedContractor.hourlyRate || 0)
: editedContractor.wage
return wageAmount + editedContractor.bonus + editedContractor.reimbursement
}
Copy link
Member

@serikjensen serikjensen Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feeling a little concerned we have logic built up in these components prior to actually connecting this to the API. My thought is that each of these components should likely be much much more simplified and just have hard coded placeholders. Then the actual logic can be wired up by the implementing dev.

It could be really unclear what things are AI generated/hallucinated vs what actually is left to implement for the engineer with the ticket. My feeling is that we should simplify this a lot with placeholder values instead of doing the actual calculations. I feel AI will be better at helping us with this logic once we can cross check it with actual data and we can do it on a more granular level

jeffredodd and others added 21 commits October 22, 2025 11:39
- Add comprehensive contractor payment epic with 8 user stories and 28 tasks
- Add product requirements document with business objectives and success metrics
- Add detailed screen flows with field specifications and validation rules
- Add technical specification with data models and integration points
- Include recovery case management and RFI blocking functionality
- Plan for 14-18 week implementation timeline with 95+ story points
…ries

- Reorganized ContractorPayment.stories.tsx with 18 comprehensive story variants
- Added detailed parity checklist (spec/contractor-payment-parity-checklist.md)
- Created comprehensive review document (CONTRACTOR_PAYMENTS_REVIEW.md)
- All Figma screens now have corresponding Ladle stories
- Documented implementation status: 95% complete
- Identified minor visual polish items needed (2-4 hours)
- Stories grouped by functional area for better navigation
- Remove non-required polish items (breadcrumbs, modal backdrop, etc.)
- Clarify that design system handles responsive and styling
- Update status to COMPLETE - READY TO SHIP
- Only verification/testing remains (no implementation work)
- Add formatDateNamedWeekdayShortPlusDate import
- Format date string (YYYY-MM-DD) to readable format (e.g. 'Sep 18')
- Fixes placeholder not being replaced in 'Payments done on {date}' string
…t menu

Stories now organized as:
- Domain/ContractorPayment/GWS-5696 - Payment History
- Domain/ContractorPayment/GWS-5700 - Hours and Earnings
- Domain/ContractorPayment/GWS-5704 - Individual Contractor Earnings
- Domain/ContractorPayment/GWS-5705 - Payment Summary
- Domain/ContractorPayment/Payment Statement Detail

Each story file contains all variants for that ticket
Removed combined ContractorPayment.stories.tsx
Update story menu structure to use just view names:
- Domain/ContractorPayment/Payment History
- Domain/ContractorPayment/Hours and Earnings
- Domain/ContractorPayment/Individual Contractor Earnings
- Domain/ContractorPayment/Payment Summary
- Domain/ContractorPayment/Payment Statement Detail
Move Flow stories from root 'ContractorPayment / Flow' to 'Domain/ContractorPayment/Complete Flow'
Replace inline formatCurrency with formatNumberAsCurrency helper
Import useLocale hook for proper locale-aware formatting
Verify empty state pattern using EmptyData + ActionsLayout

All verification checks passed:
- lint: OK
- format: OK
- build: OK
- test: OK (889 tests passed)
Replace inline formatCurrency with formatNumberAsCurrency helper
Import useLocale hook for proper locale-aware formatting
Update all currency displays in table columns

All verification checks passed:
- lint: OK
- format: OK
- build: OK
- test: OK (889 tests passed)
Replace inline formatCurrency with formatNumberAsCurrency helper
Import useLocale hook for proper locale-aware formatting
Remove unnecessary comments (code is self-explanatory)

All verification checks passed:
- lint: OK
- format: OK
- build: OK
- test: OK (889 tests passed)
Replace inline formatCurrency with formatNumberAsCurrency helper
Import useLocale hook for proper locale-aware formatting
Update all currency displays in summary and details tables

All verification checks passed:
- lint: OK
- format: OK
- build: OK
- test: OK (889 tests passed)
… states

Replace inline formatCurrency with formatNumberAsCurrency helper
Import useLocale hook for proper locale-aware formatting
Add EmptyData component for empty state handling
Wrap DataView with conditional rendering for empty/populated states
Add i18n strings for empty state

All verification checks passed:
- lint: OK
- format: OK
- build: OK
- test: OK (889 tests passed)
Final Phase 2 commit verifying all presentation layers integrate correctly
All refactored components tested through complete flow
i18n declarations updated for new empty state strings

All verification checks passed:
- lint: OK
- format: OK
- build: OK
- test: OK (889 tests passed)

Phase 2 complete: All presentation layers standardized with:
- formatNumberAsCurrency helper
- Locale-aware formatting
- Empty state handling
- Consistent DataView patterns
- Use robot3 createMachine instead of useReducer for type-safe state
- Add contractorPaymentStateMachine.ts with EventPayloads typing
- Create contextual wrapper components with useFlow hook
- Align with PayrollFlow presentation layer pattern
- Use ComponentAdapter from design system for all components
- Removed unsafe type casts and improved type safety
…Presentation.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@jeffredodd jeffredodd force-pushed the jdj/contractor-payments-refactor-phase2 branch from ac7da39 to 92ec042 Compare October 22, 2025 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants