Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,10 @@
"Enter a valid amount": "Enter a valid amount",
"Faith and Finance": "Faith and Finance",
"Family": "Family",
"IBS Cost": "IBS Cost",
"Individual (1 in room)": "Individual (1 in room)",
"Individual (2 in room)": "Individual (2 in room)",
"NSO & IBS Cost": "NSO & IBS Cost",
"NSO Cost": "NSO Cost",
"Please enter the cost details that apply to this training. All fields are required.": "Please enter the cost details that apply to this training. All fields are required.",
"Refresh Retreat": "Refresh Retreat",
"Training Costs": "Training Costs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ beforeEach(() => {
});

const filledCosts: TrainingCosts = {
nsoIbsIndividual1InRoom: 100,
nsoIbsIndividual2InRoom: 200,
nsoIbsCouple: 300,
nsoIbsFamily: 400,
refreshRetreatSingle: 500,
refreshRetreatCouple: 600,
faithAndFinanceSingle: 700,
faithAndFinanceCouple: 800,
cruConferenceSingle: 900,
cruConferenceCouple: 1000,
cruConferenceFamily: 1100,
nsoIndividual1InRoom: 100,
nsoIndividual2InRoom: 200,
nsoCouple: 300,
nsoFamily: 400,
ibsSingle: 500,
ibsCouple: 600,
refreshRetreatSingle: 700,
refreshRetreatCouple: 800,
faithAndFinanceSingle: 900,
faithAndFinanceCouple: 1000,
cruConferenceSingle: 1100,
cruConferenceCouple: 1200,
cruConferenceFamily: 1300,
};

/**
Expand All @@ -39,18 +41,25 @@ const fieldsBySection: {
fields: { label: string; value: number }[];
}[] = [
{
title: 'NSO & IBS Cost',
title: 'NSO Cost',
fields: [
{
label: 'Individual (1 in room)',
value: filledCosts.nsoIbsIndividual1InRoom,
value: filledCosts.nsoIndividual1InRoom,
},
{
label: 'Individual (2 in room)',
value: filledCosts.nsoIbsIndividual2InRoom,
value: filledCosts.nsoIndividual2InRoom,
},
{ label: 'Couple', value: filledCosts.nsoIbsCouple },
{ label: 'Family', value: filledCosts.nsoIbsFamily },
{ label: 'Couple', value: filledCosts.nsoCouple },
{ label: 'Family', value: filledCosts.nsoFamily },
],
},
{
title: 'IBS Cost',
fields: [
{ label: 'Single', value: filledCosts.ibsSingle },
{ label: 'Couple', value: filledCosts.ibsCouple },
],
},
{
Expand Down Expand Up @@ -106,8 +115,8 @@ const TestComponent: React.FC<
);

describe('EditTrainingCostsModal', () => {
it('renders the cohort-specific title, subtitle and every section', () => {
const { getByText, getByRole } = render(<TestComponent />);
it('renders the cohort-specific title, subtitle and every section in order', () => {
const { getByText, getByRole, getAllByRole } = render(<TestComponent />);
expect(
getByRole('heading', { name: 'Training Costs for Fall NSO 2026' }),
).toBeInTheDocument();
Expand All @@ -116,10 +125,14 @@ describe('EditTrainingCostsModal', () => {
'Please enter the cost details that apply to this training. All fields are required.',
),
).toBeInTheDocument();
expect(getByText('NSO & IBS Cost')).toBeInTheDocument();
expect(getByText('Refresh Retreat')).toBeInTheDocument();
expect(getByText('Faith and Finance')).toBeInTheDocument();
expect(getByText('Cru Conference')).toBeInTheDocument();
// Assert the section headings as an ordered list: NSO and IBS being distinct
// sections in this order is the point of the layout, so order is asserted
// rather than mere presence.
expect(
getAllByRole('heading', { level: 3 }).map(
(heading) => heading.textContent,
),
).toEqual(fieldsBySection.map((section) => section.title));
});

it('falls back to a generic title when no cohort name is given', () => {
Expand All @@ -129,10 +142,10 @@ describe('EditTrainingCostsModal', () => {
).toBeInTheDocument();
});

it('renders all eleven cost inputs, initially blank', () => {
it('renders all thirteen cost inputs, initially blank', () => {
const { getAllByRole } = render(<TestComponent />);
const inputs = getAllByRole('spinbutton');
expect(inputs).toHaveLength(11);
expect(inputs).toHaveLength(13);
inputs.forEach((input) => expect(input).toHaveValue(null));
});

Expand Down Expand Up @@ -208,9 +221,9 @@ describe('EditTrainingCostsModal', () => {
await userEvent.click(apply);

await waitFor(() => expect(onSave).toHaveBeenCalledWith(filledCosts));
// Typing into all eleven fields exceeds the default 5s timeout when the
// Typing into all thirteen fields exceeds the default 5s timeout when the
// full suite runs in parallel, so give this test extra headroom.
}, 15000);
}, 20000);

it('closes via the Cancel button', async () => {
const { getByRole } = render(<TestComponent />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
type FieldName = keyof TrainingCosts;

/**
* The 11 cost fields as held by Formik. Empty inputs are `''`; because these
* The cost fields as held by Formik. Empty inputs are `''`; because these
* are `type="number"` inputs, Formik's `handleChange` coerces entered text to
* a `number`, so a populated field is a `number`.
*/
Expand Down Expand Up @@ -53,10 +53,12 @@
}

