diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 1ec9ac0169..c114794d44 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -250,6 +250,8 @@ "{{count}} selected_other": "{{count}} selected", "{{count}} Selected_one": "{{count}} Selected", "{{count}} Selected_other": "{{count}} Selected", + "{{count}} Selected Staff_one": "{{count}} Selected Staff", + "{{count}} Selected Staff_other": "{{count}} Selected Staff", "{{count}} years_one": "{{count}} years", "{{count}} years_other": "{{count}} years", "{{daysLate}}+ days late": "{{daysLate}}+ days late", @@ -276,6 +278,7 @@ "{{name}}'s commitment info updated!": "{{name}}'s commitment info updated!", "{{name}}'s Gross Requested Salary exceeds their individual Maximum Allowable Salary. If this is correct, please provide reasoning for why {{name}}'s Salary should exceed {{cap}} for division head approval below.": "{{name}}'s Gross Requested Salary exceeds their individual Maximum Allowable Salary. If this is correct, please provide reasoning for why {{name}}'s Salary should exceed {{cap}} for division head approval below.", "{{preferredName}}'s Pending Additional Salary Request": "{{preferredName}}'s Pending Additional Salary Request", + "{{reassigned}} of the selected staff already have a coach.": "{{reassigned}} of the selected staff already have a coach.", "{{rows}} row selected": "{{rows}} row selected", "{{rows}} rows selected": "{{rows}} rows selected", "{{sectionKind}} Request": "{{sectionKind}} Request", @@ -629,6 +632,7 @@ "Assign Coach": "Assign Coach", "Assignee": "Assignee", "Assignee: ": "Assignee: ", + "Assigning a new coach will replace the current coach for the following staff.": "Assigning a new coach will replace the current coach for the following staff.", "at risk": "at risk", "Attrition": "Attrition", "Australia": "Australia", @@ -796,6 +800,7 @@ "Close pivot settings": "Close pivot settings", "Closing Balance": "Closing Balance", "Coach": "Coach", + "Coach assigned successfully.": "Coach assigned successfully.", "Coaches": "Coaches", "Coaching": "Coaching", "Coaching Accounts": "Coaching Accounts", diff --git a/src/components/HrTools/MpdGoalAdmin/AssignCoachModal/AssignCoachModal.test.tsx b/src/components/HrTools/MpdGoalAdmin/AssignCoachModal/AssignCoachModal.test.tsx index 847c7cbb79..af7a8da578 100644 --- a/src/components/HrTools/MpdGoalAdmin/AssignCoachModal/AssignCoachModal.test.tsx +++ b/src/components/HrTools/MpdGoalAdmin/AssignCoachModal/AssignCoachModal.test.tsx @@ -15,15 +15,18 @@ const handleAssignCoach = jest.fn(); interface TestComponentProps { coaches?: AssignCoachOption[]; + reassignedNames?: string[]; } const TestComponent: React.FC = ({ coaches: coachesProp = coaches, + reassignedNames, }) => ( @@ -55,6 +58,28 @@ describe('AssignCoachModal', () => { expect(getByRole('button', { name: 'Save' })).toBeDisabled(); }); + it('warns about staff whose existing coach will be replaced', () => { + const { getByRole } = render( + , + ); + + const alert = getByRole('alert'); + expect(alert).toHaveTextContent( + '2 of the selected staff already have a coach.', + ); + expect(alert).toHaveTextContent( + 'Assigning a new coach will replace the current coach for the following staff.', + ); + expect(alert).toHaveTextContent('John & Jane Doe'); + expect(alert).toHaveTextContent("James O'Connor"); + }); + + it('does not warn when no selected staff already have a coach', () => { + const { queryByRole } = render(); + + expect(queryByRole('alert')).not.toBeInTheDocument(); + }); + it('closes when Cancel is clicked', () => { const { getByRole } = render(); diff --git a/src/components/HrTools/MpdGoalAdmin/AssignCoachModal/AssignCoachModal.tsx b/src/components/HrTools/MpdGoalAdmin/AssignCoachModal/AssignCoachModal.tsx index 24071aea0f..7fa4525f71 100644 --- a/src/components/HrTools/MpdGoalAdmin/AssignCoachModal/AssignCoachModal.tsx +++ b/src/components/HrTools/MpdGoalAdmin/AssignCoachModal/AssignCoachModal.tsx @@ -1,9 +1,12 @@ import React, { ReactElement } from 'react'; import { + Alert, Autocomplete, + Box, DialogActions, DialogContent, TextField, + Typography, } from '@mui/material'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; @@ -23,6 +26,11 @@ interface AssignCoachModalProps { /** Name shown in the modal title, e.g. the staff member the coach is for. */ subjectName: string; coaches: AssignCoachOption[]; + /** + * Names of the staff who already have a coach and will be reassigned. When + * non-empty, a warning listing them is shown so overwrites aren't silent. + */ + reassignedNames?: string[]; handleClose: () => void; handleAssignCoach: (coachId: string) => Promise | void; } @@ -36,6 +44,7 @@ type AssignCoachFormValues = yup.InferType; export const AssignCoachModal: React.FC = ({ subjectName, coaches, + reassignedNames, handleClose, handleAssignCoach, }) => { @@ -66,6 +75,26 @@ export const AssignCoachModal: React.FC = ({ }): ReactElement => (
+ {reassignedNames && reassignedNames.length > 0 && ( + + + {t( + '{{reassigned}} of the selected staff already have a coach.', + { reassigned: reassignedNames.length }, + )} + + + {t( + 'Assigning a new coach will replace the current coach for the following staff.', + )} + + + {reassignedNames.map((name) => ( +
  • {name}
  • + ))} +
    +
    + )} { ).toBeInTheDocument(); }); + it('assigns a coach to the row from the Assign Coach modal', async () => { + const { getByRole, findByRole } = renderTable(); + // 'Carlos & Michaela Everts' (row-2) has no coach on the first page. + userEvent.click(getByRole('button', { name: 'Assign Coach' })); + + const dialog = await findByRole('dialog'); + expect(dialog).toHaveTextContent( + 'Assign Coach for Carlos & Michaela Everts', + ); + + userEvent.click(getByRole('combobox', { name: 'Coach' })); + userEvent.click(await findByRole('option', { name: 'Tom Harris' })); + userEvent.click(getByRole('button', { name: 'Save' })); + + // Formik's submit resolves asynchronously, so wait for the context update + // rather than asserting synchronously after the click. + await waitFor(() => + expect(ctx.cohorts[0].rows.find((row) => row.id === 'row-2')?.coach).toBe( + 'Tom Harris', + ), + ); + }); + it('renders a View/Edit action and a menu button for each row on the page', () => { const { getAllByText, getAllByRole } = renderTable(); const onPage = Math.min(rows.length, DEFAULT_ROWS_PER_PAGE); @@ -69,14 +92,14 @@ describe('GoalsTable', () => { it('selects a row via its checkbox', async () => { const { getAllByRole } = renderTable(); // index 0 is the header "select all" checkbox - await userEvent.click(getAllByRole('checkbox')[1]); + userEvent.click(getAllByRole('checkbox')[1]); expect(ctx.selectedRowIds.has('row-1')).toBe(true); }); it('selects every row on the page via the header checkbox', async () => { const { getAllByRole } = renderTable(); // index 0 is the header "select all" checkbox - await userEvent.click(getAllByRole('checkbox')[0]); + userEvent.click(getAllByRole('checkbox')[0]); // The header checkbox selects only the rows on the current page, not the // entire filtered set. rows.slice(0, DEFAULT_ROWS_PER_PAGE).forEach((row) => { @@ -97,12 +120,12 @@ describe('GoalsTable', () => { expect(headerCheckbox).toHaveAttribute('data-indeterminate', 'false'); // Select a single data row. - await userEvent.click(checkboxes[1]); + userEvent.click(checkboxes[1]); expect(headerCheckbox.checked).toBe(false); expect(headerCheckbox).toHaveAttribute('data-indeterminate', 'true'); // Select the rest via the header, which now reads "select all". - await userEvent.click(headerCheckbox); + userEvent.click(headerCheckbox); expect(headerCheckbox.checked).toBe(true); expect(headerCheckbox).toHaveAttribute('data-indeterminate', 'false'); }); @@ -144,7 +167,7 @@ describe('GoalsTable', () => { getByText(`Person ${DEFAULT_ROWS_PER_PAGE - 1}`), ).toBeInTheDocument(); // Advance to the next page via the pagination "next page" button. - await userEvent.click(getByRole('button', { name: /Go to next page/i })); + userEvent.click(getByRole('button', { name: /Go to next page/i })); expect(getByText(`Person ${DEFAULT_ROWS_PER_PAGE}`)).toBeInTheDocument(); // Changing the filter shrinks the result set. The table must reset to the diff --git a/src/components/HrTools/MpdGoalAdmin/GoalsTable/GoalsTable.tsx b/src/components/HrTools/MpdGoalAdmin/GoalsTable/GoalsTable.tsx index 21d31e8f68..3d525f41c8 100644 --- a/src/components/HrTools/MpdGoalAdmin/GoalsTable/GoalsTable.tsx +++ b/src/components/HrTools/MpdGoalAdmin/GoalsTable/GoalsTable.tsx @@ -21,11 +21,9 @@ import { visuallyHidden } from '@mui/utils'; import { useTranslation } from 'react-i18next'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat } from 'src/lib/intlFormat'; -import { - AssignCoachModal, - AssignCoachOption, -} from '../AssignCoachModal/AssignCoachModal'; +import { AssignCoachModal } from '../AssignCoachModal/AssignCoachModal'; import { useMpdGoalAdmin } from '../MpdGoalAdminContext'; +import { mockCoaches } from '../mockData'; import { StaffGoalRow, isSendable } from '../mpdGoalAdminHelpers'; interface GoalsTableProps { @@ -37,20 +35,31 @@ export const DEFAULT_ROWS_PER_PAGE = 5; export const GoalsTable: React.FC = ({ rows }) => { const { t } = useTranslation(); const locale = useLocale(); - const { selectedRowIds, toggleRow, toggleRows, search, selectedCohortId } = - useMpdGoalAdmin(); + const { + selectedRowIds, + toggleRow, + toggleRows, + search, + selectedCohortId, + assignCoach, + } = useMpdGoalAdmin(); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_ROWS_PER_PAGE); // The staff row whose coach is being assigned; null when the modal is closed. const [coachRow, setCoachRow] = useState(null); - // TODO(MPDX-9699): populate from the assignable-coaches query once the - // backend field exists. Empty for now so the modal renders a UI-only dropdown. - const assignableCoaches: AssignCoachOption[] = []; + // TODO(MPDX-9914): populate from the assignable-coaches query once the + // backend field exists. Same mock list the toolbar's bulk path uses. + const assignableCoaches = mockCoaches; - const handleAssignCoach = async (_coachId: string) => { - // TODO(MPDX-9699): call the assign-coach mutation with { staffId, coachId } - // and refresh the goal-admin rows. Backend contract pending. + // TODO(MPDX-9914): call the assignCoach mutation instead of the mock + // context update once the backend field is wired up. + const handleAssignCoach = (coachId: string) => { + const coach = assignableCoaches.find((option) => option.id === coachId); + if (!coach || !coachRow) { + return; + } + assignCoach([coachRow.id], coach.name); }; // Reset to the first page whenever the filter inputs change, so the user diff --git a/src/components/HrTools/MpdGoalAdmin/GoalsTableToolbar/GoalsTableToolbar.test.tsx b/src/components/HrTools/MpdGoalAdmin/GoalsTableToolbar/GoalsTableToolbar.test.tsx index d31596199d..1e17511fb4 100644 --- a/src/components/HrTools/MpdGoalAdmin/GoalsTableToolbar/GoalsTableToolbar.test.tsx +++ b/src/components/HrTools/MpdGoalAdmin/GoalsTableToolbar/GoalsTableToolbar.test.tsx @@ -1,5 +1,5 @@ import { ThemeProvider } from '@mui/material/styles'; -import { act, render } from '@testing-library/react'; +import { act, render, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SnackbarProvider } from 'notistack'; import theme from 'src/theme'; @@ -25,64 +25,82 @@ const renderToolbar = () => ); describe('GoalsTableToolbar', () => { - it('shows default bulk actions with no selection', () => { - const { getByRole, queryByRole } = renderToolbar(); - expect(getByRole('button', { name: 'Print All' })).toBeInTheDocument(); + it('disables More Actions with no selection', () => { + const { getByRole } = renderToolbar(); + expect(getByRole('button', { name: 'More Actions' })).toBeDisabled(); expect( getByRole('button', { name: 'Run and Send All' }), ).toBeInTheDocument(); - expect( - queryByRole('button', { name: 'Run & Send Selected' }), - ).not.toBeInTheDocument(); }); - it('shows selection actions once rows are selected', () => { + it('enables the More Actions menu once rows are selected', async () => { const { getByRole, getByText } = renderToolbar(); act(() => ctx.toggleRow('row-1')); expect(getByText('1 selected')).toBeInTheDocument(); + + userEvent.click(getByRole('button', { name: 'More Actions' })); + const menu = getByRole('menu'); + // Print All stays disabled until it is wired up (MPDX-9702). expect( - getByRole('button', { name: 'Run & Send Selected' }), + within(menu).getByRole('menuitem', { name: 'Print All' }), + ).toHaveAttribute('aria-disabled', 'true'); + expect( + within(menu).getByRole('menuitem', { name: 'Run & Send Selected' }), + ).toBeInTheDocument(); + expect( + within(menu).getByRole('menuitem', { name: 'Assign Coach' }), ).toBeInTheDocument(); - expect(getByRole('button', { name: 'More Actions' })).toBeInTheDocument(); }); it('drops hidden rows from the selected count when a search filters them out', async () => { - const { getByRole, getByText, queryByText, queryByRole } = renderToolbar(); + const { getByRole, getByText, queryByText } = renderToolbar(); act(() => ctx.toggleRow('row-1')); expect(getByText('1 selected')).toBeInTheDocument(); // 'row-1' (John & Jane Doe) is hidden by a search for another member, so // the count must not keep reporting a row the user can no longer see. - await userEvent.type(getByRole('textbox', { name: 'Search' }), 'carlos'); + userEvent.type(getByRole('textbox', { name: 'Search' }), 'carlos'); expect(queryByText('1 selected')).not.toBeInTheDocument(); - expect( - queryByRole('button', { name: 'Run & Send Selected' }), - ).not.toBeInTheDocument(); - expect(getByRole('button', { name: 'Print All' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'More Actions' })).toBeDisabled(); }); it('updates search on typing', async () => { const { getByRole } = renderToolbar(); - await userEvent.type(getByRole('textbox', { name: 'Search' }), 'doe'); + userEvent.type(getByRole('textbox', { name: 'Search' }), 'doe'); expect(ctx.search).toBe('doe'); }); it('opens the run-and-send confirmation from the All button', async () => { const { getByRole } = renderToolbar(); - await userEvent.click(getByRole('button', { name: 'Run and Send All' })); + userEvent.click(getByRole('button', { name: 'Run and Send All' })); expect(getByRole('dialog')).toHaveTextContent( 'Run and Send All Complete MPD Goals?', ); }); - it('confirms and sends only the selected rows', async () => { + it('targets every filtered row from the All button, ignoring the selection', async () => { + const { getByRole, getByText } = renderToolbar(); + // row-1 is Complete; a 1-row selection must not shrink the All target. + act(() => ctx.toggleRow('row-1')); + expect(getByText('1 selected')).toBeInTheDocument(); + + userEvent.click(getByRole('button', { name: 'Run and Send All' })); + + // All 13 mock rows: 4 Incomplete cannot be sent, 9 Complete can. + const dialog = getByRole('dialog'); + expect(dialog).toHaveTextContent('4 of the 13 MPD goals cannot be sent.'); + expect(dialog).toHaveTextContent('Continue with 9 out of 13 MPD goals'); + }); + + it('confirms and sends only the selected rows from the menu', async () => { const { getByRole, findByText } = renderToolbar(); // row-1 is Complete, row-7 is Incomplete → 1 sendable of 2. act(() => { ctx.toggleRow('row-1'); ctx.toggleRow('row-7'); }); - await userEvent.click(getByRole('button', { name: 'Run & Send Selected' })); + userEvent.click(getByRole('button', { name: 'More Actions' })); + userEvent.click(getByRole('menuitem', { name: 'Run & Send Selected' })); const dialog = getByRole('dialog'); expect(dialog).toHaveTextContent( @@ -91,9 +109,55 @@ describe('GoalsTableToolbar', () => { expect(dialog).toHaveTextContent('1 of the 2 MPD goals cannot be sent.'); expect(dialog).toHaveTextContent('Continue with 1 out of 2 MPD goals'); - await userEvent.click(getByRole('button', { name: 'Yes, Continue' })); + userEvent.click(getByRole('button', { name: 'Yes, Continue' })); expect( await findByText('1 MPD Goals were run and sent.'), ).toBeInTheDocument(); }); + + it('assigns a coach to every selected row from the menu', async () => { + const { getByRole, findByRole, findByText } = renderToolbar(); + // row-2 (Carlos & Michaela Everts) has no coach; row-1 has one already. + act(() => { + ctx.toggleRow('row-1'); + ctx.toggleRow('row-2'); + }); + userEvent.click(getByRole('button', { name: 'More Actions' })); + userEvent.click(getByRole('menuitem', { name: 'Assign Coach' })); + + const dialog = getByRole('dialog'); + expect(dialog).toHaveTextContent('Assign Coach for 2 Selected Staff'); + // row-1 already has a coach, so the overwrite warning names it; row-2 + // has none and is not listed. + const warning = within(dialog).getByRole('alert'); + expect(warning).toHaveTextContent( + '1 of the selected staff already have a coach.', + ); + expect(warning).toHaveTextContent('John & Jane Doe'); + expect(warning).not.toHaveTextContent('Carlos & Michaela Everts'); + + userEvent.click(within(dialog).getByRole('combobox', { name: 'Coach' })); + userEvent.click(await findByRole('option', { name: 'Tom Harris' })); + userEvent.click(getByRole('button', { name: 'Save' })); + + expect( + await findByText('Coach assigned successfully.'), + ).toBeInTheDocument(); + // Both rows now carry the coach, and the selection is cleared. + const rows = ctx.cohorts[0].rows; + expect(rows.find((row) => row.id === 'row-1')?.coach).toBe('Tom Harris'); + expect(rows.find((row) => row.id === 'row-2')?.coach).toBe('Tom Harris'); + expect(ctx.selectedRows).toHaveLength(0); + }); + + it("uses the staff member's name in the assign-coach title for a single selection", async () => { + const { getByRole } = renderToolbar(); + act(() => ctx.toggleRow('row-2')); + userEvent.click(getByRole('button', { name: 'More Actions' })); + userEvent.click(getByRole('menuitem', { name: 'Assign Coach' })); + + expect(getByRole('dialog')).toHaveTextContent( + 'Assign Coach for Carlos & Michaela Everts', + ); + }); }); diff --git a/src/components/HrTools/MpdGoalAdmin/GoalsTableToolbar/GoalsTableToolbar.tsx b/src/components/HrTools/MpdGoalAdmin/GoalsTableToolbar/GoalsTableToolbar.tsx index 30a866ff2b..303887ed62 100644 --- a/src/components/HrTools/MpdGoalAdmin/GoalsTableToolbar/GoalsTableToolbar.tsx +++ b/src/components/HrTools/MpdGoalAdmin/GoalsTableToolbar/GoalsTableToolbar.tsx @@ -1,27 +1,40 @@ import React, { useState } from 'react'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import SearchIcon from '@mui/icons-material/Search'; import { Box, Button, InputAdornment, + Menu, + MenuItem, Stack, TextField, Typography, } from '@mui/material'; import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; +import { AssignCoachModal } from '../AssignCoachModal/AssignCoachModal'; import { useMpdGoalAdmin } from '../MpdGoalAdminContext'; import { RunAndSendModal } from '../RunAndSendModal/RunAndSendModal'; +import { mockCoaches } from '../mockData'; import { StaffGoalRow } from '../mpdGoalAdminHelpers'; export const GoalsTableToolbar: React.FC = () => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); - const { search, setSearch, filteredRows, selectedRows, clearSelection } = - useMpdGoalAdmin(); + const { + search, + setSearch, + filteredRows, + selectedRows, + clearSelection, + assignCoach, + } = useMpdGoalAdmin(); const selectedCount = selectedRows.length; const hasSelection = selectedCount > 0; + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [assignCoachOpen, setAssignCoachOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false); // Kept separate from `modalOpen` so the target rows/title persist through the // dialog's close transition instead of flashing empty. @@ -44,6 +57,21 @@ export const GoalsTableToolbar: React.FC = () => { setModalOpen(false); }; + // TODO(MPDX-9914): call the assignCoach mutation instead of the mock + // context update once the backend field is wired up. + const handleAssignCoach = (coachId: string) => { + const coach = mockCoaches.find((option) => option.id === coachId); + if (!coach) { + return; + } + assignCoach( + selectedRows.map((row) => row.id), + coach.name, + ); + enqueueSnackbar(t('Coach assigned successfully.'), { variant: 'success' }); + clearSelection(); + }; + return ( { /> - {hasSelection ? ( - <> - - {t('{{count}} selected', { count: selectedCount })} - - {/* Disabled until wired up so assistive tech announces the - inert state instead of a dead control (MPDX-9696). */} - - - - ) : ( - <> - {/* Disabled until wired up so assistive tech announces the - inert state instead of a dead control (MPDX-9696). */} - - - + {hasSelection && ( + + {t('{{count}} selected', { count: selectedCount })} + )} + + setMenuAnchorEl(null)} + > + {/* Disabled until wired up so assistive tech announces the + inert state instead of a dead control (MPDX-9702). */} + {t('Print All')} + { + setMenuAnchorEl(null); + openRunAndSend( + t('Run and Send Selected Complete MPD Goals?'), + selectedRows, + ); + }} + > + {t('Run & Send Selected')} + + { + setMenuAnchorEl(null); + setAssignCoachOpen(true); + }} + > + {t('Assign Coach')} + + + { onClose={() => setModalOpen(false)} onConfirm={handleConfirm} /> + {assignCoachOpen && ( + row.coach) + .map((row) => row.name)} + handleAssignCoach={handleAssignCoach} + handleClose={() => setAssignCoachOpen(false)} + /> + )} ); }; diff --git a/src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.test.tsx b/src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.test.tsx index 3b0bdeb65d..e253cc905c 100644 --- a/src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.test.tsx +++ b/src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.test.tsx @@ -75,6 +75,20 @@ describe('MpdGoalAdminContext', () => { expect(result.current.selectedCohort?.trainingCostEntered).toBe(true); }); + it('assigns a coach to exactly the given rows', () => { + const { result } = renderHook(() => useMpdGoalAdmin(), { wrapper }); + + act(() => result.current.assignCoach(['row-1', 'row-2'], 'Tom Harris')); + + const rows = result.current.cohorts[0].rows; + expect(rows.find((row) => row.id === 'row-1')?.coach).toBe('Tom Harris'); + expect(rows.find((row) => row.id === 'row-2')?.coach).toBe('Tom Harris'); + // Untouched rows keep their original coach. Assert the literal rather than + // comparing against mockCohorts: the provider seeds state with that same + // array, so an in-place mutation would change both sides and pass trivially. + expect(rows.find((row) => row.id === 'row-3')?.coach).toBe('Nelson Jones'); + }); + it('throws when used outside its provider', () => { expect(() => renderHook(() => useMpdGoalAdmin())).toThrow( 'useMpdGoalAdmin must be used within a MpdGoalAdminProvider', diff --git a/src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.tsx b/src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.tsx index 2a84d8cb32..130fc473ee 100644 --- a/src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.tsx +++ b/src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.tsx @@ -36,6 +36,8 @@ export interface MpdGoalAdminContextValue { clearSelection: () => void; /** Saves the training cost figures for a cohort and marks them as entered. */ saveTrainingCosts: (cohortId: string, costs: TrainingCosts) => void; + /** Assigns one coach to every row in `rowIds`, across all cohorts. */ + assignCoach: (rowIds: string[], coachName: string) => void; } const MpdGoalAdminContext = createContext( @@ -92,6 +94,18 @@ export const MpdGoalAdminProvider: React.FC<{ [], ); + const assignCoach = useCallback((rowIds: string[], coachName: string) => { + const idSet = new Set(rowIds); + setCohorts((prev) => + prev.map((cohort) => ({ + ...cohort, + rows: cohort.rows.map((row) => + idSet.has(row.id) ? { ...row, coach: coachName } : row, + ), + })), + ); + }, []); + // Switching cohorts clears the selection: selecting staff across different // training cohorts is meaningless, and stale ids would otherwise linger in // the set and mislead the selection count and any bulk action. @@ -148,6 +162,7 @@ export const MpdGoalAdminProvider: React.FC<{ toggleRows, clearSelection, saveTrainingCosts, + assignCoach, }), [ activeTab, @@ -163,6 +178,7 @@ export const MpdGoalAdminProvider: React.FC<{ toggleRows, clearSelection, saveTrainingCosts, + assignCoach, ], ); diff --git a/src/components/HrTools/MpdGoalAdmin/mockData.ts b/src/components/HrTools/MpdGoalAdmin/mockData.ts index 187eb51561..14f5d76794 100644 --- a/src/components/HrTools/MpdGoalAdmin/mockData.ts +++ b/src/components/HrTools/MpdGoalAdmin/mockData.ts @@ -1,5 +1,17 @@ +import { AssignCoachOption } from './AssignCoachModal/AssignCoachModal'; import { Cohort, GoalStatusEnum } from './mpdGoalAdminHelpers'; +// Assignable coaches for the Assign Coach modal (MPDX-9693). Mirrors the coach +// names used in the rows below; replaced by the real query in MPDX-9914. +export const mockCoaches: AssignCoachOption[] = [ + { id: 'coach-1', name: 'Amy Wilson' }, + { id: 'coach-2', name: 'Bea Christians' }, + { id: 'coach-3', name: 'Nelson Jones' }, + { id: 'coach-4', name: 'Phillip Song' }, + { id: 'coach-5', name: 'Rachel Adams' }, + { id: 'coach-6', name: 'Tom Harris' }, +]; + export const mockCohorts: Cohort[] = [ { id: 'fall-nso-2026',