# 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` 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 GetDetailDataAsync(long opportunityId, bool isTracking, List 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 CreateOpportunity{Type}Async(StrategicOpportunity{Type}Dto dto, CancellationToken ct); Task UpdateOpportunity{Type}(long id, StrategicOpportunity{Type}UpdateDto dto, CancellationToken ct); Task GetStrategicOpportunity{Type}(long id, CancellationToken ct); Task 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(); ``` ### 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; onFormValuesChanged(changedValues: any, context: IFormChangeContext): void; loadEditData(data: IStrategicOpportunityEditData, context: IEditLoadContext): void; loadPrePopulationData?(params: URLSearchParams, context: IPrePopContext): Promise; create(data: IStrategicOpportunityData): Promise; update(opportunityId: number, data: IStrategicOpportunityData, shouldPopulate: boolean): Promise; 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 StrategicOpportunity({ ...props, opportunityType: OpportunityTypes.StrategicOpportunity{Type} })} key='/strategic-opportunities/create/{type}' /> 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} ? ( ) : 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 ` after editing (not `npx prettier --write`).