chore: deploy initial code base

This commit is contained in:
Thom Lamb
2026-05-12 10:34:30 -05:00
parent cce2961782
commit a81300a571
1829 changed files with 248331 additions and 1 deletions
@@ -0,0 +1,404 @@
# Strategic Opportunity Wizard Refactor — Migration Guide
## Who This Is For
If you wrote or maintain `StrategicOpportunity.tsx` (the wizard orchestrator) or any of its step components, this document explains what's changing, why, and how your code maps to the new structure.
---
## Why Are We Doing This?
The wizard was built for **Charge Code** opportunities only. Now we need to add **General Ledger (GL)** opportunities that use the same 5-step wizard flow but have completely different "Define Opportunity" logic (departments + GL accounts instead of encounter groups + charge codes).
The current orchestrator (`StrategicOpportunity.tsx`, 670 lines) has charge-code-specific state, validation, data marshaling, and service calls baked directly in. If we added GL logic the same way, the file would balloon with `if (opportunityType === 'GL')` branches everywhere.
Instead, we're extracting type-specific logic into **strategy hooks** — one for Charge Code, one for GL — and keeping the orchestrator focused on what's shared: wizard navigation, form management, and the steps that are identical for both types.
---
## The Big Picture: Before vs After
### Before (today)
```
StrategicOpportunity.tsx (670 lines)
├── Wizard navigation + form setup
├── 12+ CC-specific state variables (chargeCodeMembers, selectedChargeCodes, etc.)
├── CC-specific functions (loadChargeCodePickerModels, etc.)
├── CC-specific validation in handleChange (step 1)
├── CC-specific data marshaling in updateOpportunity (step 1)
├── CC-specific edit loading in loadEditData
├── CC-specific pre-population from exploration
├── CC-specific create/update service calls
└── CC-specific repopulation detection on edit
```
Everything lives in one file. There's no separation between "wizard infrastructure" and "charge code logic."
### After (refactored)
```
StrategicOpportunity.tsx (~300 lines) — Generic orchestrator
├── Wizard navigation + form setup (unchanged)
├── Shared state only (primaryMeasures, filters, rollups, workbooks, baseline)
├── Calls activeStrategy.validateDefineStep() instead of inline CC validation
├── Calls activeStrategy.marshalDefineStep() instead of inline CC marshaling
├── Calls activeStrategy.loadEditData() for type-specific edit fields
├── Calls activeStrategy.create() / activeStrategy.update()
└── Renders activeStrategy.renderDefineStep() for step 1
charge-code/useChargeCodeStrategy.ts (~250 lines) — All the CC logic extracted
├── CC state (chargeCodeMembers, selectedChargeCodes, etc.)
├── CC functions (loadChargeCodePickerModels, etc.)
├── CC step 1 validation + marshaling
├── CC edit loading + pre-population
└── CC create/update service calls
general-ledger/useGLStrategy.ts — New GL logic
├── GL state (accountType, departments, GL accounts)
├── GL step 1 validation + marshaling
├── GL edit loading
└── GL create/update service calls
```
---
## What Stays the Same
These things are **not changing** (or changing only trivially):
| Component | What's happening |
|-----------|-----------------|
| **StrategicOpportunityDetail.tsx** (Step 0) | Shared. No change except minor display support for GL type. |
| **StrategicOpportunityDefineTimeframe.tsx** (Step 3) | Shared. No change. |
| **StrategicOpportunityExternalLinks.tsx** (Step 4) | Shared. No change. |
| **StrategicOpportunityRenameMeasure.tsx** | Shared utility. No change. |
| **StrategicOpportunityFilters.tsx** | Shared utility. No change. |
| **All data/ files** | No changes to interfaces, services, enums. |
| **Wizard step validation for steps 0, 2, 3, 4** | Stays in the orchestrator — these steps are identical for both types. |
---
## What's Moving
### Files that move (same code, new location)
| Old Location | New Location | Why |
|-------------|-------------|-----|
| `wizard/StrategicOpportunityDefine.tsx` | `wizard/charge-code/ChargeCodeDefine.tsx` | This component is 100% charge-code-specific. Renaming makes that explicit. |
| `wizard/ChargeCodeCustomPickerModal.tsx` | `wizard/charge-code/ChargeCodeCustomPickerModal.tsx` | Only used by charge code define step. |
These are **moves + renames**, not rewrites. The component logic inside is the same.
### State that moves from orchestrator → CC strategy hook
All of these `useState` calls currently in `StrategicOpportunity.tsx` will move into `useChargeCodeStrategy.ts`:
```typescript
// These move OUT of the orchestrator:
const [chargeCodeMembers, setChargeCodeMembers] = useState(...);
const [selectedChargeCodes, setSelectedChargeCodes] = useState(...);
const [selectedChargeCodesInitial, setSelectedChargeCodesInitial] = useState(...);
const [expPopulationMembers, setExpPopulationMembers] = useState(...);
const [selectedExpPopulations, setSelectedExpPopulations] = useState(...);
const [patientPopulationsInitial, setPatientPopulationsInitial] = useState(...);
const [expPopulationsInitial, setExpPopulationsInitial] = useState(...);
const [expPopCostDriver, setExpPopCostDriver] = useState(...);
const [uploadModalVisible, setUploadModalVisible] = useState(...);
const [isPrePopulated, setIsPrePopulated] = useState(...);
```
```typescript
// These STAY in the orchestrator (shared by both types):
const [stepNumber, setStepNumber] = useState(0);
const [opportunity, setOpportunity] = useState(...); // now typed as IStrategicOpportunityData (base)
const [primaryMeasures, setPrimaryMeasures] = useState(...);
const [workbooks, setWorkbooks] = useState(...);
const [opportunityFilters, setOpportunityFilters] = useState(...);
const [baselinePeriodInitial, setBaselinePeriodInitial] = useState(...);
const [rollupColumns, setRollupColumns] = useState(...);
const [isEditMode, setIsEditMode] = useState(false);
const [loading, setLoading] = useState(false);
```
### Functions that move from orchestrator → CC strategy hook
| Function | Why it's CC-specific |
|----------|---------------------|
| `loadChargeCodePickerModels()` | Fetches charge code picker data |
| `loadExpPopulationPickerModels()` | Fetches exploration populations |
| `setPrimaryMeasureChargeCodes()` | Sets CC-specific form field |
| `setExplorationPopulations()` | Sets CC-specific encounter group |
| `handleChargeCodesUploaded()` | Handles CC upload modal result |
| Pre-population `useEffect` (lines 584-642) | Only applies to CC from exploration |
| CC-specific parts of `loadEditData` | Loading encounter groups, charge codes |
| CC-specific parts of `handleFormValuesChanged` | CC form field change handling |
| Step 1 block in `handleChange` | CC-specific validation checks |
| Step 1 block in `updateOpportunity` | CC-specific data marshaling |
| `handleSubmit` service calls | `createOpportunityChargeCode` / `updateOpportunityChargeCode` |
| Repopulation detection logic | Comparing CC-specific initial vs current state |
---
## The Strategy Interface
Both hooks implement this common interface so the orchestrator doesn't need to know which type it's working with:
```typescript
interface IOpportunityTypeStrategy {
// Default data for creating a new opportunity of this type
getDefaultData(): IStrategicOpportunityData;
// Renders the "Define Opportunity" step (step 1) — CC shows encounter groups + charge codes, GL shows departments + accounts
renderDefineStep(context): JSX.Element;
// Renders any modals this type needs (CC has upload modal, GL has none)
renderModals(context): JSX.Element | null;
// Returns true if step 1 form values are valid for this type
validateDefineStep(formValues): boolean;
// Converts form values into opportunity data fields for step 1
marshalDefineStep(formValues, context): Partial<IStrategicOpportunityData>;
// Handles form value changes that need type-specific reactions
onFormValuesChanged(changedValues, context): void;
// Loads type-specific fields when editing an existing opportunity
loadEditData(data, form, context): void;
// (CC only) Pre-populates from exploration population URL params
loadPrePopulationData?(params, form, context): Promise<void>;
// Calls the correct create API
create(data): Promise<number>;
// Calls the correct update API
update(opportunityId, data, shouldPopulate): Promise<boolean>;
// Determines if editing has changed enough to require goal recalculation
shouldRepopulateOnEdit(context): boolean;
// Returns the measure type list for the "Additional Measures" step dropdown
getMeasureTypeData(): { key: number; name: string }[];
}
```
---
## How the Orchestrator Changes
### Before: inline CC logic
```typescript
// In handleChange, step 1 validation:
} else if (currentStep === 1) {
if (!detailValues.encounterGroup || !detailValues.encounterGroups || ...) {
return;
}
}
```
### After: delegates to strategy
```typescript
} else if (currentStep === 1) {
if (!activeStrategy.validateDefineStep(detailValues)) {
return;
}
}
```
### Before: inline CC rendering
```typescript
<Wizard.Step key='1' title='Define Opportunity'>
<StrategicOpportunityDefine
encounterGroup={opportunity.encounterGroup}
chargeCodeMembers={chargeCodeMembers}
selectedChargeCodes={selectedChargeCodes}
// ... 14 props
/>
</Wizard.Step>
```
### After: strategy renders its own step
```typescript
<Wizard.Step key='1' title='Define Opportunity'>
{activeStrategy.renderDefineStep({ form, filters, updateFilters, ... })}
</Wizard.Step>
```
### Before: inline CC service call
```typescript
const opportunityId = await strategicOpportunityService.createOpportunityChargeCode(data);
```
### After: strategy handles service call
```typescript
const opportunityId = await activeStrategy.create(data);
```
---
## Hook Selection: Both Hooks Always Called
React requires hooks to be called unconditionally (same hooks, same order, every render). So the orchestrator calls **both** hooks every time:
```typescript
const ccStrategy = useChargeCodeStrategy({ form, ... });
const glStrategy = useGLStrategy({ form, ... });
const activeStrategy = opportunityType === OpportunityTypes.StrategicOpportunityGL
? glStrategy
: ccStrategy;
```
The inactive hook holds trivial default state — a few empty arrays and `false` booleans. No API calls, no heavy computation. This is a standard React pattern.
---
## New Folder Structure
```
strategic-opportunities/
wizard/
StrategicOpportunity.tsx ← Slimmed-down orchestrator
StrategicOpportunityDetail.tsx ← Step 0 (unchanged)
StrategicOpportunityDefineAdditional.tsx ← Step 2 (adds measureTypeData prop)
StrategicOpportunityDefineTimeframe.tsx ← Step 3 (unchanged)
StrategicOpportunityExternalLinks.tsx ← Step 4 (unchanged)
StrategicOpportunityRenameMeasure.tsx ← Shared utility (unchanged)
StrategicOpportunityFilters.tsx ← Shared utility (unchanged)
types/
IOpportunityTypeStrategy.ts ← Strategy interface + context types
charge-code/
useChargeCodeStrategy.ts ← CC strategy hook (extracted from orchestrator)
ChargeCodeDefine.tsx ← Moved from StrategicOpportunityDefine.tsx
ChargeCodeCustomPickerModal.tsx ← Moved from wizard root
general-ledger/
useGLStrategy.ts ← GL strategy hook (new)
GeneralLedgerDefine.tsx ← GL define step (new)
```
---
## StrategicOpportunityDefineAdditional Change
This component currently hardcodes the charge code measure type list:
```typescript
// Line 18 — imports CC-only measure types
import { getDesiredResultByMeasureType, measureTypeData } from '../data/MeasureType';
// Line 31 — hardcoded to CC measures
const secondaryMeasureItems = useMemo(() => measureTypeData.map(...), []);
```
After the refactor, it receives `measureTypeData` as a **prop** from the orchestrator, which gets it from `activeStrategy.getMeasureTypeData()`. This way CC shows CC measures and GL shows GL measures in the "Add Additional Measure" dropdown.
```typescript
// Before
<StrategicOpportunityDefineAdditional
getMeasureItem={getMeasureItem}
updateFiltersForSecondaryMeasure={updateFiltersForSecondaryMeasure}
validateAndUpdateMeasureName={validateAndUpdateMeasureName}
/>
// After — adds measureTypeData prop
<StrategicOpportunityDefineAdditional
getMeasureItem={getMeasureItem}
updateFiltersForSecondaryMeasure={updateFiltersForSecondaryMeasure}
validateAndUpdateMeasureName={validateAndUpdateMeasureName}
measureTypeData={activeStrategy.getMeasureTypeData()}
/>
```
---
## New Routes
Two new routes added to `Navigation.tsx`:
| Route | Purpose |
|-------|---------|
| `/strategic-opportunities/create/general-ledger` | Create a new GL strategic opportunity |
| `/strategic-opportunities/edit/general-ledger/:strategicOpportunityId` | Edit an existing GL strategic opportunity |
These follow the exact same pattern as the existing CC routes (lines 226-236 in Navigation.tsx).
---
## What the GL Define Step Looks Like
Instead of Encounter Groups + Charge Codes + Upload, the GL define step has:
1. **Account Type** — Radio: Revenue / Expense / Margin
2. **Department** — ScoreMemberPicker for GL departments
3. **GL Accounts** — Conditional pickers based on account type:
- Revenue → Revenue account picker
- Expense → Expense account picker
- Margin → Both revenue + expense pickers
- Optional statistic account picker (checkbox toggle)
4. **Filters** — Same shared StrategicFilterBar component
5. **Primary Measure** — Dropdown from `glMeasureTypeData` (instead of `measureTypeData`)
6. **Desired Result + Threshold** — Same UI as CC
---
## How to Verify Nothing Broke
After the refactor, test these CC flows — they should behave **identically** to today:
1. **Create CC opportunity**: Navigate to `/strategic-opportunities/create/charge-code`, complete all 5 steps, verify it saves
2. **Edit CC opportunity**: Open an existing CC strategic opportunity, click Edit, verify all fields load correctly, make a change, save
3. **Pre-populate from exploration**: From an exploration population, click "Create Strategic Opportunity" — verify encounter group, charge codes, and baseline are pre-filled
4. **Goal reset warning**: Edit an opportunity, change the filters or baseline period, click Save — verify the "resets goals" confirmation modal appears
5. **Run frontend tests**: `cd src/Strata.ContinuousImprovement.Web/continuousimprovement && npm run test-ci`
---
## Mapping Cheat Sheet: "Where Did My Code Go?"
| If you're looking for... | It's now in... |
|--------------------------|---------------|
| `chargeCodeMembers` state | `useChargeCodeStrategy.ts` |
| `selectedChargeCodes` state | `useChargeCodeStrategy.ts` |
| `loadChargeCodePickerModels()` | `useChargeCodeStrategy.ts` |
| `loadExpPopulationPickerModels()` | `useChargeCodeStrategy.ts` |
| `handleChargeCodesUploaded()` | `useChargeCodeStrategy.ts` |
| Pre-population `useEffect` | `useChargeCodeStrategy.ts``loadPrePopulationData()` |
| `StrategicOpportunityDefine` component | `charge-code/ChargeCodeDefine.tsx` (same code, new name) |
| `ChargeCodeCustomPickerModal` | `charge-code/ChargeCodeCustomPickerModal.tsx` (moved) |
| Step 1 validation in `handleChange` | `useChargeCodeStrategy.ts``validateDefineStep()` |
| Step 1 marshaling in `updateOpportunity` | `useChargeCodeStrategy.ts``marshalDefineStep()` |
| `createOpportunityChargeCode` call | `useChargeCodeStrategy.ts``create()` |
| `updateOpportunityChargeCode` call | `useChargeCodeStrategy.ts``update()` |
| Repopulation check (`shouldPopulateOpportunityDetails`) | `useChargeCodeStrategy.ts``shouldRepopulateOnEdit()` |
| Wizard navigation (`handleChange`, `setStepNumber`) | `StrategicOpportunity.tsx` (stayed) |
| Form setup (`Form.useForm()`) | `StrategicOpportunity.tsx` (stayed) |
| Shared state (primaryMeasures, filters, rollups) | `StrategicOpportunity.tsx` (stayed) |
| Steps 0, 2, 3, 4 validation | `StrategicOpportunity.tsx` (stayed) |
| `handleCancel`, `handleSubmit` skeleton | `StrategicOpportunity.tsx` (stayed) |
---
## FAQ
**Q: Why not just use `if/else` for GL vs CC in the existing orchestrator?**
A: The orchestrator would grow to 1000+ lines with interleaved branching. Strategy hooks keep each type's logic cohesive and independently testable.
**Q: Why hooks instead of classes?**
A: React state (`useState`, `useEffect`) only works inside hooks or components. A class-based strategy would need workarounds to manage React state.
**Q: Does the inactive strategy hook waste memory?**
A: Negligible. It holds a few empty arrays and `false` booleans. No API calls fire for the inactive type.
**Q: Will this break existing CC tests?**
A: No. The CC behavior is identical — logic is just relocated. Import paths in tests may need updating if they reference moved files.
**Q: Can I still debug the CC flow the same way?**
A: Yes. Set breakpoints in `useChargeCodeStrategy.ts` for CC-specific logic, or in `StrategicOpportunity.tsx` for wizard navigation. The call chain is just one level deeper for type-specific operations.