22 KiB
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 —
StrategicOpportunity{Type}.cs(TPT child table) - Enums —
OpportunityTypesvalue +MeasureTypevalues - DTOs — Create DTO + Update DTO
- Detail Service —
IStrategic{Type}OpportunityDetailService+ implementation - Query Layer — SQL files, ServiceQuery, QueryBuilder
- Distribution Process —
DistributionProcess{Type}.cs+ SQL builder - Dimension Config —
{Type}DimensionsConfiginStrategicOpportunityService - Service Methods — CRUD on
IStrategicOpportunityService - Controller Endpoints — REST endpoints
- DI Registration —
ContinuousImprovementServiceExtensions.cs - Migration — EF Core migration for child table
Frontend
- Data Model —
IStrategicOpportunity{Type}Data.ts - Measure Types — enum values + display names in
MeasureType.ts - Strategy Hook —
use{Type}Strategy.tsimplementingIOpportunityTypeStrategy - Define Component —
{Type}Define.tsx - Detail Store —
StrategicOpportunity{Type}DetailStoreTreeData.ts - Detail Grid —
StrategicOpportunity{Type}DetailGrid.tsx - Layout Config — entry in
IStrategicOpportunityLayout.ts - Dimension Enums — fields/sections/columns in
StrategicOpportunityDetailDimension.ts - Service Methods — HTTP calls in
strategicOpportunityService.tsx - Wizard Integration — hook call + strategy switch in
StrategicOpportunity.tsx - Routes — create/edit routes in
Navigation.tsx - List Page — ButtonMenu option in
StrategicItems.tsx
Cross-Cutting
- OpportunityTypes enum — both backend + frontend must match
- Detail Page Integration —
StrategicOpportunityDetailPage.tsxbranches - Tests
Backend Steps
1. Entity
Create Biz/StrategicOpportunities/StrategicOpportunity{Type}.cs inheriting from StrategicOpportunity (TPT pattern).
Pattern:
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.csBiz/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 inIAllOpportunity.tsfrontend)StrategicOpportunityGL
MeasureType (Biz/StrategicOpportunities/Enums/MeasureType.cs):
Add new measure type values after the last GL value (currently 37 = GLMarginPerAdjDischarge).
// {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:
public class StrategicOpportunity{Type}Dto : StrategicOpportunityDto
{
// Type-specific properties matching the entity
public string SomeHPath { get; set; }
}
Update DTO — StrategicOpportunity{Type}UpdateDto.cs:
public class StrategicOpportunity{Type}UpdateDto
{
public StrategicOpportunity{Type}Dto Opportunity { get; set; }
public bool ShouldPopulateOpportunityDetails { get; set; }
}
References:
Dtos/StrategicOpportunityGLDto.cs/StrategicOpportunityGLUpdateDto.csDtos/StrategicOpportunityChargeCodeDto.cs/StrategicOpportunityChargeCodeUpdateDto.cs
4. Detail Service
Create interface + implementation for populating detail data.
Interface — IStrategic{Type}OpportunityDetailService.cs:
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.csIStrategicGLOpportunityDetailService.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.csQueries/StrategicOpportunityGLServiceQuery.csStrategicOpportunityServiceQuery.cs(CC version)
6. Distribution Process
Create in Biz/StrategicOpportunities/DistributionProcess/{Type}/:
DistributionProcess{Type}.cs— orchestrates data distributionDistributionProcess{Type}SqlBuilder.cs— builds SQL for distributionDistribution{Type}Summary.cs— summary model
Update DistributionProcessFactory.cs to handle the new type.
References:
DistributionProcess/ChargeCode/DistributionProcessChargeCode.csDistributionProcess/GL/DistributionProcessGL.csDistributionProcess/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:
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:
services.AddScoped<IStrategic{Type}OpportunityDetailService, Strategic{Type}OpportunityDetailService>();
11. Database Migration
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:
// 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:
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.tsdata/IStrategicOpportunityGLData.ts
F2. Measure Types
In strategic-opportunities/data/MeasureType.ts:
- Add enum values (must match backend
MeasureType.cs):
// {Type} measure types (sync with backend, values N-M)
{Type}Measure1 = N,
{Type}Measure2,
- Add display names array:
export const {type}MeasureTypeData: IKeyNameItem[] = [
{ key: MeasureType.{Type}Measure1, name: 'Display Name 1' },
// ...
];
-
Add entries to
mapMeasureTypeToDesiredResult. -
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:
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()— returndefaultStrategicOpportunity{Type}Datawith correctopportunityTyperenderDefineStep()— render the type-specific Define component viaReact.createElementvalidateDefineStep()— return error string ortruemarshalDefineStep()— convert form values to data model fieldscreate()/update()— call type-specific service methodsgetMeasureTypeData()— return the type-specific measure list
Props pattern:
export interface I{Type}StrategyProps {
form: FormInstance;
isActive: boolean;
}
References:
wizard/charge-code/useChargeCodeStrategy.tswizard/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.tsxwizard/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:
- Add default layout:
export const defaultStrategicOpportunity{Type}Layout: IStrategicOpportunityLayout = {
rows: [strategicOpportunityDetailDimensionTotal, ...default{Type}Dimensions],
columns: [...default{Type}Columns]
};
- Add layout config:
export const {type}OpportunityLayoutConfig: IDetailLayoutConfig = {
defaultLayout: defaultStrategicOpportunity{Type}Layout,
rowLayoutList: {type}RowLayoutList,
columnLayoutList: {type}ColumnLayoutList,
requiredRows: [StrategicOpportunityDetailDimension.{TypeDimension}],
requiredColumns: [{Type}DetailField.{RequiredColumn}]
};
- Update factory function:
export function getDetailLayoutConfig(...): IDetailLayoutConfig {
if (opportunityType === OpportunityTypes.StrategicOpportunity{Type}) {
return {type}OpportunityLayoutConfig;
}
// ... existing cases
}
F8. Dimension Enums
In strategic-opportunities/detail/data/StrategicOpportunityDetailDimension.ts:
- Add dimension values to
StrategicOpportunityDetailDimensionenum (if new dimensions needed) - Add dimension ID to
StrategicOpportunityDetailDimensionIdenum - Create detail field enum:
export enum StrategicOpportunity{Type}DetailField {
// Type-specific grid columns
}
- Create column name/header maps
- Create field section enum + section-to-field mappings
- Add helper functions for parsing columns/dimensions by string
F9. Service Methods
In strategic-opportunities/data/strategicOpportunityService.tsx, add:
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:
- Import the new strategy hook:
import { use{Type}Strategy } from './{type}/use{Type}Strategy';
- Add hook call (must always be called — React rules of hooks):
const {type}Strategy = use{Type}Strategy({ form, isActive: opportunityType === OpportunityTypes.StrategicOpportunity{Type} });
- Update
activeStrategyselection:
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:
<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:
items={[
{ key: 'charge-code', label: 'Charge Code' },
{ key: 'general-ledger', label: 'General Ledger' },
{ key: '{type}', label: '{Display Name}' }
]}
And handle the click:
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:
storeTreeDataselection (around line 181):
if (is{Type}) {
return new StrategicOpportunity{Type}DetailStoreTreeData(DefaultGridItem);
}
- Detail data fetching (around line 252):
if (is{Type}) {
// Fetch type-specific detail data
}
- Grid column parsing (around line 305):
if (is{Type}) {
_gridColumns = getStrategicOpportunity{Type}DetailColumnsByString(layout.columns);
}
- Grid rendering (around line 853):
is{Type} ? (
<StrategicOpportunity{Type}DetailGrid ... />
) : isGL ? (
...
)
-
Save handler — map dirty data to goal values for the new type.
-
Edit navigation path:
const typePath = is{Type} ? '{type}' : isGL ? 'general-ledger' : 'charge-code';
- Comments drawer
opportunityTypeprop.
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
-
TPT PK collision: Always check the generated migration for duplicate PK constraint names. Rename
pk_strategic_opportunitytopk_strategic_opportunity_{type}. -
React hooks order: All strategy hooks must be called unconditionally in
StrategicOpportunity.tsx(React rules of hooks). Use theisActiveprop to no-op inactive strategies. -
MeasureType sync: Backend
MeasureTypeenum values must exactly match frontendMeasureTypeconst enum values. Double-check after adding. -
Layout compatibility: The
isLayoutCompatible()function checksrequiredColumns. Make sure your type's required columns are unique to avoid stale layouts from other types being loaded. -
Formatting: Run
npx eslint --fix <files>after editing (notnpx prettier --write).