-
Notifications
You must be signed in to change notification settings - Fork 4
Contractor Payments Presentational Views #702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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.
src/components/ContractorPayment/PaymentHistory/PaymentHistoryPresentation.tsx
Outdated
Show resolved
Hide resolved
|
|
||
| const formatWageType = (contractor: ContractorData) => { | ||
| if (contractor.wageType === 'Hourly' && contractor.hourlyRate) { | ||
| return `Hourly ${formatNumberAsCurrency(contractor.hourlyRate, locale)}/hr` |
Copilot
AI
Oct 21, 2025
There was a problem hiding this comment.
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.
| return `Hourly ${formatNumberAsCurrency(contractor.hourlyRate, locale)}/hr` | |
| return `${t('wageTypes.hourly')} ${formatNumberAsCurrency(contractor.hourlyRate, locale)}${t('perHour')}` |
|
|
||
| const formatWageType = (contractor: ContractorData) => { | ||
| if (contractor.wageType === 'Hourly' && contractor.hourlyRate) { | ||
| return `Hourly ${formatNumberAsCurrency(contractor.hourlyRate, locale)}/hr` |
Copilot
AI
Oct 21, 2025
There was a problem hiding this comment.
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.
| return `Hourly ${formatNumberAsCurrency(contractor.hourlyRate, locale)}/hr` | |
| return `${t('wageTypes.hourly')} ${formatNumberAsCurrency(contractor.hourlyRate, locale)}${t('perHour')}` |
| render: ({ hours, wageType, isTotalRow }) => ( | ||
| <div style={{ textAlign: 'right' }}> | ||
| <Text weight={isTotalRow ? 'bold' : 'regular'}> | ||
| {isTotalRow ? '' : wageType === 'Hourly' ? (hours as number).toFixed(3) : '0.0'} |
Copilot
AI
Oct 21, 2025
There was a problem hiding this comment.
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.
| }, | ||
| { | ||
| title: t('tableHeaders.hours'), | ||
| render: ({ hours }) => <Text>{hours.toFixed(1)}</Text>, |
Copilot
AI
Oct 21, 2025
There was a problem hiding this comment.
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.
| render: ({ hours }) => <Text>{hours.toFixed(1)}</Text>, | |
| render: ({ hours }) => <Text>{hours.toFixed(3)}</Text>, |
There was a problem hiding this comment.
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
embedded-react-sdk/src/components/Payroll/helpers.ts
Lines 166 to 174 in 103ec00
| export const formatHoursDisplay = (hours: number): string => { | |
| const rounded = roundToTwoDecimals(hours) | |
| if (rounded % 1 === 0) { | |
| return `${rounded}.0` | |
| } | |
| return rounded.toString() | |
| } |
| ? contractor.hours.toFixed(3) | ||
| : '0.000'} |
Copilot
AI
Oct 21, 2025
There was a problem hiding this comment.
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.
| const handleCancelPayment = (paymentId: string, contractorName: string) => { | ||
| const confirmed = window.confirm(t('cancelConfirmation')) |
Copilot
AI
Oct 21, 2025
There was a problem hiding this comment.
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.
|
should this be placed under |
There was a problem hiding this 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
| export type ContractorDataStrict = Omit<ContractorData, 'hours' | 'wageType' | 'paymentMethod'> & { | ||
| wageType: 'Fixed' | 'Hourly' | ||
| paymentMethod: PostV1CompaniesCompanyIdContractorPaymentsRequestBody['paymentMethod'] | ||
| hours: number | ||
| } |
There was a problem hiding this comment.
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' | |||
There was a problem hiding this comment.
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 = ({ |
There was a problem hiding this comment.
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
| if (editingContractor) { | ||
| return ( | ||
| <ContractorPaymentEditModal | ||
| contractor={editingContractor} | ||
| onSave={onSaveContractor} | ||
| onCancel={onCancelEdit} | ||
| /> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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
| const handleCancelPayment = (paymentId: string, contractorName: string) => { | ||
| const confirmed = window.confirm(t('cancelConfirmation')) |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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
embedded-react-sdk/src/components/Payroll/helpers.ts
Lines 166 to 174 in 103ec00
| export const formatHoursDisplay = (hours: number): string => { | |
| const rounded = roundToTwoDecimals(hours) | |
| if (rounded % 1 === 0) { | |
| return `${rounded}.0` | |
| } | |
| return rounded.toString() | |
| } |
| const handleFieldChange = (field: keyof ContractorData, value: number | string) => { | ||
| setEditedContractor(prev => ({ | ||
| ...prev, | ||
| [field]: value, | ||
| total: calculateTotal(), | ||
| })) | ||
| } |
There was a problem hiding this comment.
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?
| const calculateTotal = () => { | ||
| const wageAmount = | ||
| editedContractor.wageType === 'Hourly' | ||
| ? editedContractor.hours * (editedContractor.hourlyRate || 0) | ||
| : editedContractor.wage | ||
| return wageAmount + editedContractor.bonus + editedContractor.reimbursement | ||
| } |
There was a problem hiding this comment.
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
- 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>
ac7da39 to
92ec042
Compare
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
Components
Screenshots