From 53023ba9978525d8356d6276d611c973386be431 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:34:58 -0500 Subject: [PATCH 1/5] Add documentation for HrTools components and their usage --- src/components/HrTools/CLAUDE.md | 122 +++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/components/HrTools/CLAUDE.md diff --git a/src/components/HrTools/CLAUDE.md b/src/components/HrTools/CLAUDE.md new file mode 100644 index 000000000..6f343c802 --- /dev/null +++ b/src/components/HrTools/CLAUDE.md @@ -0,0 +1,122 @@ +# HrTools + +Staff-facing HR/finance tools: goal calculators, salary and housing-allowance +requests, savings-fund transfers, questionnaires, and admin/supervisor reports. +This file captures the cross-tool "why" — rules and seams you can't see from any +single form's code. It loads whenever you touch anything under `HrTools/`. + +Follow the root `CLAUDE.md` and `.CLAUDE/rules/code-review.md` for the usual +conventions (named exports, i18n, Formik/Yup, `GqlMockedProvider`, Luxon dates). +The financial-reporting review agent triggers on this tree — money math here is +load-bearing. + +## How the tools are reached + +There is **no** shared HrTools layout, index, or barrel component. Each tool is +exposed only through its own `pages/accountLists/[accountListId]/hrTools/` +entry, and the nav group `'hr-tools-page'` in `src/hooks/useNavPages.tsx` (gated +on `canSeeHrTools`). Cross-tool cohesion comes only from that nav group plus the +shared wizard framework below. + +**URL slugs and native GraphQL names differ from component names** — resolve the +real name before grepping: + +| Component | URL slug | Native GraphQL root field | +| ------------------------ | -------------------- | ---------------------------------- | +| MinisterHousingAllowance | `mhaCalculator/` | `ministryHousingAllowanceRequest*` | +| PdsGoalCalculator | `pdsGoalCalculator/` | `designationSupport*` | +| SalaryCalculator | `salaryCalculator/` | `salaryRequest*` | +| MinistryPartnerReminders | `partnerReminders/` | `ministryPartnerReminders` | + +`StaffSavingFund/` is not a data tool — it's a context/layout shell whose pages +render **SavingsFundTransfer** components. Don't add transfer logic to it. + +## GraphQL routing + +Every HrTools operation routes to the **primary/native API** — all their root +fields are in `src/graphql/rootFields.generated.ts`, and no HrTools `.graphql` +uses `@rest` or `@client`. None of this tree goes through the REST proxy. When +in doubt, resolve the actual root field (see the table — names differ) before +checking `rootFields.generated.ts`. + +## Reuse the shared wizard framework — don't reinvent it + +`Shared/CalculationReports/` is a full multi-step "submit a request" framework: +`PanelLayout` (stepper shell) + `StepsList` + `DirectionButtons` + `FormCard` + +`SubmitModal`/`Receipt`/`StatusCard` + `useCustomAutosave`. **AdditionalSalaryRequest, +MinisterHousingAllowance, and SalaryCalculator** all build on it. Any new +step-based request form should too — reach here before writing a new stepper. + +Other shared pieces worth knowing before you build a local copy: + +- `Shared/Adornments.tsx` — `CurrencyAdornment` / `PercentageAdornment`. Use on + every money/percent input (widest reuse in the tree). +- `Shared/HcmData/Hcm.graphql` — the shared `hcm(effectiveDate:)` query for + staff/HR data. `StaffInfoCard`, `AccountInfoBox`, `EligibilityStatusTable`, + `GoalPresentation/`, `SummaryHeaderCard`, and `useFormatters` are the other + shared leaves. Prefer them over one-off equivalents. + +## The three goal calculators are independent — and their math can drift + +`GoalCalculator`, `PdsGoalCalculator`, and `NsGoalCalculator` are **three parallel +products**, not a base + variants. Each has its own step enum, Context, layout, +and route. Their arithmetic lives in three different places with three different +strategies: + +- **GoalCalculator (MPD worksheet)** — client-side math in + `GoalCalculator/Shared/calculateTotals.ts`. +- **PdsGoalCalculator (Designation Support)** — client-side math in + `PdsGoalCalculator/calculations/`. +- **NsGoalCalculator (New Staff)** — **no client arithmetic at all.** It reads a + server-computed worksheet and recalculates unsaved edits via a server + round-trip (`previewNewStaffGoalCalculation`). Don't add math to this tree. + +⚠️ **Drift risk:** the same goal-total concepts (admin/assessment gross-up, +attrition) are computed independently in generic (`calculateTotals.ts`), PDS +(`calculations/`), and again on the server for NS — with different step ordering. +Changing the math in one place does **not** update the others. Treat these three +as a set: when you touch goal arithmetic, check whether the other copies need the +same change. Constants are year-versioned (`calculationsYear`). + +Non-obvious per-calculator rules: + +- **PDS reimbursable expenses** are clamped to a minimum; `formType === Simple` + zeroes reimbursable + 403(b); 403(b) % comes from HCM, not the form. +- **Generic budget fallback:** with no direct input, the monthly budget is + derived from net pay — that derivation is a load-bearing assumption, not raw + user entry. +- **Rounding is at submission only** — floats through the calc, rounded at the + write boundary, currency hardcoded `'USD'`. +- NS has a real-goal vs admin "scenario-goal" split behind one hook (keyed by + `accountListId` vs `scenarioGoalId`); only scenario goals may edit identity + fields. + +## Per-form domain gotchas + +- **MinisterHousingAllowance** — the online flow gates on **MHA eligibility only** + (`hcmData.mhaEit.mhaEligibility`). MHI/Italian staff (`country === 'IT'`) are + structurally excluded and use a **paper form**; there is no online MHI path. + A new request is blocked while the current one is unresolved, and separately + hidden when a board-approved request is still processing. `requests[0]` is + assumed newest-first; married state is derived from a second HCM record + (`hcm[1]` = spouse). +- **NsoMpdQuestionnaire** — **no create/upsert exists.** The record is created by + the OneApp import; the frontend only Updates/Completes, keyed by + `accountListId` (not a questionnaire id). A null query → render + `NoOpenQuestionnaire`, not a blank form (so no first-save cache write to worry + about). `0` and `false` are valid answers (only null/undefined/'' count as + empty). Required fields are variant-driven (`Sosa`, `SpouseSeniorStaff`); the + ministry dropdown is a separate OneApp source with brittle string-matched + labels. + +## Mock / prototype tools — not wired to a backend + +These render from `mockData.ts`, have **no `.graphql`**, and hit neither API. +Don't mistake them for read paths or wire tests against a real operation: + +- **MpdGoalAdmin** — `MpdGoalAdmin/mockData.ts` (modals exist, no mutations). +- **MpdSupervisorReport** — `MpdSupervisorReport/mockData.ts` + `useMockInfiniteStaff.ts`. + +Everything else (AdditionalSalaryRequest, SavingsFundTransfer, MHA, SalaryCalculator, +NsoMpdQuestionnaire, the three goal calculators, MinistryPartnerReminders) performs +real mutations against the primary API. From d0e14faf05f46e6342738715526412e1e05ac6da Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:41:46 -0500 Subject: [PATCH 2/5] Add documentation on form persistence patterns in HrTools --- src/components/HrTools/CLAUDE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/components/HrTools/CLAUDE.md b/src/components/HrTools/CLAUDE.md index 6f343c802..14bcc09dc 100644 --- a/src/components/HrTools/CLAUDE.md +++ b/src/components/HrTools/CLAUDE.md @@ -91,6 +91,27 @@ Non-obvious per-calculator rules: `accountListId` vs `scenarioGoalId`); only scenario goals may edit identity fields. +## How forms persist + +Three patterns — know which one a form uses before adding a field: + +- **Field-level autosave** (most forms) — each field saves on change/blur. Two + base hooks exist; a form wraps one into its own `Autosave*` field components. + **Don't add a third primitive — wrap one of these:** + - repo-wide `src/components/Shared/Autosave/useAutosave` → GoalCalculator, + PdsGoalCalculator, SalaryCalculator, NsoMpdQuestionnaire + - HrTools-local `Shared/CalculationReports/CustomAutosave/useCustomAutosave` → + AdditionalSalaryRequest, MinisterHousingAllowance +- **Single submit through a mapping** — NsGoalCalculator collects a Formik form + and writes once via `goalSettingsApiMapping.ts`, the one place UI-only fields + are dropped. Adding a field here means updating that mapping, not just the form. +- **Explicit modal CRUD** — SavingsFundTransfer persists through its transfer + modals + mutations, not autosave. + +The mock tools below persist nothing. Note the finalize step is separate from +persistence: NSO autosaves each field but has a distinct `Complete` mutation, and +the request forms autosave a draft but `Submit` through the wizard's `SubmitModal`. + ## Per-form domain gotchas - **MinisterHousingAllowance** — the online flow gates on **MHA eligibility only** From aa3d2c756f01d5f5dc7447aeb3db0f3a6b69285d Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:51:51 -0500 Subject: [PATCH 3/5] Refine documentation in CLAUDE.md for HrTools, clarifying navigation and autosave patterns --- src/components/HrTools/CLAUDE.md | 55 ++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/components/HrTools/CLAUDE.md b/src/components/HrTools/CLAUDE.md index 14bcc09dc..323846dbf 100644 --- a/src/components/HrTools/CLAUDE.md +++ b/src/components/HrTools/CLAUDE.md @@ -14,9 +14,10 @@ load-bearing. There is **no** shared HrTools layout, index, or barrel component. Each tool is exposed only through its own `pages/accountLists/[accountListId]/hrTools/` -entry, and the nav group `'hr-tools-page'` in `src/hooks/useNavPages.tsx` (gated -on `canSeeHrTools`). Cross-tool cohesion comes only from that nav group plus the -shared wizard framework below. +entry, and the nav group `'hr-tools-page'` in `src/hooks/useNavPages.tsx` +(visibility-gated on `canSeeHrTools`, among other `hideTab` conditions). +Cross-tool cohesion comes only from that nav group plus the shared wizard +framework below. **URL slugs and native GraphQL names differ from component names** — resolve the real name before grepping: @@ -43,9 +44,11 @@ checking `rootFields.generated.ts`. `Shared/CalculationReports/` is a full multi-step "submit a request" framework: `PanelLayout` (stepper shell) + `StepsList` + `DirectionButtons` + `FormCard` + -`SubmitModal`/`Receipt`/`StatusCard` + `useCustomAutosave`. **AdditionalSalaryRequest, -MinisterHousingAllowance, and SalaryCalculator** all build on it. Any new -step-based request form should too — reach here before writing a new stepper. +`SubmitModal`/`Receipt`/`StatusCard` + `useCustomAutosave`. The request forms +(**AdditionalSalaryRequest, MinisterHousingAllowance, SalaryCalculator**) build on +it, and its `PanelLayout` stepper shell is also what the three goal calculators +and NsoMpdQuestionnaire wrap for their own step navigation. Any new step-based +form should reach here before writing a new stepper. Other shared pieces worth knowing before you build a local copy: @@ -67,9 +70,10 @@ strategies: `GoalCalculator/Shared/calculateTotals.ts`. - **PdsGoalCalculator (Designation Support)** — client-side math in `PdsGoalCalculator/calculations/`. -- **NsGoalCalculator (New Staff)** — **no client arithmetic at all.** It reads a - server-computed worksheet and recalculates unsaved edits via a server - round-trip (`previewNewStaffGoalCalculation`). Don't add math to this tree. +- **NsGoalCalculator (New Staff)** — **no goal-formula math on the client.** It + reads a server-computed worksheet and recalculates unsaved edits via a server + round-trip (`previewNewStaffGoalCalculation`); only display-level arithmetic + (rounding, remaining-need) lives here. Don't add goal math to this tree. ⚠️ **Drift risk:** the same goal-total concepts (admin/assessment gross-up, attrition) are computed independently in generic (`calculateTotals.ts`), PDS @@ -95,13 +99,14 @@ Non-obvious per-calculator rules: Three patterns — know which one a form uses before adding a field: -- **Field-level autosave** (most forms) — each field saves on change/blur. Two - base hooks exist; a form wraps one into its own `Autosave*` field components. - **Don't add a third primitive — wrap one of these:** - - repo-wide `src/components/Shared/Autosave/useAutosave` → GoalCalculator, - PdsGoalCalculator, SalaryCalculator, NsoMpdQuestionnaire - - HrTools-local `Shared/CalculationReports/CustomAutosave/useCustomAutosave` → - AdditionalSalaryRequest, MinisterHousingAllowance +- **Field-level autosave** (most forms) — each field saves on change/blur. + **Wrap an existing autosave primitive rather than writing your own** `Autosave*` + fields. Two base hooks cover this: + - repo-wide `src/components/Shared/Autosave/useAutosave` (currently + GoalCalculator, PdsGoalCalculator, SalaryCalculator, NsoMpdQuestionnaire; a + `useAutosaveCheckbox` sibling variant exists for checkbox fields) + - HrTools-local `Shared/CalculationReports/CustomAutosave/useCustomAutosave` + (currently AdditionalSalaryRequest, MinisterHousingAllowance) - **Single submit through a mapping** — NsGoalCalculator collects a Formik form and writes once via `goalSettingsApiMapping.ts`, the one place UI-only fields are dropped. Adding a field here means updating that mapping, not just the form. @@ -115,12 +120,13 @@ the request forms autosave a draft but `Submit` through the wizard's `SubmitModa ## Per-form domain gotchas - **MinisterHousingAllowance** — the online flow gates on **MHA eligibility only** - (`hcmData.mhaEit.mhaEligibility`). MHI/Italian staff (`country === 'IT'`) are - structurally excluded and use a **paper form**; there is no online MHI path. - A new request is blocked while the current one is unresolved, and separately - hidden when a board-approved request is still processing. `requests[0]` is - assumed newest-first; married state is derived from a second HCM record - (`hcm[1]` = spouse). + (`hcmData.mhaEit.mhaEligibility`). Italian/MHI staff fall out because their + `mhaEligibility` is false — not via a country check; `staffInfo.country` only + drives an informational note pointing them to the **paper MHI form**. There is + no online MHI path. A new request is blocked while the current one is + unresolved, and separately hidden when a board-approved request is still + processing. The request list is assumed newest-first; married state is derived + from the HCM query returning a second record (the spouse). - **NsoMpdQuestionnaire** — **no create/upsert exists.** The record is created by the OneApp import; the frontend only Updates/Completes, keyed by `accountListId` (not a questionnaire id). A null query → render @@ -132,8 +138,9 @@ the request forms autosave a draft but `Submit` through the wizard's `SubmitModa ## Mock / prototype tools — not wired to a backend -These render from `mockData.ts`, have **no `.graphql`**, and hit neither API. -Don't mistake them for read paths or wire tests against a real operation: +These are prototypes — they currently render from `mockData.ts`, have **no +`.graphql`**, and hit neither API (that will change once they're wired). Don't +mistake them for read paths or wire tests against a real operation: - **MpdGoalAdmin** — `MpdGoalAdmin/mockData.ts` (modals exist, no mutations). - **MpdSupervisorReport** — `MpdSupervisorReport/mockData.ts` + `useMockInfiniteStaff.ts`. From b463e1bd6a78a1a35f20f4a13cdade47d1463d4a Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:26:07 -0500 Subject: [PATCH 4/5] Small inconsistency fixes --- src/components/HrTools/CLAUDE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/HrTools/CLAUDE.md b/src/components/HrTools/CLAUDE.md index 323846dbf..29a7dbcf4 100644 --- a/src/components/HrTools/CLAUDE.md +++ b/src/components/HrTools/CLAUDE.md @@ -5,7 +5,7 @@ requests, savings-fund transfers, questionnaires, and admin/supervisor reports. This file captures the cross-tool "why" — rules and seams you can't see from any single form's code. It loads whenever you touch anything under `HrTools/`. -Follow the root `CLAUDE.md` and `.CLAUDE/rules/code-review.md` for the usual +Follow the root `CLAUDE.md` and `.claude/rules/code-review.md` for the usual conventions (named exports, i18n, Formik/Yup, `GqlMockedProvider`, Luxon dates). The financial-reporting review agent triggers on this tree — money math here is load-bearing. @@ -80,7 +80,8 @@ attrition) are computed independently in generic (`calculateTotals.ts`), PDS (`calculations/`), and again on the server for NS — with different step ordering. Changing the math in one place does **not** update the others. Treat these three as a set: when you touch goal arithmetic, check whether the other copies need the -same change. Constants are year-versioned (`calculationsYear`). +same change. NS captures a user-selected `calculationsYear`; the client +constants themselves are not year-versioned. Non-obvious per-calculator rules: From 7c89e3270faf26b5ae10071d54f162cd43f6c35e Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:16:43 -0500 Subject: [PATCH 5/5] Enhance documentation in CLAUDE.md regarding access gating for HrTools, detailing user type and group eligibility for each tool. --- src/components/HrTools/CLAUDE.md | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/components/HrTools/CLAUDE.md b/src/components/HrTools/CLAUDE.md index 29a7dbcf4..125842b6d 100644 --- a/src/components/HrTools/CLAUDE.md +++ b/src/components/HrTools/CLAUDE.md @@ -32,6 +32,43 @@ real name before grepping: `StaffSavingFund/` is not a data tool — it's a context/layout shell whose pages render **SavingsFundTransfer** components. Don't add transfer logic to it. +## Who can see which tool — two-layer access gating + +Two independent client-side layers gate visibility, both driven by the `user` +query (`User/GetUser.graphql`). This is UX gating, **not** the security +boundary — every HrTools operation (query and mutation) requires auth +server-side, and resolvers scope data to the user's own account lists. + +- **Layer 1 — user type (whole tab):** `canSeeHrTools` in `useNavPages.tsx` + shows the group only for `userType` `UsStaff`/`HybridStaff` (plus + `reportsDisabled` + `developerBypass`). Hybrid staff are US-payroll global staff. +- **Layer 2 — US staff group (per tool):** each tool gates on `usStaffGroup` / + `spouseUsStaffGroup` via `useIneligibleByGroup.ts` (nav) **and** a page-level + `UserTypeAccess` guard. The group is resolved server-side from HCM; the client + only sees the resolved `UsStaffGroupEnum` — don't reintroduce raw HCM codes. + +The table below is an orientation aid — verify against each tool's +`UserTypeAccess` guard, which is authoritative and can lag this list. + +| Tool | Eligible group (self unless noted) | +| -------------------------------------- | ----------------------------------------------------------- | +| AdditionalSalaryRequest | SeniorStaff / NewStaff / NationalExpat (self **or spouse**) | +| MinisterHousingAllowance | SeniorStaff / NationalExpat (self **or spouse**) | +| SalaryCalculator | SeniorStaff / NationalExpat | +| GoalCalculator (MPD) | SeniorStaff | +| NsGoalCalculator + NsoMpdQuestionnaire | NewStaff | +| PdsGoalCalculator | PaidWithDesignation | +| StaffSavingFund | any staff account (`requireStaffAccount`, no group gate) | + +⚠️ **MpdSupervisorReport** and **MinistryPartnerReminders** have no +`UserTypeAccess` page guard — nav visibility + `blockImpersonatingNonDevelopers` +only. Direct-URL access isn't blocked at the page level, but that's UX-only: +read data is authorized server-side (`ministryPartnerReminders` scopes to the +user's own account lists). **MpdSupervisorReport currently renders mock data** +(`mockData.ts`); when wiring it to real data, add the page guard and verify +server-side authz — a supervisor report shows _other_ staff's data, so +account-list scoping alone won't be the right check. + ## GraphQL routing Every HrTools operation routes to the **primary/native API** — all their root