Template
chore: deploy initial code base
This commit is contained in:
@@ -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*
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user