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
+347
View File
@@ -0,0 +1,347 @@
# Feature Flag Usage Documentation - IFeatureFlagServiceClient.IsEnabledAsync
This document provides a comprehensive overview of all feature flags used throughout the Continuous Improvement codebase via the `IFeatureFlagServiceClient.IsEnabledAsync` method.
## Overview
The application uses feature flags to control functionality on a per-client basis in this multi-tenant system. Feature flags are checked using the `IFeatureFlagServiceClient` service and are typically scoped to a specific database GUID.
### Feature Flag Index
| # | Feature Flag | Description | Section |
|---|--------------|-------------|---------|
| 1 | `ciflexibleexplorationenabled` | Controls access to flexible exploration features for non-SDT employees | [Section 1](#1-ciflexibleexplorationenabled) |
| 2 | `enableencounteropportunityintempo` | Controls whether encounter opportunities are enabled in the Tempo interface | [Section 2](#2-enableencounteropportunityintempo) |
| 3 | `ciflexibleexplorationpage3enabled` | Controls access to the flexible exploration information page (third page) | [Section 3](#3-ciflexibleexplorationpage3enabled) |
| 4 | `enablepayrollopportunityintempo` | Controls whether payroll opportunities are enabled in the Tempo interface | [Section 4](#4-enablepayrollopportunityintempo) |
| 5 | `chargecodebeta` | Controls access to charge code beta features | [Section 5](#5-chargecodebeta) |
| 6 | `cibenchmarkingenabled` | Controls benchmarking features in continuous improvement | [Section 6](#6-cibenchmarkingenabled) |
| 7 | `ciopportunityworkbooks` | Controls opportunity workbook features | [Section 7](#7-ciopportunityworkbooks) |
| 8 | `networkopportunitiesenabled` | Controls network opportunity features | [Section 8](#8-networkopportunitiesenabled) |
| 9 | `strategicopportunityforchargecode` | Controls strategic opportunity features specifically for charge codes | [Section 9](#9-strategicopportunityforchargecode) |
| 10 | `ciexplorationpopulationmanagementlaunchesstrategicopportunitywizard` | Controls if the Add Opportunity button on the Exploration Population page launches the Strategic Opportunity Wizard |
## Feature Flag Keys and Usage
### 1. "ciflexibleexplorationenabled"
**Purpose**: Controls access to flexible exploration features for non-SDT employees
**Usage Locations**:
- `AllOpportunitiesController.cs` (line 40)
- `ExplorationOpportunitiesController.cs` (line 45)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> SomeAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationenabled"))
{
// Code for flexible exploration features
}
else
{
// Fallback or default behavior
}
}
```
**Context**: Enables flexible exploration functionality. SDT employees always have access, while client users need this feature flag enabled.
---
### 2. "enableencounteropportunityintempo"
**Purpose**: Controls whether encounter opportunities are enabled in the Tempo interface
**Usage Locations**:
- `EncounterOpportunitiesController.cs` (lines 90, 175)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> TempoAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("enableencounteropportunityintempo"))
{
// Code for encounter opportunities in Tempo
}
else
{
// Fallback to Jazz interface or show message
}
}
```
**Context**: Feature flag to enable encounter opportunity functionality in the modern Tempo interface versus legacy Jazz interface.
---
### 3. "ciflexibleexplorationpage3enabled"
**Purpose**: Controls access to the flexible exploration information page (third page)
**Usage Locations**:
- `ExplorationController.cs` (line 45)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> ExplorationPageAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationpage3enabled"))
{
// Code for the third page of flexible exploration
}
else
{
// Redirect or show less data
}
}
```
**Context**: Enables the third page of flexible exploration functionality for enhanced data exploration capabilities.
---
### 4. "enablepayrollopportunityintempo"
**Purpose**: Controls whether payroll opportunities are enabled in the Tempo interface
**Usage Locations**:
- `PayrollOpportunitiesController.cs` (line 85)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> PayrollTempoAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("enablepayrollopportunityintempo"))
{
// Code for payroll opportunities in Tempo
}
else
{
// Fallback to other methods or show message
}
}
```
**Context**: Feature flag to enable payroll opportunity functionality in the modern Tempo interface.
---
### 5. "chargecodebeta"
**Purpose**: Controls access to charge code beta features
**Usage Locations**:
- `ChargeCodeOpportunitiesController.cs` (line 243)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> ChargeCodeBetaAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("chargecodebeta"))
{
// Code for charge code beta features
}
else
{
// Hide or disable beta features
}
}
```
**Context**: Beta feature flag for charge code opportunities functionality, controlling access to new charge code features.
---
### 6. "cibenchmarkingenabled"
**Purpose**: Controls benchmarking features in continuous improvement
**Usage Locations**:
- Frontend TypeScript components
- Configuration and peer group functionality
**Implementation Pattern**:
```typescript
// TypeScript example
if (featureFlagService.isEnabled("cibenchmarkingenabled")) {
// Code for benchmarking features
} else {
// Alternative code path
}
```
**Context**: Enables benchmarking functionality in the CI application, allowing comparison against peer groups.
---
### 7. "ciopportunityworkbooks"
**Purpose**: Controls opportunity workbook features
**Usage Locations**:
- Referenced in TypeScript interface definitions
- Workbook management components
**Context**: Feature flag for workbook functionality related to opportunities.
---
### 8. "networkopportunitiesenabled"
**Purpose**: Controls network opportunity features
**Usage Locations**:
- Frontend navigation and components
- Network opportunity controllers
**Implementation Pattern**:
```typescript
// TypeScript example
if (featureFlagService.isEnabled("networkopportunitiesenabled")) {
// Code for network opportunity features
} else {
// Code for standard opportunities
}
```
**Context**: Enables network opportunity functionality for multi-client opportunity management.
---
### 9. "strategicopportunityforchargecode"
**Purpose**: Controls strategic opportunity features specifically for charge codes
**Usage Locations**:
- Frontend configuration components
- Strategic opportunity management
**Implementation Pattern**:
```typescript
// TypeScript example
if (featureFlagService.isEnabled("strategicopportunityforchargecode")) {
// Code for strategic opportunities for charge codes
} else {
// Fallback code
}
```
**Context**: Enables strategic opportunity functionality specifically for charge code-based opportunities.
---
### 10. "ciexplorationpopulationmanagementlaunchesstrategicopportunitywizard"
**Purpose**: Controls strategic opportunity features specifically for charge codes
**Usage Locations**:
- Frontend configuration components
- Strategic opportunity management
**Implementation Pattern**:
```typescript
// TypeScript example
if (featureFlagService.isEnabled("ciexplorationpopulationmanagementlaunchesstrategicopportunitywizard")) {
// Code to launch strategic opportunities wizard
} else {
// Fallback code
}
```
**Context**: Enables the Add Opportunity button on the Exploration Population page to launch the Strategic Opportunity wizard.
## Key Controllers Using Feature Flags
| Controller | Feature Flags Used | Purpose |
|------------|-------------------|---------|
| `AllOpportunitiesController` | `ciflexibleexplorationenabled` | Flexible exploration access control |
| `ExplorationOpportunitiesController` | `ciflexibleexplorationenabled` | Exploration features |
| `ExplorationController` | `ciflexibleexplorationpage3enabled` | Advanced exploration pages |
| `EncounterOpportunitiesController` | `enableencounteropportunityintempo` | Tempo interface integration |
| `PayrollOpportunitiesController` | `enablepayrollopportunityintempo` | Payroll Tempo integration |
| `ChargeCodeOpportunitiesController` | `chargecodebeta` | Beta charge code features |
| `SettingsController` | All CI area flags | Global feature management |
## Frontend Integration
The frontend TypeScript code integrates with feature flags through:
- **Navigation Control**: Menu items visibility based on feature flags
- **Component Rendering**: Conditional rendering of UI components
- **Service Availability**: API service availability checks
- **User Interface**: Feature-specific UI elements and workflows
### Example Frontend Usage
```typescript
// TypeScript example
async function loadOpportunities() {
if (await featureFlagService.isEnabled("ciflexibleexplorationenabled")) {
// Load flexible exploration opportunities
} else {
// Load standard opportunities
}
}
```
## Database Scope
All feature flags are scoped to specific database GUIDs, enabling per-client feature control in the multi-tenant architecture. This allows different clients to have different features enabled based on their subscription level or beta participation.
## Testing Considerations
When testing feature flag functionality:
1. **Mock the `IFeatureFlagServiceClient`** in unit tests
2. **Test both enabled and disabled states** for each feature
3. **Verify SDT employee bypass logic** where applicable
4. **Test database GUID scoping** for multi-tenant scenarios
### Example Test Mock
```csharp
// C# example using Moq
var featureFlagMock = new Mock<IFeatureFlagServiceClient>();
featureFlagMock.Setup(ff => ff.IsEnabledAsync("ciflexibleexplorationenabled"))
.ReturnsAsync(true);
// Use featureFlagMock.Object in tests
```
## Service Registration
Feature flags are registered in the dependency injection container via the `IFeatureFlagServiceClient` service, typically configured in the `Startup.cs` or `Program.cs` file for the application.
## Logging and Monitoring
Many feature flag checks include structured logging to track usage patterns:
```csharp
// C# example
if (await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationenabled"))
{
_logger.LogInformation("Flexible exploration feature enabled");
}
else
{
_logger.LogInformation("Flexible exploration feature disabled");
}
```
## Maintenance Notes
- All feature flag keys should be documented when added
- Consider feature flag lifecycle and removal strategy for permanent features
- Monitor feature flag usage through logging for analysis
- Ensure consistent naming conventions for new feature flags
- Regularly review and clean up obsolete feature flags
- Update this documentation when new feature flags are added
## Feature Flag Lifecycle
1. **Development**: New feature flags are added for experimental or beta features
2. **Testing**: Features are tested with flags enabled/disabled
3. **Rollout**: Gradual enabling of features for specific clients
4. **Stabilization**: Features become stable and widely adopted
5. **Cleanup**: Feature flags are removed once features are permanent
---
*Last Updated: November 2025*
*Document Version: 1.0*
*Total Feature Flags Documented: 9*
+602
View File
@@ -0,0 +1,602 @@
# Add Opportunity Type Playbook
Single source of truth for adding a new strategic opportunity type. Derived from the Charge Code (CC) and General Ledger (GL) implementations.
## Quick-Scan Checklist
Use this to track progress. Each item references a detailed section below.
### Backend
- [ ] [Entity](#1-entity) — `StrategicOpportunity{Type}.cs` (TPT child table)
- [ ] [Enums](#2-enums) — `OpportunityTypes` value + `MeasureType` values
- [ ] [DTOs](#3-dtos) — Create DTO + Update DTO
- [ ] [Detail Service](#4-detail-service) — `IStrategic{Type}OpportunityDetailService` + implementation
- [ ] [Query Layer](#5-query-layer) — SQL files, ServiceQuery, QueryBuilder
- [ ] [Distribution Process](#6-distribution-process) — `DistributionProcess{Type}.cs` + SQL builder
- [ ] [Dimension Config](#7-dimension-config) — `{Type}DimensionsConfig` in `StrategicOpportunityService`
- [ ] [Service Methods](#8-service-methods) — CRUD on `IStrategicOpportunityService`
- [ ] [Controller Endpoints](#9-controller-endpoints) — REST endpoints
- [ ] [DI Registration](#10-di-registration) — `ContinuousImprovementServiceExtensions.cs`
- [ ] [Migration](#11-database-migration) — EF Core migration for child table
### Frontend
- [ ] [Data Model](#f1-data-model) — `IStrategicOpportunity{Type}Data.ts`
- [ ] [Measure Types](#f2-measure-types) — enum values + display names in `MeasureType.ts`
- [ ] [Strategy Hook](#f3-strategy-hook) — `use{Type}Strategy.ts` implementing `IOpportunityTypeStrategy`
- [ ] [Define Component](#f4-define-component) — `{Type}Define.tsx`
- [ ] [Detail Store](#f5-detail-store) — `StrategicOpportunity{Type}DetailStoreTreeData.ts`
- [ ] [Detail Grid](#f6-detail-grid) — `StrategicOpportunity{Type}DetailGrid.tsx`
- [ ] [Layout Config](#f7-layout-config) — entry in `IStrategicOpportunityLayout.ts`
- [ ] [Dimension Enums](#f8-dimension-enums) — fields/sections/columns in `StrategicOpportunityDetailDimension.ts`
- [ ] [Service Methods](#f9-service-methods) — HTTP calls in `strategicOpportunityService.tsx`
- [ ] [Wizard Integration](#f10-wizard-integration) — hook call + strategy switch in `StrategicOpportunity.tsx`
- [ ] [Routes](#f11-routes) — create/edit routes in `Navigation.tsx`
- [ ] [List Page](#f12-list-page) — ButtonMenu option in `StrategicItems.tsx`
### Cross-Cutting
- [ ] [OpportunityTypes enum](#x1-opportunitytypes-enum) — both backend + frontend must match
- [ ] [Detail Page Integration](#x2-detail-page-integration) — `StrategicOpportunityDetailPage.tsx` branches
- [ ] [Tests](#x3-testing)
---
## Backend Steps
### 1. Entity
Create `Biz/StrategicOpportunities/StrategicOpportunity{Type}.cs` inheriting from `StrategicOpportunity` (TPT pattern).
**Pattern:**
```csharp
using System.ComponentModel.DataAnnotations.Schema;
namespace Strata.ContinuousImprovement.Biz.StrategicOpportunities
{
[Table("strategic_opportunity_{type}")]
public class StrategicOpportunity{Type} : StrategicOpportunity
{
// Type-specific properties
public string SomeHPath { get; set; }
public StrategicOpportunity{Type}() { }
// Copy constructor (used by CopyStrategicOpportunity)
public StrategicOpportunity{Type}(StrategicOpportunity{Type} opportunity, string opportunityName)
{
// Copy all base properties
Name = opportunityName;
OpportunityType = opportunity.OpportunityType;
OpportunityFiltersJSON = opportunity.OpportunityFiltersJSON;
Rollup1Id = opportunity.Rollup1Id;
Rollup2Id = opportunity.Rollup2Id;
Rollup3Id = opportunity.Rollup3Id;
BaselineType = opportunity.BaselineType;
BaselineDistribution = opportunity.BaselineDistribution;
BaselineStartDate = opportunity.BaselineStartDate;
BaselineEndDate = opportunity.BaselineEndDate;
EstimatedTrackingDuration = opportunity.EstimatedTrackingDuration;
StrataId = opportunity.StrataId;
LayoutJSON = opportunity.LayoutJSON;
TeamMembers = opportunity.TeamMembers;
OwnerGuid = opportunity.OwnerGuid;
ExecutiveSponsorGuid = opportunity.ExecutiveSponsorGuid;
CostLeaderGuid = opportunity.CostLeaderGuid;
// Copy type-specific properties
SomeHPath = opportunity.SomeHPath;
}
}
}
```
**DbContext:** Add `DbSet<StrategicOpportunity{Type}>` to `CentralDbContext` and configure in `OnModelCreating`.
**References:**
- `Biz/StrategicOpportunities/StrategicOpportunityChargeCode.cs`
- `Biz/StrategicOpportunities/StrategicOpportunityGL.cs`
- Base: `Biz/StrategicOpportunities/StrategicOpportunity.cs`
### 2. Enums
**OpportunityTypes** (`Biz/Generics/OpportunityTypes.cs` or wherever defined):
Add a new enum value. Current values:
- `StrategicOpportunityChargeCode` (value in `IAllOpportunity.ts` frontend)
- `StrategicOpportunityGL`
**MeasureType** (`Biz/StrategicOpportunities/Enums/MeasureType.cs`):
Add new measure type values after the last GL value (currently 37 = `GLMarginPerAdjDischarge`).
```csharp
// {Type} measure types (38+)
{Type}Measure1 = 38,
{Type}Measure2,
// ...
```
Also add a `{TYPE}_MEASURE_TYPES` constant list in `MeasureTypeConstants`.
**Other enums:** If the type has a concept like GL's `GLAccountType`, create a new enum file in `Biz/StrategicOpportunities/Enums/`.
### 3. DTOs
Create two DTOs in `Biz/StrategicOpportunities/Dtos/`:
**Create DTO**`StrategicOpportunity{Type}Dto.cs`:
```csharp
public class StrategicOpportunity{Type}Dto : StrategicOpportunityDto
{
// Type-specific properties matching the entity
public string SomeHPath { get; set; }
}
```
**Update DTO**`StrategicOpportunity{Type}UpdateDto.cs`:
```csharp
public class StrategicOpportunity{Type}UpdateDto
{
public StrategicOpportunity{Type}Dto Opportunity { get; set; }
public bool ShouldPopulateOpportunityDetails { get; set; }
}
```
**References:**
- `Dtos/StrategicOpportunityGLDto.cs` / `StrategicOpportunityGLUpdateDto.cs`
- `Dtos/StrategicOpportunityChargeCodeDto.cs` / `StrategicOpportunityChargeCodeUpdateDto.cs`
### 4. Detail Service
Create interface + implementation for populating detail data.
**Interface**`IStrategic{Type}OpportunityDetailService.cs`:
```csharp
public interface IStrategic{Type}OpportunityDetailService
{
Task PopulateOpportunityDetailsAsync(StrategicOpportunity{Type} opportunity, CancellationToken ct);
Task<StrategicOpportunityDetailData> GetDetailDataAsync(long opportunityId, bool isTracking, List<int> dimensions, CancellationToken ct);
}
```
**Implementation**`Strategic{Type}OpportunityDetailService.cs`:
Uses primary constructor DI, queries Snowflake/SQL for dimension data, populates `StrategicOpportunityDetail` rows.
**References:**
- `IStrategicChargeCodeOpportunityDetailService.cs` + `StrategicChargeCodeOpportunityDetailService.cs`
- `IStrategicGLOpportunityDetailService.cs` + `StrategicGLOpportunityDetailService.cs`
### 5. Query Layer
**SQL files** (embedded resources in `Biz/StrategicOpportunities/Queries/` or similar):
- `StrategicOpportunity{Type}DetailCTE.sql` — main detail data query
- Dimension-specific queries as needed
**ServiceQuery**`StrategicOpportunity{Type}ServiceQuery.cs`:
Loads SQL from embedded resources, provides query text to the detail service.
**QueryBuilder**`StrategicOpportunity{Type}DetailQueryBuilder.cs`:
Implements `IStrategicOpportunityDetailQueryBuilder`, builds parameterized queries for dimensions.
**References:**
- `Queries/StrategicOpportunityGLDetailQueryBuilder.cs`
- `Queries/StrategicOpportunityGLServiceQuery.cs`
- `StrategicOpportunityServiceQuery.cs` (CC version)
### 6. Distribution Process
Create in `Biz/StrategicOpportunities/DistributionProcess/{Type}/`:
- `DistributionProcess{Type}.cs` — orchestrates data distribution
- `DistributionProcess{Type}SqlBuilder.cs` — builds SQL for distribution
- `Distribution{Type}Summary.cs` — summary model
Update `DistributionProcessFactory.cs` to handle the new type.
**References:**
- `DistributionProcess/ChargeCode/DistributionProcessChargeCode.cs`
- `DistributionProcess/GL/DistributionProcessGL.cs`
- `DistributionProcess/DistributionProcessFactory.cs`
### 7. Dimension Config
In `StrategicOpportunityService.cs`, add a `{Type}DimensionsConfig` that maps dimension IDs to dimension names/hierarchy paths for the new type.
### 8. Service Methods
Add to `IStrategicOpportunityService.cs`:
```csharp
Task<long> CreateOpportunity{Type}Async(StrategicOpportunity{Type}Dto dto, CancellationToken ct);
Task<long> UpdateOpportunity{Type}(long id, StrategicOpportunity{Type}UpdateDto dto, CancellationToken ct);
Task<StrategicOpportunityEditData> GetStrategicOpportunity{Type}(long id, CancellationToken ct);
Task<bool> Delete{Type}OpportunityAsync(long id, CancellationToken ct);
```
Implement in `StrategicOpportunityService.cs`.
### 9. Controller Endpoints
Add endpoints to the strategic opportunities controller (or create a new controller if separation is needed):
```
POST /api/v1.0/strategic-opportunities/{type} — Create
PUT /api/v1.0/strategic-opportunities/{type}/{id} — Update
GET /api/v1.0/strategic-opportunities/{id}/{type} — Get for edit
DELETE /api/v1.0/strategic-opportunities/{type}/{id} — Delete
POST /api/v1.0/strategic-opportunities/{id}/{type}-detail — Get detail data
PUT /api/v1.0/strategic-opportunities/{id}/{type}-details — Update goal values
```
### 10. DI Registration
In `Biz/Configurations/ContinuousImprovementServiceExtensions.cs`:
```csharp
services.AddScoped<IStrategic{Type}OpportunityDetailService, Strategic{Type}OpportunityDetailService>();
```
### 11. Database Migration
```bash
dotnet ef migrations add AddStrategicOpportunity{Type}Table -p src/Strata.ContinuousImprovement.Biz -s src/Strata.ContinuousImprovement.Api
```
**TPT gotcha:** EF Core generates the child table PK with the same name as the parent (`pk_strategic_opportunity`). You must manually rename it in the migration file:
```csharp
// Change this:
name: "pk_strategic_opportunity",
// To this:
name: "pk_strategic_opportunity_{type}",
```
---
## Frontend Steps
All paths relative to `src/Strata.ContinuousImprovement.Web/continuousimprovement/src/`.
### F1. Data Model
Create `strategic-opportunities/data/IStrategicOpportunity{Type}Data.ts`:
```typescript
import { IStrategicOpportunityData, defaultStrategicOpportunityData } from './IStrategicOpportunityData';
export interface IStrategicOpportunity{Type}Data extends IStrategicOpportunityData {
// Type-specific properties
someHPath: string;
}
export const defaultStrategicOpportunity{Type}Data: IStrategicOpportunity{Type}Data = {
...defaultStrategicOpportunityData,
someHPath: ''
};
```
**References:**
- `data/IStrategicOpportunityChargeCodeData.ts`
- `data/IStrategicOpportunityGLData.ts`
### F2. Measure Types
In `strategic-opportunities/data/MeasureType.ts`:
1. Add enum values (must match backend `MeasureType.cs`):
```typescript
// {Type} measure types (sync with backend, values N-M)
{Type}Measure1 = N,
{Type}Measure2,
```
2. Add display names array:
```typescript
export const {type}MeasureTypeData: IKeyNameItem[] = [
{ key: MeasureType.{Type}Measure1, name: 'Display Name 1' },
// ...
];
```
3. Add entries to `mapMeasureTypeToDesiredResult`.
4. Add helper like `is{Type}PerMeasureType()` if the type has "per unit" style measures.
### F3. Strategy Hook
Create `strategic-opportunities/wizard/{type}/use{Type}Strategy.ts`:
Must implement `IOpportunityTypeStrategy` from `wizard/types/IOpportunityTypeStrategy.ts`:
```typescript
export interface IOpportunityTypeStrategy {
getDefaultData(): IStrategicOpportunityData;
renderDefineStep(context: IDefineStepContext): React.ReactElement | null;
renderModals(context: IModalContext): React.ReactElement | null;
validateDefineStep(formValues: any): string | true;
marshalDefineStep(formValues: any, context: IMarshalContext): Partial<IStrategicOpportunityData>;
onFormValuesChanged(changedValues: any, context: IFormChangeContext): void;
loadEditData(data: IStrategicOpportunityEditData, context: IEditLoadContext): void;
loadPrePopulationData?(params: URLSearchParams, context: IPrePopContext): Promise<void>;
create(data: IStrategicOpportunityData): Promise<number>;
update(opportunityId: number, data: IStrategicOpportunityData, shouldPopulate: boolean): Promise<boolean>;
shouldRepopulateOnEdit(context: IRepopulateContext): boolean;
getMeasureTypeData(): IKeyNameItem[];
}
```
**Key responsibilities:**
- `getDefaultData()` — return `defaultStrategicOpportunity{Type}Data` with correct `opportunityType`
- `renderDefineStep()` — render the type-specific Define component via `React.createElement`
- `validateDefineStep()` — return error string or `true`
- `marshalDefineStep()` — convert form values to data model fields
- `create()/update()` — call type-specific service methods
- `getMeasureTypeData()` — return the type-specific measure list
**Props pattern:**
```typescript
export interface I{Type}StrategyProps {
form: FormInstance;
isActive: boolean;
}
```
**References:**
- `wizard/charge-code/useChargeCodeStrategy.ts`
- `wizard/general-ledger/useGLStrategy.ts`
### F4. Define Component
Create `strategic-opportunities/wizard/{type}/{Type}Define.tsx`:
Type-specific Step 2 content — pickers, filters, measure type selection, desired result.
**References:**
- `wizard/charge-code/ChargeCodeDefine.tsx`
- `wizard/general-ledger/GeneralLedgerDefine.tsx`
### F5. Detail Store
Create `strategic-opportunities/data/store/StrategicOpportunity{Type}DetailStoreTreeData.ts`:
Extends `StoreTreeData` with the type-specific grid item interface. Handles:
- Loading data into tree structure
- Goal editing (onChange)
- Row config by dimensions
**References:**
- `data/store/StrategicOpportunityDetailStoreTreeData.ts` (CC)
- `data/store/StrategicOpportunityGLDetailStoreTreeData.ts` (GL)
### F6. Detail Grid
Create `strategic-opportunities/detail/StrategicOpportunity{Type}DetailGrid.tsx`:
DataGrid component with type-specific columns. Receives `root`, `columns`, `levelsCount`, `loading`, `expandedKeys`, `onChange`, `isEditable`.
**References:**
- `detail/StrategicOpportunityDetailGrid.tsx` (CC)
- `detail/StrategicOpportunityGLDetailGrid.tsx` (GL)
### F7. Layout Config
In `strategic-opportunities/detail/data/IStrategicOpportunityLayout.ts`:
1. Add default layout:
```typescript
export const defaultStrategicOpportunity{Type}Layout: IStrategicOpportunityLayout = {
rows: [strategicOpportunityDetailDimensionTotal, ...default{Type}Dimensions],
columns: [...default{Type}Columns]
};
```
2. Add layout config:
```typescript
export const {type}OpportunityLayoutConfig: IDetailLayoutConfig = {
defaultLayout: defaultStrategicOpportunity{Type}Layout,
rowLayoutList: {type}RowLayoutList,
columnLayoutList: {type}ColumnLayoutList,
requiredRows: [StrategicOpportunityDetailDimension.{TypeDimension}],
requiredColumns: [{Type}DetailField.{RequiredColumn}]
};
```
3. Update factory function:
```typescript
export function getDetailLayoutConfig(...): IDetailLayoutConfig {
if (opportunityType === OpportunityTypes.StrategicOpportunity{Type}) {
return {type}OpportunityLayoutConfig;
}
// ... existing cases
}
```
### F8. Dimension Enums
In `strategic-opportunities/detail/data/StrategicOpportunityDetailDimension.ts`:
1. Add dimension values to `StrategicOpportunityDetailDimension` enum (if new dimensions needed)
2. Add dimension ID to `StrategicOpportunityDetailDimensionId` enum
3. Create detail field enum:
```typescript
export enum StrategicOpportunity{Type}DetailField {
// Type-specific grid columns
}
```
4. Create column name/header maps
5. Create field section enum + section-to-field mappings
6. Add helper functions for parsing columns/dimensions by string
### F9. Service Methods
In `strategic-opportunities/data/strategicOpportunityService.tsx`, add:
```typescript
createOpportunity{Type}: async (opportunity) => httpPost(`${url}/{type}`, opportunity),
updateOpportunity{Type}: async (id, opportunity, shouldPopulate) =>
httpPut(`${url}/{type}/${id}`, { opportunity, shouldPopulateOpportunityDetails: shouldPopulate }),
getOpportunity{Type}: async (id) => httpGet(`${url}/${id}/GetOpportunity{Type}`),
getOpportunity{Type}DetailData: async (id, isTracking, dimensions) =>
httpPost(`${url}/${id}/${isTracking ? '{type}-tracking-detail' : '{type}-detail'}`, dimensions),
updateOpportunity{Type}DetailsData: async (id, data) =>
httpPut(`${url}/${id}/{type}-details`, data),
```
Also update `IStrategicOpportunityService.ts` interface with the new methods.
### F10. Wizard Integration
In `strategic-opportunities/wizard/StrategicOpportunity.tsx`:
1. Import the new strategy hook:
```typescript
import { use{Type}Strategy } from './{type}/use{Type}Strategy';
```
2. Add hook call (must always be called — React rules of hooks):
```typescript
const {type}Strategy = use{Type}Strategy({ form, isActive: opportunityType === OpportunityTypes.StrategicOpportunity{Type} });
```
3. Update `activeStrategy` selection:
```typescript
const is{Type} = opportunityType === OpportunityTypes.StrategicOpportunity{Type};
// All hooks always called
const ccStrategy = useChargeCodeStrategy({ form, isActive: !isGL && !is{Type} });
const glStrategy = useGLStrategy({ form, isActive: isGL });
const {type}Strategy = use{Type}Strategy({ form, isActive: is{Type} });
const activeStrategy = is{Type} ? {type}Strategy : isGL ? glStrategy : ccStrategy;
```
### F11. Routes
In `shared/Navigation.tsx`, add create and edit routes:
```tsx
<Route
path={['/strategic-opportunities/create/{type}']}
exact
component={(props) => StrategicOpportunity({ ...props, opportunityType: OpportunityTypes.StrategicOpportunity{Type} })}
key='/strategic-opportunities/create/{type}'
/>
<Route
path={['/strategic-opportunities/edit/{type}/:id']}
exact
component={(props) => StrategicOpportunity({ ...props, opportunityType: OpportunityTypes.StrategicOpportunity{Type} })}
key='/strategic-opportunities/edit/{type}'
/>
```
### F12. List Page
In `strategic-opportunities/opportunities/StrategicItems.tsx`, add to the ButtonMenu items:
```typescript
items={[
{ key: 'charge-code', label: 'Charge Code' },
{ key: 'general-ledger', label: 'General Ledger' },
{ key: '{type}', label: '{Display Name}' }
]}
```
And handle the click:
```typescript
onClick={(e) => {
// ... existing cases
else if (e.key === '{type}') {
history.push('/strategic-opportunities/create/{type}');
}
}}
```
---
## Cross-Cutting
### X1. OpportunityTypes Enum
Both backend and frontend must have matching values:
**Backend**`Biz/Generics/OpportunityTypes.cs` (or wherever `OpportunityTypes` is defined)
**Frontend**`opportunities/opportunities/data/IAllOpportunity.ts`
Current values:
```
StrategicOpportunityChargeCode = 17 (approx)
StrategicOpportunityGL = 18 (approx)
```
New type should be the next sequential value.
### X2. Detail Page Integration
In `strategic-opportunities/detail/StrategicOpportunityDetailPage.tsx`, add branches for:
1. `storeTreeData` selection (around line 181):
```typescript
if (is{Type}) {
return new StrategicOpportunity{Type}DetailStoreTreeData(DefaultGridItem);
}
```
2. Detail data fetching (around line 252):
```typescript
if (is{Type}) {
// Fetch type-specific detail data
}
```
3. Grid column parsing (around line 305):
```typescript
if (is{Type}) {
_gridColumns = getStrategicOpportunity{Type}DetailColumnsByString(layout.columns);
}
```
4. Grid rendering (around line 853):
```typescript
is{Type} ? (
<StrategicOpportunity{Type}DetailGrid ... />
) : isGL ? (
...
)
```
5. Save handler — map dirty data to goal values for the new type.
6. Edit navigation path:
```typescript
const typePath = is{Type} ? '{type}' : isGL ? 'general-ledger' : 'charge-code';
```
7. Comments drawer `opportunityType` prop.
### X3. Testing
**Backend:**
- Unit tests for the detail service
- Unit tests for the distribution process
- Unit tests for new service methods
**Frontend:**
- Unit test for the strategy hook (validate, marshal, create/update)
- Unit test for the Define component
- Unit test for the detail grid component
---
## File Reference Table
| Pattern | CC Reference | GL Reference |
|---------|-------------|--------------|
| Entity | `StrategicOpportunityChargeCode.cs` | `StrategicOpportunityGL.cs` |
| Create DTO | `StrategicOpportunityChargeCodeDto.cs` | `StrategicOpportunityGLDto.cs` |
| Update DTO | `StrategicOpportunityChargeCodeUpdateDto.cs` | `StrategicOpportunityGLUpdateDto.cs` |
| Detail Service | `StrategicChargeCodeOpportunityDetailService.cs` | `StrategicGLOpportunityDetailService.cs` |
| Distribution | `DistributionProcess/ChargeCode/` | `DistributionProcess/GL/` |
| Query Builder | `StrategicOpportunityDetailQueryBuilder.cs` | `StrategicOpportunityGLDetailQueryBuilder.cs` |
| Service Query | `StrategicOpportunityServiceQuery.cs` | `StrategicOpportunityGLServiceQuery.cs` |
| Data Model | `IStrategicOpportunityChargeCodeData.ts` | `IStrategicOpportunityGLData.ts` |
| Strategy Hook | `useChargeCodeStrategy.ts` | `useGLStrategy.ts` |
| Define Component | `ChargeCodeDefine.tsx` | `GeneralLedgerDefine.tsx` |
| Store | `StrategicOpportunityDetailStoreTreeData.ts` | `StrategicOpportunityGLDetailStoreTreeData.ts` |
| Detail Grid | `StrategicOpportunityDetailGrid.tsx` | `StrategicOpportunityGLDetailGrid.tsx` |
## Tips and Gotchas
1. **TPT PK collision**: Always check the generated migration for duplicate PK constraint names. Rename `pk_strategic_opportunity` to `pk_strategic_opportunity_{type}`.
2. **React hooks order**: All strategy hooks must be called unconditionally in `StrategicOpportunity.tsx` (React rules of hooks). Use the `isActive` prop to no-op inactive strategies.
3. **MeasureType sync**: Backend `MeasureType` enum values must exactly match frontend `MeasureType` const enum values. Double-check after adding.
4. **Layout compatibility**: The `isLayoutCompatible()` function checks `requiredColumns`. Make sure your type's required columns are unique to avoid stale layouts from other types being loaded.
5. **Formatting**: Run `npx eslint --fix <files>` after editing (not `npx prettier --write`).
@@ -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.