const FIELD_NAMES: FieldName[] = [
'nsoIbsIndividual1InRoom',
'nsoIbsIndividual2InRoom',
'nsoIbsCouple',
'nsoIbsFamily',
'nsoIndividual1InRoom',
'nsoIndividual2InRoom',
'nsoCouple',
'nsoFamily',
'ibsSingle',
'ibsCouple',
'refreshRetreatSingle',
'refreshRetreatCouple',
'faithAndFinanceSingle',
Expand Down Expand Up @@ -103,30 +105,47 @@
const sections = useMemo<SectionConfig[]>(
() => [
{
title: t('NSO & IBS Cost'),
title: t('NSO Cost'),
fields: [
{
name: 'nsoIbsIndividual1InRoom',
name: 'nsoIndividual1InRoom',
label: t('Individual (1 in room)'),
size: { xs: 12, sm: 6, md: 3 },
},
{
name: 'nsoIbsIndividual2InRoom',
name: 'nsoIndividual2InRoom',
label: t('Individual (2 in room)'),
size: { xs: 12, sm: 6, md: 3 },
},
{
name: 'nsoIbsCouple',
name: 'nsoCouple',
label: t('Couple'),
size: { xs: 12, sm: 6, md: 3 },
},
{
name: 'nsoIbsFamily',
name: 'nsoFamily',
label: t('Family'),
size: { xs: 12, sm: 6, md: 3 },
},
],
},
// IBS is costed separately from NSO so an attendee who goes to NSO but not
// IBS can be priced without the IBS portion.
{
title: t('IBS Cost'),
fields: [
{
name: 'ibsSingle',
label: t('Single'),
size: { xs: 12, sm: 6 },
},
{
name: 'ibsCouple',
label: t('Couple'),
size: { xs: 12, sm: 6 },
},
],
},

Check warning on line 148 in src/components/HrTools/MpdGoalAdmin/EditTrainingCostsModal/EditTrainingCostsModal.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

EditTrainingCostsModal:React.FC<EditTrainingCostsModalProps> increases from 244 to 259 lines of code, threshold = 120 Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
{
title: t('Refresh Retreat'),
fields: [
Expand Down
24 changes: 13 additions & 11 deletions src/components/HrTools/MpdGoalAdmin/MpdGoalAdminContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { mockCohorts } from './mockData';
import { MpdGoalAdminTabEnum, TrainingCosts } from './mpdGoalAdminHelpers';

const trainingCosts: TrainingCosts = {
nsoIbsIndividual1InRoom: 100,
nsoIbsIndividual2InRoom: 200,
nsoIbsCouple: 300,
nsoIbsFamily: 400,
refreshRetreatSingle: 500,
refreshRetreatCouple: 600,
faithAndFinanceSingle: 700,
faithAndFinanceCouple: 800,
cruConferenceSingle: 900,
cruConferenceCouple: 1000,
cruConferenceFamily: 1100,
nsoIndividual1InRoom: 100,
nsoIndividual2InRoom: 200,
nsoCouple: 300,
nsoFamily: 400,
ibsSingle: 500,
ibsCouple: 600,
refreshRetreatSingle: 700,
refreshRetreatCouple: 800,
faithAndFinanceSingle: 900,
faithAndFinanceCouple: 1000,
cruConferenceSingle: 1100,
cruConferenceCouple: 1200,
cruConferenceFamily: 1300,
};

const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
Expand Down
21 changes: 15 additions & 6 deletions src/components/HrTools/MpdGoalAdmin/mpdGoalAdminHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,24 @@ export interface StaffGoalRow {

/**
* Per-training cost figures captured in the "Edit Training Costs" modal. Every
* value is a USD amount; the keys mirror the modal's four cost sections. All
* value is a USD amount; the keys mirror the modal's five cost sections. All
* fields are required in the UI, so a saved `TrainingCosts` is fully populated.
*
* There is one key per column in `NewStaffCohort::COST_FIELDS` in mpdx_api, named
* to match. Note the columns are nullable while these are required, so wiring this
* up will need to decide how an unentered cost arrives. NSO and IBS are separate
* so an attendee going to NSO but not IBS can be costed (MPDX-9811; Figma node
* 789-32532).
*/
export interface TrainingCosts {
/** NSO & IBS Cost */
nsoIbsIndividual1InRoom: number;
nsoIbsIndividual2InRoom: number;
nsoIbsCouple: number;
nsoIbsFamily: number;
/** NSO Cost */
nsoIndividual1InRoom: number;
nsoIndividual2InRoom: number;
nsoCouple: number;
nsoFamily: number;
/** IBS Cost */
ibsSingle: number;
ibsCouple: number;
/** Refresh Retreat */
refreshRetreatSingle: number;
refreshRetreatCouple: number;
Expand Down
Loading