11import { useEffect , useMemo , useState , type ReactNode } from 'react'
2- import { useEmployeesListSuspense } from '@gusto/embedded-api/react-query/employeesList'
2+ import { useEmployeesList } from '@gusto/embedded-api/react-query/employeesList'
33import { usePayrollsGetSuspense } from '@gusto/embedded-api/react-query/payrollsGet'
4+ import { keepPreviousData } from '@tanstack/react-query'
45import { usePayrollsCalculateMutation } from '@gusto/embedded-api/react-query/payrollsCalculate'
56import type { Employee } from '@gusto/embedded-api/models/components/employee'
67import type { PayrollProcessingRequest } from '@gusto/embedded-api/models/components/payrollprocessingrequest'
@@ -9,6 +10,7 @@ import { useTranslation } from 'react-i18next'
910import { usePayrollsUpdateMutation } from '@gusto/embedded-api/react-query/payrollsUpdate'
1011import type { PayrollEmployeeCompensationsType } from '@gusto/embedded-api/models/components/payrollemployeecompensationstype'
1112import type { PayrollUpdateEmployeeCompensations } from '@gusto/embedded-api/models/components/payrollupdate'
13+ import type { EmployeeCompensations } from '@gusto/embedded-api/models/components/payrollshow'
1214import { usePreparedPayrollData } from '../usePreparedPayrollData'
1315import { payrollSubmitHandler , type ApiPayrollBlocker } from '../PayrollBlocker/payrollHelpers'
1416import { PayrollConfigurationPresentation } from './PayrollConfigurationPresentation'
@@ -18,7 +20,9 @@ import { componentEvents } from '@/shared/constants'
1820import { useComponentDictionary , useI18n } from '@/i18n'
1921import { useBase } from '@/components/Base'
2022import type { PaginationItemsPerPage } from '@/components/Common/PaginationControl/PaginationControlTypes'
23+ import { Loading } from '@/components/Common'
2124import { useDateFormatter } from '@/hooks/useDateFormatter'
25+ import { usePagination } from '@/hooks/usePagination'
2226
2327const isCalculating = ( processingRequest ?: PayrollProcessingRequest | null ) =>
2428 processingRequest ?. status === PayrollProcessingRequestStatus . Calculating
@@ -32,6 +36,12 @@ interface PayrollConfigurationProps extends BaseComponentInterface<'Payroll.Payr
3236 withReimbursements ?: boolean
3337}
3438
39+ interface CommittedTableData {
40+ employees : Employee [ ]
41+ compensations : EmployeeCompensations [ ]
42+ page : number
43+ }
44+
3545export function PayrollConfiguration ( props : PayrollConfigurationProps & BaseComponentInterface ) {
3646 return (
3747 < BaseComponent { ...props } >
@@ -53,56 +63,42 @@ export const Root = ({
5363 const { t } = useTranslation ( 'Payroll.PayrollConfiguration' )
5464 const { baseSubmitHandler } = useBase ( )
5565 const dateFormatter = useDateFormatter ( )
56- const defaultItemsPerPage = 10
5766
5867 const [ currentPage , setCurrentPage ] = useState ( 1 )
59- const [ itemsPerPage , setItemsPerPage ] = useState < PaginationItemsPerPage > ( defaultItemsPerPage )
68+ const [ itemsPerPage , setItemsPerPage ] = useState < PaginationItemsPerPage > ( 10 )
6069 const [ isPolling , setIsPolling ] = useState ( false )
6170 const [ payrollBlockers , setPayrollBlockers ] = useState < ApiPayrollBlocker [ ] > ( [ ] )
6271
63- const { data : employeeData , isFetching : isFetchingEmployeeData } = useEmployeesListSuspense ( {
64- companyId,
65- payrollUuid : payrollId , // get back list of employees to specific payroll
66- per : itemsPerPage ,
67- page : currentPage ,
68- sortBy : 'name' , // sort alphanumeric by employee last_names
69- } )
70-
71- // get list of employee uuids to filter into prepare endpoint to get back employee_compensation data
72- const employeeUuids = useMemo ( ( ) => {
73- return employeeData . showEmployees ?. map ( e => e . uuid ) || [ ]
74- } , [ employeeData . showEmployees ] )
75-
76- const totalPages = Number ( employeeData . httpMeta . response . headers . get ( 'x-total-pages' ) ?? 1 )
77-
78- const handleItemsPerPageChange = ( newCount : PaginationItemsPerPage ) => {
79- setItemsPerPage ( newCount )
80- }
81- const handleFirstPage = ( ) => {
82- setCurrentPage ( 1 )
83- }
84- const handlePreviousPage = ( ) => {
85- setCurrentPage ( prevPage => Math . max ( prevPage - 1 , 1 ) )
86- }
87- const handleNextPage = ( ) => {
88- setCurrentPage ( prevPage => Math . min ( prevPage + 1 , totalPages ) )
89- }
90- const handleLastPage = ( ) => {
91- setCurrentPage ( totalPages )
92- }
72+ const {
73+ data : employeeData ,
74+ isFetching : isFetchingEmployeeData ,
75+ fetchStatus,
76+ } = useEmployeesList (
77+ {
78+ companyId,
79+ payrollUuid : payrollId ,
80+ per : itemsPerPage ,
81+ page : currentPage ,
82+ sortBy : 'name' ,
83+ } ,
84+ { placeholderData : keepPreviousData } ,
85+ )
9386
9487 const pagination = {
95- currentPage,
96- handleFirstPage,
97- handlePreviousPage,
98- handleNextPage,
99- handleLastPage,
100- handleItemsPerPageChange,
101- totalPages,
88+ ...usePagination ( employeeData ?. httpMeta , {
89+ currentPage,
90+ itemsPerPage,
91+ setCurrentPage,
92+ setItemsPerPage,
93+ } ) ,
10294 isFetching : isFetchingEmployeeData ,
103- itemsPerPage,
10495 }
10596
97+ const employeeUuids = useMemo (
98+ ( ) => employeeData ?. showEmployees ?. map ( e => e . uuid ) ?? [ ] ,
99+ [ employeeData ?. showEmployees ] ,
100+ )
101+
106102 const { data : payrollData } = usePayrollsGetSuspense (
107103 {
108104 companyId,
@@ -125,11 +121,83 @@ export const Root = ({
125121 companyId,
126122 payrollId,
127123 employeeUuids,
128- sortBy : 'last_name' , // sort alphanumeric by employee last_names to match employees GET
124+ sortBy : 'last_name' ,
129125 } )
130126
127+ const [ committedData , setCommittedData ] = useState < CommittedTableData | null > ( null )
128+
129+ const compensationUuids = useMemo (
130+ ( ) => new Set ( preparedPayroll ?. employeeCompensations ?. map ( c => c . employeeUuid ) ?? [ ] ) ,
131+ [ preparedPayroll ?. employeeCompensations ] ,
132+ )
133+
134+ const isEmptyPage = employeeData ?. showEmployees !== undefined && employeeUuids . length === 0
135+
136+ const doUuidsMatch = useMemo (
137+ ( ) => employeeUuids . length > 0 && employeeUuids . every ( uuid => compensationUuids . has ( uuid ) ) ,
138+ [ employeeUuids , compensationUuids ] ,
139+ )
140+
141+ const isDataInSync =
142+ ! isFetchingEmployeeData &&
143+ ! isPrepareLoading &&
144+ employeeData ?. showEmployees !== undefined &&
145+ ( isEmptyPage || ( preparedPayroll ?. employeeCompensations && doUuidsMatch ) )
146+
147+ useEffect ( ( ) => {
148+ if ( isDataInSync ) {
149+ setCommittedData ( {
150+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
151+ employees : employeeData ?. showEmployees ?? [ ] ,
152+ compensations : isEmptyPage ? [ ] : ( preparedPayroll ?. employeeCompensations ?? [ ] ) ,
153+ page : currentPage ,
154+ } )
155+ }
156+ } , [
157+ isDataInSync ,
158+ employeeData ?. showEmployees ,
159+ preparedPayroll ?. employeeCompensations ,
160+ currentPage ,
161+ isEmptyPage ,
162+ ] )
163+
164+ useEffect ( ( ) => {
165+ if ( ! payrollData . payrollShow ) return
166+
167+ if ( isCalculating ( payrollData . payrollShow . processingRequest ) && ! isPolling ) {
168+ setIsPolling ( true )
169+ }
170+ if ( isPolling && isCalculated ( payrollData . payrollShow . processingRequest ) ) {
171+ onEvent ( componentEvents . RUN_PAYROLL_CALCULATED , {
172+ payrollId,
173+ alert : { type : 'success' , title : t ( 'alerts.progressSaved' ) } ,
174+ payPeriod : payrollData . payrollShow . payPeriod ,
175+ } )
176+ setPayrollBlockers ( [ ] )
177+ setIsPolling ( false )
178+ }
179+ if (
180+ isPolling &&
181+ payrollData . payrollShow . processingRequest ?. status ===
182+ PayrollProcessingRequestStatus . ProcessingFailed
183+ ) {
184+ onEvent ( componentEvents . RUN_PAYROLL_PROCESSING_FAILED )
185+ setIsPolling ( false )
186+ }
187+ } , [
188+ payrollData . payrollShow ?. processingRequest ,
189+ isPolling ,
190+ onEvent ,
191+ t ,
192+ payrollId ,
193+ payrollData . payrollShow ?. calculatedAt ,
194+ ] )
195+
196+ if ( fetchStatus === 'fetching' && ! employeeData ) {
197+ return < Loading />
198+ }
199+
131200 const onCalculatePayroll = async ( ) => {
132- // Clear any existing blockers before attempting calculation
133201 setPayrollBlockers ( [ ] )
134202
135203 await baseSubmitHandler ( { } , async ( ) => {
@@ -148,24 +216,25 @@ export const Root = ({
148216 }
149217 } )
150218 }
219+
151220 const onEdit = ( employee : Employee ) => {
152221 onEvent ( componentEvents . RUN_PAYROLL_EMPLOYEE_EDIT , {
153222 employeeId : employee . uuid ,
154223 firstName : employee . firstName ,
155224 lastName : employee . lastName ,
156225 } )
157226 }
227+
158228 const transformEmployeeCompensation = ( {
159229 paymentMethod,
160230 reimbursements,
161231 ...compensation
162- } : PayrollEmployeeCompensationsType ) : PayrollUpdateEmployeeCompensations => {
163- return {
164- ...compensation ,
165- ...( paymentMethod && paymentMethod !== 'Historical' ? { paymentMethod } : { } ) ,
166- memo : compensation . memo || undefined ,
167- }
168- }
232+ } : PayrollEmployeeCompensationsType ) : PayrollUpdateEmployeeCompensations => ( {
233+ ...compensation ,
234+ ...( paymentMethod && paymentMethod !== 'Historical' ? { paymentMethod } : { } ) ,
235+ memo : compensation . memo || undefined ,
236+ } )
237+
169238 const onToggleExclude = async ( employeeCompensation : PayrollEmployeeCompensationsType ) => {
170239 onEvent ( componentEvents . RUN_PAYROLL_EMPLOYEE_SKIP , {
171240 employeeId : employeeCompensation . employeeUuid ,
@@ -186,49 +255,10 @@ export const Root = ({
186255 onEvent ( componentEvents . RUN_PAYROLL_EMPLOYEE_SAVED , {
187256 payrollPrepared : result . payrollPrepared ,
188257 } )
189- // Refresh preparedPayroll to get updated data
190258 await handlePreparePayroll ( )
191259 } )
192260 }
193261
194- const onViewBlockers = ( ) => {
195- onEvent ( componentEvents . RUN_PAYROLL_BLOCKERS_VIEW_ALL )
196- }
197-
198- useEffect ( ( ) => {
199- // Start polling when payroll is calculating and not already polling
200- if ( isCalculating ( payrollData . payrollShow ?. processingRequest ) && ! isPolling ) {
201- setIsPolling ( true )
202- }
203- // Stop polling and emit event when payroll is calculated successfully
204- if ( isPolling && isCalculated ( payrollData . payrollShow ?. processingRequest ) ) {
205- onEvent ( componentEvents . RUN_PAYROLL_CALCULATED , {
206- payrollId,
207- alert : { type : 'success' , title : t ( 'alerts.progressSaved' ) } ,
208- payPeriod : payrollData . payrollShow ?. payPeriod ,
209- } )
210- // Clear blockers on successful calculation
211- setPayrollBlockers ( [ ] )
212- setIsPolling ( false )
213- }
214- // If we are polling and payroll is in failed state, stop polling, and emit failure event
215- if (
216- isPolling &&
217- payrollData . payrollShow ?. processingRequest ?. status ===
218- PayrollProcessingRequestStatus . ProcessingFailed
219- ) {
220- onEvent ( componentEvents . RUN_PAYROLL_PROCESSING_FAILED )
221- setIsPolling ( false )
222- }
223- } , [
224- payrollData . payrollShow ?. processingRequest ,
225- isPolling ,
226- onEvent ,
227- t ,
228- payrollId ,
229- payrollData . payrollShow ?. calculatedAt ,
230- ] )
231-
232262 const payrollDeadlineNotice = payrollData . payrollShow
233263 ? {
234264 label : t ( 'alerts.directDepositDeadline' , {
@@ -239,20 +269,29 @@ export const Root = ({
239269 }
240270 : undefined
241271
272+ // Fallback: Use committed (synced) data first, then live data, then empty array.
273+ // This ensures employee names and compensations display together after pagination.
274+ const displayCompensations =
275+ committedData ?. compensations ?? preparedPayroll ?. employeeCompensations ?? [ ]
276+ const displayEmployees = committedData ?. employees ?? employeeData ?. showEmployees ?? [ ]
277+
242278 return (
243279 < PayrollConfigurationPresentation
244280 onCalculatePayroll = { onCalculatePayroll }
245281 onEdit = { onEdit }
246282 onToggleExclude = { onToggleExclude }
247- onViewBlockers = { onViewBlockers }
248- employeeCompensations = { preparedPayroll ?. employeeCompensations || [ ] }
249- employeeDetails = { employeeData . showEmployees || [ ] }
283+ onViewBlockers = { ( ) => {
284+ onEvent ( componentEvents . RUN_PAYROLL_BLOCKERS_VIEW_ALL )
285+ } }
286+ employeeCompensations = { displayCompensations }
287+ employeeDetails = { displayEmployees }
250288 payPeriod = { preparedPayroll ?. payPeriod }
251289 paySchedule = { paySchedule }
252290 isOffCycle = { preparedPayroll ?. offCycle }
253291 alerts = { alerts }
254292 payrollDeadlineNotice = { payrollDeadlineNotice }
255- isPending = { isPolling || isPrepareLoading || isUpdatingPayroll }
293+ isPending = { isPolling || ( isPrepareLoading && ! preparedPayroll ) || isUpdatingPayroll }
294+ isFetching = { isFetchingEmployeeData || isPrepareLoading }
256295 payrollBlockers = { payrollBlockers }
257296 pagination = { pagination }
258297 withReimbursements = { withReimbursements }
0 commit comments