# 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; // 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; // Calls the correct create API create(data): Promise; // Calls the correct update API update(opportunityId, data, shouldPopulate): Promise; // 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 ``` ### After: strategy renders its own step ```typescript {activeStrategy.renderDefineStep({ form, filters, updateFilters, ... })} ``` ### 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 // After — adds measureTypeData prop ``` --- ## 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.