From f50cb9609ec6b81049246f563e316ce632e84497 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 22 Apr 2026 15:36:04 -0400 Subject: [PATCH 1/8] Addressing Fragment Concern https://github.com/CruGlobal/mpdx-react/pull/1737\#discussion_r3126204715 --- .../GoalsList/PdsGoalCalculations.graphql | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql b/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql index 0ed9a14176..555bfab2c8 100644 --- a/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql +++ b/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql @@ -1,3 +1,13 @@ +fragment DesignationSupportHoursItemFields on DesignationSupportHoursItem { + id + hoursPerWeek + numberOfWeeks + name + label + position + predefined +} + fragment PdsGoalCalculationFields on DesignationSupportCalculation { id name @@ -19,13 +29,7 @@ fragment PdsGoalCalculationFields on DesignationSupportCalculation { ministryTravelMeals otherAnnualReimbursements designationSupportHoursItems { - id - hoursPerWeek - numberOfWeeks - name - label - position - predefined + ...DesignationSupportHoursItemFields } } @@ -78,13 +82,7 @@ mutation CreateDesignationSupportHoursItem( ) { createDesignationSupportHoursItem(input: { attributes: $attributes }) { designationSupportHoursItem { - id - hoursPerWeek - numberOfWeeks - name - label - position - predefined + ...DesignationSupportHoursItemFields } } } @@ -94,13 +92,7 @@ mutation UpdateDesignationSupportHoursItem( ) { updateDesignationSupportHoursItem(input: { attributes: $attributes }) { designationSupportHoursItem { - id - hoursPerWeek - numberOfWeeks - name - label - position - predefined + ...DesignationSupportHoursItemFields } } } From 7ca54001dd99335a6b40c02828e890e7259f1239 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 22 Apr 2026 15:38:20 -0400 Subject: [PATCH 2/8] Addressing double snackbar https://github.com/CruGlobal/mpdx-react/pull/1737\#discussion_r3126219630 --- .../Shared/Autosave/useSaveField.ts | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.ts b/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.ts index c6242873ef..2a8ce9e6cb 100644 --- a/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.ts +++ b/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.ts @@ -1,6 +1,4 @@ import { useCallback } from 'react'; -import { useSnackbar } from 'notistack'; -import { useTranslation } from 'react-i18next'; import { usePdsGoalCalculator } from 'src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext'; import { DesignationSupportCalculationUpdateInput } from 'src/graphql/types.generated'; import { useUpdatePdsGoalCalculationMutation } from '../../GoalsList/PdsGoalCalculations.generated'; @@ -8,8 +6,6 @@ import { useUpdatePdsGoalCalculationMutation } from '../../GoalsList/PdsGoalCalc export const useSaveField = () => { const { calculation, trackMutation } = usePdsGoalCalculator(); const [updatePdsGoalCalculation] = useUpdatePdsGoalCalculationMutation(); - const { enqueueSnackbar } = useSnackbar(); - const { t } = useTranslation(); const saveField = useCallback( async (attributes: Partial) => { @@ -24,35 +20,29 @@ export const useSaveField = () => { return; } - try { - return await trackMutation( - updatePdsGoalCalculation({ - variables: { - attributes: { - id: calculation.id, - ...attributes, - }, + return trackMutation( + updatePdsGoalCalculation({ + variables: { + attributes: { + id: calculation.id, + ...attributes, }, - optimisticResponse: { - updateDesignationSupportCalculation: { - __typename: - 'DesignationSupportCalculationUpdateMutationPayload', - designationSupportCalculation: { - __typename: 'DesignationSupportCalculation', - ...calculation, - ...attributes, - }, + }, + optimisticResponse: { + updateDesignationSupportCalculation: { + __typename: + 'DesignationSupportCalculationUpdateMutationPayload', + designationSupportCalculation: { + __typename: 'DesignationSupportCalculation', + ...calculation, + ...attributes, }, }, - }), - ); - } catch { - enqueueSnackbar(t('Failed to save changes. Please try again.'), { - variant: 'error', - }); - } + }, + }), + ); }, - [calculation, trackMutation, updatePdsGoalCalculation, enqueueSnackbar, t], + [calculation, trackMutation, updatePdsGoalCalculation], ); return saveField; From 9098dda426d336e02547d87d9691ebfa90c30eca Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 22 Apr 2026 15:50:23 -0400 Subject: [PATCH 3/8] Removing block no longer needed https://github.com/CruGlobal/mpdx-react/pull/1737\#discussion_r3126236399 --- .../HoursPerWeekGrid/HoursPerWeekGrid.tsx | 55 +++++-------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx index fe1f5aa157..0f89d37034 100644 --- a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx +++ b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx @@ -118,53 +118,26 @@ export const HoursPerWeekGrid: React.FC = ({ const weeksRemaining = MAX_TOTAL_WEEKS - totalWeeks; const saveHoursItem = useCallback( - async (entry: HoursPerWeekEntry, currentEntries: HoursPerWeekEntry[]) => { + async (entry: HoursPerWeekEntry) => { if (!calculation) { return; } try { - if (entry.id.startsWith('default-')) { - const result = await trackMutation( - createHoursItem({ - variables: { - attributes: { - designationSupportCalculationId: calculation.id, - label: entry.label, - hoursPerWeek: entry.hoursPerWeek, - numberOfWeeks: entry.weeks, - position: currentEntries.findIndex((e) => e.id === entry.id), - }, + await trackMutation( + updateHoursItem({ + variables: { + attributes: { + id: entry.id, + designationSupportCalculationId: calculation.id, + label: entry.label, + hoursPerWeek: entry.hoursPerWeek, + numberOfWeeks: entry.weeks, }, - refetchQueries: ['PdsGoalCalculation'], - }), - ); - const created = - result.data?.createDesignationSupportHoursItem - ?.designationSupportHoursItem; - if (created) { - setEntries((prev) => - prev.map((e) => - e.id === entry.id ? { ...e, id: created.id } : e, - ), - ); - } - } else { - await trackMutation( - updateHoursItem({ - variables: { - attributes: { - id: entry.id, - designationSupportCalculationId: calculation.id, - label: entry.label, - hoursPerWeek: entry.hoursPerWeek, - numberOfWeeks: entry.weeks, - }, - }, - refetchQueries: ['PdsGoalCalculation'], - }), - ); - } + }, + refetchQueries: ['PdsGoalCalculation'], + }), + ); } catch (error) { enqueueSnackbar(t('Failed to save hours entry. Please try again.'), { variant: 'error', From ddab623668cc259deb12c5ee184a53651f47f55a Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 22 Apr 2026 15:53:13 -0400 Subject: [PATCH 4/8] Remove extra snackbars https://github.com/CruGlobal/mpdx-react/pull/1737\#discussion_r3126242447 --- .../HoursPerWeekGrid/HoursPerWeekGrid.tsx | 35 +++---------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx index 0f89d37034..8b934bfd9b 100644 --- a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx +++ b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx @@ -21,7 +21,6 @@ import { GridColDef, GridValidRowModel, } from '@mui/x-data-grid'; -import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import { BaseGrid } from 'src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/BaseGrid'; import { @@ -64,7 +63,6 @@ export const HoursPerWeekGrid: React.FC = ({ onApply, }) => { const { t } = useTranslation(); - const { enqueueSnackbar } = useSnackbar(); const { calculation, trackMutation } = usePdsGoalCalculator(); const [createHoursItem] = useCreateDesignationSupportHoursItemMutation(); const [updateHoursItem] = useUpdateDesignationSupportHoursItemMutation(); @@ -138,21 +136,11 @@ export const HoursPerWeekGrid: React.FC = ({ refetchQueries: ['PdsGoalCalculation'], }), ); - } catch (error) { - enqueueSnackbar(t('Failed to save hours entry. Please try again.'), { - variant: 'error', - }); - throw error; + } catch { + throw new Error(t('Failed to save hours entry.')); } }, - [ - calculation, - createHoursItem, - updateHoursItem, - trackMutation, - enqueueSnackbar, - t, - ], + [calculation, createHoursItem, updateHoursItem, trackMutation, t], ); const updateEntry = useCallback( @@ -210,18 +198,8 @@ export const HoursPerWeekGrid: React.FC = ({ } } catch { setEntries((prev) => prev.filter((e) => e.id !== tempId)); - enqueueSnackbar(t('Failed to add entry. Please try again.'), { - variant: 'error', - }); } - }, [ - t, - calculation, - createHoursItem, - trackMutation, - entries.length, - enqueueSnackbar, - ]); + }, [t, calculation, createHoursItem, trackMutation, entries.length]); const deleteEntry = useCallback( async (id: string | number) => { @@ -255,12 +233,9 @@ export const HoursPerWeekGrid: React.FC = ({ }); } catch { setEntries(previousEntries); - enqueueSnackbar(t('Failed to delete entry. Please try again.'), { - variant: 'error', - }); } }, - [entries, deleteHoursItem, trackMutation, saveField, enqueueSnackbar, t], + [entries, deleteHoursItem, trackMutation, saveField], ); const processRowUpdate = useCallback( From aafc236003670f82e2c0bbe5e500237be651e720 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 22 Apr 2026 16:05:22 -0400 Subject: [PATCH 5/8] Extract logic into hook https://github.com/CruGlobal/mpdx-react/pull/1737\#discussion_r3126255662 --- .../HoursPerWeekGrid/HoursPerWeekGrid.tsx | 289 +----------- .../useHoursPerWeekGrid.test.tsx | 422 ++++++++++++++++++ .../HoursPerWeekGrid/useHoursPerWeekGrid.ts | 280 ++++++++++++ 3 files changed, 715 insertions(+), 276 deletions(-) create mode 100644 src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.test.tsx create mode 100644 src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.ts diff --git a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx index 8b934bfd9b..b0d289babb 100644 --- a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx +++ b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useMemo } from 'react'; import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; import { @@ -16,20 +10,10 @@ import { Typography, styled, } from '@mui/material'; -import { - GridActionsCellItem, - GridColDef, - GridValidRowModel, -} from '@mui/x-data-grid'; +import { GridActionsCellItem, GridColDef } from '@mui/x-data-grid'; import { useTranslation } from 'react-i18next'; import { BaseGrid } from 'src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/BaseGrid'; -import { - useCreateDesignationSupportHoursItemMutation, - useDeleteDesignationSupportHoursItemMutation, - useUpdateDesignationSupportHoursItemMutation, -} from '../../GoalsList/PdsGoalCalculations.generated'; -import { useSaveField } from '../../Shared/Autosave/useSaveField'; -import { usePdsGoalCalculator } from '../../Shared/PdsGoalCalculatorContext'; +import { useHoursPerWeekGrid } from './useHoursPerWeekGrid'; const StyledCard = styled(Card)(({ theme }) => ({ borderRadius: theme.shape.borderRadius, @@ -43,270 +27,23 @@ const FooterRow = styled(Box)(({ theme }) => ({ fontWeight: 'bold', })); -export interface HoursPerWeekEntry { - id: string; - label: string; - hoursPerWeek: number; - weeks: number; - canDelete: boolean; - predefined: boolean; - position: number; -} - interface HoursPerWeekGridProps { onApply?: (averageHoursPerWeek: number) => void; } -const MAX_TOTAL_WEEKS = 52; - export const HoursPerWeekGrid: React.FC = ({ onApply, }) => { const { t } = useTranslation(); - const { calculation, trackMutation } = usePdsGoalCalculator(); - const [createHoursItem] = useCreateDesignationSupportHoursItemMutation(); - const [updateHoursItem] = useUpdateDesignationSupportHoursItemMutation(); - const [deleteHoursItem] = useDeleteDesignationSupportHoursItemMutation(); - const saveField = useSaveField(); - - const [entries, setEntries] = useState([]); - const initializedRef = useRef(false); - const nextEntryIdRef = useRef(0); - - useEffect(() => { - const items = calculation?.designationSupportHoursItems; - if (!initializedRef.current && items && items.length > 0) { - initializedRef.current = true; - setEntries( - items - .slice() - .sort( - (hoursItemA, hoursItemB) => - (hoursItemA.position ?? 0) - (hoursItemB.position ?? 0), - ) - .map((item) => ({ - id: item.id, - label: item.label, - hoursPerWeek: item.hoursPerWeek ?? 0, - weeks: item.numberOfWeeks ?? 0, - canDelete: !item.predefined, - predefined: item.predefined, - position: item.position ?? 0, - })), - ); - } - }, [calculation?.designationSupportHoursItems]); - - const totalWeeks = useMemo( - () => entries.reduce((sum, entry) => sum + entry.weeks, 0), - [entries], - ); - - const totalHours = useMemo( - () => - entries.reduce((sum, entry) => sum + entry.hoursPerWeek * entry.weeks, 0), - [entries], - ); - - const averageHoursPerWeek = useMemo( - () => (totalWeeks > 0 ? totalHours / totalWeeks : 0), - [totalHours, totalWeeks], - ); - - const weeksRemaining = MAX_TOTAL_WEEKS - totalWeeks; - - const saveHoursItem = useCallback( - async (entry: HoursPerWeekEntry) => { - if (!calculation) { - return; - } - - try { - await trackMutation( - updateHoursItem({ - variables: { - attributes: { - id: entry.id, - designationSupportCalculationId: calculation.id, - label: entry.label, - hoursPerWeek: entry.hoursPerWeek, - numberOfWeeks: entry.weeks, - }, - }, - refetchQueries: ['PdsGoalCalculation'], - }), - ); - } catch { - throw new Error(t('Failed to save hours entry.')); - } - }, - [calculation, createHoursItem, updateHoursItem, trackMutation, t], - ); - - const updateEntry = useCallback( - (id: string, updates: Partial) => { - setEntries((prev) => - prev.map((entry) => - entry.id === id ? { ...entry, ...updates } : entry, - ), - ); - }, - [], - ); - - const addEntry = useCallback(async () => { - if (!calculation) { - return; - } - - const tempId = `temp-${nextEntryIdRef.current++}`; - const newPosition = - entries.length > 0 ? Math.max(...entries.map((e) => e.position)) + 1 : 0; - const newEntry: HoursPerWeekEntry = { - id: tempId, - label: t('New Entry'), - hoursPerWeek: 0, - weeks: 0, - canDelete: true, - predefined: false, - position: newPosition, - }; - setEntries((prev) => [...prev, newEntry]); - - try { - const result = await trackMutation( - createHoursItem({ - variables: { - attributes: { - designationSupportCalculationId: calculation.id, - label: t('New Entry'), - hoursPerWeek: 0, - numberOfWeeks: 0, - position: newPosition, - }, - }, - refetchQueries: ['PdsGoalCalculation'], - }), - ); - const created = - result.data?.createDesignationSupportHoursItem - ?.designationSupportHoursItem; - if (created) { - setEntries((prev) => - prev.map((e) => (e.id === tempId ? { ...e, id: created.id } : e)), - ); - } - } catch { - setEntries((prev) => prev.filter((e) => e.id !== tempId)); - } - }, [t, calculation, createHoursItem, trackMutation, entries.length]); - - const deleteEntry = useCallback( - async (id: string | number) => { - const entryId = id.toString(); - const previousEntries = entries; - const remainingEntries = entries.filter((entry) => entry.id !== entryId); - setEntries(remainingEntries); - - // Recalculate and autosave the average - const newTotalWeeks = remainingEntries.reduce( - (sum, e) => sum + e.weeks, - 0, - ); - const newTotalHours = remainingEntries.reduce( - (sum, e) => sum + e.hoursPerWeek * e.weeks, - 0, - ); - const newAverage = newTotalWeeks > 0 ? newTotalHours / newTotalWeeks : 0; - - try { - if (!entryId.startsWith('temp-') && !entryId.startsWith('default-')) { - await trackMutation( - deleteHoursItem({ - variables: { id: entryId }, - refetchQueries: ['PdsGoalCalculation'], - }), - ); - } - saveField({ - averageHoursPerWeek: newAverage, - }); - } catch { - setEntries(previousEntries); - } - }, - [entries, deleteHoursItem, trackMutation, saveField], - ); - - const processRowUpdate = useCallback( - async (newRow: GridValidRowModel) => { - if (newRow.id === 'total') { - return newRow; - } - - const newWeeks = Number(newRow.weeks) || 0; - const otherWeeks = entries - .filter((e) => e.id !== newRow.id) - .reduce((sum, e) => sum + e.weeks, 0); - const clampedWeeks = Math.min(newWeeks, MAX_TOTAL_WEEKS - otherWeeks); - - const updatedEntry: HoursPerWeekEntry = { - id: newRow.id as string, - label: newRow.label as string, - hoursPerWeek: Number(newRow.hoursPerWeek) || 0, - weeks: Math.max(0, clampedWeeks), - canDelete: newRow.canDelete as boolean, - predefined: newRow.predefined as boolean, - position: Number(newRow.position) || 0, - }; - - updateEntry(newRow.id as string, { - label: updatedEntry.label, - hoursPerWeek: updatedEntry.hoursPerWeek, - weeks: updatedEntry.weeks, - }); - - await saveHoursItem(updatedEntry, entries); - - // Autosave the recalculated average to the calculation - const updatedEntries = entries.map((entry) => - entry.id === updatedEntry.id ? updatedEntry : entry, - ); - const newTotalWeeks = updatedEntries.reduce((sum, e) => sum + e.weeks, 0); - const newTotalHours = updatedEntries.reduce( - (sum, e) => sum + e.hoursPerWeek * e.weeks, - 0, - ); - const newAverage = newTotalWeeks > 0 ? newTotalHours / newTotalWeeks : 0; - saveField({ - averageHoursPerWeek: newAverage, - }); - - return { ...newRow, weeks: Math.max(0, clampedWeeks) }; - }, - [updateEntry, entries, saveHoursItem, saveField], - ); - - const dataWithTotal = useMemo( - () => [ - ...entries - .slice() - .sort((a, b) => a.position - b.position) - .map((entry) => ({ - ...entry, - totalHours: entry.hoursPerWeek * entry.weeks, - })), - { - id: 'total', - label: t('Total'), - hoursPerWeek: null, - weeks: totalWeeks, - totalHours: totalHours, - canDelete: false, - }, - ], - [entries, totalWeeks, totalHours, t], - ); + const { + totalHours, + averageHoursPerWeek, + weeksRemaining, + dataWithTotal, + addEntry, + deleteEntry, + processRowUpdate, + } = useHoursPerWeekGrid(); const columns: GridColDef[] = useMemo( () => [ @@ -446,7 +183,7 @@ export const HoursPerWeekGrid: React.FC = ({ {weeksRemaining > 0 && ( {t('Weeks must add up to {{max}}. {{remaining}} week(s) remaining.', { - max: MAX_TOTAL_WEEKS, + max: 52, remaining: weeksRemaining, })} diff --git a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.test.tsx b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.test.tsx new file mode 100644 index 0000000000..05ebbdb849 --- /dev/null +++ b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.test.tsx @@ -0,0 +1,422 @@ +import React from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import { + PdsGoalCalculationMock, + PdsGoalCalculatorTestWrapper, +} from '../../PdsGoalCalculatorTestWrapper'; +import { useHoursPerWeekGrid } from './useHoursPerWeekGrid'; + +const defaultCalculationMock: PdsGoalCalculationMock = { + id: 'goal-1', + designationSupportHoursItems: [ + { + id: 'item-regular', + label: 'Regular Week', + hoursPerWeek: 40, + numberOfWeeks: 48, + name: 'regular', + position: 0, + predefined: true, + }, + { + id: 'item-travel', + label: 'Travel', + hoursPerWeek: 0, + numberOfWeeks: 0, + name: 'travel', + position: 1, + predefined: true, + }, + { + id: 'item-vacation', + label: 'Unpaid Vacation', + hoursPerWeek: 0, + numberOfWeeks: 0, + name: 'vacation', + position: 2, + predefined: true, + }, + ], +}; + +const mutationSpy = jest.fn(); + +const createWrapper = ( + calculationMock: PdsGoalCalculationMock = defaultCalculationMock, +): React.FC<{ children: React.ReactNode }> => { + const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + + {children} + + ); + return Wrapper; +}; + +const waitForDataToLoad = async () => { + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('PdsGoalCalculation'), + ); +}; + +describe('useHoursPerWeekGrid', () => { + beforeEach(() => { + mutationSpy.mockClear(); + }); + + it('initializes entries from server data sorted by position', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + expect(result.current.entries[0]).toMatchObject({ + id: 'item-regular', + label: 'Regular Week', + hoursPerWeek: 40, + weeks: 48, + position: 0, + }); + expect(result.current.entries[1]).toMatchObject({ + id: 'item-travel', + label: 'Travel', + position: 1, + }); + expect(result.current.entries[2]).toMatchObject({ + id: 'item-vacation', + label: 'Unpaid Vacation', + position: 2, + }); + }); + + it('computes totalWeeks, totalHours, and averageHoursPerWeek', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + // Regular Week: 40 hrs * 48 wks = 1920 total hours + expect(result.current.totalWeeks).toBe(48); + expect(result.current.totalHours).toBe(1920); + expect(result.current.averageHoursPerWeek).toBe(40); + }); + + it('computes weeksRemaining as 52 minus totalWeeks', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + // 52 - 48 = 4 weeks remaining + expect(result.current.weeksRemaining).toBe(4); + }); + + it('returns 0 for averageHoursPerWeek when totalWeeks is 0', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper({ + id: 'goal-1', + designationSupportHoursItems: [ + { + id: 'item-1', + label: 'Entry', + hoursPerWeek: 10, + numberOfWeeks: 0, + name: 'entry', + position: 0, + predefined: false, + }, + ], + }), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(1); + }); + + expect(result.current.averageHoursPerWeek).toBe(0); + }); + + it('builds dataWithTotal with a total row appended', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.dataWithTotal).toHaveLength(4); + }); + + const totalRow = + result.current.dataWithTotal[result.current.dataWithTotal.length - 1]; + expect(totalRow).toMatchObject({ + id: 'total', + label: 'Total', + weeks: 48, + totalHours: 1920, + canDelete: false, + }); + }); + + it('adds totalHours to each entry in dataWithTotal', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.dataWithTotal).toHaveLength(4); + }); + + // Regular Week: 40 * 48 = 1920 + expect(result.current.dataWithTotal[0]).toMatchObject({ + id: 'item-regular', + totalHours: 1920, + }); + // Travel: 0 * 0 = 0 + expect(result.current.dataWithTotal[1]).toMatchObject({ + id: 'item-travel', + totalHours: 0, + }); + }); + + it('addEntry creates a temporary entry and fires a create mutation', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + await act(async () => { + await result.current.addEntry(); + }); + + expect(result.current.entries).toHaveLength(4); + expect(result.current.entries[3]).toMatchObject({ + label: 'New Entry', + hoursPerWeek: 0, + weeks: 0, + canDelete: true, + predefined: false, + position: 3, + }); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation( + 'CreateDesignationSupportHoursItem', + { + attributes: { + designationSupportCalculationId: 'goal-1', + label: 'New Entry', + hoursPerWeek: 0, + numberOfWeeks: 0, + position: 3, + }, + }, + ), + ); + }); + + it('deleteEntry removes the entry and fires a delete mutation', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + // Add a custom entry first, then delete it + await act(async () => { + await result.current.addEntry(); + }); + + expect(result.current.entries).toHaveLength(4); + const newEntryId = result.current.entries[3].id; + + await act(async () => { + await result.current.deleteEntry(newEntryId); + }); + + expect(result.current.entries).toHaveLength(3); + }); + + it('deleteEntry autosaves the recalculated average', async () => { + const fullMock: PdsGoalCalculationMock = { + id: 'goal-1', + designationSupportHoursItems: [ + { + id: 'item-a', + label: 'A', + hoursPerWeek: 40, + numberOfWeeks: 26, + name: 'a', + position: 0, + predefined: false, + }, + { + id: 'item-b', + label: 'B', + hoursPerWeek: 20, + numberOfWeeks: 26, + name: 'b', + position: 1, + predefined: false, + }, + ], + }; + + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(fullMock), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(2); + }); + + // Average before delete: (40*26 + 20*26) / 52 = 30 + expect(result.current.averageHoursPerWeek).toBe(30); + + await act(async () => { + await result.current.deleteEntry('item-b'); + }); + + // After deleting B: average = (40*26) / 26 = 40 + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation', { + attributes: { + averageHoursPerWeek: 40, + }, + }), + ); + }); + + it('processRowUpdate clamps weeks to not exceed 52 total', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + // Regular has 48 weeks. Try to set Travel to 10 weeks — should clamp to 4 + const updatedRow = await act(async () => + result.current.processRowUpdate({ + id: 'item-travel', + label: 'Travel', + hoursPerWeek: 5, + weeks: 10, + canDelete: true, + predefined: true, + position: 1, + }), + ); + + expect(updatedRow.weeks).toBe(4); + }); + + it('processRowUpdate autosaves the recalculated average', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + await act(async () => { + await result.current.processRowUpdate({ + id: 'item-regular', + label: 'Regular Week', + hoursPerWeek: 20, + weeks: 48, + canDelete: false, + predefined: true, + position: 0, + }); + }); + + // Average = 20 * 48 / 48 = 20 + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation', { + attributes: { + averageHoursPerWeek: 20, + }, + }), + ); + }); + + it('processRowUpdate skips mutation for the total row', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + const returned = await act(async () => + result.current.processRowUpdate({ + id: 'total', + label: 'Total', + hoursPerWeek: null, + weeks: 48, + }), + ); + + expect(returned).toMatchObject({ id: 'total' }); + }); + + it('sets predefined entries as non-deletable', async () => { + const { result } = renderHook(useHoursPerWeekGrid, { + wrapper: createWrapper(), + }); + + await waitForDataToLoad(); + + await waitFor(() => { + expect(result.current.entries).toHaveLength(3); + }); + + // All default entries are predefined + result.current.entries.forEach((entry) => { + expect(entry.canDelete).toBe(false); + expect(entry.predefined).toBe(true); + }); + }); +}); diff --git a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.ts b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.ts new file mode 100644 index 0000000000..6f3a743e13 --- /dev/null +++ b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.ts @@ -0,0 +1,280 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { GridValidRowModel } from '@mui/x-data-grid'; +import { useTranslation } from 'react-i18next'; +import { + useCreateDesignationSupportHoursItemMutation, + useDeleteDesignationSupportHoursItemMutation, + useUpdateDesignationSupportHoursItemMutation, +} from '../../GoalsList/PdsGoalCalculations.generated'; +import { useSaveField } from '../../Shared/Autosave/useSaveField'; +import { usePdsGoalCalculator } from '../../Shared/PdsGoalCalculatorContext'; + +export interface HoursPerWeekEntry { + id: string; + label: string; + hoursPerWeek: number; + weeks: number; + canDelete: boolean; + predefined: boolean; + position: number; +} + +const MAX_TOTAL_WEEKS = 52; + +export const useHoursPerWeekGrid = () => { + const { t } = useTranslation(); + const { calculation, trackMutation } = usePdsGoalCalculator(); + const [createHoursItem] = useCreateDesignationSupportHoursItemMutation(); + const [updateHoursItem] = useUpdateDesignationSupportHoursItemMutation(); + const [deleteHoursItem] = useDeleteDesignationSupportHoursItemMutation(); + const saveField = useSaveField(); + + const [entries, setEntries] = useState([]); + const initializedRef = useRef(false); + const nextEntryIdRef = useRef(0); + + useEffect(() => { + const items = calculation?.designationSupportHoursItems; + if (!initializedRef.current && items && items.length > 0) { + initializedRef.current = true; + setEntries( + items + .slice() + .sort( + (hoursItemA, hoursItemB) => + (hoursItemA.position ?? 0) - (hoursItemB.position ?? 0), + ) + .map((item) => ({ + id: item.id, + label: item.label, + hoursPerWeek: item.hoursPerWeek ?? 0, + weeks: item.numberOfWeeks ?? 0, + canDelete: !item.predefined, + predefined: item.predefined, + position: item.position ?? 0, + })), + ); + } + }, [calculation?.designationSupportHoursItems]); + + const totalWeeks = useMemo( + () => entries.reduce((sum, entry) => sum + entry.weeks, 0), + [entries], + ); + + const totalHours = useMemo( + () => + entries.reduce((sum, entry) => sum + entry.hoursPerWeek * entry.weeks, 0), + [entries], + ); + + const averageHoursPerWeek = useMemo( + () => (totalWeeks > 0 ? totalHours / totalWeeks : 0), + [totalHours, totalWeeks], + ); + + const weeksRemaining = MAX_TOTAL_WEEKS - totalWeeks; + + const saveHoursItem = useCallback( + async (entry: HoursPerWeekEntry) => { + if (!calculation) { + return; + } + + try { + await trackMutation( + updateHoursItem({ + variables: { + attributes: { + id: entry.id, + designationSupportCalculationId: calculation.id, + label: entry.label, + hoursPerWeek: entry.hoursPerWeek, + numberOfWeeks: entry.weeks, + }, + }, + refetchQueries: ['PdsGoalCalculation'], + }), + ); + } catch { + throw new Error(t('Failed to save hours entry.')); + } + }, + [calculation, createHoursItem, updateHoursItem, trackMutation, t], + ); + + const updateEntry = useCallback( + (id: string, updates: Partial) => { + setEntries((prev) => + prev.map((entry) => + entry.id === id ? { ...entry, ...updates } : entry, + ), + ); + }, + [], + ); + + const addEntry = useCallback(async () => { + if (!calculation) { + return; + } + + const tempId = `temp-${nextEntryIdRef.current++}`; + const newPosition = + entries.length > 0 ? Math.max(...entries.map((e) => e.position)) + 1 : 0; + const newEntry: HoursPerWeekEntry = { + id: tempId, + label: t('New Entry'), + hoursPerWeek: 0, + weeks: 0, + canDelete: true, + predefined: false, + position: newPosition, + }; + setEntries((prev) => [...prev, newEntry]); + + try { + const result = await trackMutation( + createHoursItem({ + variables: { + attributes: { + designationSupportCalculationId: calculation.id, + label: t('New Entry'), + hoursPerWeek: 0, + numberOfWeeks: 0, + position: newPosition, + }, + }, + refetchQueries: ['PdsGoalCalculation'], + }), + ); + const created = + result.data?.createDesignationSupportHoursItem + ?.designationSupportHoursItem; + if (created) { + setEntries((prev) => + prev.map((e) => (e.id === tempId ? { ...e, id: created.id } : e)), + ); + } + } catch { + setEntries((prev) => prev.filter((e) => e.id !== tempId)); + } + }, [t, calculation, createHoursItem, trackMutation, entries.length]); + + const deleteEntry = useCallback( + async (id: string | number) => { + const entryId = id.toString(); + const previousEntries = entries; + const remainingEntries = entries.filter((entry) => entry.id !== entryId); + setEntries(remainingEntries); + + const newTotalWeeks = remainingEntries.reduce( + (sum, e) => sum + e.weeks, + 0, + ); + const newTotalHours = remainingEntries.reduce( + (sum, e) => sum + e.hoursPerWeek * e.weeks, + 0, + ); + const newAverage = newTotalWeeks > 0 ? newTotalHours / newTotalWeeks : 0; + + try { + if (!entryId.startsWith('temp-') && !entryId.startsWith('default-')) { + await trackMutation( + deleteHoursItem({ + variables: { id: entryId }, + refetchQueries: ['PdsGoalCalculation'], + }), + ); + } + saveField({ + averageHoursPerWeek: newAverage, + }); + } catch { + setEntries(previousEntries); + } + }, + [entries, deleteHoursItem, trackMutation, saveField], + ); + + const processRowUpdate = useCallback( + async (newRow: GridValidRowModel) => { + if (newRow.id === 'total') { + return newRow; + } + + const newWeeks = Number(newRow.weeks) || 0; + const otherWeeks = entries + .filter((e) => e.id !== newRow.id) + .reduce((sum, e) => sum + e.weeks, 0); + const clampedWeeks = Math.min(newWeeks, MAX_TOTAL_WEEKS - otherWeeks); + + const updatedEntry: HoursPerWeekEntry = { + id: newRow.id as string, + label: newRow.label as string, + hoursPerWeek: Number(newRow.hoursPerWeek) || 0, + weeks: Math.max(0, clampedWeeks), + canDelete: newRow.canDelete as boolean, + predefined: newRow.predefined as boolean, + position: Number(newRow.position) || 0, + }; + + updateEntry(newRow.id as string, { + label: updatedEntry.label, + hoursPerWeek: updatedEntry.hoursPerWeek, + weeks: updatedEntry.weeks, + }); + + await saveHoursItem(updatedEntry); + + const updatedEntries = entries.map((entry) => + entry.id === updatedEntry.id ? updatedEntry : entry, + ); + const newTotalWeeks = updatedEntries.reduce((sum, e) => sum + e.weeks, 0); + const newTotalHours = updatedEntries.reduce( + (sum, e) => sum + e.hoursPerWeek * e.weeks, + 0, + ); + const newAverage = newTotalWeeks > 0 ? newTotalHours / newTotalWeeks : 0; + saveField({ + averageHoursPerWeek: newAverage, + }); + + return { ...newRow, weeks: Math.max(0, clampedWeeks) }; + }, + [updateEntry, entries, saveHoursItem, saveField], + ); + + const dataWithTotal = useMemo( + () => [ + ...entries + .slice() + .sort((a, b) => a.position - b.position) + .map((entry) => ({ + ...entry, + totalHours: entry.hoursPerWeek * entry.weeks, + })), + { + id: 'total', + label: t('Total'), + hoursPerWeek: null, + weeks: totalWeeks, + totalHours: totalHours, + canDelete: false, + }, + ], + [entries, totalWeeks, totalHours, t], + ); + + return { + entries, + totalWeeks, + totalHours, + averageHoursPerWeek, + weeksRemaining, + dataWithTotal, + addEntry, + deleteEntry, + processRowUpdate, + }; +}; From 9cfa79857403407c634c8574c8d10de18d322ae5 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 22 Apr 2026 16:22:35 -0400 Subject: [PATCH 6/8] Fix TypeScript errors in useHoursPerWeekGrid tests Restructure act() calls to avoid returning values directly from act(), which expects void return. Capture return values via outer variables instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../useHoursPerWeekGrid.test.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.test.tsx b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.test.tsx index 05ebbdb849..70ff583857 100644 --- a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.test.tsx +++ b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/useHoursPerWeekGrid.test.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { GridValidRowModel } from '@mui/x-data-grid'; import { act, renderHook, waitFor } from '@testing-library/react'; import { PdsGoalCalculationMock, @@ -331,8 +332,9 @@ describe('useHoursPerWeekGrid', () => { }); // Regular has 48 weeks. Try to set Travel to 10 weeks — should clamp to 4 - const updatedRow = await act(async () => - result.current.processRowUpdate({ + let updatedRow: GridValidRowModel | undefined; + await act(async () => { + updatedRow = await result.current.processRowUpdate({ id: 'item-travel', label: 'Travel', hoursPerWeek: 5, @@ -340,10 +342,10 @@ describe('useHoursPerWeekGrid', () => { canDelete: true, predefined: true, position: 1, - }), - ); + }); + }); - expect(updatedRow.weeks).toBe(4); + expect(updatedRow?.weeks).toBe(4); }); it('processRowUpdate autosaves the recalculated average', async () => { @@ -390,14 +392,15 @@ describe('useHoursPerWeekGrid', () => { expect(result.current.entries).toHaveLength(3); }); - const returned = await act(async () => - result.current.processRowUpdate({ + let returned: GridValidRowModel | undefined; + await act(async () => { + returned = await result.current.processRowUpdate({ id: 'total', label: 'Total', hoursPerWeek: null, weeks: 48, - }), - ); + }); + }); expect(returned).toMatchObject({ id: 'total' }); }); From c90e5f77db6528e45d97c9420c8f565a0c691277 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 22 Apr 2026 16:30:44 -0400 Subject: [PATCH 7/8] Move right panel title from 'Details' to 'Hours Per Week Calculator' Moved the title to the page-level right panel header and removed the duplicate heading from HoursPerWeekGrid component. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx | 4 +++- .../Setup/HoursPerWeekGrid/HoursPerWeekGrid.test.tsx | 1 - .../Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx | 1 - .../PdsGoalCalculator/Setup/SetupStep.test.tsx | 12 ++++++++++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx b/pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx index c3cd3d7bd8..a40eb81ca8 100644 --- a/pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx +++ b/pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx @@ -67,7 +67,9 @@ const PdsGoalCalculatorContent: React.FC = ({ const rightPanel = ( <> - {t('Details')} + + {t('Hours Per Week Calculator')} + { , ); - expect(await findByText('Hours Per Week Calculator')).toBeInTheDocument(); await waitForDataToLoad(); expect(await findByText('Regular Week')).toBeInTheDocument(); expect(getByText('Travel')).toBeInTheDocument(); diff --git a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx index b0d289babb..d95aada7f5 100644 --- a/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx +++ b/src/components/Reports/PdsGoalCalculator/Setup/HoursPerWeekGrid/HoursPerWeekGrid.tsx @@ -114,7 +114,6 @@ export const HoursPerWeekGrid: React.FC = ({ return ( - {t('Hours Per Week Calculator')} {t( 'This calculator is based on a 52-week year. Weeks are capped at 52 and a warning will appear if the total falls short.', diff --git a/src/components/Reports/PdsGoalCalculator/Setup/SetupStep.test.tsx b/src/components/Reports/PdsGoalCalculator/Setup/SetupStep.test.tsx index 7edf35e4e5..d56fff27aa 100644 --- a/src/components/Reports/PdsGoalCalculator/Setup/SetupStep.test.tsx +++ b/src/components/Reports/PdsGoalCalculator/Setup/SetupStep.test.tsx @@ -284,10 +284,18 @@ describe('SetupStep', () => { , ); - expect(queryByText('Hours Per Week Calculator')).not.toBeInTheDocument(); + expect( + queryByText( + 'This calculator is based on a 52-week year. Weeks are capped at 52 and a warning will appear if the total falls short.', + ), + ).not.toBeInTheDocument(); userEvent.click(await findByLabelText('Open hours per week calculator')); - expect(await findByText('Hours Per Week Calculator')).toBeInTheDocument(); + expect( + await findByText( + 'This calculator is based on a 52-week year. Weeks are capped at 52 and a warning will appear if the total falls short.', + ), + ).toBeInTheDocument(); }); }); From 1d05c18d5052b0c95ee4666c0b451e1490987bec Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 22 Apr 2026 16:33:01 -0400 Subject: [PATCH 8/8] Make right panel title dynamic via context --- .../pdsGoalCalculator/[pdsGoalId].page.tsx | 3 ++- .../PdsGoalCalculator/Setup/SetupStep.tsx | 5 +++-- .../Shared/PdsGoalCalculatorContext.tsx | 19 ++++++++++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx b/pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx index a40eb81ca8..ebec9c2017 100644 --- a/pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx +++ b/pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx @@ -57,6 +57,7 @@ const PdsGoalCalculatorContent: React.FC = ({ }) => { const { rightPanelContent, + rightPanelTitle, closeRightPanel, calculation, calculationLoading, @@ -68,7 +69,7 @@ const PdsGoalCalculatorContent: React.FC = ({ <> - {t('Hours Per Week Calculator')} + {rightPanelTitle ?? t('Details')} { const { t } = useTranslation(); const theme = useTheme(); - const { calculation, hcmUser, setRightPanelContent } = usePdsGoalCalculator(); + const { calculation, hcmUser, setRightPanel } = usePdsGoalCalculator(); const { data: userData } = useGetUserQuery(); const fourOThreeB = hcmUser?.fourOThreeB; const totalFourOThreeBContributionPercentage = fourOThreeB @@ -81,7 +81,8 @@ export const SetupStep: React.FC = () => { : t('Enter hourly rate'); const handleOpenHoursCalculator = () => { - setRightPanelContent( + setRightPanel( + t('Hours Per Week Calculator'), { saveField({ hoursWorkedPerWeek: average }); diff --git a/src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx b/src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx index 73a3eeb240..7db3690a7b 100644 --- a/src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx +++ b/src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx @@ -25,7 +25,8 @@ export type PdsGoalCalculatorType = { trackMutation: (mutation: Promise) => Promise; rightPanelContent: React.ReactNode; - setRightPanelContent: (content: React.ReactNode) => void; + rightPanelTitle: string | null; + setRightPanel: (title: string, content: React.ReactNode) => void; closeRightPanel: () => void; stepIndex: number; @@ -75,6 +76,7 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { const [stepIndex, setStepIndex] = useState(0); const [rightPanelContent, setRightPanelContent] = useState(null); + const [rightPanelTitle, setRightPanelTitle] = useState(null); const [isDrawerOpen, setIsDrawerOpen] = useState(true); const { trackMutation, isMutating } = useTrackMutation(); @@ -106,8 +108,17 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { } }, [stepIndex]); + const setRightPanel = useCallback( + (title: string, content: React.ReactNode) => { + setRightPanelTitle(title); + setRightPanelContent(content); + }, + [], + ); + const closeRightPanel = useCallback(() => { setRightPanelContent(null); + setRightPanelTitle(null); }, []); const toggleDrawer = useCallback(() => { @@ -125,11 +136,12 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { trackMutation, hcmUser, rightPanelContent, + rightPanelTitle, isDrawerOpen, handleStepChange, handleContinue, handlePreviousStep, - setRightPanelContent, + setRightPanel, closeRightPanel, toggleDrawer, setDrawerOpen: setIsDrawerOpen, @@ -144,11 +156,12 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { trackMutation, hcmUser, rightPanelContent, + rightPanelTitle, isDrawerOpen, handleStepChange, handleContinue, handlePreviousStep, - setRightPanelContent, + setRightPanel, closeRightPanel, toggleDrawer, setIsDrawerOpen,