chore: deploy initial code base #1

Merged
bermudalamb merged 1 commits from feature/initial-code-base into main 2026-05-12 10:42:39 -05:00
1829 changed files with 248331 additions and 1 deletions
+172
View File
@@ -0,0 +1,172 @@
# Strata Continuous Improvement — Claude Code Guide
## About
Healthcare finance analytics microservice owned by **The-Outliers** team. Helps hospitals identify cost-saving opportunities across encounter, charge code, general ledger, payroll, utilization variation, quality variation, network, and strategic opportunity domains.
**Tech stack:** .NET 8 (C# 12) + React 17 + TypeScript 4.9 + PostgreSQL + SQL Server + Snowflake
## Shell / repo navigation
- Claude is launched from the repo root (`C:/git/microservices/continuousimprovement`). Do not `cd` to the repo in commands.
- Avoid compound commands that combine `cd` with pipes/redirection (e.g., `cd ... && ... 2>/dev/null | head ...`) because they trigger manual-approval warnings.
- Prefer:
- `find . ...` (when already in repo root), or
- `find "C:/git/microservices/continuousimprovement" ...` (when an explicit path is needed).
- If output limiting is needed, prefer `| head -50` without `2>/dev/null`, or run redirection in a separate step.
## Commands
```bash
# Backend
dotnet restore
dotnet build
dotnet test
# Frontend (run from src/Strata.ContinuousImprovement.Web/continuousimprovement/)
npm install
npm start # Dev server
npm test # Interactive test runner
npm run test-ci # CI test runner (no watch)
```
## Coding Standards
Follow `.github/copilot-instructions.md` for naming, capitalization, project organization, API routes, DTOs, and principles. Key points:
- Booleans: `is`, `has`, `can` prefix
- Methods/properties/constants: PascalCase; variables: camelCase
- No full acronyms (`EntityIdsCsv` not `EntityIdsCSV`)
- Organize by business domain, not by type
- RESTful routes: plural nouns, kebab-case
- DTOs: no `DTO` suffix, only create when restricting/combining models
## Architecture
### Backend (C#/.NET 8)
| Project | Purpose | Path |
|---------|---------|------|
| **Api** | ASP.NET Core Web API host, controllers | `src/Strata.ContinuousImprovement.Api/` |
| **Biz** | All business logic, services, DI registration | `src/Strata.ContinuousImprovement.Biz/` |
| **Client** | HTTP client wrapper (`IContinuousImprovementService`) | `src/Strata.ContinuousImprovement.Client/` |
| **Service** | Windows/hosted service, Hangfire worker host | `src/Strata.ContinuousImprovement.Service/` |
| **Dashboard** | Separate web host (Startup + Program) | `src/Strata.ContinuousImprovement.Dashboard/` |
| **DecisionSupport.Biz** | Encounter query services, Snowflake queries | `src/Strata.DecisionSupport.Biz/` |
| **DecisionSupport.Models** | Shared encounter/Jazz/Snowflake DTOs | `src/Strata.DecisionSupport.Models/` |
### Frontend (React 17 + TypeScript)
**Root:** `src/Strata.ContinuousImprovement.Web/continuousimprovement/`
Feature modules live directly under `src/` (not a `features/` folder):
- `charge-code/`, `encounter/`, `exploration/`, `free-form/`, `general-ledger/`, `initiatives/`, `network/`, `opportunities/`, `payroll/`, `quality-variation/`, `strategic-opportunities/`, `utilization-variation/`, `configuration/`
Each module follows: page component + `/components` + `/hooks` + `/data` + `/utils`
**Shared module:** `src/shared/` — components, hooks, data services, contexts, constants
### Key Constraints
- **React 17** — do NOT use React 18 features (useId, automatic batching, etc.)
- **react-router v5** — use `Switch`/`Route`/`Redirect`, NOT v6 patterns
- **Create React App** (`react-scripts ^5.0.1`) — not Vite
## Strata Ecosystem (Proprietary — No Public Docs)
These packages are internal to Strata. No public documentation exists. Read source/types when needed:
| Package | Purpose |
|---------|---------|
| `@strata/tempo` | Design system (v6.33+). Import: `@strata/tempo/lib/<component>`. Query Tempo MCP server for docs. |
| `@strata/core` | Auth (`useOidcSession`), HTTP (`getSecureService`), URL helpers |
| `@strata/charts` | Charting library |
| `@strata/jazz` | Jazz platform integration |
| `@strata/logging` | Client-side logging |
| `@strata/navbar` | Shared navigation bar |
| `@strata/score` | Score calculations |
| `@strata/signalr` | SignalR client wrapper |
| `@strata/settings` | Settings service |
| `@strata/intl` | Internationalization |
| `Strata.CS.Jazz.*` | Backend Jazz platform libraries (DI, EF, auth, caching) |
| `Riok.Mapperly` | Compile-time object mapping source generator |
| `EasyCaching` | In-memory caching (provider: `continuousImprovement_cache`) |
## Commit Requirements
Format: `type(scope): subject` — enforced by `.commitlintrc.js`
**Types:** `build`, `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
**Scopes:** `api`, `biz`, `client`, `dashboard`, `service`, `web`, `ds-web`, `ds-models`
Rules:
- Subject max 72 chars, no period, no sentence/start/pascal/upper case
- Scope required (warning), lowercase only
- Body and footer must have leading blank line
- Footer: `Refs: JAZZ-XXXX` (Jira ticket reference)
Example:
```
feat(biz): add case type family drill-down for exploration
Refs: JAZZ-12345
```
## Testing
### Backend (NUnit + Moq)
- Unit tests: `tests/Strata.ContinuousImprovement.*.Test.Unit/`
- Integration tests: `tests/Strata.ContinuousImprovement.*.Test.Integration/`
- Jazz EF stub: `tests/Strata.ContinuousImprovement.JazzEntityFrameworkStub/`
- Pattern: `[TestFixture]`, `[Test]`, `[SetUp]`/`[OneTimeSetUp]`, `Mock<IService>`
- Run: `dotnet test`
### Frontend (Jest + Testing Library)
- Test files: `<Component>.test.tsx` co-located with components
- Custom render wrapper: `src/test-utils/testUtils.tsx` (wraps MemoryRouter + contexts)
- Pattern: `render()`, `screen.findByRole()`, `jest.spyOn(service, 'method')`
- Run: `npm test` (from `continuousimprovement/` dir) or `npm run test-ci` for CI
### E2E (Playwright)
- Located in separate test infrastructure
## Feature Flags
Backend pattern:
```csharp
// Inject IFeatureFlagWrapper, call named convenience method or generic:
await featureFlagWrapper.IsCiBenchmarkingEnabled();
await featureFlagWrapper.IsFeatureFlagOn(FeatureFlag.ChargeCodeBeta, clientId);
```
Frontend pattern:
```tsx
const { isBenchmarkingEnabled } = useFeatureFlags();
```
Centralized definitions: `Biz/Utilities/FeatureFlag.cs` (enum) + `FeatureFlagLookup.cs` (key mapping)
See `docs/FeatureFlagUsage.md` for full flag inventory.
## Key Patterns
- **Interface-based DI** — All services have `IXxxService` interface, registered in `Biz/Configurations/ContinuousImprovementServiceExtensions.cs` via `services.AddScoped<IXxxService, XxxService>()`
- **Primary constructor DI** (C# 12) — Services use `public class XxxService(IDep1 dep1, IDep2 dep2) : IXxxService`
- **Riok.Mapperly** — Compile-time mapping via `[Mapper]` attribute on static partial classes (in `DecisionSupport.Biz/Mapperly/`)
- **Embedded SQL resources** — `.sql` files as embedded resources, loaded via `Assembly.GetExecutingAssembly().ReadResource("QueryName.sql")` (see `Biz/Utilities/ResourceExtensions.cs`)
- **EasyCaching** — In-memory provider named `continuousImprovement_cache`
- **SignalR** — Typed hub `NotificationHub : Hub<INotificationHub>`, mapped at `/api/NotificationHub`
- **Multi-database** — PostgreSQL (CentralDbContext), SQL Server (JazzDbContext via async factory), Snowflake (SnowflakeDatabaseContext)
- **API versioning** — `[ApiVersion("1.0")]` + `[Route("api/v{api-version:apiVersion}/[controller]")]` + `SlugifyParameterTransformer`
- **Frontend data services** — `getSecureService()` from `@strata/core/lib` providing typed HTTP helpers, exported as plain objects implementing interfaces
- **Frontend state** — React Context pattern (`FeatureFlagContext`, `ConfigurationsContext`, `UserSettingsContext`) + custom hooks
## MCP Servers
- **Tempo** — Query `@strata/tempo` component docs, props, examples. Always check before using Tempo components.
- **Jira/Atlassian** — Fetch ticket details, search issues, manage PRs via `mcp__atlassian__*` tools.
## Reference Docs
- `.claude/docs/backend-patterns.md` — Biz module anatomy, DI, controllers, EF, SQL, testing
- `.claude/docs/frontend-patterns.md` — Feature modules, hooks, data services, Tempo, testing
- `.claude/docs/agent-workflow.md` — AI-assisted development pipeline and context management
- `.claude/docs/adversarial-prompts.md` — Review criteria templates
+183
View File
@@ -0,0 +1,183 @@
# Contributing Guidelines
Welcome, brave engineer. Before you type `git commit -m "oops"` and vanish for 3 weeks of PTO, let's align on how commits work in this repo.
We follow [**Conventional Commits**](https://www.conventionalcommits.org/) with **semantic versioning**.\
This gives us:
- Automatic changelogs (no one likes writing them manually)
- Predictable version bumps
- Clear history that future-you (or your replacement) can actually read
---
## 1. Commit Message Format
Every commit **MUST** follow this format:
```
<type>(<scope>): <description>
[optional body]
[optional BREAKING CHANGE: ...]
[optional footer(s)]
```
- \`\` → what you did
- \`\` *(optional, but strongly encouraged)* → where you did it
- \`\` → short explanation (imperative tense)
- **Body** → additional details, rationale, or context (wrap at 72 chars if possible)
- **Footer** → metadata such as references to backlog items or related issues
> ❗ There must be a blank line separating `BREAKING CHANGE:` from any footer lines.
### Examples
```
feat(auth): add token refresh endpoint
Added a new endpoint for refreshing authentication tokens to reduce
full login frequency.
Refs: JIRA-1234
```
```
fix(api): correctly parse null values
Previously, null values in payloads caused 500 errors due to improper
validation. Updated parser to handle null safely.
BREAKING CHANGE: Parser behavior has changed for null payloads.
Refs: JIRA-9876
```
---
## 2. Allowed Commit Types
| Type | When to Use |
| ---------- | ----------------------------------------- |
| `build` | Build system changes (configs, scripts) |
| `feat` | A new feature |
| `fix` | A bug fix |
| `docs` | Documentation updates |
| `style` | Non-functional style changes (formatting) |
| `refactor` | Code change that isnt a bug or feature |
| `test` | Adding/fixing tests |
| `chore` | Maintenance tasks (deps, configs) |
---
## 3. Scopes
Scopes keep commits relevant. Commonly you'll find service, or module names, used as the scope:
```
feat(api): add user endpoint
fix(web): handle expired token
```
> While unrelated areas should be split into separate commits, multiple scopes can be combined in a single commit if they are related:
`refactor(web,api): clean up regex for widget payloads`
If you're touching multiple unrelated areas, split the work.\
If youre touching the **entire repo**, you may omit the scope (e.g. `chore: update prettier config`).
---
## 4. Commit Body
Use the body to:
- Explain **why** the change was made
- Add context or reasoning (if its not obvious from the diff)
- Mention relevant technical details
> ❗ Keep the body wrapped at 72 characters where possible for better readability in CLI tools.
Example:
```
fix(auth): improve token handling
Added stricter validation to prevent expired tokens from being used
in refresh calls. This fixes intermittent 401 errors for long-lived
sessions.
```
---
## 5. Commit Footer
Use the footer for:
- `BREAKING CHANGE:` declarations (with a blank line after it)
- References to backlog items, tickets, or issues
- Co-authorship metadata if needed (`Co-authored-by:`)
Example:
```
feat(ui): add dark mode toggle
Added a dark mode toggle in the user settings page.
BREAKING CHANGE: Removed old theme switcher.
Refs: JIRA-4567
```
---
## 6. Linting & Enforcement
This repo uses:
- [**commitlint**](https://github.com/conventional-changelog/commitlint) to reject bad commit messages
- [**husky**](https://typicode.github.io/husky) to run lint checks pre-commit
After cloning the repository, run `npm install` to ensure husky hooks are installed and initialized locally. If you skip this step, GitHub Actions will run the same commit linting on your pull requests. Any bad commits that bypass local checks will result in a failed PR check.
If you try to commit `do some stuff`, the hook will fail and mock you.\
Run `npm run commit` if you want a guided prompt.
---
## 7. Pull Requests
- Keep PRs small and focused.
- Please use `--amend` commits or `--fixup` commits where appropriate
- Squash `fixup:` commits, and any commits that dont add value (e.g. `chore: typo`).
- Ensure commit messages still follow Conventional Commits after squashing.
---
## 8. Versioning
We use **semantic versioning**:
- `fix:` → Patch (`1.0.1`)
- `feat:` → Minor (`1.1.0`)
- `BREAKING CHANGE:` → Major (`2.0.0`)
Your commit messages drive the versioning calculation.
---
### TL;DR
- Use Conventional Commits.
- Include a scope if it makes sense.
- Write meaningful bodies when needed.
- Use footers for breaking changes or ticket references.
- Leave a blank line between BREAKING CHANGE and footer.
- Your branch's commit history should tell the story of what you did.
- Bad commits will be rejected by hooks, mocked by your peers, and possibly framed on Slack or in some PowerPoint presentation as "what not to do."
---
Happy committing!
+284
View File
@@ -0,0 +1,284 @@
ARG PROJECT=Strata.ContinuousImprovement
ARG DSPROJECT=Strata.DecisionSupport
ARG VERSION=0.0.0
ARG SONARURL=''
ARG SONARLOGIN=''
ARG SONARBRANCH=''
##############
# Base image #
##############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.aspnet:8.0 AS base
# Add TMZ files for CST
RUN cp /usr/share/zoneinfo/America/Chicago "/usr/share/zoneinfo/Central Standard Time"
RUN sed -i 's/^Components: main$/& contrib/' /etc/apt/sources.list.d/debian.sources \
&& apt-get -qq update --allow-releaseinfo-change \
&& apt-get -qq install -y \
libc6 \
libc6-dev \
libgdiplus \
libx11-dev \
ttf-mscorefonts-installer \
fontconfig \
&& rm -rf /var/lib/apt/lists/* \
&& ln -s /usr/lib/x86_64-linux-gnu/libdl.so.2 /usr/lib/x86_64-linux-gnu/libdl.so
###############
# Build image #
###############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.sdk:8.0 AS build
ARG PROJECT
ARG DSPROJECT
ARG VERSION
ARG SONARURL
ARG SONARLOGIN
ARG SONARBRANCH
# Add TMZ files for CST
RUN cp /usr/share/zoneinfo/America/Chicago "/usr/share/zoneinfo/Central Standard Time"
RUN sed -i 's/^Components: main$/& contrib/' /etc/apt/sources.list.d/debian.sources \
&& apt-get -qq update --allow-releaseinfo-change \
&& apt-get -qq install -y \
libc6 \
libc6-dev \
libgdiplus \
libx11-dev \
ttf-mscorefonts-installer \
fontconfig \
awscli \
jq \
&& rm -rf /var/lib/apt/lists/*\
&& ln -s /usr/lib/x86_64-linux-gnu/libdl.so.2 /usr/lib/x86_64-linux-gnu/libdl.so
WORKDIR /src
# Copy project files and restore
COPY ["${PROJECT}.sln", "${PROJECT}.sln"]
COPY src/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
COPY tests/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p tests/${file%.*}/ && mv $file tests/${file%.*}/; done
RUN dotnet restore "${PROJECT}.sln" \
--source https://api.nuget.org/v3/index.json \
--source https://proget.sdt.local/nuget/nuget/v3/index.json
# Copy files and build
COPY . .
RUN if [ "${SONARLOGIN}" != "" ] ; then dotnet sonarscanner begin \
/k:"${PROJECT}" \
/d:sonar.scm.provider=git \
/d:sonar.host.url="${SONARURL}" \
${SONARBRANCH} \
/d:sonar.login="${SONARLOGIN}" \
/d:sonar.cs.opencover.reportsPaths="/test-results/cover.xml" \
/d:sonar.cs.vstest.reportsPaths="/test-results/*.trx" \
/d:sonar.dependencyCheck.jsonReportPath=/src/buildlogs/dependency-check-report.json \
/d:sonar.dependencyCheck.htmlReportPath=/src/buildlogs/dependency-check-report.html \
/v:sonar.projectVersion="${VERSION}"; fi
RUN dotnet build "${PROJECT}.sln" \
--configuration Release \
--no-restore
# Run tests
# #############################
# # ContinuousImprovement Api #
# #############################
RUN dotnet test "tests/${PROJECT}.Api.Test.Unit/${PROJECT}.Api.Test.Unit.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/api-api-tests.trx" \
-p:CollectCoverage=true \
-p:CoverletOutput='/test-results/' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN CREDS=$(aws sts assume-role --role-arn arn:aws:iam::809820578507:role/sdt-continuous-improvement-service-role --role-session-name "DockerBuildSession"); \
export AWS_ACCESS_KEY_ID="$(echo $CREDS | jq -r '.Credentials.AccessKeyId')"; \
export AWS_SECRET_ACCESS_KEY="$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')"; \
export AWS_SESSION_TOKEN="$(echo $CREDS | jq -r '.Credentials.SessionToken')"; \
dotnet test "tests/${PROJECT}.Api.Test.Integration/${PROJECT}.Api.Test.Integration.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/integration-tests-api.trx" \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=opencover \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Api.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
# #############################
# # ContinuousImprovement Biz #
# #############################
RUN dotnet test "/src/tests/${PROJECT}.Biz.Test.Unit/${PROJECT}.Biz.Test.Unit.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/unit-tests-biz.trx" \
-p:CollectCoverage=true \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN CREDS=$(aws sts assume-role --role-arn arn:aws:iam::809820578507:role/sdt-continuous-improvement-service-role --role-session-name "DockerBuildSession"); \
export AWS_ACCESS_KEY_ID="$(echo $CREDS | jq -r '.Credentials.AccessKeyId')"; \
export AWS_SECRET_ACCESS_KEY="$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')"; \
export AWS_SESSION_TOKEN="$(echo $CREDS | jq -r '.Credentials.SessionToken')"; \
dotnet test "tests/${PROJECT}.Biz.Test.Integration/${PROJECT}.Biz.Test.Integration.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/integration-tests-biz.trx" \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=opencover \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
# ################################
# # ContinuousImprovement Client #
# ################################
RUN dotnet test "tests/${PROJECT}.Client.Test.Unit/${PROJECT}.Client.Test.Unit.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/unit-tests-client.trx" \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=opencover \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Client.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN CREDS=$(aws sts assume-role --role-arn arn:aws:iam::809820578507:role/sdt-continuous-improvement-service-role --role-session-name "DockerBuildSession"); \
export AWS_ACCESS_KEY_ID="$(echo $CREDS | jq -r '.Credentials.AccessKeyId')"; \
export AWS_SECRET_ACCESS_KEY="$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')"; \
export AWS_SESSION_TOKEN="$(echo $CREDS | jq -r '.Credentials.SessionToken')"; \
dotnet test "tests/${PROJECT}.Client.Test.Integration/${PROJECT}.Client.Test.Integration.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/integration-tests-client.trx" \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=opencover \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Client.Test.Integration]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
# ###################
# # DecisionSupport #
# ###################
RUN dotnet test "tests/${DSPROJECT}.Biz.Test.Unit/${DSPROJECT}.Biz.Test.Unit.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/unit-tests-dsbiz.trx" \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=opencover \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN CREDS=$(aws sts assume-role --role-arn arn:aws:iam::809820578507:role/sdt-continuous-improvement-service-role --role-session-name "DockerBuildSession"); \
export AWS_ACCESS_KEY_ID="$(echo $CREDS | jq -r '.Credentials.AccessKeyId')"; \
export AWS_SECRET_ACCESS_KEY="$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')"; \
export AWS_SESSION_TOKEN="$(echo $CREDS | jq -r '.Credentials.SessionToken')"; \
dotnet test "tests/${DSPROJECT}.Biz.Test.Integration/${DSPROJECT}.Biz.Test.Integration.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/integration-tests-dsbiz.trx" \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=opencover \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN mv /test-results/coverage.opencover.xml /test-results/cover.xml
# Publish project
RUN dotnet publish "src/${PROJECT}.Api/${PROJECT}.Api.csproj" \
--configuration Release \
--no-restore \
--no-build \
--output /app
# Pack client project
RUN dotnet pack "src/${PROJECT}.Client/${PROJECT}.Client.csproj" \
--configuration Release \
--no-restore \
--no-build \
--include-symbols \
--include-source \
--output /pack \
-property:PackageVersion=${VERSION}
# Pack DS Biz project
RUN dotnet pack "src/${DSPROJECT}.Biz/${DSPROJECT}.Biz.csproj" \
--configuration Release \
--no-restore \
--no-build \
--include-symbols \
--include-source \
--output /pack \
-property:PackageVersion=${VERSION}
# Pack DS models project
RUN dotnet pack "src/${DSPROJECT}.Models/${DSPROJECT}.Models.csproj" \
--configuration Release \
--no-restore \
--no-build \
--include-symbols \
--include-source \
--output /pack \
-property:PackageVersion=${VERSION}
RUN if [ "${SONARLOGIN}" != "" ] ; then \
/dc/dependency-check/bin/dependency-check.sh -f JSON -f HTML -s . -o ./buildlogs \
--suppression ./sonarsuppressions.xml \
--noupdate --nodeAuditSkipDevDependencies --disableNodeJS --disableRetireJS --ossIndexRemoteErrorWarnOnly true \
--dbDriverName "com.microsoft.sqlserver.jdbc.SQLServerDriver" \
--connectionString "jdbc:sqlserver://ddensqldevops01.sdt.local;Database=DependencyCheck;encrypt=true;trustServerCertificate=true;" \
--dbUser "dcuser" --dbPassword "${SONARLOGIN}" && \
dotnet sonarscanner end /d:sonar.login="${SONARLOGIN}"; \
fi
RUN mkdir -p /src/buildlogs && touch /src/buildlogs/dependency-check-report.html
###############
# Final image #
###############
FROM base AS final
ARG PROJECT
WORKDIR /app
ARG PROJECT
ENV PROJECTDLL=${PROJECT}.Api.dll
ENV DOTNET_ENVIRONMENT=Development
ENV ASPNETCORE_ENVIRONMENT=Development
COPY --from=build /app /app
COPY --from=build /pack /pack
COPY --from=build /test-results /test-results
COPY --from=build /src/buildlogs/dependency-check-report.html /dc/dependency-check-report.html
ENTRYPOINT dotnet ${PROJECTDLL}
+73
View File
@@ -0,0 +1,73 @@
ARG PROJECT=Strata.ContinuousImprovement
ARG VERSION=0.0.0
##############
# Base image #
##############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.aspnet:8.0 AS base
# Add TMZ files for CST
RUN cp /usr/share/zoneinfo/America/Chicago "/usr/share/zoneinfo/Central Standard Time"
RUN sed -i 's/^Components: main$/& contrib/' /etc/apt/sources.list.d/debian.sources \
&& apt-get update --allow-releaseinfo-change \
&& apt-get install -y \
libc6 \
libc6-dev \
libgdiplus \
libx11-dev \
ttf-mscorefonts-installer \
fontconfig \
&& rm -rf /var/lib/apt/lists/*\
&& ln -s /usr/lib/x86_64-linux-gnu/libdl.so.2 /usr/lib/x86_64-linux-gnu/libdl.so
###############
# Build image #
###############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.sdk:8.0 as build
ARG PROJECT
ARG VERSION
# Add TMZ files for CST
RUN cp /usr/share/zoneinfo/America/Chicago "/usr/share/zoneinfo/Central Standard Time"
WORKDIR /src
# Copy project files and restore
COPY ["${PROJECT}.sln", "${PROJECT}.sln"]
COPY src/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
COPY tests/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p tests/${file%.*}/ && mv $file tests/${file%.*}/; done
RUN dotnet restore "${PROJECT}.sln" \
--source https://api.nuget.org/v3/index.json \
--source https://proget.sdt.local/nuget/nuget/v3/index.json
# Copy files and build
COPY . .
RUN dotnet build "${PROJECT}.sln" \
--configuration Release \
--no-restore
# Publish project
RUN dotnet publish "src/${PROJECT}.Dashboard/${PROJECT}.Dashboard.csproj" \
--configuration Release \
--no-restore \
--no-build \
--output /app
###############
# Final image #
###############
FROM base AS final
ARG PROJECT
WORKDIR /app
ARG PROJECT
ENV PROJECTDLL=${PROJECT}.Dashboard.dll
ENV DOTNET_ENVIRONMENT=Development
ENV ASPNETCORE_ENVIRONMENT=Development
COPY --from=build /app /app
ENTRYPOINT dotnet ${PROJECTDLL}
+73
View File
@@ -0,0 +1,73 @@
ARG PROJECT=Strata.ContinuousImprovement
ARG VERSION=0.0.0
##############
# Base image #
##############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.aspnet:8.0 AS base
# Add TMZ files for CST
RUN cp /usr/share/zoneinfo/America/Chicago "/usr/share/zoneinfo/Central Standard Time"
RUN sed -i 's/^Components: main$/& contrib/' /etc/apt/sources.list.d/debian.sources \
&& apt-get update --allow-releaseinfo-change \
&& apt-get install -y \
libc6 \
libc6-dev \
libgdiplus \
libx11-dev \
ttf-mscorefonts-installer \
fontconfig \
&& rm -rf /var/lib/apt/lists/*\
&& ln -s /usr/lib/x86_64-linux-gnu/libdl.so.2 /usr/lib/x86_64-linux-gnu/libdl.so
###############
# Build image #
###############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.sdk:8.0 as build
ARG PROJECT
ARG VERSION
# Add TMZ files for CST
RUN cp /usr/share/zoneinfo/America/Chicago "/usr/share/zoneinfo/Central Standard Time"
WORKDIR /src
# Copy project files and restore
COPY ["${PROJECT}.sln", "${PROJECT}.sln"]
COPY src/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
COPY tests/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p tests/${file%.*}/ && mv $file tests/${file%.*}/; done
RUN dotnet restore "${PROJECT}.sln" \
--source https://api.nuget.org/v3/index.json \
--source https://proget.sdt.local/nuget/nuget/v3/index.json
# Copy files and build
COPY . .
RUN dotnet build "${PROJECT}.sln" \
--configuration Release \
--no-restore
# Publish project
RUN dotnet publish "src/${PROJECT}.Service/${PROJECT}.Service.csproj" \
--configuration Release \
--no-restore \
--no-build \
--output /app
###############
# Final image #
###############
FROM base AS final
ARG PROJECT
WORKDIR /app
ARG PROJECT
ENV PROJECTDLL=${PROJECT}.Service.dll
ENV DOTNET_ENVIRONMENT=Development
ENV ASPNETCORE_ENVIRONMENT=Development
COPY --from=build /app /app
ENTRYPOINT dotnet ${PROJECTDLL}
+199
View File
@@ -0,0 +1,199 @@
ARG PROJECT=Strata.ContinuousImprovement
ARG DSPROJECT=Strata.DecisionSupport
ARG VERSION=0.0.0
ARG SONARURL=''
ARG SONARLOGIN=''
ARG SONARBRANCH=''
##############
# Base image #
##############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.aspnet:8.0 AS base
# Add TMZ files for CST
RUN cp /usr/share/zoneinfo/America/Chicago "/usr/share/zoneinfo/Central Standard Time"
RUN sed -i 's/^Components: main$/& contrib/' /etc/apt/sources.list.d/debian.sources \
&& apt-get -qq update --allow-releaseinfo-change \
&& apt-get -qq install -y \
libc6 \
libc6-dev \
libgdiplus \
libx11-dev \
ttf-mscorefonts-installer \
fontconfig \
&& rm -rf /var/lib/apt/lists/* \
&& ln -s /usr/lib/x86_64-linux-gnu/libdl.so.2 /usr/lib/x86_64-linux-gnu/libdl.so
###############
# Build image #
###############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.sdk:8.0 AS build
ARG PROJECT
ARG DSPROJECT
ARG VERSION
ARG SONARURL
ARG SONARLOGIN
ARG SONARBRANCH
# Add TMZ files for CST
RUN cp /usr/share/zoneinfo/America/Chicago "/usr/share/zoneinfo/Central Standard Time"
RUN sed -i 's/^Components: main$/& contrib/' /etc/apt/sources.list.d/debian.sources \
&& apt-get -qq update --allow-releaseinfo-change \
&& apt-get -qq install -y \
libc6 \
libc6-dev \
libgdiplus \
libx11-dev \
ttf-mscorefonts-installer \
fontconfig \
&& rm -rf /var/lib/apt/lists/*\
&& ln -s /usr/lib/x86_64-linux-gnu/libdl.so.2 /usr/lib/x86_64-linux-gnu/libdl.so
WORKDIR /src
# Copy project files and restore
COPY ["${PROJECT}.sln", "${PROJECT}.sln"]
COPY src/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
COPY tests/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p tests/${file%.*}/ && mv $file tests/${file%.*}/; done
RUN dotnet restore "${PROJECT}.sln" \
--source https://api.nuget.org/v3/index.json \
--source https://proget.sdt.local/nuget/nuget/v3/index.json
# Copy files and build
COPY . .
RUN if [ "${SONARLOGIN}" != "" ] ; then dotnet sonarscanner begin \
/k:"${PROJECT}" \
/d:sonar.scm.provider=git \
/d:sonar.host.url="${SONARURL}" \
${SONARBRANCH} \
/d:sonar.login="${SONARLOGIN}" \
/d:sonar.cs.opencover.reportsPaths="/test-results/cover.xml" \
/d:sonar.cs.vstest.reportsPaths="/test-results/*.trx" \
/d:sonar.dependencyCheck.jsonReportPath=/src/buildlogs/dependency-check-report.json \
/d:sonar.dependencyCheck.htmlReportPath=/src/buildlogs/dependency-check-report.html \
/v:sonar.projectVersion="${VERSION}"; fi
RUN dotnet build "${PROJECT}.sln" \
--configuration Release \
--no-restore
# Run tests
RUN dotnet test "tests/${PROJECT}.Api.Test.Unit/${PROJECT}.Api.Test.Unit.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/api-api-tests.trx" \
-p:CollectCoverage=true \
-p:CoverletOutput='/test-results/' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN dotnet test "/src/tests/${PROJECT}.Biz.Test.Unit/${PROJECT}.Biz.Test.Unit.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/unit-tests-biz.trx" \
-p:CollectCoverage=true \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN dotnet test "tests/${PROJECT}.Client.Test.Unit/${PROJECT}.Client.Test.Unit.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/unit-tests-client.trx" \
-p:CollectCoverage=true \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN dotnet test "tests/${DSPROJECT}.Biz.Test.Unit/${DSPROJECT}.Biz.Test.Unit.csproj" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
--logger="trx;LogFileName=/test-results/unit-tests-dsbiz.trx" \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=opencover \
-p:CoverletOutput='/test-results/' \
-p:MergeWith='/test-results/coverage.json' \
-p:Exclude=\"[Strata.ContinuousImprovement.Biz.Test.Unit]*,[Strata.ContinuousImprovement.JazzEntityFrameworkStub]*\"
RUN mv /test-results/coverage.opencover.xml /test-results/cover.xml
# Publish project
RUN dotnet publish "src/${PROJECT}.Api/${PROJECT}.Api.csproj" \
--configuration Release \
--no-restore \
--no-build \
--output /app
# Pack client project
RUN dotnet pack "src/${PROJECT}.Client/${PROJECT}.Client.csproj" \
--configuration Release \
--no-restore \
--no-build \
--include-symbols \
--include-source \
--output /pack \
-property:PackageVersion=${VERSION}
# Pack DS Biz project
RUN dotnet pack "src/${DSPROJECT}.Biz/${DSPROJECT}.Biz.csproj" \
--configuration Release \
--no-restore \
--no-build \
--include-symbols \
--include-source \
--output /pack \
-property:PackageVersion=${VERSION}
# Pack DS models project
RUN dotnet pack "src/${DSPROJECT}.Models/${DSPROJECT}.Models.csproj" \
--configuration Release \
--no-restore \
--no-build \
--include-symbols \
--include-source \
--output /pack \
-property:PackageVersion=${VERSION}
# /* Running locally
# RUN if [ "${SONARLOGIN}" != "" ] ; then \
# /dc/dependency-check/bin/dependency-check.sh -f JSON -f HTML -s . -o ./buildlogs \
# --suppression ./sonarsuppressions.xml \
# --noupdate --nodeAuditSkipDevDependencies --disableNodeJS --disableRetireJS --ossIndexRemoteErrorWarnOnly true \
# --dbDriverName "com.microsoft.sqlserver.jdbc.SQLServerDriver" \
# --connectionString "jdbc:sqlserver://ddensqldevops01.sdt.local;Database=DependencyCheck;encrypt=true;trustServerCertificate=true;" \
# --dbUser "dcuser" --dbPassword "${SONARLOGIN}" && \
# dotnet sonarscanner end /d:sonar.login="${SONARLOGIN}"; \
# fi
# */
###############
# Final image #
###############
FROM base AS final
ARG PROJECT
WORKDIR /app
ARG PROJECT
ENV PROJECTDLL=${PROJECT}.Api.dll
ENV DOTNET_ENVIRONMENT=Development
ENV ASPNETCORE_ENVIRONMENT=Development
COPY --from=build /app /app
COPY --from=build /pack /pack
COPY --from=build /test-results /test-results
ENTRYPOINT dotnet ${PROJECTDLL}
+28
View File
@@ -0,0 +1,28 @@
ARG SONARLOGIN=''
ARG VERSION=0.0.0
FROM node:18.17.0 as build
ARG SONARLOGIN
ARG VERSION
# Set working directory
WORKDIR /app
# Install dependencies
RUN npm config set @strata:registry http://proget.sdt.local/npm/npm
RUN npm config set strict-ssl false
COPY src/Strata.ContinuousImprovement.Web/continuousimprovement/package*.json ./
RUN npm install --legacy-peer-deps
RUN npm ci --legacy-peer-deps
# Copy in rest of app and build
COPY src/Strata.ContinuousImprovement.Web/continuousimprovement/ .
RUN npm run build
# Run tests
# RUN npm run test-ci
# Build release container
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY src/Strata.ContinuousImprovement.Web/nginx.conf /etc/nginx/conf.d/default.conf
+54 -1
View File
@@ -1,2 +1,55 @@
# continuousimprovement
# Strata.ContinuousImprovement
[![build](https://github.com/stratadecision/continuousimprovement/actions/workflows/build.yaml/badge.svg)](https://github.com/stratadecision/continuousimprovement/actions/workflows/build.yaml)
[![End-to-end tests](https://github.com/stratadecision/continuousimprovement/actions/workflows/e2e-testrunner.yml/badge.svg)](https://github.com/stratadecision/continuousimprovement/actions/workflows/e2e-testrunner.yml)
## General Information
This microserivce and react UI provide access for the Continuous Improvements Opportunities
* whenever a change is made locally, i think the continuousimprovement needs to be unistalled and reinstalled
## Credits
This microservice is developed and maintained by The-Outliers
## install continuousimprovement
dotnet new -i C:\git\strata.continuousimprovements.microservices\continuousimprovements\strata.microservice
## unistall continuousimprovement
dotnet new -u C:\git\strata.continuousimprovements.microservices\continuousimprovements\strata.microservice
# create new api project with postgres project from continuousimprovement
dotnet new strata.microservice -o C:\git\strata.foobar -n Strata.Foobar --api --biz --postgres
## Branching and Versioning
| branch | version format | example |
| ---------- | --------------------- | --------------- |
| master | #.#.# | 1.2.3 |
| feature/\* | #.#+1.0-featureName.# | 1.3.0-newfeat.1 |
| fix/\* | #.#.#+1-fixName.# | 1.2.4-bug.1 |
See our [confluence page](https://confluence.sdt.local/display/DOP/Branching+and+Versioning) for more information
## Usage
### Build and Run
```
docker-compose up -d --build
```
- https://localhost:8443/index (swagger api)
- https://localhost:8443/hangfire (hangfire dashboard)
- http://localhost:8081 (redis commander)
### Extracting nuget package
```
docker create --name throwaway stratacontinuousimprovement_continuousimprovement-api
docker cp throwaway:/pack .
docker rm throwaway
```
### Cleanup
```
docker-compose down
```
+120
View File
@@ -0,0 +1,120 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33403.182
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Api", "src\Strata.ContinuousImprovement.Api\Strata.ContinuousImprovement.Api.csproj", "{C4CD8DC4-3B88-4BA6-9F78-C641D365C2EE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Biz.Test.Unit", "tests\Strata.ContinuousImprovement.Biz.Test.Unit\Strata.ContinuousImprovement.Biz.Test.Unit.csproj", "{775E79BD-A8F5-4756-8E94-390A72018BF8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Service", "src\Strata.ContinuousImprovement.Service\Strata.ContinuousImprovement.Service.csproj", "{EB523C92-2FED-4631-A73B-17F37B742E7C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Biz", "src\Strata.ContinuousImprovement.Biz\Strata.ContinuousImprovement.Biz.csproj", "{533D0BEF-FBF2-47E2-98E7-11AE930E2E65}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Web", "src\Strata.ContinuousImprovement.Web\Strata.ContinuousImprovement.Web.csproj", "{ABCB26F8-6044-41A8-BBBE-B00E95BB585D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Dashboard", "src\Strata.ContinuousImprovement.Dashboard\Strata.ContinuousImprovement.Dashboard.csproj", "{51D04755-17C4-49CF-944B-D4347E7C550F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Client", "src\Strata.ContinuousImprovement.Client\Strata.ContinuousImprovement.Client.csproj", "{EDBE4F6B-46D0-4332-9A38-7F2C011AA490}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Api.Test.Unit", "tests\Strata.ContinuousImprovement.Api.Test.Unit\Strata.ContinuousImprovement.Api.Test.Unit.csproj", "{DA1AE588-5B04-43CA-BA0C-9DF8F737C97A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.JazzEntityFrameworkStub", "tests\Strata.ContinuousImprovement.JazzEntityFrameworkStub\Strata.ContinuousImprovement.JazzEntityFrameworkStub.csproj", "{D04952E8-BDF1-44C6-819C-A066BADE165B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.DecisionSupport.Biz", "src\Strata.DecisionSupport.Biz\Strata.DecisionSupport.Biz.csproj", "{12F6978F-C62B-4268-8B7B-72887B53C223}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.DecisionSupport.Biz.Test.Unit", "tests\Strata.DecisionSupport.Biz.Test.Unit\Strata.DecisionSupport.Biz.Test.Unit.csproj", "{EAB9B29E-CAA6-4974-A8D5-D0AAF3FF2B0C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.DecisionSupport.Models", "src\Strata.DecisionSupport.Models\Strata.DecisionSupport.Models.csproj", "{7743C7CC-91E6-4347-95EC-F18B2F173291}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.ContinuousImprovement.Client.Test.Unit", "tests\Strata.ContinuousImprovement.Client.Test.Unit\Strata.ContinuousImprovement.Client.Test.Unit.csproj", "{F4FCAF36-CDD7-4341-8546-5BF6DD8501CE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.DecisionSupport.Biz.Test.Integration", "tests\Strata.DecisionSupport.Biz.Test.Integration\Strata.DecisionSupport.Biz.Test.Integration.csproj", "{0DE4A5ED-BA94-40AD-9553-3C4312DCC86C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.ContinuousImprovement.Biz.Test.Integration", "tests\Strata.ContinuousImprovement.Biz.Test.Integration\Strata.ContinuousImprovement.Biz.Test.Integration.csproj", "{EFC6B15D-C22B-480B-9E50-9BBAA33A9030}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.ContinuousImprovement.Client.Test.Integration", "tests\Strata.ContinuousImprovement.Client.Test.Integration\Strata.ContinuousImprovement.Client.Test.Integration.csproj", "{4515A4CD-906B-4204-86A4-5DE5BCE43E1D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.ContinuousImprovement.Api.Test.Integration", "tests\Strata.ContinuousImprovement.Api.Test.Integration\Strata.ContinuousImprovement.Api.Test.Integration.csproj", "{A730D5B3-50A1-5419-8945-E1C70294B5B0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C4CD8DC4-3B88-4BA6-9F78-C641D365C2EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4CD8DC4-3B88-4BA6-9F78-C641D365C2EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4CD8DC4-3B88-4BA6-9F78-C641D365C2EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4CD8DC4-3B88-4BA6-9F78-C641D365C2EE}.Release|Any CPU.Build.0 = Release|Any CPU
{775E79BD-A8F5-4756-8E94-390A72018BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{775E79BD-A8F5-4756-8E94-390A72018BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{775E79BD-A8F5-4756-8E94-390A72018BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{775E79BD-A8F5-4756-8E94-390A72018BF8}.Release|Any CPU.Build.0 = Release|Any CPU
{EB523C92-2FED-4631-A73B-17F37B742E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB523C92-2FED-4631-A73B-17F37B742E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB523C92-2FED-4631-A73B-17F37B742E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB523C92-2FED-4631-A73B-17F37B742E7C}.Release|Any CPU.Build.0 = Release|Any CPU
{533D0BEF-FBF2-47E2-98E7-11AE930E2E65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{533D0BEF-FBF2-47E2-98E7-11AE930E2E65}.Debug|Any CPU.Build.0 = Debug|Any CPU
{533D0BEF-FBF2-47E2-98E7-11AE930E2E65}.Release|Any CPU.ActiveCfg = Release|Any CPU
{533D0BEF-FBF2-47E2-98E7-11AE930E2E65}.Release|Any CPU.Build.0 = Release|Any CPU
{ABCB26F8-6044-41A8-BBBE-B00E95BB585D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABCB26F8-6044-41A8-BBBE-B00E95BB585D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABCB26F8-6044-41A8-BBBE-B00E95BB585D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABCB26F8-6044-41A8-BBBE-B00E95BB585D}.Release|Any CPU.Build.0 = Release|Any CPU
{51D04755-17C4-49CF-944B-D4347E7C550F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51D04755-17C4-49CF-944B-D4347E7C550F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51D04755-17C4-49CF-944B-D4347E7C550F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51D04755-17C4-49CF-944B-D4347E7C550F}.Release|Any CPU.Build.0 = Release|Any CPU
{EDBE4F6B-46D0-4332-9A38-7F2C011AA490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDBE4F6B-46D0-4332-9A38-7F2C011AA490}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDBE4F6B-46D0-4332-9A38-7F2C011AA490}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDBE4F6B-46D0-4332-9A38-7F2C011AA490}.Release|Any CPU.Build.0 = Release|Any CPU
{DA1AE588-5B04-43CA-BA0C-9DF8F737C97A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA1AE588-5B04-43CA-BA0C-9DF8F737C97A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA1AE588-5B04-43CA-BA0C-9DF8F737C97A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA1AE588-5B04-43CA-BA0C-9DF8F737C97A}.Release|Any CPU.Build.0 = Release|Any CPU
{D04952E8-BDF1-44C6-819C-A066BADE165B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D04952E8-BDF1-44C6-819C-A066BADE165B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D04952E8-BDF1-44C6-819C-A066BADE165B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D04952E8-BDF1-44C6-819C-A066BADE165B}.Release|Any CPU.Build.0 = Release|Any CPU
{12F6978F-C62B-4268-8B7B-72887B53C223}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12F6978F-C62B-4268-8B7B-72887B53C223}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12F6978F-C62B-4268-8B7B-72887B53C223}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12F6978F-C62B-4268-8B7B-72887B53C223}.Release|Any CPU.Build.0 = Release|Any CPU
{EAB9B29E-CAA6-4974-A8D5-D0AAF3FF2B0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EAB9B29E-CAA6-4974-A8D5-D0AAF3FF2B0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EAB9B29E-CAA6-4974-A8D5-D0AAF3FF2B0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EAB9B29E-CAA6-4974-A8D5-D0AAF3FF2B0C}.Release|Any CPU.Build.0 = Release|Any CPU
{7743C7CC-91E6-4347-95EC-F18B2F173291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7743C7CC-91E6-4347-95EC-F18B2F173291}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7743C7CC-91E6-4347-95EC-F18B2F173291}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7743C7CC-91E6-4347-95EC-F18B2F173291}.Release|Any CPU.Build.0 = Release|Any CPU
{F4FCAF36-CDD7-4341-8546-5BF6DD8501CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4FCAF36-CDD7-4341-8546-5BF6DD8501CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4FCAF36-CDD7-4341-8546-5BF6DD8501CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4FCAF36-CDD7-4341-8546-5BF6DD8501CE}.Release|Any CPU.Build.0 = Release|Any CPU
{0DE4A5ED-BA94-40AD-9553-3C4312DCC86C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0DE4A5ED-BA94-40AD-9553-3C4312DCC86C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DE4A5ED-BA94-40AD-9553-3C4312DCC86C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DE4A5ED-BA94-40AD-9553-3C4312DCC86C}.Release|Any CPU.Build.0 = Release|Any CPU
{EFC6B15D-C22B-480B-9E50-9BBAA33A9030}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EFC6B15D-C22B-480B-9E50-9BBAA33A9030}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EFC6B15D-C22B-480B-9E50-9BBAA33A9030}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EFC6B15D-C22B-480B-9E50-9BBAA33A9030}.Release|Any CPU.Build.0 = Release|Any CPU
{4515A4CD-906B-4204-86A4-5DE5BCE43E1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4515A4CD-906B-4204-86A4-5DE5BCE43E1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4515A4CD-906B-4204-86A4-5DE5BCE43E1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4515A4CD-906B-4204-86A4-5DE5BCE43E1D}.Release|Any CPU.Build.0 = Release|Any CPU
{A730D5B3-50A1-5419-8945-E1C70294B5B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A730D5B3-50A1-5419-8945-E1C70294B5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A730D5B3-50A1-5419-8945-E1C70294B5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A730D5B3-50A1-5419-8945-E1C70294B5B0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F6CD834D-51B1-4B10-9DE1-DFE5A16D4177}
EndGlobalSection
EndGlobal
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="Strata.ContinuousImprovement.Biz" ModuleVersionMask="*" ClassMask="Microsoft.Extensions.DependencyInjection.*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String></wpf:ResourceDictionary>
+347
View File
@@ -0,0 +1,347 @@
# Feature Flag Usage Documentation - IFeatureFlagServiceClient.IsEnabledAsync
This document provides a comprehensive overview of all feature flags used throughout the Continuous Improvement codebase via the `IFeatureFlagServiceClient.IsEnabledAsync` method.
## Overview
The application uses feature flags to control functionality on a per-client basis in this multi-tenant system. Feature flags are checked using the `IFeatureFlagServiceClient` service and are typically scoped to a specific database GUID.
### Feature Flag Index
| # | Feature Flag | Description | Section |
|---|--------------|-------------|---------|
| 1 | `ciflexibleexplorationenabled` | Controls access to flexible exploration features for non-SDT employees | [Section 1](#1-ciflexibleexplorationenabled) |
| 2 | `enableencounteropportunityintempo` | Controls whether encounter opportunities are enabled in the Tempo interface | [Section 2](#2-enableencounteropportunityintempo) |
| 3 | `ciflexibleexplorationpage3enabled` | Controls access to the flexible exploration information page (third page) | [Section 3](#3-ciflexibleexplorationpage3enabled) |
| 4 | `enablepayrollopportunityintempo` | Controls whether payroll opportunities are enabled in the Tempo interface | [Section 4](#4-enablepayrollopportunityintempo) |
| 5 | `chargecodebeta` | Controls access to charge code beta features | [Section 5](#5-chargecodebeta) |
| 6 | `cibenchmarkingenabled` | Controls benchmarking features in continuous improvement | [Section 6](#6-cibenchmarkingenabled) |
| 7 | `ciopportunityworkbooks` | Controls opportunity workbook features | [Section 7](#7-ciopportunityworkbooks) |
| 8 | `networkopportunitiesenabled` | Controls network opportunity features | [Section 8](#8-networkopportunitiesenabled) |
| 9 | `strategicopportunityforchargecode` | Controls strategic opportunity features specifically for charge codes | [Section 9](#9-strategicopportunityforchargecode) |
| 10 | `ciexplorationpopulationmanagementlaunchesstrategicopportunitywizard` | Controls if the Add Opportunity button on the Exploration Population page launches the Strategic Opportunity Wizard |
## Feature Flag Keys and Usage
### 1. "ciflexibleexplorationenabled"
**Purpose**: Controls access to flexible exploration features for non-SDT employees
**Usage Locations**:
- `AllOpportunitiesController.cs` (line 40)
- `ExplorationOpportunitiesController.cs` (line 45)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> SomeAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationenabled"))
{
// Code for flexible exploration features
}
else
{
// Fallback or default behavior
}
}
```
**Context**: Enables flexible exploration functionality. SDT employees always have access, while client users need this feature flag enabled.
---
### 2. "enableencounteropportunityintempo"
**Purpose**: Controls whether encounter opportunities are enabled in the Tempo interface
**Usage Locations**:
- `EncounterOpportunitiesController.cs` (lines 90, 175)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> TempoAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("enableencounteropportunityintempo"))
{
// Code for encounter opportunities in Tempo
}
else
{
// Fallback to Jazz interface or show message
}
}
```
**Context**: Feature flag to enable encounter opportunity functionality in the modern Tempo interface versus legacy Jazz interface.
---
### 3. "ciflexibleexplorationpage3enabled"
**Purpose**: Controls access to the flexible exploration information page (third page)
**Usage Locations**:
- `ExplorationController.cs` (line 45)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> ExplorationPageAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationpage3enabled"))
{
// Code for the third page of flexible exploration
}
else
{
// Redirect or show less data
}
}
```
**Context**: Enables the third page of flexible exploration functionality for enhanced data exploration capabilities.
---
### 4. "enablepayrollopportunityintempo"
**Purpose**: Controls whether payroll opportunities are enabled in the Tempo interface
**Usage Locations**:
- `PayrollOpportunitiesController.cs` (line 85)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> PayrollTempoAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("enablepayrollopportunityintempo"))
{
// Code for payroll opportunities in Tempo
}
else
{
// Fallback to other methods or show message
}
}
```
**Context**: Feature flag to enable payroll opportunity functionality in the modern Tempo interface.
---
### 5. "chargecodebeta"
**Purpose**: Controls access to charge code beta features
**Usage Locations**:
- `ChargeCodeOpportunitiesController.cs` (line 243)
**Implementation Pattern**:
```csharp
// C# example
public async Task<IActionResult> ChargeCodeBetaAction()
{
if (await _featureFlagServiceClient.IsEnabledAsync("chargecodebeta"))
{
// Code for charge code beta features
}
else
{
// Hide or disable beta features
}
}
```
**Context**: Beta feature flag for charge code opportunities functionality, controlling access to new charge code features.
---
### 6. "cibenchmarkingenabled"
**Purpose**: Controls benchmarking features in continuous improvement
**Usage Locations**:
- Frontend TypeScript components
- Configuration and peer group functionality
**Implementation Pattern**:
```typescript
// TypeScript example
if (featureFlagService.isEnabled("cibenchmarkingenabled")) {
// Code for benchmarking features
} else {
// Alternative code path
}
```
**Context**: Enables benchmarking functionality in the CI application, allowing comparison against peer groups.
---
### 7. "ciopportunityworkbooks"
**Purpose**: Controls opportunity workbook features
**Usage Locations**:
- Referenced in TypeScript interface definitions
- Workbook management components
**Context**: Feature flag for workbook functionality related to opportunities.
---
### 8. "networkopportunitiesenabled"
**Purpose**: Controls network opportunity features
**Usage Locations**:
- Frontend navigation and components
- Network opportunity controllers
**Implementation Pattern**:
```typescript
// TypeScript example
if (featureFlagService.isEnabled("networkopportunitiesenabled")) {
// Code for network opportunity features
} else {
// Code for standard opportunities
}
```
**Context**: Enables network opportunity functionality for multi-client opportunity management.
---
### 9. "strategicopportunityforchargecode"
**Purpose**: Controls strategic opportunity features specifically for charge codes
**Usage Locations**:
- Frontend configuration components
- Strategic opportunity management
**Implementation Pattern**:
```typescript
// TypeScript example
if (featureFlagService.isEnabled("strategicopportunityforchargecode")) {
// Code for strategic opportunities for charge codes
} else {
// Fallback code
}
```
**Context**: Enables strategic opportunity functionality specifically for charge code-based opportunities.
---
### 10. "ciexplorationpopulationmanagementlaunchesstrategicopportunitywizard"
**Purpose**: Controls strategic opportunity features specifically for charge codes
**Usage Locations**:
- Frontend configuration components
- Strategic opportunity management
**Implementation Pattern**:
```typescript
// TypeScript example
if (featureFlagService.isEnabled("ciexplorationpopulationmanagementlaunchesstrategicopportunitywizard")) {
// Code to launch strategic opportunities wizard
} else {
// Fallback code
}
```
**Context**: Enables the Add Opportunity button on the Exploration Population page to launch the Strategic Opportunity wizard.
## Key Controllers Using Feature Flags
| Controller | Feature Flags Used | Purpose |
|------------|-------------------|---------|
| `AllOpportunitiesController` | `ciflexibleexplorationenabled` | Flexible exploration access control |
| `ExplorationOpportunitiesController` | `ciflexibleexplorationenabled` | Exploration features |
| `ExplorationController` | `ciflexibleexplorationpage3enabled` | Advanced exploration pages |
| `EncounterOpportunitiesController` | `enableencounteropportunityintempo` | Tempo interface integration |
| `PayrollOpportunitiesController` | `enablepayrollopportunityintempo` | Payroll Tempo integration |
| `ChargeCodeOpportunitiesController` | `chargecodebeta` | Beta charge code features |
| `SettingsController` | All CI area flags | Global feature management |
## Frontend Integration
The frontend TypeScript code integrates with feature flags through:
- **Navigation Control**: Menu items visibility based on feature flags
- **Component Rendering**: Conditional rendering of UI components
- **Service Availability**: API service availability checks
- **User Interface**: Feature-specific UI elements and workflows
### Example Frontend Usage
```typescript
// TypeScript example
async function loadOpportunities() {
if (await featureFlagService.isEnabled("ciflexibleexplorationenabled")) {
// Load flexible exploration opportunities
} else {
// Load standard opportunities
}
}
```
## Database Scope
All feature flags are scoped to specific database GUIDs, enabling per-client feature control in the multi-tenant architecture. This allows different clients to have different features enabled based on their subscription level or beta participation.
## Testing Considerations
When testing feature flag functionality:
1. **Mock the `IFeatureFlagServiceClient`** in unit tests
2. **Test both enabled and disabled states** for each feature
3. **Verify SDT employee bypass logic** where applicable
4. **Test database GUID scoping** for multi-tenant scenarios
### Example Test Mock
```csharp
// C# example using Moq
var featureFlagMock = new Mock<IFeatureFlagServiceClient>();
featureFlagMock.Setup(ff => ff.IsEnabledAsync("ciflexibleexplorationenabled"))
.ReturnsAsync(true);
// Use featureFlagMock.Object in tests
```
## Service Registration
Feature flags are registered in the dependency injection container via the `IFeatureFlagServiceClient` service, typically configured in the `Startup.cs` or `Program.cs` file for the application.
## Logging and Monitoring
Many feature flag checks include structured logging to track usage patterns:
```csharp
// C# example
if (await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationenabled"))
{
_logger.LogInformation("Flexible exploration feature enabled");
}
else
{
_logger.LogInformation("Flexible exploration feature disabled");
}
```
## Maintenance Notes
- All feature flag keys should be documented when added
- Consider feature flag lifecycle and removal strategy for permanent features
- Monitor feature flag usage through logging for analysis
- Ensure consistent naming conventions for new feature flags
- Regularly review and clean up obsolete feature flags
- Update this documentation when new feature flags are added
## Feature Flag Lifecycle
1. **Development**: New feature flags are added for experimental or beta features
2. **Testing**: Features are tested with flags enabled/disabled
3. **Rollout**: Gradual enabling of features for specific clients
4. **Stabilization**: Features become stable and widely adopted
5. **Cleanup**: Feature flags are removed once features are permanent
---
*Last Updated: November 2025*
*Document Version: 1.0*
*Total Feature Flags Documented: 9*
+602
View File
@@ -0,0 +1,602 @@
# Add Opportunity Type Playbook
Single source of truth for adding a new strategic opportunity type. Derived from the Charge Code (CC) and General Ledger (GL) implementations.
## Quick-Scan Checklist
Use this to track progress. Each item references a detailed section below.
### Backend
- [ ] [Entity](#1-entity) — `StrategicOpportunity{Type}.cs` (TPT child table)
- [ ] [Enums](#2-enums) — `OpportunityTypes` value + `MeasureType` values
- [ ] [DTOs](#3-dtos) — Create DTO + Update DTO
- [ ] [Detail Service](#4-detail-service) — `IStrategic{Type}OpportunityDetailService` + implementation
- [ ] [Query Layer](#5-query-layer) — SQL files, ServiceQuery, QueryBuilder
- [ ] [Distribution Process](#6-distribution-process) — `DistributionProcess{Type}.cs` + SQL builder
- [ ] [Dimension Config](#7-dimension-config) — `{Type}DimensionsConfig` in `StrategicOpportunityService`
- [ ] [Service Methods](#8-service-methods) — CRUD on `IStrategicOpportunityService`
- [ ] [Controller Endpoints](#9-controller-endpoints) — REST endpoints
- [ ] [DI Registration](#10-di-registration) — `ContinuousImprovementServiceExtensions.cs`
- [ ] [Migration](#11-database-migration) — EF Core migration for child table
### Frontend
- [ ] [Data Model](#f1-data-model) — `IStrategicOpportunity{Type}Data.ts`
- [ ] [Measure Types](#f2-measure-types) — enum values + display names in `MeasureType.ts`
- [ ] [Strategy Hook](#f3-strategy-hook) — `use{Type}Strategy.ts` implementing `IOpportunityTypeStrategy`
- [ ] [Define Component](#f4-define-component) — `{Type}Define.tsx`
- [ ] [Detail Store](#f5-detail-store) — `StrategicOpportunity{Type}DetailStoreTreeData.ts`
- [ ] [Detail Grid](#f6-detail-grid) — `StrategicOpportunity{Type}DetailGrid.tsx`
- [ ] [Layout Config](#f7-layout-config) — entry in `IStrategicOpportunityLayout.ts`
- [ ] [Dimension Enums](#f8-dimension-enums) — fields/sections/columns in `StrategicOpportunityDetailDimension.ts`
- [ ] [Service Methods](#f9-service-methods) — HTTP calls in `strategicOpportunityService.tsx`
- [ ] [Wizard Integration](#f10-wizard-integration) — hook call + strategy switch in `StrategicOpportunity.tsx`
- [ ] [Routes](#f11-routes) — create/edit routes in `Navigation.tsx`
- [ ] [List Page](#f12-list-page) — ButtonMenu option in `StrategicItems.tsx`
### Cross-Cutting
- [ ] [OpportunityTypes enum](#x1-opportunitytypes-enum) — both backend + frontend must match
- [ ] [Detail Page Integration](#x2-detail-page-integration) — `StrategicOpportunityDetailPage.tsx` branches
- [ ] [Tests](#x3-testing)
---
## Backend Steps
### 1. Entity
Create `Biz/StrategicOpportunities/StrategicOpportunity{Type}.cs` inheriting from `StrategicOpportunity` (TPT pattern).
**Pattern:**
```csharp
using System.ComponentModel.DataAnnotations.Schema;
namespace Strata.ContinuousImprovement.Biz.StrategicOpportunities
{
[Table("strategic_opportunity_{type}")]
public class StrategicOpportunity{Type} : StrategicOpportunity
{
// Type-specific properties
public string SomeHPath { get; set; }
public StrategicOpportunity{Type}() { }
// Copy constructor (used by CopyStrategicOpportunity)
public StrategicOpportunity{Type}(StrategicOpportunity{Type} opportunity, string opportunityName)
{
// Copy all base properties
Name = opportunityName;
OpportunityType = opportunity.OpportunityType;
OpportunityFiltersJSON = opportunity.OpportunityFiltersJSON;
Rollup1Id = opportunity.Rollup1Id;
Rollup2Id = opportunity.Rollup2Id;
Rollup3Id = opportunity.Rollup3Id;
BaselineType = opportunity.BaselineType;
BaselineDistribution = opportunity.BaselineDistribution;
BaselineStartDate = opportunity.BaselineStartDate;
BaselineEndDate = opportunity.BaselineEndDate;
EstimatedTrackingDuration = opportunity.EstimatedTrackingDuration;
StrataId = opportunity.StrataId;
LayoutJSON = opportunity.LayoutJSON;
TeamMembers = opportunity.TeamMembers;
OwnerGuid = opportunity.OwnerGuid;
ExecutiveSponsorGuid = opportunity.ExecutiveSponsorGuid;
CostLeaderGuid = opportunity.CostLeaderGuid;
// Copy type-specific properties
SomeHPath = opportunity.SomeHPath;
}
}
}
```
**DbContext:** Add `DbSet<StrategicOpportunity{Type}>` to `CentralDbContext` and configure in `OnModelCreating`.
**References:**
- `Biz/StrategicOpportunities/StrategicOpportunityChargeCode.cs`
- `Biz/StrategicOpportunities/StrategicOpportunityGL.cs`
- Base: `Biz/StrategicOpportunities/StrategicOpportunity.cs`
### 2. Enums
**OpportunityTypes** (`Biz/Generics/OpportunityTypes.cs` or wherever defined):
Add a new enum value. Current values:
- `StrategicOpportunityChargeCode` (value in `IAllOpportunity.ts` frontend)
- `StrategicOpportunityGL`
**MeasureType** (`Biz/StrategicOpportunities/Enums/MeasureType.cs`):
Add new measure type values after the last GL value (currently 37 = `GLMarginPerAdjDischarge`).
```csharp
// {Type} measure types (38+)
{Type}Measure1 = 38,
{Type}Measure2,
// ...
```
Also add a `{TYPE}_MEASURE_TYPES` constant list in `MeasureTypeConstants`.
**Other enums:** If the type has a concept like GL's `GLAccountType`, create a new enum file in `Biz/StrategicOpportunities/Enums/`.
### 3. DTOs
Create two DTOs in `Biz/StrategicOpportunities/Dtos/`:
**Create DTO**`StrategicOpportunity{Type}Dto.cs`:
```csharp
public class StrategicOpportunity{Type}Dto : StrategicOpportunityDto
{
// Type-specific properties matching the entity
public string SomeHPath { get; set; }
}
```
**Update DTO**`StrategicOpportunity{Type}UpdateDto.cs`:
```csharp
public class StrategicOpportunity{Type}UpdateDto
{
public StrategicOpportunity{Type}Dto Opportunity { get; set; }
public bool ShouldPopulateOpportunityDetails { get; set; }
}
```
**References:**
- `Dtos/StrategicOpportunityGLDto.cs` / `StrategicOpportunityGLUpdateDto.cs`
- `Dtos/StrategicOpportunityChargeCodeDto.cs` / `StrategicOpportunityChargeCodeUpdateDto.cs`
### 4. Detail Service
Create interface + implementation for populating detail data.
**Interface**`IStrategic{Type}OpportunityDetailService.cs`:
```csharp
public interface IStrategic{Type}OpportunityDetailService
{
Task PopulateOpportunityDetailsAsync(StrategicOpportunity{Type} opportunity, CancellationToken ct);
Task<StrategicOpportunityDetailData> GetDetailDataAsync(long opportunityId, bool isTracking, List<int> dimensions, CancellationToken ct);
}
```
**Implementation**`Strategic{Type}OpportunityDetailService.cs`:
Uses primary constructor DI, queries Snowflake/SQL for dimension data, populates `StrategicOpportunityDetail` rows.
**References:**
- `IStrategicChargeCodeOpportunityDetailService.cs` + `StrategicChargeCodeOpportunityDetailService.cs`
- `IStrategicGLOpportunityDetailService.cs` + `StrategicGLOpportunityDetailService.cs`
### 5. Query Layer
**SQL files** (embedded resources in `Biz/StrategicOpportunities/Queries/` or similar):
- `StrategicOpportunity{Type}DetailCTE.sql` — main detail data query
- Dimension-specific queries as needed
**ServiceQuery**`StrategicOpportunity{Type}ServiceQuery.cs`:
Loads SQL from embedded resources, provides query text to the detail service.
**QueryBuilder**`StrategicOpportunity{Type}DetailQueryBuilder.cs`:
Implements `IStrategicOpportunityDetailQueryBuilder`, builds parameterized queries for dimensions.
**References:**
- `Queries/StrategicOpportunityGLDetailQueryBuilder.cs`
- `Queries/StrategicOpportunityGLServiceQuery.cs`
- `StrategicOpportunityServiceQuery.cs` (CC version)
### 6. Distribution Process
Create in `Biz/StrategicOpportunities/DistributionProcess/{Type}/`:
- `DistributionProcess{Type}.cs` — orchestrates data distribution
- `DistributionProcess{Type}SqlBuilder.cs` — builds SQL for distribution
- `Distribution{Type}Summary.cs` — summary model
Update `DistributionProcessFactory.cs` to handle the new type.
**References:**
- `DistributionProcess/ChargeCode/DistributionProcessChargeCode.cs`
- `DistributionProcess/GL/DistributionProcessGL.cs`
- `DistributionProcess/DistributionProcessFactory.cs`
### 7. Dimension Config
In `StrategicOpportunityService.cs`, add a `{Type}DimensionsConfig` that maps dimension IDs to dimension names/hierarchy paths for the new type.
### 8. Service Methods
Add to `IStrategicOpportunityService.cs`:
```csharp
Task<long> CreateOpportunity{Type}Async(StrategicOpportunity{Type}Dto dto, CancellationToken ct);
Task<long> UpdateOpportunity{Type}(long id, StrategicOpportunity{Type}UpdateDto dto, CancellationToken ct);
Task<StrategicOpportunityEditData> GetStrategicOpportunity{Type}(long id, CancellationToken ct);
Task<bool> Delete{Type}OpportunityAsync(long id, CancellationToken ct);
```
Implement in `StrategicOpportunityService.cs`.
### 9. Controller Endpoints
Add endpoints to the strategic opportunities controller (or create a new controller if separation is needed):
```
POST /api/v1.0/strategic-opportunities/{type} — Create
PUT /api/v1.0/strategic-opportunities/{type}/{id} — Update
GET /api/v1.0/strategic-opportunities/{id}/{type} — Get for edit
DELETE /api/v1.0/strategic-opportunities/{type}/{id} — Delete
POST /api/v1.0/strategic-opportunities/{id}/{type}-detail — Get detail data
PUT /api/v1.0/strategic-opportunities/{id}/{type}-details — Update goal values
```
### 10. DI Registration
In `Biz/Configurations/ContinuousImprovementServiceExtensions.cs`:
```csharp
services.AddScoped<IStrategic{Type}OpportunityDetailService, Strategic{Type}OpportunityDetailService>();
```
### 11. Database Migration
```bash
dotnet ef migrations add AddStrategicOpportunity{Type}Table -p src/Strata.ContinuousImprovement.Biz -s src/Strata.ContinuousImprovement.Api
```
**TPT gotcha:** EF Core generates the child table PK with the same name as the parent (`pk_strategic_opportunity`). You must manually rename it in the migration file:
```csharp
// Change this:
name: "pk_strategic_opportunity",
// To this:
name: "pk_strategic_opportunity_{type}",
```
---
## Frontend Steps
All paths relative to `src/Strata.ContinuousImprovement.Web/continuousimprovement/src/`.
### F1. Data Model
Create `strategic-opportunities/data/IStrategicOpportunity{Type}Data.ts`:
```typescript
import { IStrategicOpportunityData, defaultStrategicOpportunityData } from './IStrategicOpportunityData';
export interface IStrategicOpportunity{Type}Data extends IStrategicOpportunityData {
// Type-specific properties
someHPath: string;
}
export const defaultStrategicOpportunity{Type}Data: IStrategicOpportunity{Type}Data = {
...defaultStrategicOpportunityData,
someHPath: ''
};
```
**References:**
- `data/IStrategicOpportunityChargeCodeData.ts`
- `data/IStrategicOpportunityGLData.ts`
### F2. Measure Types
In `strategic-opportunities/data/MeasureType.ts`:
1. Add enum values (must match backend `MeasureType.cs`):
```typescript
// {Type} measure types (sync with backend, values N-M)
{Type}Measure1 = N,
{Type}Measure2,
```
2. Add display names array:
```typescript
export const {type}MeasureTypeData: IKeyNameItem[] = [
{ key: MeasureType.{Type}Measure1, name: 'Display Name 1' },
// ...
];
```
3. Add entries to `mapMeasureTypeToDesiredResult`.
4. Add helper like `is{Type}PerMeasureType()` if the type has "per unit" style measures.
### F3. Strategy Hook
Create `strategic-opportunities/wizard/{type}/use{Type}Strategy.ts`:
Must implement `IOpportunityTypeStrategy` from `wizard/types/IOpportunityTypeStrategy.ts`:
```typescript
export interface IOpportunityTypeStrategy {
getDefaultData(): IStrategicOpportunityData;
renderDefineStep(context: IDefineStepContext): React.ReactElement | null;
renderModals(context: IModalContext): React.ReactElement | null;
validateDefineStep(formValues: any): string | true;
marshalDefineStep(formValues: any, context: IMarshalContext): Partial<IStrategicOpportunityData>;
onFormValuesChanged(changedValues: any, context: IFormChangeContext): void;
loadEditData(data: IStrategicOpportunityEditData, context: IEditLoadContext): void;
loadPrePopulationData?(params: URLSearchParams, context: IPrePopContext): Promise<void>;
create(data: IStrategicOpportunityData): Promise<number>;
update(opportunityId: number, data: IStrategicOpportunityData, shouldPopulate: boolean): Promise<boolean>;
shouldRepopulateOnEdit(context: IRepopulateContext): boolean;
getMeasureTypeData(): IKeyNameItem[];
}
```
**Key responsibilities:**
- `getDefaultData()` — return `defaultStrategicOpportunity{Type}Data` with correct `opportunityType`
- `renderDefineStep()` — render the type-specific Define component via `React.createElement`
- `validateDefineStep()` — return error string or `true`
- `marshalDefineStep()` — convert form values to data model fields
- `create()/update()` — call type-specific service methods
- `getMeasureTypeData()` — return the type-specific measure list
**Props pattern:**
```typescript
export interface I{Type}StrategyProps {
form: FormInstance;
isActive: boolean;
}
```
**References:**
- `wizard/charge-code/useChargeCodeStrategy.ts`
- `wizard/general-ledger/useGLStrategy.ts`
### F4. Define Component
Create `strategic-opportunities/wizard/{type}/{Type}Define.tsx`:
Type-specific Step 2 content — pickers, filters, measure type selection, desired result.
**References:**
- `wizard/charge-code/ChargeCodeDefine.tsx`
- `wizard/general-ledger/GeneralLedgerDefine.tsx`
### F5. Detail Store
Create `strategic-opportunities/data/store/StrategicOpportunity{Type}DetailStoreTreeData.ts`:
Extends `StoreTreeData` with the type-specific grid item interface. Handles:
- Loading data into tree structure
- Goal editing (onChange)
- Row config by dimensions
**References:**
- `data/store/StrategicOpportunityDetailStoreTreeData.ts` (CC)
- `data/store/StrategicOpportunityGLDetailStoreTreeData.ts` (GL)
### F6. Detail Grid
Create `strategic-opportunities/detail/StrategicOpportunity{Type}DetailGrid.tsx`:
DataGrid component with type-specific columns. Receives `root`, `columns`, `levelsCount`, `loading`, `expandedKeys`, `onChange`, `isEditable`.
**References:**
- `detail/StrategicOpportunityDetailGrid.tsx` (CC)
- `detail/StrategicOpportunityGLDetailGrid.tsx` (GL)
### F7. Layout Config
In `strategic-opportunities/detail/data/IStrategicOpportunityLayout.ts`:
1. Add default layout:
```typescript
export const defaultStrategicOpportunity{Type}Layout: IStrategicOpportunityLayout = {
rows: [strategicOpportunityDetailDimensionTotal, ...default{Type}Dimensions],
columns: [...default{Type}Columns]
};
```
2. Add layout config:
```typescript
export const {type}OpportunityLayoutConfig: IDetailLayoutConfig = {
defaultLayout: defaultStrategicOpportunity{Type}Layout,
rowLayoutList: {type}RowLayoutList,
columnLayoutList: {type}ColumnLayoutList,
requiredRows: [StrategicOpportunityDetailDimension.{TypeDimension}],
requiredColumns: [{Type}DetailField.{RequiredColumn}]
};
```
3. Update factory function:
```typescript
export function getDetailLayoutConfig(...): IDetailLayoutConfig {
if (opportunityType === OpportunityTypes.StrategicOpportunity{Type}) {
return {type}OpportunityLayoutConfig;
}
// ... existing cases
}
```
### F8. Dimension Enums
In `strategic-opportunities/detail/data/StrategicOpportunityDetailDimension.ts`:
1. Add dimension values to `StrategicOpportunityDetailDimension` enum (if new dimensions needed)
2. Add dimension ID to `StrategicOpportunityDetailDimensionId` enum
3. Create detail field enum:
```typescript
export enum StrategicOpportunity{Type}DetailField {
// Type-specific grid columns
}
```
4. Create column name/header maps
5. Create field section enum + section-to-field mappings
6. Add helper functions for parsing columns/dimensions by string
### F9. Service Methods
In `strategic-opportunities/data/strategicOpportunityService.tsx`, add:
```typescript
createOpportunity{Type}: async (opportunity) => httpPost(`${url}/{type}`, opportunity),
updateOpportunity{Type}: async (id, opportunity, shouldPopulate) =>
httpPut(`${url}/{type}/${id}`, { opportunity, shouldPopulateOpportunityDetails: shouldPopulate }),
getOpportunity{Type}: async (id) => httpGet(`${url}/${id}/GetOpportunity{Type}`),
getOpportunity{Type}DetailData: async (id, isTracking, dimensions) =>
httpPost(`${url}/${id}/${isTracking ? '{type}-tracking-detail' : '{type}-detail'}`, dimensions),
updateOpportunity{Type}DetailsData: async (id, data) =>
httpPut(`${url}/${id}/{type}-details`, data),
```
Also update `IStrategicOpportunityService.ts` interface with the new methods.
### F10. Wizard Integration
In `strategic-opportunities/wizard/StrategicOpportunity.tsx`:
1. Import the new strategy hook:
```typescript
import { use{Type}Strategy } from './{type}/use{Type}Strategy';
```
2. Add hook call (must always be called — React rules of hooks):
```typescript
const {type}Strategy = use{Type}Strategy({ form, isActive: opportunityType === OpportunityTypes.StrategicOpportunity{Type} });
```
3. Update `activeStrategy` selection:
```typescript
const is{Type} = opportunityType === OpportunityTypes.StrategicOpportunity{Type};
// All hooks always called
const ccStrategy = useChargeCodeStrategy({ form, isActive: !isGL && !is{Type} });
const glStrategy = useGLStrategy({ form, isActive: isGL });
const {type}Strategy = use{Type}Strategy({ form, isActive: is{Type} });
const activeStrategy = is{Type} ? {type}Strategy : isGL ? glStrategy : ccStrategy;
```
### F11. Routes
In `shared/Navigation.tsx`, add create and edit routes:
```tsx
<Route
path={['/strategic-opportunities/create/{type}']}
exact
component={(props) => StrategicOpportunity({ ...props, opportunityType: OpportunityTypes.StrategicOpportunity{Type} })}
key='/strategic-opportunities/create/{type}'
/>
<Route
path={['/strategic-opportunities/edit/{type}/:id']}
exact
component={(props) => StrategicOpportunity({ ...props, opportunityType: OpportunityTypes.StrategicOpportunity{Type} })}
key='/strategic-opportunities/edit/{type}'
/>
```
### F12. List Page
In `strategic-opportunities/opportunities/StrategicItems.tsx`, add to the ButtonMenu items:
```typescript
items={[
{ key: 'charge-code', label: 'Charge Code' },
{ key: 'general-ledger', label: 'General Ledger' },
{ key: '{type}', label: '{Display Name}' }
]}
```
And handle the click:
```typescript
onClick={(e) => {
// ... existing cases
else if (e.key === '{type}') {
history.push('/strategic-opportunities/create/{type}');
}
}}
```
---
## Cross-Cutting
### X1. OpportunityTypes Enum
Both backend and frontend must have matching values:
**Backend**`Biz/Generics/OpportunityTypes.cs` (or wherever `OpportunityTypes` is defined)
**Frontend**`opportunities/opportunities/data/IAllOpportunity.ts`
Current values:
```
StrategicOpportunityChargeCode = 17 (approx)
StrategicOpportunityGL = 18 (approx)
```
New type should be the next sequential value.
### X2. Detail Page Integration
In `strategic-opportunities/detail/StrategicOpportunityDetailPage.tsx`, add branches for:
1. `storeTreeData` selection (around line 181):
```typescript
if (is{Type}) {
return new StrategicOpportunity{Type}DetailStoreTreeData(DefaultGridItem);
}
```
2. Detail data fetching (around line 252):
```typescript
if (is{Type}) {
// Fetch type-specific detail data
}
```
3. Grid column parsing (around line 305):
```typescript
if (is{Type}) {
_gridColumns = getStrategicOpportunity{Type}DetailColumnsByString(layout.columns);
}
```
4. Grid rendering (around line 853):
```typescript
is{Type} ? (
<StrategicOpportunity{Type}DetailGrid ... />
) : isGL ? (
...
)
```
5. Save handler — map dirty data to goal values for the new type.
6. Edit navigation path:
```typescript
const typePath = is{Type} ? '{type}' : isGL ? 'general-ledger' : 'charge-code';
```
7. Comments drawer `opportunityType` prop.
### X3. Testing
**Backend:**
- Unit tests for the detail service
- Unit tests for the distribution process
- Unit tests for new service methods
**Frontend:**
- Unit test for the strategy hook (validate, marshal, create/update)
- Unit test for the Define component
- Unit test for the detail grid component
---
## File Reference Table
| Pattern | CC Reference | GL Reference |
|---------|-------------|--------------|
| Entity | `StrategicOpportunityChargeCode.cs` | `StrategicOpportunityGL.cs` |
| Create DTO | `StrategicOpportunityChargeCodeDto.cs` | `StrategicOpportunityGLDto.cs` |
| Update DTO | `StrategicOpportunityChargeCodeUpdateDto.cs` | `StrategicOpportunityGLUpdateDto.cs` |
| Detail Service | `StrategicChargeCodeOpportunityDetailService.cs` | `StrategicGLOpportunityDetailService.cs` |
| Distribution | `DistributionProcess/ChargeCode/` | `DistributionProcess/GL/` |
| Query Builder | `StrategicOpportunityDetailQueryBuilder.cs` | `StrategicOpportunityGLDetailQueryBuilder.cs` |
| Service Query | `StrategicOpportunityServiceQuery.cs` | `StrategicOpportunityGLServiceQuery.cs` |
| Data Model | `IStrategicOpportunityChargeCodeData.ts` | `IStrategicOpportunityGLData.ts` |
| Strategy Hook | `useChargeCodeStrategy.ts` | `useGLStrategy.ts` |
| Define Component | `ChargeCodeDefine.tsx` | `GeneralLedgerDefine.tsx` |
| Store | `StrategicOpportunityDetailStoreTreeData.ts` | `StrategicOpportunityGLDetailStoreTreeData.ts` |
| Detail Grid | `StrategicOpportunityDetailGrid.tsx` | `StrategicOpportunityGLDetailGrid.tsx` |
## Tips and Gotchas
1. **TPT PK collision**: Always check the generated migration for duplicate PK constraint names. Rename `pk_strategic_opportunity` to `pk_strategic_opportunity_{type}`.
2. **React hooks order**: All strategy hooks must be called unconditionally in `StrategicOpportunity.tsx` (React rules of hooks). Use the `isActive` prop to no-op inactive strategies.
3. **MeasureType sync**: Backend `MeasureType` enum values must exactly match frontend `MeasureType` const enum values. Double-check after adding.
4. **Layout compatibility**: The `isLayoutCompatible()` function checks `requiredColumns`. Make sure your type's required columns are unique to avoid stale layouts from other types being loaded.
5. **Formatting**: Run `npx eslint --fix <files>` after editing (not `npx prettier --write`).
@@ -0,0 +1,404 @@
# Strategic Opportunity Wizard Refactor — Migration Guide
## Who This Is For
If you wrote or maintain `StrategicOpportunity.tsx` (the wizard orchestrator) or any of its step components, this document explains what's changing, why, and how your code maps to the new structure.
---
## Why Are We Doing This?
The wizard was built for **Charge Code** opportunities only. Now we need to add **General Ledger (GL)** opportunities that use the same 5-step wizard flow but have completely different "Define Opportunity" logic (departments + GL accounts instead of encounter groups + charge codes).
The current orchestrator (`StrategicOpportunity.tsx`, 670 lines) has charge-code-specific state, validation, data marshaling, and service calls baked directly in. If we added GL logic the same way, the file would balloon with `if (opportunityType === 'GL')` branches everywhere.
Instead, we're extracting type-specific logic into **strategy hooks** — one for Charge Code, one for GL — and keeping the orchestrator focused on what's shared: wizard navigation, form management, and the steps that are identical for both types.
---
## The Big Picture: Before vs After
### Before (today)
```
StrategicOpportunity.tsx (670 lines)
├── Wizard navigation + form setup
├── 12+ CC-specific state variables (chargeCodeMembers, selectedChargeCodes, etc.)
├── CC-specific functions (loadChargeCodePickerModels, etc.)
├── CC-specific validation in handleChange (step 1)
├── CC-specific data marshaling in updateOpportunity (step 1)
├── CC-specific edit loading in loadEditData
├── CC-specific pre-population from exploration
├── CC-specific create/update service calls
└── CC-specific repopulation detection on edit
```
Everything lives in one file. There's no separation between "wizard infrastructure" and "charge code logic."
### After (refactored)
```
StrategicOpportunity.tsx (~300 lines) — Generic orchestrator
├── Wizard navigation + form setup (unchanged)
├── Shared state only (primaryMeasures, filters, rollups, workbooks, baseline)
├── Calls activeStrategy.validateDefineStep() instead of inline CC validation
├── Calls activeStrategy.marshalDefineStep() instead of inline CC marshaling
├── Calls activeStrategy.loadEditData() for type-specific edit fields
├── Calls activeStrategy.create() / activeStrategy.update()
└── Renders activeStrategy.renderDefineStep() for step 1
charge-code/useChargeCodeStrategy.ts (~250 lines) — All the CC logic extracted
├── CC state (chargeCodeMembers, selectedChargeCodes, etc.)
├── CC functions (loadChargeCodePickerModels, etc.)
├── CC step 1 validation + marshaling
├── CC edit loading + pre-population
└── CC create/update service calls
general-ledger/useGLStrategy.ts — New GL logic
├── GL state (accountType, departments, GL accounts)
├── GL step 1 validation + marshaling
├── GL edit loading
└── GL create/update service calls
```
---
## What Stays the Same
These things are **not changing** (or changing only trivially):
| Component | What's happening |
|-----------|-----------------|
| **StrategicOpportunityDetail.tsx** (Step 0) | Shared. No change except minor display support for GL type. |
| **StrategicOpportunityDefineTimeframe.tsx** (Step 3) | Shared. No change. |
| **StrategicOpportunityExternalLinks.tsx** (Step 4) | Shared. No change. |
| **StrategicOpportunityRenameMeasure.tsx** | Shared utility. No change. |
| **StrategicOpportunityFilters.tsx** | Shared utility. No change. |
| **All data/ files** | No changes to interfaces, services, enums. |
| **Wizard step validation for steps 0, 2, 3, 4** | Stays in the orchestrator — these steps are identical for both types. |
---
## What's Moving
### Files that move (same code, new location)
| Old Location | New Location | Why |
|-------------|-------------|-----|
| `wizard/StrategicOpportunityDefine.tsx` | `wizard/charge-code/ChargeCodeDefine.tsx` | This component is 100% charge-code-specific. Renaming makes that explicit. |
| `wizard/ChargeCodeCustomPickerModal.tsx` | `wizard/charge-code/ChargeCodeCustomPickerModal.tsx` | Only used by charge code define step. |
These are **moves + renames**, not rewrites. The component logic inside is the same.
### State that moves from orchestrator → CC strategy hook
All of these `useState` calls currently in `StrategicOpportunity.tsx` will move into `useChargeCodeStrategy.ts`:
```typescript
// These move OUT of the orchestrator:
const [chargeCodeMembers, setChargeCodeMembers] = useState(...);
const [selectedChargeCodes, setSelectedChargeCodes] = useState(...);
const [selectedChargeCodesInitial, setSelectedChargeCodesInitial] = useState(...);
const [expPopulationMembers, setExpPopulationMembers] = useState(...);
const [selectedExpPopulations, setSelectedExpPopulations] = useState(...);
const [patientPopulationsInitial, setPatientPopulationsInitial] = useState(...);
const [expPopulationsInitial, setExpPopulationsInitial] = useState(...);
const [expPopCostDriver, setExpPopCostDriver] = useState(...);
const [uploadModalVisible, setUploadModalVisible] = useState(...);
const [isPrePopulated, setIsPrePopulated] = useState(...);
```
```typescript
// These STAY in the orchestrator (shared by both types):
const [stepNumber, setStepNumber] = useState(0);
const [opportunity, setOpportunity] = useState(...); // now typed as IStrategicOpportunityData (base)
const [primaryMeasures, setPrimaryMeasures] = useState(...);
const [workbooks, setWorkbooks] = useState(...);
const [opportunityFilters, setOpportunityFilters] = useState(...);
const [baselinePeriodInitial, setBaselinePeriodInitial] = useState(...);
const [rollupColumns, setRollupColumns] = useState(...);
const [isEditMode, setIsEditMode] = useState(false);
const [loading, setLoading] = useState(false);
```
### Functions that move from orchestrator → CC strategy hook
| Function | Why it's CC-specific |
|----------|---------------------|
| `loadChargeCodePickerModels()` | Fetches charge code picker data |
| `loadExpPopulationPickerModels()` | Fetches exploration populations |
| `setPrimaryMeasureChargeCodes()` | Sets CC-specific form field |
| `setExplorationPopulations()` | Sets CC-specific encounter group |
| `handleChargeCodesUploaded()` | Handles CC upload modal result |
| Pre-population `useEffect` (lines 584-642) | Only applies to CC from exploration |
| CC-specific parts of `loadEditData` | Loading encounter groups, charge codes |
| CC-specific parts of `handleFormValuesChanged` | CC form field change handling |
| Step 1 block in `handleChange` | CC-specific validation checks |
| Step 1 block in `updateOpportunity` | CC-specific data marshaling |
| `handleSubmit` service calls | `createOpportunityChargeCode` / `updateOpportunityChargeCode` |
| Repopulation detection logic | Comparing CC-specific initial vs current state |
---
## The Strategy Interface
Both hooks implement this common interface so the orchestrator doesn't need to know which type it's working with:
```typescript
interface IOpportunityTypeStrategy {
// Default data for creating a new opportunity of this type
getDefaultData(): IStrategicOpportunityData;
// Renders the "Define Opportunity" step (step 1) — CC shows encounter groups + charge codes, GL shows departments + accounts
renderDefineStep(context): JSX.Element;
// Renders any modals this type needs (CC has upload modal, GL has none)
renderModals(context): JSX.Element | null;
// Returns true if step 1 form values are valid for this type
validateDefineStep(formValues): boolean;
// Converts form values into opportunity data fields for step 1
marshalDefineStep(formValues, context): Partial<IStrategicOpportunityData>;
// Handles form value changes that need type-specific reactions
onFormValuesChanged(changedValues, context): void;
// Loads type-specific fields when editing an existing opportunity
loadEditData(data, form, context): void;
// (CC only) Pre-populates from exploration population URL params
loadPrePopulationData?(params, form, context): Promise<void>;
// Calls the correct create API
create(data): Promise<number>;
// Calls the correct update API
update(opportunityId, data, shouldPopulate): Promise<boolean>;
// Determines if editing has changed enough to require goal recalculation
shouldRepopulateOnEdit(context): boolean;
// Returns the measure type list for the "Additional Measures" step dropdown
getMeasureTypeData(): { key: number; name: string }[];
}
```
---
## How the Orchestrator Changes
### Before: inline CC logic
```typescript
// In handleChange, step 1 validation:
} else if (currentStep === 1) {
if (!detailValues.encounterGroup || !detailValues.encounterGroups || ...) {
return;
}
}
```
### After: delegates to strategy
```typescript
} else if (currentStep === 1) {
if (!activeStrategy.validateDefineStep(detailValues)) {
return;
}
}
```
### Before: inline CC rendering
```typescript
<Wizard.Step key='1' title='Define Opportunity'>
<StrategicOpportunityDefine
encounterGroup={opportunity.encounterGroup}
chargeCodeMembers={chargeCodeMembers}
selectedChargeCodes={selectedChargeCodes}
// ... 14 props
/>
</Wizard.Step>
```
### After: strategy renders its own step
```typescript
<Wizard.Step key='1' title='Define Opportunity'>
{activeStrategy.renderDefineStep({ form, filters, updateFilters, ... })}
</Wizard.Step>
```
### Before: inline CC service call
```typescript
const opportunityId = await strategicOpportunityService.createOpportunityChargeCode(data);
```
### After: strategy handles service call
```typescript
const opportunityId = await activeStrategy.create(data);
```
---
## Hook Selection: Both Hooks Always Called
React requires hooks to be called unconditionally (same hooks, same order, every render). So the orchestrator calls **both** hooks every time:
```typescript
const ccStrategy = useChargeCodeStrategy({ form, ... });
const glStrategy = useGLStrategy({ form, ... });
const activeStrategy = opportunityType === OpportunityTypes.StrategicOpportunityGL
? glStrategy
: ccStrategy;
```
The inactive hook holds trivial default state — a few empty arrays and `false` booleans. No API calls, no heavy computation. This is a standard React pattern.
---
## New Folder Structure
```
strategic-opportunities/
wizard/
StrategicOpportunity.tsx ← Slimmed-down orchestrator
StrategicOpportunityDetail.tsx ← Step 0 (unchanged)
StrategicOpportunityDefineAdditional.tsx ← Step 2 (adds measureTypeData prop)
StrategicOpportunityDefineTimeframe.tsx ← Step 3 (unchanged)
StrategicOpportunityExternalLinks.tsx ← Step 4 (unchanged)
StrategicOpportunityRenameMeasure.tsx ← Shared utility (unchanged)
StrategicOpportunityFilters.tsx ← Shared utility (unchanged)
types/
IOpportunityTypeStrategy.ts ← Strategy interface + context types
charge-code/
useChargeCodeStrategy.ts ← CC strategy hook (extracted from orchestrator)
ChargeCodeDefine.tsx ← Moved from StrategicOpportunityDefine.tsx
ChargeCodeCustomPickerModal.tsx ← Moved from wizard root
general-ledger/
useGLStrategy.ts ← GL strategy hook (new)
GeneralLedgerDefine.tsx ← GL define step (new)
```
---
## StrategicOpportunityDefineAdditional Change
This component currently hardcodes the charge code measure type list:
```typescript
// Line 18 — imports CC-only measure types
import { getDesiredResultByMeasureType, measureTypeData } from '../data/MeasureType';
// Line 31 — hardcoded to CC measures
const secondaryMeasureItems = useMemo(() => measureTypeData.map(...), []);
```
After the refactor, it receives `measureTypeData` as a **prop** from the orchestrator, which gets it from `activeStrategy.getMeasureTypeData()`. This way CC shows CC measures and GL shows GL measures in the "Add Additional Measure" dropdown.
```typescript
// Before
<StrategicOpportunityDefineAdditional
getMeasureItem={getMeasureItem}
updateFiltersForSecondaryMeasure={updateFiltersForSecondaryMeasure}
validateAndUpdateMeasureName={validateAndUpdateMeasureName}
/>
// After — adds measureTypeData prop
<StrategicOpportunityDefineAdditional
getMeasureItem={getMeasureItem}
updateFiltersForSecondaryMeasure={updateFiltersForSecondaryMeasure}
validateAndUpdateMeasureName={validateAndUpdateMeasureName}
measureTypeData={activeStrategy.getMeasureTypeData()}
/>
```
---
## New Routes
Two new routes added to `Navigation.tsx`:
| Route | Purpose |
|-------|---------|
| `/strategic-opportunities/create/general-ledger` | Create a new GL strategic opportunity |
| `/strategic-opportunities/edit/general-ledger/:strategicOpportunityId` | Edit an existing GL strategic opportunity |
These follow the exact same pattern as the existing CC routes (lines 226-236 in Navigation.tsx).
---
## What the GL Define Step Looks Like
Instead of Encounter Groups + Charge Codes + Upload, the GL define step has:
1. **Account Type** — Radio: Revenue / Expense / Margin
2. **Department** — ScoreMemberPicker for GL departments
3. **GL Accounts** — Conditional pickers based on account type:
- Revenue → Revenue account picker
- Expense → Expense account picker
- Margin → Both revenue + expense pickers
- Optional statistic account picker (checkbox toggle)
4. **Filters** — Same shared StrategicFilterBar component
5. **Primary Measure** — Dropdown from `glMeasureTypeData` (instead of `measureTypeData`)
6. **Desired Result + Threshold** — Same UI as CC
---
## How to Verify Nothing Broke
After the refactor, test these CC flows — they should behave **identically** to today:
1. **Create CC opportunity**: Navigate to `/strategic-opportunities/create/charge-code`, complete all 5 steps, verify it saves
2. **Edit CC opportunity**: Open an existing CC strategic opportunity, click Edit, verify all fields load correctly, make a change, save
3. **Pre-populate from exploration**: From an exploration population, click "Create Strategic Opportunity" — verify encounter group, charge codes, and baseline are pre-filled
4. **Goal reset warning**: Edit an opportunity, change the filters or baseline period, click Save — verify the "resets goals" confirmation modal appears
5. **Run frontend tests**: `cd src/Strata.ContinuousImprovement.Web/continuousimprovement && npm run test-ci`
---
## Mapping Cheat Sheet: "Where Did My Code Go?"
| If you're looking for... | It's now in... |
|--------------------------|---------------|
| `chargeCodeMembers` state | `useChargeCodeStrategy.ts` |
| `selectedChargeCodes` state | `useChargeCodeStrategy.ts` |
| `loadChargeCodePickerModels()` | `useChargeCodeStrategy.ts` |
| `loadExpPopulationPickerModels()` | `useChargeCodeStrategy.ts` |
| `handleChargeCodesUploaded()` | `useChargeCodeStrategy.ts` |
| Pre-population `useEffect` | `useChargeCodeStrategy.ts``loadPrePopulationData()` |
| `StrategicOpportunityDefine` component | `charge-code/ChargeCodeDefine.tsx` (same code, new name) |
| `ChargeCodeCustomPickerModal` | `charge-code/ChargeCodeCustomPickerModal.tsx` (moved) |
| Step 1 validation in `handleChange` | `useChargeCodeStrategy.ts``validateDefineStep()` |
| Step 1 marshaling in `updateOpportunity` | `useChargeCodeStrategy.ts``marshalDefineStep()` |
| `createOpportunityChargeCode` call | `useChargeCodeStrategy.ts``create()` |
| `updateOpportunityChargeCode` call | `useChargeCodeStrategy.ts``update()` |
| Repopulation check (`shouldPopulateOpportunityDetails`) | `useChargeCodeStrategy.ts``shouldRepopulateOnEdit()` |
| Wizard navigation (`handleChange`, `setStepNumber`) | `StrategicOpportunity.tsx` (stayed) |
| Form setup (`Form.useForm()`) | `StrategicOpportunity.tsx` (stayed) |
| Shared state (primaryMeasures, filters, rollups) | `StrategicOpportunity.tsx` (stayed) |
| Steps 0, 2, 3, 4 validation | `StrategicOpportunity.tsx` (stayed) |
| `handleCancel`, `handleSubmit` skeleton | `StrategicOpportunity.tsx` (stayed) |
---
## FAQ
**Q: Why not just use `if/else` for GL vs CC in the existing orchestrator?**
A: The orchestrator would grow to 1000+ lines with interleaved branching. Strategy hooks keep each type's logic cohesive and independently testable.
**Q: Why hooks instead of classes?**
A: React state (`useState`, `useEffect`) only works inside hooks or components. A class-based strategy would need workarounds to manage React state.
**Q: Does the inactive strategy hook waste memory?**
A: Negligible. It holds a few empty arrays and `false` booleans. No API calls fire for the inactive type.
**Q: Will this break existing CC tests?**
A: No. The CC behavior is identical — logic is just relocated. Import paths in tests may need updating if they reference moved files.
**Q: Can I still debug the CC flow the same way?**
A: Yes. Set breakpoints in `useChargeCodeStrategy.ts` for CC-specific logic, or in `StrategicOpportunity.tsx` for wizard navigation. The call chain is just one level deeper for type-specific operations.
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="defaultPushSource" value="https://proget.ops.stratanetwork.net/nuget/nuget" />
</config>
<packageSources>
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
<add key="Strata package source" value="https://proget.ops.stratanetwork.net/nuget/nuget" />
</packageSources>
</configuration>
+1717
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"scripts": {
"commit": "commit",
"prepare": "husky",
"generate-swagger": "node scripts/fetch-swagger.js"
},
"devDependencies": {
"@commitlint/cli": "^19.2.1",
"@commitlint/prompt-cli": "^19.2.1",
"@commitlint/config-conventional": "^19.1.0",
"husky": "^9.0.11"
}
}
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env node
/**
* Generates swagger.json from the API DLL using the Swashbuckle CLI.
* Does NOT require the API to be running.
*
* Prerequisites:
* dotnet tool install --global Swashbuckle.AspNetCore.Cli --version 6.8.1
* dotnet build src/Strata.ContinuousImprovement.Api/
*
* Usage: npm run generate-swagger
*/
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const ROOT = path.join(__dirname, '..');
const DLL = path.join(ROOT, 'src/Strata.ContinuousImprovement.Api/bin/Debug/net8.0/Strata.ContinuousImprovement.Api.dll');
const OUTPUT = path.join(ROOT, 'src/Strata.ContinuousImprovement.Web/continuousimprovement/swagger.json');
const DOC_NAME = 'v1';
if (!fs.existsSync(DLL)) {
console.log('API DLL not found — building first...');
execSync('dotnet build src/Strata.ContinuousImprovement.Api/ --configuration Debug', {
cwd: ROOT,
stdio: 'inherit',
});
}
console.log('Generating swagger.json from API DLL...');
execSync(`swagger tofile --output "${OUTPUT}" "${DLL}" ${DOC_NAME}`, {
cwd: ROOT,
stdio: 'inherit',
});
const size = fs.statSync(OUTPUT).size;
console.log(`swagger.json written to ${OUTPUT} (${(size / 1024).toFixed(1)} KB)`);
+105
View File
@@ -0,0 +1,105 @@
<#
.SYNOPSIS
Get Octopus Deploy release information for a specific project and environments.
.DESCRIPTION
This script retrieves the latest release information from Octopus Deploy for a specified project across multiple environments
and generates a markdown report.
.PARAMETER yourApiKey
The API key for authenticating with the Octopus Deploy server (mandatory).
.PARAMETER pr_projectName
The name of the Octopus Deploy project (default is "strata.continuousimprovement").
.PARAMETER pr_path
The path to the git repository (default is the parent directory of the script).
.EXAMPLE
``` powershell
.\src\Get_Octo_Information.ps1 -yourApiKey
```
#>
[CmdletBinding()]
param (
[parameter(Mandatory=$true)] [string]$yourApiKey,
[string]$pr_projectName = "strata.continuousimprovement",
[string]$pr_path = (Join-Path $PSScriptRoot "..")
)
# Set variables
$apiKey = $yourApiKey
$octopusUrl = "https://octopus.sdt.local/"
$projectName = $pr_projectName # "strata.continuousimprovement"
#region Classes
class octoRelease {
[string]$environmentName
[string]$release
[string]$releaseNotes
# Default constructor
octoRelease() { $this.Init(@{}) }
# Convenience constructor
octoRelease([hashtable]$properties) { $this.Init($properties) }
octoRelease([string]$envName, [string]$rels, [string]$notes)
{
$this.Init(@{ environmentName = $envName; release = $rels; releaseNotes = $notes })
}
# Shared initializer method
[void] Init([hashtable]$Properties) {
foreach ($Property in $Properties.Keys) {
$this.$Property = $Properties.$Property
}
#$this.end = $this.range[-1];
}
}
#endregion
# Set headers
$headers = @{
"X-Octopus-ApiKey" = $apiKey
}
$octoReleaseList = @()
@("Dev","QA","Prod") | ForEach-Object {
$environmentName = $_
# Get project ID
$projectResponse = Invoke-RestMethod -Uri "$octopusUrl/api/projects/all" -Headers $headers
$project = $projectResponse | Where-Object { $_.Name -eq $projectName }
$projectId = $project.Id
# Get environment ID
$envResponse = Invoke-RestMethod -Uri "$octopusUrl/api/environments" -Headers $headers
$environment = $envResponse.Items | Where-Object { $_.Name -eq $environmentName }
$environmentId = $environment.Id
# Get latest deployment
$deploymentResponse = Invoke-RestMethod -Uri "$octopusUrl/api/deployments?projects=$projectId&environments=$environmentId&take=1&order=desc" -Headers $headers
$latestDeployment = $deploymentResponse.Items[0]
$releaseId = $latestDeployment.ReleaseId
# Get release details
$releaseDetails = Invoke-RestMethod -Uri "$octopusUrl/api/releases/$releaseId" -Headers $headers
# Output results
$release = $releaseDetails.Version
$releaseNotes = $releaseDetails.ReleaseNotes
$octoReleaseList += [octoRelease]::new($environmentName, $release, $releaseNotes)
#Write-Host $releaseDetails
}
$filePath = Join-Path $pr_path "/Octo_Release$((Get-Date).ToString("yyyyMMdd")).md"
$report = "# " + $projectName +" Octo Releases " + (Get-Date).ToString("yyyy-MM-dd")+ "`n## Details`n"
$report += $octoReleaseList | `
Format-Table @{name="environment"; Expression={$_.environmentName}}, @{Name="| release"; Expression={"| " + $_.release}}, @{Name="| notes"; Expression={"| " + $_.releaseNotes}} -AutoSize | `
Out-String
$report = $report -replace "--\s+--", "-- | --"
Write-Output $report.Replace("- | -", "- + -")
Set-Content -Path $filePath -Value $report
Write-Host -ForegroundColor DarkRed $filePath
+241
View File
@@ -0,0 +1,241 @@
<#
.SYNOPSIS
Get git tags and messages from multiple repositories.
.DESCRIPTION
This script retrieves git tags and their associated messages from multiple repositories and generates a markdown report.
.PARAMETER pr_patch
The patch version number to start from (default is 0).
.PARAMETER pr_minor_start
The starting minor version number (mandatory).
.PARAMETER pr_minor_end
The ending minor version number (mandatory).
.PARAMETER pr_tag
The tag string to filter by (default is "- CI -").
.PARAMETER pr_path
The path to the git repository (default is the parent directory of the script).
.PARAMETER pr_giturl
The base URL for the git repository (default is "https://github.com/stratadecision/continuousimprovement/pull/").
.PARAMETER pr_codeFreeze
The code freeze version string (optional).
.PARAMETER pr_qaRelease
The QA release version string (optional).
.PARAMETER pr_withPRLinks
Boolean indicating whether to include PR links in the report (default is false).
.EXAMPLE
``` powershell
.\src\Get_Tags_and_messages.ps1 -pr_patch 0 -pr_minor_start 587 -pr_minor_end 630 -pr_codeFreeze "0.30.4" -pr_qaRelease "0.30.4"
```
#>
[CmdletBinding()]
param (
[int]$pr_patch,
[parameter(Mandatory=$true)] [int]$pr_minor_start,
[parameter(Mandatory=$true)] [int]$pr_minor_end,
[string]$pr_tag = "- CI -",
[string]$pr_path = (Join-Path $PSScriptRoot ".."),
[string]$pr_giturl = "https://github.com/stratadecision/continuousimprovement/pull/",
[string]$pr_codeFreeze,
[string]$pr_qaRelease,
[bool]$pr_withPRLinks = $false
)
if ($pr_minor_start -eq 0) { $pr_minor_start = 587 }
if ($pr_minor_end -eq 0) { $pr_minor_end = 630 }
#region Classes
class gitGroup {
[string]$tag
[string]$path
[int]$idx
[int[]]$range
[string]$codeFreeze
[string]$qaRelease
[string]$docName
[int]$end
# Default constructor
gitGroup() { $this.Init(@{}) }
# Convenience constructor
gitGroup([hashtable]$properties) { $this.Init($properties) }
gitGroup([string]$pth, [int]$id, [int[]]$rng)
{
$this.Init(@{ path = $pth; idx = $id; range = $rng})
}
# Shared initializer method
[void] Init([hashtable]$Properties) {
foreach ($Property in $Properties.Keys) {
$this.$Property = $Properties.$Property
}
$this.end = $this.range[-1];
}
updateEnd([int]$end)
{
$this.end = $end;
}
}
class gitInfo {
[string]$Release
[string]$PR
[string]$Team
[string]$Name
[int]$Feature
[int]$Bugfix
[string]$Pipe
# Default constructor
gitInfo() { $this.Init(@{}) }
# Convenience constructor from hashtable
gitInfo([hashtable]$Properties) { $this.Init($Properties) }
gitInfo([string]$id, [string]$pr, [string]$name, [string]$team)
{
$this.Init(@{ Release = $id; PR = $pr; Name = $name; Team = $team; })
}
gitInfo([string]$vars, [int]$feature, [int]$bugfix, [bool]$withPRLink, [string]$giturl)
{
try{
if ($vars -match '(?s)^([\d.]*)\s{2,}(.*)$')
{
$vars = $vars -creplace '(?s)^([\d.]*)\s{2,}(.*)$', '$1|||$2'
}
$vars += "||||"
Write-Output $vars
$vars
$arr = $vars.Split("|")
$arr | ForEach-Object { Write-Output '{0}' -f $_ }
$arr
$prTag = $arr[1];
if (![string]::IsNullOrEmpty($prTag) -and $withPRLink) {
$prTag = "[{0}]({1}{2})" -f $arr[1], $giturl, $arr[1];
}
$this.Init(@{
Release = $arr[0];
PR = $prTag;
Team = if ($arr[2].Trim() -eq 'snowflake') { 'the-outliers' } else { $arr[2] };
Name = $arr[3];
Feature = $feature;
Bugfix = $bugfix;
Pipe = "|";
})
} catch {
$msg = "`n`nInput ERROR`n`n{0}" -f $_.Exception.Message
Write-Host -ForegroundColor Red $msg
}
}
# Shared initializer method
[void] Init([hashtable]$Properties) {
foreach ($Property in $Properties.Keys) {
if ($Properties.$Property -is [string]) {
$this.$Property = $Properties.$Property.Trim()
} else {
$this.$Property = $Properties.$Property
}
}
}
}
#endregion
$date = Get-Date
$dateTag = $date.ToString('yyyyMMdd')
$gitGroups = @(
[gitGroup]::new(@{
tag = $pr_tag # "- CI -"
path = $pr_path # "C:\Git\continuousimprovement"
idx = $pr_patch # 7
range = $pr_minor_start..$pr_minor_end # 587..630
codeFreeze = $pr_codeFreeze # "0.614.0"
qaRelease = $pr_qaRelease # "0.602.1"
docName = "PR_Report_"+$dateTag+".md"
})
)
$gitGroups | `
#Sort-Object -Property Team, Release | `
Sort-Object -Property path | `
Format-Table tag, path, range, idx, codeFreeze -Autosize
$regExp = '^([\d.]*)[^#]*#([\d]*).*\/(.*)\/(.*)$'
$currDir = (Get-Location).Path
$gitGroups | ForEach-Object {
Clear-Host
$gitGrp = $_
$_ | `
Sort-Object -Property path | `
Format-Table tag, path, range, idx, codeFreeze -Autosize
$path = $_.path
Set-Location $path
$idx = $_.idx
$range = $_.range
$codeFreeze = if ($_.codeFreeze -ne "") {
"Code Freeze: {0}\`n" -f $_.codeFreeze
} else {""};
Write-Host $codeFreeze.Replace("\`n", "`n")
$qaRelease = $_.qaRelease
$end = $_.end
$gitList = @()
$range | ForEach-Object {
$feature = $_
$idx..9 | ForEach-Object {
$bugfix = $_
$x = (git tag 0.$feature.$bugfix -n)
if ($x -ne $null -and $x -ne "")
{
$vars = $x -replace $regExp,'$1|$2|$3|$4|'
$arr = $vars.Split('|')
if ($arr.Length -lt 5)
{
$vars = $x -replace '^([\d.]*)\s*(JAZZ.*)$','$1|||$2|'
}
if ($vars -ne $null)
{
$gitList += [gitInfo]::new($vars, $feature, $bugfix, $pr_withPRLinks, $pr_giturl)
}
$end = $feature
$gitGrp.updateEnd($end)
}
}
$idx = 0
}
$myDocuments = [Environment]::GetFolderPath("MyDocuments")
$tempText = Join-Path $myDocuments $_.docName
$tempText = Join-Path "./" $_.docName
$report = "# " + $_.tag +" " + (Get-Date).ToString("yyyy-MM-dd")+ "`n## Summary`nFrom: {0} -> {1} ({2}){5}\`n{3}QA Release: {4}`n## Details" -f `
$range[0], `
$end, `
($gitList | Measure-Object -Property Release).Count, `
$codeFreeze, $qaRelease,
($pr_withPRLinks ? "\`n*PR links included*" : "")
$report += $gitList | `
Sort-Object -Property Feature, Bugfix | `
Format-Table Release, @{Name="| PR"; Expression={"| " + $_.PR}}, @{Name="| Name"; Expression={"| " + $_.Name}} -Autosize | Out-String
# Clean up the report formatting for markdown
while ($report -match '--\s+--') {
$report = $report -replace '--\s+--', '-- | --' # Replace spaces with pipe
}
Set-Content -Path $tempText -Value $report
$report = $report.Replace('-- | --','-- + --') # Replace pipe with plus
Write-Output $report
Write-Host -ForegroundColor DarkRed $tempText
}
Set-Location $currDir
@@ -0,0 +1,49 @@
using ClosedXML.Excel;
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api
{
public class ExcelContentResult : ActionResult
{
public IXLWorkbook Workbook { get; set; }
public string FileName { get; set; }
public ExcelContentResult()
{
}
public ExcelContentResult(IXLWorkbook workbook, string fileName = "Export") : base()
{
Workbook = workbook;
FileName = fileName;
}
public override void ExecuteResult(ActionContext context)
{
GetFileContentResult().ExecuteResult(context);
}
public override Task ExecuteResultAsync(ActionContext context)
{
return GetFileContentResult().ExecuteResultAsync(context);
}
protected FileContentResult GetFileContentResult()
{
// Flush the workbook to the Response.OutputStream
using (var memoryStream = new MemoryStream())
{
Workbook.SaveAs(memoryStream);
return new FileContentResult(memoryStream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = $"{FileName}_{DateTime.UtcNow:MM-dd-yyyy}.xlsx"
};
}
}
}
}
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
namespace Strata.ContinuousImprovement.Api.Attributes
{
/// <summary>
/// Used to disable model binding to prevent buffering the entire request body before entering the controller action.
/// We need this to stream the file as it is being uploaded to the server.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
}
@@ -0,0 +1,107 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.All.Opportunities;
using Strata.ContinuousImprovement.Biz.All.Opportunities.Filters;
using Strata.ContinuousImprovement.Biz.Generics;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
using Strata.FeatureFlags.Client;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class AllOpportunitiesController : OpportunityController<AllOpportunity>
{
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
private readonly ILogger<AllOpportunitiesController> _logger;
private readonly ClaimsPrincipal _user;
public AllOpportunitiesController(IAllOpportunityService opportunityService,
IClaimsPrincipalAccessor claimsPrincipalAccessor,
IFeatureFlagServiceClient featureFlagServiceClient,
ILogger<AllOpportunitiesController> logger)
: base(opportunityService)
{
_featureFlagServiceClient = featureFlagServiceClient;
_logger = logger;
_user = claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
}
[HttpPost("")]
public async Task<PagedApiResponse<AllOpportunity>> GetOpportunities(AllOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
var hasFlexibleExplorartion = _user.IsSdtEmployee()
|| (await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationenabled", databaseGuid, cancellationToken: cancellationToken));
var dict = new Dictionary<string, object> { { "isSdtEmployee", _user.IsSdtEmployee() }, { "hasFlexibleExploration", hasFlexibleExplorartion } };
_logger.LogServiceInformation("CI Flexible Exploration Enabled", dict);
loadOptions.Filters.HasFlexibleExploration = hasFlexibleExplorartion;
return await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpPost("[action]")]
public async Task<bool> IsNameValid(ValidateName name, CancellationToken cancellationToken)
{
return await ((IAllOpportunityService)OpportunityService).NameIsValid(name.ConfigurationGuid, name.OpportunityType, name.Name, name?.OpportunityGuid, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(AllOpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((IAllOpportunityService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "EN_Opportunities");
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<AllFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
var isStrataUser = _user.IsSdtEmployee();
var filterOptions = await ((IAllOpportunityService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
if (!isStrataUser)
{
filterOptions.OpportunityTypes = filterOptions.OpportunityTypes.Where(type => Int32.Parse(type.Id) != (int)OpportunityTypes.Blank);
}
return filterOptions;
}
}
[ExcludeFromCodeCoverage]
public class ValidateName
{
public Guid ConfigurationGuid { get; set; }
public int OpportunityType { get; set; }
public string Name { get; set; }
public Guid? OpportunityGuid { get; set; }
}
[ExcludeFromCodeCoverage]
public class AllOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public AllFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class AllOpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public AllFilters Filters { get; set; }
}
}
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.BackgroundClientJob;
using System.Threading;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class BackgroundClientJobController(IBackgroundClientJobService backgroundClientJobService) : ControllerBase
{
[HttpGet("{jobId}/status")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public ActionResult<string> GetJobStatus(string jobId, CancellationToken cancellationToken)
{
var status = backgroundClientJobService.GetJobStatus(jobId, cancellationToken);
if (status == "NotFound")
{
return NotFound(status);
}
return Ok(status);
}
}
}
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.ChargeCode.Initiatives;
using Strata.ContinuousImprovement.Biz.Initiatives;
using Strata.ContinuousImprovement.Biz.Mappers;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ChargeCodeInitiativesController : ControllerBase
{
private readonly IClaimsPrincipalAccessor _claimsPrincipalAccessor;
private readonly ICCInitiativeService _chargeCodeInitiativeService;
private readonly IStakeholderUser _stakeholderUser;
public ChargeCodeInitiativesController(
IClaimsPrincipalAccessor claimsPrincipalAccessor,
ICCInitiativeService chargeCodeInitiativeService,
IStakeholderUser stakeholderUser)
{
_claimsPrincipalAccessor = claimsPrincipalAccessor;
_chargeCodeInitiativeService = chargeCodeInitiativeService;
_stakeholderUser = stakeholderUser;
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> Create(InitiativeDto initiative, CancellationToken cancellationToken)
{
try
{
var user = _claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
var newInitiative = initiative.MapToInitiative(_stakeholderUser);
var dbGuid = user.GetDatabaseGuid();
return await _chargeCodeInitiativeService.CreateChargeCodeInitiative(dbGuid, newInitiative, initiative.OpportunityGuid, cancellationToken);
}
catch
{
return Guid.Empty;
}
}
}
}
@@ -0,0 +1,264 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.All.Opportunities;
using Strata.ContinuousImprovement.Biz.ChargeCode.Initiatives;
using Strata.ContinuousImprovement.Biz.ChargeCode.Opportunities;
using Strata.ContinuousImprovement.Biz.Configuration;
using Strata.ContinuousImprovement.Biz.Dtos;
using Strata.ContinuousImprovement.Biz.Encounter.Opportunities;
using Strata.ContinuousImprovement.Biz.Generics;
using Strata.ContinuousImprovement.Biz.Initiatives;
using Strata.ContinuousImprovement.Biz.OrgDefined;
using Strata.ContinuousImprovement.Biz.OrgDefined.Queries;
using Strata.ContinuousImprovement.Biz.Shared;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
using Strata.FeatureFlags.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ChargeCodeOpportunitiesController : OpportunityController<CCOpportunityDataModel>
{
private readonly IClaimsPrincipalAccessor _claimsPrincipalAccessor;
private readonly IConfigurationService _configurationService;
private readonly ICCOpportunityService _opportunityService;
private readonly ICCInitiativeService _initiativeService;
private readonly IOpportunityMetricCohortService _opportunityMetricCohortService;
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
private readonly IAllOpportunityService _allOpportunityService;
public ChargeCodeOpportunitiesController(
IClaimsPrincipalAccessor claimsPrincipalAccessor,
IConfigurationService configurationService,
ICCOpportunityService opportunityService,
ICCInitiativeService initiativeService,
IOpportunityMetricCohortService opportunityMetricCohortService,
IFeatureFlagServiceClient featureFlagServiceClient,
IAllOpportunityService allOpportunityService) : base(opportunityService)
{
_configurationService = configurationService;
_claimsPrincipalAccessor = claimsPrincipalAccessor;
_opportunityService = opportunityService;
_initiativeService = initiativeService;
_opportunityMetricCohortService = opportunityMetricCohortService;
_featureFlagServiceClient = featureFlagServiceClient;
_allOpportunityService = allOpportunityService;
}
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> UpdateOpportunity(Guid opportunityGuid, [FromBody] CCOpportunitySaveModel opportunity, CancellationToken cancellationToken)
{
var isNameValid = await _allOpportunityService.NameIsValid(opportunity.ConfigurationGuid, (int)OpportunityTypes.ChargeCode, opportunity.Name,
opportunity?.OpportunityGuid, cancellationToken);
if (!isNameValid)
{
return BadRequest();
}
var user = _claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
var updatedOpportunity = opportunity.MapToDataModel();
var dbGuid = user?.GetDatabaseGuid() ?? Guid.Empty;
var result = await _opportunityService.UpdateAsync(dbGuid, updatedOpportunity, cancellationToken);
return Ok(result);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> ValidateChargeCodesAsync([FromBody] ValidateChargeCodeDto dto, CancellationToken cancellationToken)
{
var result = await _opportunityService.ValidateChargeCodesAsync(dto.ChargeCodes, cancellationToken);
return Ok(result);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<ChargeCodePickerModel>>> GetChargeCodeMembers([FromBody] List<string> hPaths, CancellationToken cancellationToken)
{
var result = await _opportunityService.GetChargeCodePickerModelMembers(hPaths, cancellationToken);
return Ok(result);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> ValidateOpportunityLevelsAsync([FromBody] OpportunityLevelSelectionInfo info, CancellationToken cancellationToken)
{
var result = await _opportunityService.ValidateOpportunityLevelsAsync(info.OpportunityLevel, info.OpportunityLevels, info.Configuration, cancellationToken);
return Ok(result);
}
[HttpDelete("{id}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<bool> Delete(Guid id, CancellationToken cancellationToken)
=> await OpportunityService.DeleteAsync<CCOpportunityDataModel>(id, cancellationToken);
[HttpGet("{opportunityGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<CCOpportunityDto>> GetById(Guid opportunityGuid, CancellationToken cancellationToken)
{
var opportunity = await _opportunityService.GetById(opportunityGuid, cancellationToken);
if (opportunity == null)
{
return NotFound();
}
var opportunityDto = opportunity.MapToDto();
return Ok(opportunityDto);
}
[HttpGet("{opportunityGuid}/[action]")]
public async Task<CCOpportunitySaveModel> GetEditModel(Guid opportunityGuid, CancellationToken cancellationToken)
{
var opportunity = await _opportunityService.GetById(opportunityGuid, cancellationToken);
return opportunity.MapToSaveModel();
}
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<bool> UpdateGoalTotalUnitsOfService(Guid opportunityGuid, [FromBody] GoalSetDto goalSetDto, CancellationToken cancellationToken)
{
var hasUpdatedUOS = await _opportunityService.UpdateGoalTotalUnitsOfServiceAsync(opportunityGuid, goalSetDto.Key, goalSetDto.GoalValue, cancellationToken);
if (hasUpdatedUOS)
{
return await _initiativeService.UpdateSavings(opportunityGuid, cancellationToken);
}
return false;
}
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<bool> UpdateGoalCostPerUnit(Guid opportunityGuid, [FromBody] GoalSetDto goalSetDto, CancellationToken cancellationToken)
{
var hasUpdatedCostPerUnit = await _opportunityService.UpdateGoalCostPerUnitAsync(opportunityGuid, goalSetDto.Key, goalSetDto.GoalValue, cancellationToken);
if (hasUpdatedCostPerUnit)
{
return await _initiativeService.UpdateSavings(opportunityGuid, cancellationToken);
}
return false;
}
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdateGoals(Guid opportunityGuid, [FromBody] IEnumerable<ChargeCodeOpportunityDetailChanges> changes, CancellationToken cancellationToken)
{
var existsOpportunity = await _opportunityService.Contains(opportunityGuid, cancellationToken);
if (!existsOpportunity)
{
return NotFound();
}
var hasUpdatedCostPerUnit = await _opportunityService.UpdateGoalsAsync(opportunityGuid, changes, cancellationToken);
if (hasUpdatedCostPerUnit)
{
return await _initiativeService.UpdateSavings(opportunityGuid, cancellationToken);
}
return false;
}
[HttpPost("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> Export(
[FromRoute] Guid opportunityGuid,
[FromBody] ChargeCodeItems chargeCodes, CancellationToken cancellationToken)
{
var user = _claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
var workbook = await _opportunityService.ExportAsync(opportunityGuid, chargeCodes.ChargeCodes, user.Identity?.Name, cancellationToken);
return new ExcelContentResult(workbook, "ChargeCodeTemplate");
}
[HttpGet("[action]")]
public async Task<bool> OpportunityCreateEnabled(CancellationToken cancellationToken)
{
var user = _claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
return await _featureFlagServiceClient.IsEnabledAsync("chargecodebeta", user.GetDatabaseGuid(), cancellationToken: cancellationToken);
}
public class GoalSetDto
{
public string Key { get; set; }
public double GoalValue { get; set; }
}
[HttpGet("{opportunityGuid}/[action]")]
public async Task<InitiativeInfo> GetInitiativeInfo(Guid opportunityGuid, CancellationToken cancellationToken)
{
return await _opportunityService.GetInitiativeInfoAsync(opportunityGuid, cancellationToken);
}
[HttpPost("[action]")]
public async Task<string> Refresh(RefreshData refreshData, CancellationToken cancellationToken)
{
return await _opportunityService.RefreshOpportunityTrackingData(refreshData.DatabaseGuid, refreshData.Guid, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<OpportunityMetricCohortData>>> OpportunityMetricCohortData([FromBody] OpportunityMetricCohortOptions options, CancellationToken cancellationToken)
{
var config = await _configurationService.GetConfigurationAsync(options.ConfigurationGuid, cancellationToken);
if (config == null)
{
return BadRequest();
}
var result = Enumerable.Empty<OpportunityMetricCohortData>();
result = await _opportunityMetricCohortService.GetChargeCodeOpportunityMetricCohortData(config, options.Data, options.InnerJoin, cancellationToken);
return Ok(result);
}
public class RefreshData
{
public Guid DatabaseGuid { get; set; }
public Guid Guid { get; set; }
}
public class ChargeCodeItems
{
public IEnumerable<string> ChargeCodes { get; set; }
}
public class OpportunityLevelSelectionInfo
{
public OpportunityLevel OpportunityLevel { get; set; }
public List<string> OpportunityLevels { get; set; }
public Guid Configuration { get; set; }
}
public class ValidateChargeCodeDto
{
public List<string> ChargeCodes { get; set; }
}
}
}
@@ -0,0 +1,98 @@
using Hangfire;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.ContinuousImprovement.Biz.ChargeCode.Jobs;
using Strata.ContinuousImprovement.Biz.Utilities;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ChargeCodeOpportunityDetailsController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobClient;
private readonly ICCOpportunityClientJob _ccOpportunityClientJob;
private readonly ILogger<ChargeCodeOpportunityDetailsController> _logger;
public ChargeCodeOpportunityDetailsController(IBackgroundJobClient backgroundJobClient, ICCOpportunityClientJob ccOpportunityClientJob, ILogger<ChargeCodeOpportunityDetailsController> logger)
{
_backgroundJobClient = backgroundJobClient;
_ccOpportunityClientJob = ccOpportunityClientJob;
_logger = logger;
}
[Obsolete("Replacing w/ explicitly named endpoints")]
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public void Create([FromBody] ChargeCodeDetailInfo detailInfo, CancellationToken cancellationToken)
{
_backgroundJobClient.Enqueue<CCOpportunityDetailJob>(job
=> job.PopulateOpportunityDetailsAsync(detailInfo.StrataId, string.Empty, detailInfo.OpportunityGuid, detailInfo.IsBaseline, detailInfo.IsUpdate, false, cancellationToken));
}
[HttpPost("refresh-baseline-details")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> RefreshBaselineDetails(
[FromBody] ChargeCodeOpportunityJobRequestInfo request,
CancellationToken cancellationToken)
{
try
{
var jobId = await _ccOpportunityClientJob.RefreshClientOpportunityBaselineDetails(
request.StrataId, request.OpportunityGuid, cancellationToken);
return Ok(jobId);
}
catch (InvalidOperationException ex)
{
_logger.LogServiceError(ex);
return BadRequest();
}
}
[HttpPost("refresh-tracking-details")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> RefreshTrackingDetails(
[FromBody] ChargeCodeOpportunityJobRequestInfo request,
CancellationToken cancellationToken)
{
try
{
var jobId = await _ccOpportunityClientJob.RefreshClientOpportunityTrackingDetails(
request.StrataId, request.OpportunityGuid, cancellationToken);
return Ok(jobId);
}
catch (InvalidOperationException ex)
{
_logger.LogServiceError(ex);
return BadRequest();
}
}
/// <summary>
/// Contains information required to request a charge code opportunity job.
/// </summary>
public class ChargeCodeOpportunityJobRequestInfo
{
public int StrataId { get; set; }
public Guid OpportunityGuid { get; set; }
}
public class ChargeCodeDetailInfo
{
public int StrataId { get; set; }
public Guid OpportunityGuid { get; set; }
public bool IsBaseline { get; set; }
public bool IsUpdate { get; set; }
}
}
}
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.ChargeCode.Opportunities;
using Strata.ContinuousImprovement.Biz.Encounter.Opportunities.Filters;
using Strata.ContinuousImprovement.Biz.Pagination;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ChargeCodeOppsController : OpportunityController<CCOpportunity>
{
public ChargeCodeOppsController(ICCOpportunityViewService opportunityService) : base(opportunityService)
{
}
[HttpPost("")]
public async Task<PagedApiResponse<CCOpportunity>> GetOpportunities(CCOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(CCOpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((ICCOpportunityViewService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "CC_Opportunities");
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<CCFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await ((ICCOpportunityViewService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
[ExcludeFromCodeCoverage]
public class CCOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public CCFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class CCOpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public CCFilters Filters { get; set; }
}
}
}
@@ -0,0 +1,175 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Biz.Dtos;
using Strata.ContinuousImprovement.Biz.Network;
using Strata.ContinuousImprovement.Biz.Network.Filters;
using Strata.ContinuousImprovement.Biz.Shared;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ClientNetworkOpportunitiesController : ControllerBase
{
private readonly IClientNetworkOpportunityService _clientNetworkOpportunityService;
private readonly ILogger<ClientNetworkOpportunitiesController> _logger;
private readonly ClaimsPrincipal _user;
public ClientNetworkOpportunitiesController(
IClaimsPrincipalAccessor claimsPrincipalAccessor,
IClientNetworkOpportunityService clientNetworkOpportunityService,
ILogger<ClientNetworkOpportunitiesController> logger)
{
_clientNetworkOpportunityService = clientNetworkOpportunityService;
_logger = logger;
_user = claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
public async Task<ActionResult<PagedApiResponse<NetworkOpportunitySummary>>> GetOpportunities(NetworkOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid != Guid.Empty)
{
return await _clientNetworkOpportunityService.GetOpportunitiesAsync(databaseGuid, loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
return NoContent();
}
[HttpGet("{networkOpportunityId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesDefaultResponseType]
public async Task<ActionResult<ClientNetworkOpportunityDto>> GetById(int networkOpportunityId, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid != Guid.Empty)
{
try
{
var clientNetworkOpportunity = await _clientNetworkOpportunityService.GetByIdAsync(databaseGuid, networkOpportunityId, cancellationToken);
return new ClientNetworkOpportunityDto(clientNetworkOpportunity);
}
catch (CreateWorkbookFromTemplateException ex)
{
_logger.LogServiceError(ex);
return StatusCode(500, new { message = "Error creating client workbook. Verify template data source is a published standard data source with client permissions set." });
}
}
return NoContent();
}
[HttpPut("{networkOpportunityId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Save(int networkOpportunityId, ClientNetworkOpportunityDto dto, CancellationToken cancellationToken)
{
if (networkOpportunityId != dto.NetworkOpportunityId)
{
return BadRequest();
}
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid != Guid.Empty)
{
ClientNetworkOpportunity clientOpportunity = dto.MappedtoClientOpportunity();
NetworkOpportunitySaveModel networkOpportunity = dto.MappedtoNetworkOpportunitySaveModel();
await _clientNetworkOpportunityService.ConfigureAsync(databaseGuid, networkOpportunityId, clientOpportunity, networkOpportunity, cancellationToken);
}
return NoContent();
}
[HttpDelete("{networkOpportunityId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete(int networkOpportunityId, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid != Guid.Empty)
{
await _clientNetworkOpportunityService.DeleteAsync(databaseGuid, networkOpportunityId, CancellationToken.None);
}
return NoContent();
}
[HttpPut("{networkOpportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> Publish(int networkOpportunityId, bool isPublished, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid == Guid.Empty)
{
return BadRequest();
}
var opportunity = await _clientNetworkOpportunityService.GetByIdAsync(databaseGuid, networkOpportunityId, cancellationToken);
if (opportunity is null)
{
return BadRequest();
}
return Ok(await _clientNetworkOpportunityService.PublishAsync(databaseGuid, opportunity, isPublished, cancellationToken));
}
[HttpPut("[action]/{networkOpportunityId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> UpdateInitiativeStatus(int networkOpportunityId, [FromBody] InitiativeStatuses status, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid != Guid.Empty)
{
await _clientNetworkOpportunityService.UpdateInitiativeStatusAsync(databaseGuid, networkOpportunityId, status, cancellationToken);
}
return NoContent();
}
[HttpPut("{networkOpportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> RevertWorkbookToTemplate(int networkOpportunityId, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid == Guid.Empty)
{
return BadRequest();
}
var opportunity = await _clientNetworkOpportunityService.GetByIdAsync(databaseGuid, networkOpportunityId, cancellationToken);
if (opportunity is null)
{
return BadRequest();
}
return Ok(await _clientNetworkOpportunityService.RevertWorkbookToTemplateAsync(databaseGuid, opportunity, cancellationToken));
}
}
}
@@ -0,0 +1,205 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.ContinuousImprovement.Biz.All.Opportunities;
using Strata.ContinuousImprovement.Biz.Initiatives;
using Strata.ContinuousImprovement.Biz.Network;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ClientNetworkOpportunityLinkController : Controller
{
private readonly IClientNetworkOpportunityService _clientNetworkOpportunityService;
private readonly IClientNetworkOpportunityLinkService _clientNetworkOpportunityLinkService;
private readonly IAllOpportunityService _allOpportunityService;
private readonly IInitiativeService _initiativeService;
private readonly ILogger<ClientNetworkOpportunityLinkController> _logger;
private readonly ClaimsPrincipal _user;
public ClientNetworkOpportunityLinkController(
IClaimsPrincipalAccessor claimsPrincipalAccessor,
IClientNetworkOpportunityService clientNetworkOpportunityService,
IClientNetworkOpportunityLinkService clientNetworkOpportunityLinkService,
IAllOpportunityService allOpportunityService,
IInitiativeService initiativeService,
ILogger<ClientNetworkOpportunityLinkController> logger)
{
_clientNetworkOpportunityService = clientNetworkOpportunityService;
_clientNetworkOpportunityLinkService = clientNetworkOpportunityLinkService;
_allOpportunityService = allOpportunityService;
_initiativeService = initiativeService;
_logger = logger;
_user = claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
}
[HttpPut]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<ClientNetworkOpportunityResultDto>> AddOpportunityLink([FromBody] CreateNetworkOpportunityLinkDto createNetworkOpportunityLink, CancellationToken cancellationToken)
{
if (createNetworkOpportunityLink.OpportunityGuid == Guid.Empty) { return BadRequest(); }
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid == Guid.Empty) { return BadRequest(); }
var cnoExists = await _clientNetworkOpportunityService.ContainsAsync(databaseGuid, createNetworkOpportunityLink.NetworkOpportunityId, cancellationToken);
if (!cnoExists) { return NotFound(); }
var opportunityExists = await _allOpportunityService.ContainsAsync(createNetworkOpportunityLink.OpportunityGuid, cancellationToken);
if (!opportunityExists) { return NotFound(); }
var link = await _clientNetworkOpportunityLinkService.AddOpportunityLinkAsync(databaseGuid, createNetworkOpportunityLink.NetworkOpportunityId, createNetworkOpportunityLink.OpportunityGuid, cancellationToken);
return Ok(new ClientNetworkOpportunityResultDto(link));
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ClientNetworkOpportunityResultDto>> RolloverOpportunityLink([FromBody] RolloverOpportunityDto rolloverOpportunityInfo, CancellationToken cancellationToken)
{
var databaseGuid = rolloverOpportunityInfo.DatabaseGuid ?? _user.GetDatabaseGuid();
if (databaseGuid == Guid.Empty) { return BadRequest(); }
try
{
var link = await _clientNetworkOpportunityLinkService.RolloverOpportunityLinkAsync(databaseGuid,
rolloverOpportunityInfo.SourceConfigurationGuid, rolloverOpportunityInfo.SourceOpportunityGuid,
rolloverOpportunityInfo.RolloverConfigurationGuid, rolloverOpportunityInfo.RolloverOpportunityGuid, rolloverOpportunityInfo.RolloverInitiativeGuid, cancellationToken);
return Ok(new ClientNetworkOpportunityResultDto(link));
}
catch (OpportunityNotFoundException nfe)
{
_logger.LogServiceError(nfe);
return NotFound(nfe.OpportunityGuid);
}
catch (ClientNetworkOpportunityLinkNotFoundException lnfe)
{
_logger.LogServiceError(lnfe);
return NotFound(new { lnfe.StrataId, lnfe.ConfigurationGuid, lnfe.OpportunityGuid });
}
catch (ClientNetworkOpportunityLinkExistsException linkExists)
{
_logger.LogServiceError(linkExists);
return Ok(new ClientNetworkOpportunityResultDto(linkExists.ClientNetworkOpportunityLink));
}
catch (Exception ex)
{
_logger.LogServiceError(ex);
return BadRequest();
}
}
[HttpDelete("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> DeleteOpportunityLink([FromBody] ClientNetworkOpportunityLinkDto opportunity, CancellationToken cancellationToken)
{
var databaseGuid = opportunity.DatabaseGuid ?? _user.GetDatabaseGuid();
if (databaseGuid == Guid.Empty) { return BadRequest(); }
var cnoLinkExists = await _clientNetworkOpportunityLinkService.ContainsOpportunityLinkAsync(databaseGuid, opportunity.ConfigurationGuid, opportunity.Id, cancellationToken);
if (!cnoLinkExists) { return NotFound(); }
await _clientNetworkOpportunityLinkService.DeleteOpportunityLinkAsync(databaseGuid, opportunity.ConfigurationGuid, opportunity.Id, cancellationToken);
return Ok();
}
[HttpDelete("{opportunityGuid}/{configurationGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> DeleteOpportunityLinkIfExists(Guid opportunityGuid, Guid configurationGuid, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid == Guid.Empty) { return BadRequest(); }
var cnoLinkExists = await _clientNetworkOpportunityLinkService.ContainsOpportunityLinkAsync(databaseGuid, configurationGuid, opportunityGuid, cancellationToken);
if (!cnoLinkExists) { return Ok(); }
await _clientNetworkOpportunityLinkService.DeleteOpportunityLinkAsync(databaseGuid, configurationGuid, opportunityGuid, cancellationToken);
return Ok();
}
[HttpDelete("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> DeleteInitiativeLink([FromBody] ClientNetworkOpportunityLinkDto initiative, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid == Guid.Empty) { return BadRequest(); }
var cnoLinkExists = await _clientNetworkOpportunityLinkService.ContainsInitiativeLinkAsync(databaseGuid, initiative.ConfigurationGuid, initiative.Id, cancellationToken);
if (!cnoLinkExists) { return NotFound(); }
await _clientNetworkOpportunityLinkService.DeleteInitiativeLinkAsync(databaseGuid, initiative.ConfigurationGuid, initiative.Id, cancellationToken);
return Ok();
}
[HttpDelete("{initiativeGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> DeleteInitiativeLink(Guid initiativeGuid, CancellationToken cancellationToken)
{
var databaseGuid = _user.GetDatabaseGuid();
if (databaseGuid == Guid.Empty) { return BadRequest(); }
var initiative = await _initiativeService.GetById(initiativeGuid, cancellationToken);
var cnoLinkExists = await _clientNetworkOpportunityLinkService.ContainsInitiativeLinkAsync(databaseGuid, initiative.ConfigurationGUID, initiative.InitiativeGUID, cancellationToken);
if (!cnoLinkExists) { return NotFound(); }
await _clientNetworkOpportunityLinkService.DeleteInitiativeLinkAsync(databaseGuid, initiative.ConfigurationGUID, initiative.InitiativeGUID, cancellationToken);
return Ok();
}
public class RolloverOpportunityDto
{
public Guid? DatabaseGuid { get; set; }
public Guid SourceConfigurationGuid { get; set; }
public Guid SourceOpportunityGuid { get; set; }
public Guid RolloverConfigurationGuid { get; set; }
public Guid RolloverOpportunityGuid { get; set; }
public Guid RolloverInitiativeGuid { get; set; }
}
public class ClientNetworkOpportunityLinkDto
{
public Guid? DatabaseGuid { get; set; }
public Guid ConfigurationGuid { get; set; }
public Guid Id { get; set; }
}
public class CreateNetworkOpportunityLinkDto
{
public int NetworkOpportunityId { get; set; }
public Guid OpportunityGuid { get; set; }
}
public class ClientNetworkOpportunityResultDto
{
public int NetworkOpportunityId { get; set; }
public Guid ConfigurationGuid { get; set; }
public Guid OpportunityGuid { get; set; }
public Guid InitiativeGuid { get; set; }
public string Name { get; set; }
public ClientNetworkOpportunityResultDto(ClientNetworkOpportunityLink link, NetworkOpportunity networkOpportunity = default)
{
if (link == null) return;
NetworkOpportunityId = link.ClientNetworkOpportunity.NetworkOpportunityId;
ConfigurationGuid = link.ConfigurationGuid;
OpportunityGuid = link.OpportunityGuid;
InitiativeGuid = link.InitiativeGuid;
Name = link.ClientNetworkOpportunity.NetworkOpportunity?.Name ?? networkOpportunity?.Name ?? "";
}
}
}
}
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.Analytics.Models.Utils;
using Strata.ContinuousImprovement.Biz.OpportunityTypeWorkbooks;
using Strata.ContinuousImprovement.Biz.WorkbookClient;
using Strata.DecisionSupport.Models.Jazz;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ClientOpportunityTypeWorkbooksController : ControllerBase
{
private readonly IOpportunityTypeWorkbookService _opportunityTypeWorkbookService;
public ClientOpportunityTypeWorkbooksController(IOpportunityTypeWorkbookService opportunityTypeWorkbookService)
{
_opportunityTypeWorkbookService = opportunityTypeWorkbookService;
}
[HttpGet("{opportunityType}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<OpportunityTypeWorkbookClientInfo>>> GetOpportunityTypeWorkbooks([FromRoute] int opportunityType, CancellationToken cancellationToken)
{
var oppTypeWorkbooks = await _opportunityTypeWorkbookService.GetOpportunityTypeWorkbooksAsync(opportunityType, cancellationToken);
return Ok(oppTypeWorkbooks.Select(workbook => workbook.Mapped()));
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<ActionResult<IEnumerable<OpportunityTypeWorkbook>>> CreateOpportunityTypeWorkbooks([FromBody] IEnumerable<OpportunityTypeWorkbook> opportunityTypeWorkbooks, CancellationToken cancellationToken)
{
var createdOppTypeWorkbooks = await _opportunityTypeWorkbookService.CreateOpportunityTypeWorkbooksAsync(opportunityTypeWorkbooks, cancellationToken);
return CreatedAtAction(nameof(CreateOpportunityTypeWorkbooks), createdOppTypeWorkbooks);
}
[HttpDelete("{opportunityType}/workbooks/{workbookId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete([FromRoute] int opportunityType, [FromRoute] int workbookId, CancellationToken cancellationToken)
{
if (!await _opportunityTypeWorkbookService.ContainsAsync(opportunityType, workbookId, cancellationToken))
{
return NotFound();
}
await _opportunityTypeWorkbookService.DeleteAsync(opportunityType, workbookId, cancellationToken);
return NoContent();
}
}
}
@@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.Analytics.Models.Utils;
using Strata.ContinuousImprovement.Biz.OpportunityWorkbooks;
using Strata.ContinuousImprovement.Biz.WorkbookClient;
using Strata.DecisionSupport.Models.Jazz;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ClientOpportunityWorkbooksController : ControllerBase
{
private readonly IOpportunityWorkbookService _opportunityWorkbookService;
public ClientOpportunityWorkbooksController(IOpportunityWorkbookService opportunityWorkbookService)
{
_opportunityWorkbookService = opportunityWorkbookService;
}
[HttpGet("workbooks")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<WorkbookClientInfo>>> GetWorkbooks(CancellationToken cancellationToken)
{
var workbooks = await _opportunityWorkbookService.GetAllOpportunityWorkbooksAsync(false, cancellationToken);
return Ok(workbooks.Select(workbook => workbook.Mapped()));
}
[HttpGet("{opportunityGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<OpportunityWorkbookClientInfo>>> GetOpportunityWorkbooks([FromRoute] Guid opportunityGuid, CancellationToken cancellationToken)
{
var oppWorkbooks = await _opportunityWorkbookService.GetOpportunityWorkbooksAsync(opportunityGuid, cancellationToken);
return Ok(oppWorkbooks.Select(workbook => workbook.Mapped()));
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<ActionResult<IEnumerable<OpportunityWorkbook>>> CreateOpportunityWorkbooks([FromBody] IEnumerable<OpportunityWorkbook> opportunityWorkbooks, CancellationToken cancellationToken)
{
var createdOppWorkbooks = await _opportunityWorkbookService.CreateOpportunityWorkbooksAsync(opportunityWorkbooks, cancellationToken);
return CreatedAtAction(nameof(CreateOpportunityWorkbooks), createdOppWorkbooks);
}
[HttpDelete("{opportunityGuid}/workbooks/{workbookId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete([FromRoute] Guid opportunityGuid, [FromRoute] int workbookId, CancellationToken cancellationToken)
{
if (!await _opportunityWorkbookService.ContainsAsync(opportunityGuid, workbookId, cancellationToken))
{
return NotFound();
}
await _opportunityWorkbookService.DeleteAsync(opportunityGuid, workbookId, cancellationToken);
return NoContent();
}
}
}
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.ClientWarehouses;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[Route("api/v{api-version:apiVersion}/[controller]")]
[ApiController]
public class ClientWarehousesController(IClientWarehouseService clientWarehouseService) : ControllerBase
{
[HttpGet]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<ClientWarehouse>>> GetClientWarehouses(CancellationToken cancellationToken)
{
var clientWarehouses = await clientWarehouseService.GetAll(cancellationToken);
if (clientWarehouses.Count != 0)
{
return Ok(clientWarehouses);
}
return NoContent();
}
[HttpPost]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<int>> AddClientWarehouse([FromBody] ClientWarehouse clientWarehouse, CancellationToken cancellationToken)
{
return CreatedAtAction(nameof(AddClientWarehouse), await clientWarehouseService.Add(clientWarehouse, cancellationToken));
}
[HttpDelete("{databaseGuid}")]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DeleteClientWarehouse([FromRoute] Guid databaseGuid, CancellationToken cancellationToken)
{
await clientWarehouseService.Delete(databaseGuid, cancellationToken);
return NoContent();
}
}
}
@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.OpportunityComments;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class CommentsController : ControllerBase
{
private readonly ICommentService _commentService;
private readonly IClaimsPrincipalAccessor _claimsPrincipalAccessor;
public CommentsController(ICommentService commentService,
IClaimsPrincipalAccessor claimsPrincipalAccessor)
{
_commentService = commentService;
_claimsPrincipalAccessor = claimsPrincipalAccessor;
}
[HttpPost("")]
public async Task<IEnumerable<Comment>> GetComments(CommentOptions options, CancellationToken cancellationToken)
{
var user = _claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
var databaseGuid = options.IsGlobal ? Guid.Empty : user?.GetDatabaseGuid() ?? Guid.Empty;
var opportunityType = int.Parse(options.OpportunityType);
return await _commentService.GetCommentsAsync(databaseGuid, opportunityType, options.OpportunityId, cancellationToken);
}
[HttpPost("[action]")]
public async Task<Comment> Create(NewComment comment, CancellationToken cancellationToken)
{
var user = _claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
if (user == null || string.IsNullOrEmpty(comment.Comment)) { return default; }
var databaseGuid = comment.IsGlobal ? Guid.Empty : user.GetDatabaseGuid();
var author = $"{user.GetUserLastName()}, {user.GetUserFirstName()}";
var opportunityType = int.Parse(comment.OpportunityType);
return await _commentService.AddCommentAsync(databaseGuid, opportunityType, comment.OpportunityId, comment.Comment, comment.CommentDate, author, cancellationToken);
}
public class CommentOptions
{
public bool IsGlobal { get; set; }
public string OpportunityType { get; set; }
public long OpportunityId { get; set; }
}
public class NewComment : CommentOptions
{
public string Comment { get; set; }
public DateTimeOffset? CommentDate { get; set; }
}
}
}
@@ -0,0 +1,131 @@
using Microsoft.AspNetCore.Mvc;
using MoreLinq;
using Pluralize.NET;
using Strata.ContinuousImprovement.Biz.Configuration;
using Strata.Schema.Client;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ConfigurationsController : ControllerBase
{
private readonly IConfigurationService _configurationService;
private readonly ISchemaServiceClient _schemaServiceClient;
public ConfigurationsController(IConfigurationService configurationService, ISchemaServiceClient schemaServiceClient)
{
_configurationService = configurationService;
_schemaServiceClient = schemaServiceClient;
}
[HttpGet("")]
public async Task<IEnumerable<Biz.Configuration.Configuration>> GetAllConfigurations(CancellationToken cancellationToken)
{
var configurations = await _configurationService.GetConfigurationsAsync(cancellationToken);
return configurations.OrderBy(c => c.DisplayOrder);
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<IEnumerable<Member>> GetConfigurationEntities(Guid configurationGuid, CancellationToken cancellationToken)
{
var configuration = await _configurationService.GetConfigurationAsync(configurationGuid, cancellationToken);
var globalId = configuration.IsUsingCustomEntityDimension ? configuration.DepartmentRollupGlobalId : "Entity";
var members = await _schemaServiceClient.GetDimensionMembersByGlobalIdAsync<Member>(globalId);
var configuredEntitiesLookup = new HashSet<int>(configuration.ConfiguredEntityIds);
var configuredMembers = members.Where(member => configuredEntitiesLookup.Contains(member.Id));
return configuredMembers;
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<EntityInfo> GetConfigurationEntityInfo(Guid configurationGuid, CancellationToken cancellationToken)
{
var configuration = await _configurationService.GetConfigurationAsync(configurationGuid, cancellationToken);
var globalId = configuration.IsUsingCustomEntityDimension ? configuration.DepartmentRollupGlobalId : "Entity";
var entity = await _schemaServiceClient.GetDimensionByGlobalIdAsync(globalId);
var pluralize = new Pluralizer();
var entityInfo = new EntityInfo
{
EntityTitle = pluralize.Pluralize(entity.FriendlyName),
EntityLabel = entity.FriendlyName,
EntityText = entity.FriendlyName,
DepartmentRollupGlobalId = globalId,
HierarchicalGuid = entity.DefaultHierarchyGuid.ToString()
};
return entityInfo;
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<ServiceLineInfo> GetConfigurationServiceLineInfo(Guid configurationGuid, CancellationToken cancellationToken)
{
var configuration = await _configurationService.GetConfigurationAsync(configurationGuid, cancellationToken);
var dimensionGuid = configuration.ServiceLineDimensionGuid;
Schema.Client.Dtos.Info.ScoreDimensionInfoDto serviceLine = null;
if (dimensionGuid == Guid.Empty)
{
serviceLine = await _schemaServiceClient.GetDimensionByGlobalIdAsync("Service Line");
}
else
{
try
{
serviceLine = await _schemaServiceClient.GetDimensionByIdAsync(dimensionGuid);
}
catch
{
// empty to ensure code will fall thru to next block
}
if (serviceLine is null)
{
serviceLine = await _schemaServiceClient.GetDimensionByGlobalIdAsync("Service Line");
}
}
var pluralize = new Pluralizer();
var serviceLineInfo = new ServiceLineInfo
{
ServiceLineGlobalId = string.Empty,
ServiceLineDimensionGuid = dimensionGuid.ToString(),
ServiceLineHierarchicalGuid = serviceLine.DefaultHierarchyGuid.ToString(),
ServiceLineName = serviceLine.FriendlyName,
ServiceLineTitle = pluralize.Pluralize(serviceLine.FriendlyName)
};
return serviceLineInfo;
}
}
[ExcludeFromCodeCoverage]
public class EntityInfo
{
public string EntityTitle { get; set; }
public string EntityLabel { get; set; }
public string EntityText { get; set; }
public string DepartmentRollupGlobalId { get; set; }
public string HierarchicalGuid { get; set; }
}
[ExcludeFromCodeCoverage]
public class ServiceLineInfo
{
public string ServiceLineGlobalId { get; set; }
public string ServiceLineTitle { get; set; }
public string ServiceLineName { get; set; }
public string ServiceLineDimensionGuid { get; set; }
public string ServiceLineHierarchicalGuid { get; set; }
}
}
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.Databases;
using Strata.ContinuousImprovement.Biz.Replication;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[Route("api/v{api-version:apiVersion}/[controller]")]
[ApiController]
public class DatabasesController : ControllerBase
{
private readonly IDatabaseService _databaseService;
public DatabasesController(IDatabaseService databaseService)
{
_databaseService = databaseService;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<Database>>> Get(CancellationToken cancellationToken)
{
var databases = await _databaseService.GetDatabasesAsync(cancellationToken);
if (databases.Count != 0)
{
return Ok(databases);
}
return NoContent();
}
}
}
@@ -0,0 +1,33 @@
using Hangfire;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.GMDN.Jobs;
using System.Threading;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
[Authorize(Policy = Policies.StrataOnly)]
public class DeviceIdentifiersController(IBackgroundJobClient backgroundJobClient) : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobClient = backgroundJobClient;
/// <summary>
/// Clears device identifier data from all non-licensed CI clients
/// </summary>
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public ActionResult<string> ClearDeviceIdentifiers(CancellationToken cancellationToken)
{
var jobId = _backgroundJobClient.Enqueue<DeviceIdentifierClearingJob>(job
=> job.ClearDimDeviceIdentifierFromJazz(cancellationToken));
return Ok(jobId);
}
}
}
@@ -0,0 +1,216 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.All.Opportunities;
using Strata.ContinuousImprovement.Biz.ChargeCode.Opportunities;
using Strata.ContinuousImprovement.Biz.Configuration;
using Strata.ContinuousImprovement.Biz.Dtos;
using Strata.ContinuousImprovement.Biz.Encounter.Initiatives;
using Strata.ContinuousImprovement.Biz.Encounter.Opportunities;
using Strata.ContinuousImprovement.Biz.Encounter.Opportunities.Filters;
using Strata.ContinuousImprovement.Biz.Generics;
using Strata.ContinuousImprovement.Biz.Initiatives;
using Strata.ContinuousImprovement.Biz.OrgDefined;
using Strata.ContinuousImprovement.Biz.OrgDefined.Queries;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
using Strata.FeatureFlags.Client;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class EncounterOpportunitiesController : OpportunityController<ENOpportunity>
{
private readonly IClaimsPrincipalAccessor _claimsPrincipalAccessor;
private readonly IConfigurationService _configurationService;
private readonly IENOpportunityService _opportunityService;
private readonly IENInitiativeService _initiativeService;
private readonly IOpportunityMetricCohortService _opportunityMetricCohortService;
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
private readonly IAllOpportunityService _allOpportunityService;
public EncounterOpportunitiesController(
IConfigurationService configurationService,
IENOpportunityService opportunityService,
IENInitiativeService initiativeService,
IOpportunityMetricCohortService opportunityMetricCohortService,
IFeatureFlagServiceClient featureFlagServiceClient,
IClaimsPrincipalAccessor claimsPrincipalAccessor,
IAllOpportunityService allOpportunityService)
: base(opportunityService)
{
_configurationService = configurationService;
_opportunityService = opportunityService;
_initiativeService = initiativeService;
_opportunityMetricCohortService = opportunityMetricCohortService;
_featureFlagServiceClient = featureFlagServiceClient;
_claimsPrincipalAccessor = claimsPrincipalAccessor;
_allOpportunityService = allOpportunityService;
}
[HttpPost("")]
public async Task<PagedApiResponse<ENOpportunity>> GetOpportunities(ENOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<OpportunityJobResultsDto>> Create(ENOpportunityDto opportunity, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
var opportunityWithFilters = opportunity.MapToDataModel(User);
await ((IENOpportunityService)OpportunityService).SetOpportunityFilters(opportunityWithFilters, cancellationToken);
var newOpportunity = opportunityWithFilters.MapToDataModel(User);
var dbGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
if (dbGuid == Guid.Empty)
{
return BadRequest();
}
var isTempoPage = await _featureFlagServiceClient.IsEnabledAsync("enableencounteropportunityintempo", dbGuid, cancellationToken: cancellationToken);
var result = await ((IENOpportunityService)OpportunityService).CreateAsync(newOpportunity, dbGuid, isTempoPage, cancellationToken);
return Ok(new OpportunityJobResultsDto(result, newOpportunity.OpportunityGuid));
}
[HttpDelete("{id}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<bool> Delete(Guid id, CancellationToken cancellationToken)
=> await OpportunityService.DeleteAsync<ENOpportunityDataModel>(id, cancellationToken);
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(ENOpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((IENOpportunityService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "EN_Opportunities");
}
[HttpGet("{opportunityGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<ENOpportunityDto>> GetById(Guid opportunityGuid, CancellationToken cancellationToken)
{
var opportunityDto = await _opportunityService.GetENOpportunityById(opportunityGuid, cancellationToken);
if (opportunityDto == null)
{
return NotFound();
}
return Ok(opportunityDto);
}
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdateGoals(Guid opportunityGuid, [FromBody] IEnumerable<ENOpportunityRowDataChanges> changes, CancellationToken cancellationToken)
{
var existsOpportunity = await _opportunityService.Contains(opportunityGuid, cancellationToken);
if (!existsOpportunity)
{
return NotFound();
}
var hasUpdatedCostPerUnit = await _opportunityService.UpdateGoalsAsync(opportunityGuid, changes, cancellationToken);
if (hasUpdatedCostPerUnit)
{
return await _initiativeService.UpdateSavings(opportunityGuid, cancellationToken);
}
return false;
}
[HttpGet("{opportunityGuid}/[action]")]
public async Task<ENOpportunitySaveModel> GetEditModel(Guid opportunityGuid, CancellationToken cancellationToken)
{
var opportunity = await _opportunityService.GetENOpportunityById(opportunityGuid, cancellationToken);
return opportunity.MapToSaveModel();
}
[HttpGet("{opportunityGuid}/[action]")]
public async Task<InitiativeInfo> GetInitiativeInfo(Guid opportunityGuid, CancellationToken cancellationToken)
{
return await _opportunityService.GetInitiativeInfoAsync(opportunityGuid, cancellationToken);
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<ENFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await ((IENOpportunityService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
[HttpGet("[action]")]
public async Task<bool> TempoPageEnabled(CancellationToken cancellationToken)
=> await _featureFlagServiceClient.IsEnabledAsync("enableencounteropportunityintempo", User?.GetDatabaseGuid() ?? Guid.Empty, cancellationToken: cancellationToken);
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> UpdateOpportunity(Guid opportunityGuid, [FromBody] ENOpportunityDto opportunityDto, CancellationToken cancellationToken)
{
var dbGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
var isNameValid = await _allOpportunityService.NameIsValid(opportunityDto.ConfigurationGuid, (int)OpportunityTypes.Encounter, opportunityDto.Name,
opportunityDto?.OpportunityGuid, cancellationToken);
if (dbGuid == Guid.Empty || !isNameValid)
{
return BadRequest();
}
var opportunityWithFilters = opportunityDto.MapToDataModel(User, false);
var result = await ((IENOpportunityService)OpportunityService).UpdateAsync(dbGuid, opportunityWithFilters, cancellationToken);
return Ok(result);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<OpportunityMetricCohortData>>> OpportunityMetricCohortData([FromBody] OpportunityMetricCohortOptions options, CancellationToken cancellationToken)
{
var config = await _configurationService.GetConfigurationAsync(options.ConfigurationGuid, cancellationToken);
if (config == null)
{
return BadRequest();
}
var result = Enumerable.Empty<OpportunityMetricCohortData>();
result = await _opportunityMetricCohortService.GetEncounterOpportunityMetricCohortData(config, options.Data, options.InnerJoin, cancellationToken);
return Ok(result);
}
}
[ExcludeFromCodeCoverage]
public class ENOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public ENFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class ENOpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public ENFilters Filters { get; set; }
}
}
@@ -0,0 +1,129 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.ContinuousImprovement.Biz.Configuration;
using Strata.ContinuousImprovement.Biz.Exploration;
using Strata.ContinuousImprovement.Biz.Exploration.Benchmark;
using Strata.ContinuousImprovement.Biz.Exploration.CaseTypeFamilies;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.CoreLib.Claims.Extensions;
using Strata.FeatureFlags.Client;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ExplorationController : ControllerBase
{
private readonly IExplorationService _explorationService;
private readonly IConfigurationService _configurationService;
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
private readonly ILogger<ExplorationController> _logger;
public ExplorationController(IExplorationService explorationService,
IConfigurationService configurationService,
IFeatureFlagServiceClient featureFlagServiceClient,
ILogger<ExplorationController> logger)
{
_explorationService = explorationService;
_configurationService = configurationService;
_featureFlagServiceClient = featureFlagServiceClient;
_logger = logger;
}
[HttpPost("case-type-family-summary")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<CaseTypeFamilySummaryData>>> GetCaseTypeFamilySummaries([FromBody] ExplorationInfo explorationInfo, CancellationToken cancellationToken)
{
Stopwatch performanceTimer = null;
var isFlexExPerfLoggingEnabled = await _explorationService.IsFlexExPerfLogging(cancellationToken);
if (isFlexExPerfLoggingEnabled)
{
_logger.LogPerformanceInformation($"CI FlexEx Perf * GetCaseTypeFamilySummaries Controller Entered");
performanceTimer = Stopwatch.StartNew();
}
if (!await _configurationService.IsConfigurationValid(explorationInfo.ConfigurationGuid, cancellationToken)) { return BadRequest(); }
var summaries = await _explorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, cancellationToken);
if (isFlexExPerfLoggingEnabled) _logger.LogPerformanceInformation($"CI FlexEx Perf * GetCaseTypeFamilySummaries Controller * Total Elapsed time: {performanceTimer.Elapsed}");
return Ok(summaries);
}
[HttpPost("case-type-family-cost-driver-info")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<CaseTypeFamilySummaryData>>> GetCaseTypeFamilyCostDriverInfo([FromBody] ExplorationCostDriverInfo explorationCostDriverInfo, CancellationToken cancellationToken)
{
Stopwatch performanceTimer = null;
var isFlexExPerfLoggingEnabled = await _explorationService.IsFlexExPerfLogging(cancellationToken);
if (isFlexExPerfLoggingEnabled)
{
_logger.LogPerformanceInformation($"CI FlexEx Perf * GetCaseTypeFamilyCostDriverInfo Controller Entered");
performanceTimer = Stopwatch.StartNew();
}
if (!await _configurationService.IsConfigurationValid(explorationCostDriverInfo.ConfigurationGuid, cancellationToken)) { return BadRequest(); }
var summaries = await _explorationService.GetCaseTypeFamilySummaryDetailsAsync(explorationCostDriverInfo, cancellationToken);
if (isFlexExPerfLoggingEnabled) _logger.LogPerformanceInformation($"CI FlexEx Perf * GetCaseTypeFamilyCostDriverInfo Controller * Total Elapsed time: {performanceTimer.Elapsed}");
return Ok(summaries);
}
[HttpPost("case-type-family-details")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<CaseTypeFamilyDetailInfo>> GetCaseTypeFamilyDetails([FromBody] ExplorationDetailInfo explorationInfo, CancellationToken cancellationToken)
{
Stopwatch performanceTimer = null;
var isFlexExPerfLoggingEnabled = await _explorationService.IsFlexExPerfLogging(cancellationToken);
if (isFlexExPerfLoggingEnabled)
{
_logger.LogPerformanceInformation($"CI FlexEx Perf * GetCaseTypeFamilyDetails Controller Entered");
performanceTimer = Stopwatch.StartNew();
}
if (!await _configurationService.IsConfigurationValid(explorationInfo.ConfigurationGuid, cancellationToken)) { return BadRequest(); }
await _configurationService.GetQualityVariationAprdrgFlagAsync(explorationInfo.ConfigurationGuid, cancellationToken);
var ctfEntityDetails = await _explorationService.GetCaseTypeFamilyDetailsAsync(explorationInfo, cancellationToken);
if (isFlexExPerfLoggingEnabled) _logger.LogPerformanceInformation($"CI FlexEx Perf * GetCaseTypeFamilyDetails Controller * Total Elapsed time: {performanceTimer.Elapsed}");
return Ok(ctfEntityDetails);
}
[HttpPost("case-type-family-summary-heatmap")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<HeatMapData>>> GetHeatMapSummary([FromBody] ExplorationInfo explorationInfo, CancellationToken cancellationToken)
{
if (!await _configurationService.IsConfigurationValid(explorationInfo.ConfigurationGuid, cancellationToken)) { return BadRequest(); }
var summaries = await _explorationService.GetBenchmarkingDataForHeatMapAsync(explorationInfo, explorationInfo.Granularity, cancellationToken);
return Ok(summaries);
}
[HttpGet("flexible-exploration-information-page")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> IsFlexibleExplorationInformationPageEnabled(CancellationToken cancellationToken)
{
var databaseGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
var isExplorationInformationPageEnable = await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationpage3enabled", databaseGuid, cancellationToken: cancellationToken);
return Ok(isExplorationInformationPageEnable);
}
}
}
@@ -0,0 +1,179 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.Exploration.Filters;
using Strata.ContinuousImprovement.Biz.Exploration.Opportunities;
using Strata.ContinuousImprovement.Biz.Generics;
using Strata.ContinuousImprovement.Biz.OpportunityComments;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.Shared;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.CoreLib.Claims.Extensions;
using Strata.FeatureFlags.Client;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ExplorationOpportunitiesController : OpportunityController<EXOpportunity>
{
private readonly IEXOpportunityService _opportunityService;
private readonly ICommentService _commentService;
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
private readonly ILogger<ExplorationOpportunitiesController> _logger;
public ExplorationOpportunitiesController(IEXOpportunityService opportunityService,
ICommentService commentService,
IFeatureFlagServiceClient featureFlagServiceClient,
ILogger<ExplorationOpportunitiesController> logger) : base(opportunityService)
{
_opportunityService = opportunityService;
_commentService = commentService;
_featureFlagServiceClient = featureFlagServiceClient;
_logger = logger;
}
[HttpGet("[action]")]
public async Task<bool> GetEnabled(CancellationToken cancellationToken)
{
var databaseGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
var hasFlexibleExplorartion = User.IsSdtEmployee()
|| (await _featureFlagServiceClient.IsEnabledAsync("ciflexibleexplorationenabled", databaseGuid, cancellationToken: cancellationToken));
var dict = new Dictionary<string, object> { { "isSdtEmployee", User.IsSdtEmployee() }, { "hasFlexibleExploration", hasFlexibleExplorartion } };
_logger.LogServiceInformation("CI Flexible Exploration Enabled", dict);
return hasFlexibleExplorartion;
}
[HttpPost("")]
public async Task<PagedApiResponse<EXOpportunity>> GetOpportunities(EXOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpGet("{opportunityGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
public async Task<ActionResult<EXOpportunity>> GetById(Guid opportunityGuid, CancellationToken cancellationToken)
{
return Ok(await _opportunityService.GetById(opportunityGuid, cancellationToken));
}
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
public async Task CheckWorkbook(Guid opportunityGuid, CancellationToken cancellationToken)
=> await _opportunityService.CheckWorkbook(opportunityGuid, cancellationToken);
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<EXOpportunity>> Create(EXOpportunityDto savedOpportunity, CancellationToken cancellationToken)
{
var newOpportunity = new EXOpportunity
{
ConfigurationGuid = savedOpportunity.ConfigurationGuid,
Name = savedOpportunity.Name,
Description = savedOpportunity.Description
};
return Ok(await _opportunityService.CreateAsync(newOpportunity, User?.Identity?.Name, cancellationToken));
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(EXOpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((IEXOpportunityService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "EX_Opportunities");
}
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<EXOpportunity>> Update(Guid opportunityGuid, EXOpportunity opportunity, CancellationToken cancellationToken)
{
var opp = await _opportunityService.GetById(opportunityGuid, cancellationToken);
if (opp == null)
{
return BadRequest();
}
return Ok(await _opportunityService.UpdateAsync(opportunity, cancellationToken));
}
[HttpPut("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<EXOpportunity>> SetStatus(Guid opportunityGuid, [FromBody] InitiativeStatuses status, CancellationToken cancellationToken)
{
var opp = await _opportunityService.GetById(opportunityGuid, cancellationToken);
if (opp == null)
{
return BadRequest();
}
opp.InitiativeStatus = status;
return Ok(await _opportunityService.UpdateAsync(opp, cancellationToken));
}
[HttpDelete("{opportunityGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult> Delete(Guid opportunityGuid, CancellationToken cancellationToken)
{
var opp = await _opportunityService.GetById(opportunityGuid, cancellationToken);
if (opp == null)
{
return BadRequest();
}
await ((IEXOpportunityService)_opportunityService).DeleteAsync(opportunityGuid, cancellationToken);
var databaseGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
await _commentService.DeleteCommentsAsync(databaseGuid, (int)OpportunityTypes.Blank, opp.OpportunityId, cancellationToken);
return Ok();
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<EXFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await ((IEXOpportunityService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
public class EXOpportunityDto
{
[Required]
[StringLength(200)]
public string Name { get; set; }
public string Description { get; set; }
public Guid ConfigurationGuid { get; set; }
}
[ExcludeFromCodeCoverage]
public class EXOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public EXFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class EXOpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public EXFilters Filters { get; set; }
}
}
}
@@ -0,0 +1,241 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.Dtos;
using Strata.ContinuousImprovement.Biz.Exploration;
using Strata.ContinuousImprovement.Biz.Mappers;
using Strata.ContinuousImprovement.Biz.Population;
using Strata.ContinuousImprovement.Biz.UserSetting;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.CoreLib.Claims.Extensions;
using Strata.DecisionSupport.Models.Encounters;
using Strata.DecisionSupport.Models.Jazz;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ExplorationPopulationsController : ControllerBase
{
private readonly IUserSettingService _userSettingService;
private readonly IExplorationPopulationService _populationService;
private readonly IExplorationService _explorationService;
private readonly ILogger<ExplorationPopulationsController> _logger;
public ExplorationPopulationsController(IUserSettingService userSettingService, IExplorationPopulationService populationService, IExplorationService explorationService,
ILogger<ExplorationPopulationsController> logger)
{
_userSettingService = userSettingService;
_populationService = populationService;
_explorationService = explorationService;
_logger = logger;
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<bool> IsNameUnique([FromBody] string name, CancellationToken cancellationToken)
{
var isValid = await _populationService.IsNameUnique(name, cancellationToken);
return isValid;
}
[HttpPost("")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> AddPopulation([FromBody] ExplorationPopulationDto populationDto, CancellationToken cancellationToken)
{
var isAdded = await _populationService.AddPopulationAsync(populationDto, cancellationToken);
return Ok(isAdded);
}
[HttpGet("{populationId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Tuple<DateTime, DateTime>>> GetDischargeDates(long populationId, CancellationToken cancellationToken)
{
var population = await _populationService.ContainsPopulation(populationId, cancellationToken);
if (!population) return NotFound();
var result = await _populationService.GetDischargeDates(populationId, cancellationToken);
return Ok(new Tuple<DateTime, DateTime>(result.Item1, result.Item2));
}
[HttpPut("{populationId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult> UpdatePopulation(long populationId, ExplorationPopulationDto populationDto, CancellationToken cancellationToken)
{
var population = await _populationService.ContainsPopulation(populationId, cancellationToken);
if (!population) return NotFound();
var isUpdated = await _populationService.UpdatePopulationAsync(populationDto, cancellationToken);
if (isUpdated)
{
var counts = await _populationService.GetPopulationCountOpportunitiesAsync([populationId], cancellationToken);
if (counts.TryGetValue(populationId, out int opportunitiesCount) && opportunitiesCount > 0)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
var userSetting = await _userSettingService.GetById(userGuid, cancellationToken);
if (userSetting != null)
{
await _populationService.CreateExplorationPopulationOpportunity(userSetting.ConfigurationGuid, populationId, false, cancellationToken);
}
}
return NoContent();
}
return BadRequest();
}
[HttpGet("")]
public async Task<ActionResult<IEnumerable<ExplorationPopulationDataDto>>> GetPopulations(CancellationToken cancellationToken)
{
var result = await _populationService.GetPopulationsAsync(cancellationToken);
return Ok(result);
}
[HttpPut("{populationId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdatePopulationName(long populationId,[FromBody] string name, CancellationToken cancellationToken)
{
var isUpdated = await _populationService.UpdatePopulationName(populationId, name, cancellationToken);
return Ok(isUpdated);
}
[HttpGet("{populationId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ExplorationPopulationDto>> GetPopulationData(long populationId, CancellationToken cancellationToken)
{
var population = await _populationService.ContainsPopulation(populationId, cancellationToken);
if (!population) return NotFound();
var result = await _populationService.GetPopulationDataAsync(populationId, cancellationToken);
return result != null ? Ok(result) : BadRequest();
}
[HttpGet("{populationId}/details")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<ExplorationPopulationDetailsDto>> GetPopulationDetails(long populationId, CancellationToken cancellationToken)
{
var result = await _populationService.GetPopulationDetailsAsync(populationId, cancellationToken);
if (result == null)
{
return BadRequest();
}
return Ok(result);
}
[HttpGet("info/{populationId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IExplorationPopulationInfo>> GetPopulationInfo(long populationId, CancellationToken cancellationToken)
{
var result = await _populationService.GetPopulationInfoAsync(populationId, cancellationToken);
if (result == null)
{
return BadRequest();
}
return Ok(result);
}
[HttpGet("{populationId}/encounters")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<IJazzExplorationEncounterInfo>>> GetEncounters(long populationId, Guid configurationGuid, CancellationToken cancellationToken)
{
_logger.LogServiceInformation($"Performing {nameof(GetEncounters)} for configuration: {configurationGuid}, populationid: {populationId}");
var populationData = await _populationService.GetPopulationDataAsync(populationId, cancellationToken);
if (populationData != null)
{
_logger.LogServiceInformation($"{nameof(GetEncounters)} populationData is not null, totalEncounters:{populationData.TotalEncounters}");
var encounterPhysicians = await _explorationService.GetEncountersAsync(populationData.MapToExplorationDetailInfo(), configurationGuid, cancellationToken);
if (encounterPhysicians != null)
{
if (configurationGuid == Guid.Empty)
{
var newIncludedEncounterIds = encounterPhysicians.Where(e => !e.IsExcluded).Select(x => x.EncounterId);
var newExcludedEncounterIds = encounterPhysicians.Where(e => e.IsExcluded).Select(x => x.EncounterId);
var updated = await _populationService.UpdatePopulationMappingsAsync(populationId, newIncludedEncounterIds, newExcludedEncounterIds, cancellationToken);
}
var result = encounterPhysicians.Where(enc => !enc.IsExcluded).Select(x => x.MapToExplorationEncounterInfo());
return Ok(result);
}
}
else
{
_logger.LogServiceInformation($"{nameof(GetEncounters)} populationData is null");
}
_logger.LogServiceInformation($"{nameof(GetEncounters)} returned a BadRequest()");
return BadRequest();
}
[HttpDelete("{populationId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<IActionResult> DeletePopulation(long populationId, CancellationToken cancellationToken)
{
var population = await _populationService.ContainsPopulation(populationId, cancellationToken);
if (!population) return NotFound();
var result = await _populationService.DeletePopulation(populationId, cancellationToken);
return result ? NoContent() : BadRequest();
}
[HttpPost("{populationId}/duplicate")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<ExplorationPopulationDataDto>> DuplicatePopulation(long populationId, [FromBody] string name, CancellationToken cancellationToken)
{
var populationData = await _populationService.DuplicatePopulation(populationId, name, cancellationToken);
return populationData != null ? Ok(populationData) : BadRequest();
}
[HttpPost("{populationId}/opportunities")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<ExplorationPopulationOpportunityDto>> CreateOpportunity(long populationId, CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
var userSetting = await _userSettingService.GetById(userGuid, cancellationToken);
if (userSetting != null)
{
var result = await _populationService.CreateExplorationPopulationOpportunity(userSetting.ConfigurationGuid, populationId, true, cancellationToken);
if (result)
{
var opportunity = (await _populationService.GetPopulationOpportunitiesAsync([populationId], cancellationToken)).FirstOrDefault()?.FirstOrDefault();
return Ok(opportunity);
}
}
return BadRequest();
}
}
}
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.ChargeCode;
using Strata.ContinuousImprovement.Biz.Exploration.Filters;
using Strata.ContinuousImprovement.Biz.Filter;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[Route("api/v{api-version:apiVersion}/[controller]")]
[ApiController]
public class FiltersController : ControllerBase
{
private readonly IChargeCodeService _chargeCodeService;
private readonly IFilterService _filterService;
public FiltersController(IChargeCodeService chargeCodeService, IFilterService filterService)
{
_chargeCodeService = chargeCodeService;
_filterService = filterService;
}
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<List<FilterListItem>> GetCostDriverFilters(CancellationToken cancellationToken)
{
var costDrivers = await _chargeCodeService.GetChargeCodeCostDrivers(cancellationToken);
return costDrivers.Select(costDriver => new FilterListItem { Key = costDriver, Title = costDriver }).ToList();
}
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<List<FilterListItem>> GetSupplyFilters(CancellationToken cancellationToken)
{
return await _filterService.GetSupplyList(cancellationToken);
}
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<List<FilterListItem>> GetCostComponentFilters(CancellationToken cancellationToken)
{
return await _filterService.GetCostComponentList(cancellationToken);
}
[HttpGet("[action]/{chipKey}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<List<FilterListItem>> GetSphCriteriaFilters([FromRoute] ChipKey chipKey, CancellationToken cancellationToken)
{
return await _filterService.GetSphCriteriaList(chipKey, cancellationToken);
}
}
}
@@ -0,0 +1,131 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.ChargeCode.Opportunities;
using Strata.ContinuousImprovement.Biz.Dtos;
using Strata.ContinuousImprovement.Biz.FreeForm.Initiatives;
using Strata.ContinuousImprovement.Biz.FreeForm.Opportunities;
using Strata.ContinuousImprovement.Biz.FreeForm.Opportunities.Filters;
using Strata.ContinuousImprovement.Biz.Initiatives;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class FreeFormOpportunitiesController : OpportunityController<FFOpportunity>
{
private readonly IInitiativeService _initiativeService;
private readonly IFFOpportunityService _ffOpportunityService;
public FreeFormOpportunitiesController(IFFOpportunityService opportunityService,
IInitiativeService initiativeService) : base(opportunityService)
{
_initiativeService = initiativeService;
_ffOpportunityService = opportunityService;
}
[HttpPost("")]
public async Task<PagedApiResponse<FFOpportunity>> GetOpportunities(FFOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
=> await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
[HttpGet("{opportunityGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<FFOpportunityDetailModel>> GetById(Guid opportunityGuid, CancellationToken cancellationToken)
{
var dataModel = await _ffOpportunityService.GetDataModelById(opportunityGuid, cancellationToken);
var detailModel = dataModel.MapToDetailModel(dataModel.Configuration.FiscalMonthResolver);
return Ok(detailModel);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> Create(FFOpportunityDto opportunity, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
var newOpportunity = opportunity.MapToDataModel();
newOpportunity.AuthorGuid = User.GetUserGuid();
newOpportunity.AuthorName = User.Identity.Name;
var opportunityGuid = await ((IFFOpportunityService)OpportunityService).CreateAsync(newOpportunity, cancellationToken);
return Ok(opportunityGuid);
}
[HttpDelete("{id}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<bool> Delete(Guid id, CancellationToken cancellationToken)
=> await OpportunityService.DeleteAsync<FFOpportunityDataModel>(id, cancellationToken);
[HttpGet("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<FreeFormInitiativeDto>> GetInitiative(Guid opportunityGuid, CancellationToken cancellationToken)
{
var opportunity = await ((IFFOpportunityService)OpportunityService).GetDataModelById(opportunityGuid, cancellationToken);
if (opportunity == null) { return BadRequest(); }
var initiative = await _initiativeService.GetById(opportunity.InitiativeGuid, cancellationToken);
return initiative.MapToFreeFormInitiative(opportunity);
}
[HttpPost("{opportunityGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task SetGoalsAsync(Guid opportunityGuid, [FromBody] FFOpportunityMonthValuesDto dto, CancellationToken cancellationToken)
=> await ((IFFOpportunityService)OpportunityService).SetGoalsAsync(opportunityGuid, dto.OpportunityDetail, dto.MonthValues, cancellationToken);
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(FFOpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((IFFOpportunityService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "FF_Opportunities");
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<FFFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
=> await ((IFFOpportunityService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
[ExcludeFromCodeCoverage]
public class FFOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public FFFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class FFOpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public FFFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class FFOpportunityMonthValuesDto
{
public FFOpportunityDetailModel OpportunityDetail { get; set; }
public IList<FFOpportunityDetailRowData> MonthValues { get; set; }
}
}
@@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.GMDN;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class GMDNFileUploadController : ControllerBase
{
private readonly IGMDNService _gmdnService;
public GMDNFileUploadController(IGMDNService gmdnService)
{
_gmdnService = gmdnService;
}
[HttpPost("{fileType}/[action]")]
[DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = int.MaxValue, ValueLengthLimit = int.MaxValue)]
public async Task<ActionResult> UploadGMDNFile(GMDNFileType fileType, IFormFile file, CancellationToken cancellationToken)
{
var userName = User?.Identity?.Name;
switch (fileType)
{
case GMDNFileType.Term:
await _gmdnService.SeedGMDNTerms(file, userName, cancellationToken);
break;
case GMDNFileType.TermCategory:
await _gmdnService.SeedGMDNTermCategories(file, userName, cancellationToken);
break;
case GMDNFileType.Category:
await _gmdnService.SeedGMDNCategories(file, userName, cancellationToken);
break;
}
return Ok();
}
}
public enum GMDNFileType
{
Term,
TermCategory,
Category
}
}
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.GeneralLedger.Details;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class GeneralLedgerDetailsController : ControllerBase
{
private readonly IGLDetailService _glDetailService;
public GeneralLedgerDetailsController(IGLDetailService glDetailService)
{
_glDetailService = glDetailService;
}
[HttpPost("{configurationGuid}/[action]")]
public async Task<GLDetail> GetDepartmentData(Guid configurationGuid, GLDepartmentAccountData departmentAccountData, CancellationToken cancellationToken)
{
var result = await _glDetailService.GetOverallDetailAsync(configurationGuid, departmentAccountData, cancellationToken);
return result;
}
[HttpPost("{configurationGuid}/[action]")]
public async Task<GLDetail> GetStatisticData(Guid configurationGuid, GLDepartmentAccountData departmentAccountData, CancellationToken cancellationToken)
{
var result = await _glDetailService.GetStatisticDetailAsync(configurationGuid, departmentAccountData, cancellationToken);
return result;
}
}
}
@@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.GeneralLedger.Opportunities;
using Strata.ContinuousImprovement.Biz.GeneralLedger.Opportunities.Filters;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class GeneralLedgerOpportunitiesController : OpportunityController<GLOpportunity>
{
public GeneralLedgerOpportunitiesController(IGLOpportunityService opportunityService)
: base(opportunityService)
{
}
[HttpPost("")]
public async Task<PagedApiResponse<GLOpportunity>> GetOpportunities(GLOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(GLOpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((IGLOpportunityService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "GL_Opportunities");
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<GLFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await ((IGLOpportunityService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> Create(GLOpportunitySaveModel opportunity, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
opportunity.AuthorGuid = User.GetUserGuid();
opportunity.AuthorName = User.Identity.Name;
var newOpportunityId = await ((IGLOpportunityService)OpportunityService).CreateAsync(opportunity, cancellationToken);
return Ok(newOpportunityId);
}
[HttpDelete("{id}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<bool> Delete(Guid id, CancellationToken cancellationToken)
=> await OpportunityService.DeleteAsync<GLOpportunityDataModel>(id, cancellationToken);
}
[ExcludeFromCodeCoverage]
public class GLOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public GLFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class GLOpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public GLFilters Filters { get; set; }
}
}
@@ -0,0 +1,87 @@
using Hangfire;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Strata.ContinuousImprovement.Biz.GMDN;
using Strata.ContinuousImprovement.Api.Extensions;
using Strata.ContinuousImprovement.Api.Attributes;
using Strata.ContinuousImprovement.Biz.GMDN.Jobs;
using System;
using Microsoft.Extensions.Options;
using Strata.ContinuousImprovement.Biz.Hangfire;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class GudidFileController : ControllerBase
{
private readonly IGUDIDService _gudidService;
private readonly IBatchJobClient _batchJobClient;
private readonly string _datadogUrl;
private readonly string _hangfireUrl;
public GudidFileController(
IBatchJobClient batchJobClient,
IGUDIDService gudidService,
IOptions<AccessGudidSection> accessGudidAccessor,
IOptions<DatadogSection> datadogAccessor,
IOptions<HangfireSection> sectionAccessor)
{
_gudidService = gudidService;
_batchJobClient = batchJobClient;
var datadog = datadogAccessor.Value ?? throw new ArgumentNullException(nameof(datadogAccessor));
_datadogUrl = datadog.SeedGMDNJob;
var hangfire = sectionAccessor.Value ?? throw new ArgumentNullException(nameof(sectionAccessor));
_hangfireUrl = hangfire.Url;
}
[HttpPost("{dataGroup}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status415UnsupportedMediaType)]
[DisableFormValueModelBinding]
[DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = int.MaxValue, ValueLengthLimit = int.MaxValue)]
public async Task<ActionResult<string>> SeedGUDID(DataGroupGudId dataGroup, [BindNever] IFormFile file, CancellationToken cancellationToken)
{
var userName = User?.Identity?.Name;
var fileSection = await HttpContext.Request.GetFileFromMultipartUpload(cancellationToken);
if (fileSection?.FileStream == null)
{
// prob should return something to indicate exactly what went wrong.
return new UnsupportedMediaTypeResult();
}
switch (dataGroup)
{
case DataGroupGudId.Identifiers:
await _gudidService.SeedGUDID("tmp", "gudid_identifiers", fileSection.FileStream, fileSection.FileName, userName, cancellationToken);
var jobId = _batchJobClient.StartNew(batch => { batch.Enqueue<SeedGUDIDIdentifierDataJob>(job => job.MergeGUDIDIdentifierData(cancellationToken)); });
return $"{_hangfireUrl}/jobs/details/{jobId}";
case DataGroupGudId.Terms:
await _gudidService.SeedGUDID("public", "gudid_terms", fileSection.FileStream, fileSection.FileName, userName, cancellationToken);
break;
case DataGroupGudId.Devices:
await _gudidService.SeedGUDID("public", "gudid_devices", fileSection.FileStream, fileSection.FileName, userName, cancellationToken);
break;
}
return Ok();
}
[HttpGet("[action]")]
public string DatadogUrl()
{ return _datadogUrl; }
public enum DataGroupGudId
{
Terms,
Devices,
Identifiers
}
}
}
@@ -0,0 +1,62 @@
using Hangfire;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Strata.ContinuousImprovement.Biz.GMDN;
using Strata.ContinuousImprovement.Biz.Hangfire;
using Strata.Hangfire.Jazz.Client;
using Strata.Hangfire.Jazz.Client.Models;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class HangfireController : ControllerBase
{
private readonly IJazzHangfireServiceClient _hangfireService;
private readonly string _accessGudidUrl;
private readonly string _datadogUrl;
public HangfireController(
IJazzHangfireServiceClient jazzHangfireServiceClient,
IOptions<AccessGudidSection> accessGudidAccessor,
IOptions<DatadogSection> datadogAccessor,
IOptions<HangfireSection> sectionAccessor)
{
_hangfireService = jazzHangfireServiceClient;
var accessGudid = accessGudidAccessor.Value ?? throw new ArgumentNullException(nameof(accessGudidAccessor));
_accessGudidUrl = accessGudid.Url;
var datadog = datadogAccessor.Value ?? throw new ArgumentNullException(nameof(datadogAccessor));
_datadogUrl = datadog.SeedGMDNJob;
var hangfire = sectionAccessor.Value ?? throw new ArgumentNullException(nameof(sectionAccessor));
}
[HttpGet("{jobId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<JobInfoDto>> JobStatus(Guid jobId, CancellationToken cancellationToken)
{
var result = await _hangfireService.GetJobInfoAsync(jobId, cancellationToken);
return Ok(result);
}
[HttpGet("[action]")]
public string DatadogUrl()
{ return _datadogUrl; }
[HttpGet("[action]")]
public string AccessGudidUrl()
{ return _accessGudidUrl; }
public enum DataGroup
{
Terms,
Devices,
Identifiers
}
}
}
@@ -0,0 +1,91 @@
using Microsoft.AspNetCore.Mvc;
using MoreLinq;
using Strata.ContinuousImprovement.Biz.InitiativeDetailColumns;
using Strata.ContinuousImprovement.Biz.InitiativeRollupColumns;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class InitiativeDetailColumnsController : ControllerBase
{
private readonly IInitiativeDetailColumnService _initiativeDetailColumnService;
private readonly IInitiativeRollupColumnService _initiativeRollupColumnService;
public InitiativeDetailColumnsController(IInitiativeDetailColumnService initiativeDetailColumnService, IInitiativeRollupColumnService initiativeRollupColumnService)
{
_initiativeDetailColumnService = initiativeDetailColumnService;
_initiativeRollupColumnService = initiativeRollupColumnService;
}
[HttpGet("[action]")]
public async Task<IEnumerable<InitiativeDetailColumn>> GetColumnList(CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
return await ManageRollupColumns((await _initiativeDetailColumnService.GetColumnList(userGuid, cancellationToken)), cancellationToken);
}
[HttpGet("[action]")]
public async Task<IEnumerable<InitiativeDetailColumn>> GetDefaultColumnList(CancellationToken cancellationToken)
{
return await ManageRollupColumns(_initiativeDetailColumnService.GetDefaultColumnList(), cancellationToken);
}
private async Task<IEnumerable<InitiativeDetailColumn>> ManageRollupColumns(IEnumerable<InitiativeDetailColumn> columns, CancellationToken cancellationToken)
{
var columnList = columns.ToList();
// Add any custom rollups to default list
var rollupColumns = (await _initiativeRollupColumnService.GetAllAsync(cancellationToken))
.OrderBy(x => x.InitiativeRollupColumnId).ToList();
if (rollupColumns.Any())
{
Enumerable.Range(1, 3).ForEach(id =>
{
var column = columnList.Find(dflt => rollupColumns.Any(rc => dflt.Name.StartsWith($"InitiativeRollup{id}", StringComparison.CurrentCultureIgnoreCase)));
if (column != null)
{
var rollupColumn = rollupColumns.Find(rc => rc.InitiativeRollupColumnId == id);
if (rollupColumn != null)
{
column.Heading = rollupColumn.Name;
column.InitiativeRollupColumnId = rollupColumn.InitiativeRollupColumnId;
}
else
{
columnList.Remove(column);
}
}
});
}
else
{
// no rollups configured - remove rollup columns
columnList.RemoveAll(x => x.Name.StartsWith("InitiativeRollup"));
}
return columnList;
}
[HttpPost("[action]")]
public async Task SetColumnList(InitiativeDetailColumnSaveInfo columnInfo, CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
await _initiativeDetailColumnService.SetColumnList(userGuid, columnInfo.Columns, cancellationToken);
}
[ExcludeFromCodeCoverage]
public class InitiativeDetailColumnSaveInfo
{
public IEnumerable<InitiativeDetailColumn> Columns { get; set; }
}
}
}
@@ -0,0 +1,127 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.InitiativeRollupColumns;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class InitiativeRollupColumnsController : ControllerBase
{
private readonly IInitiativeRollupColumnService _initiativeRollupColumnService;
public InitiativeRollupColumnsController(IInitiativeRollupColumnService initiativeRollupColumnService)
{
_initiativeRollupColumnService = initiativeRollupColumnService;
}
[HttpGet]
public async Task<IEnumerable<InitiativeRollupColumn>> GetInitiativeRollupColumnsAsync(CancellationToken cancellationToken)
{
return await _initiativeRollupColumnService.GetAllAsync(cancellationToken);
}
[HttpGet("{initiativeRollupColumnId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<InitiativeRollupColumn>> GetByIdAsync([FromRoute] int initiativeRollupColumnId, CancellationToken cancellationToken)
{
var rollupColumn = await _initiativeRollupColumnService.GetByIdAsync(initiativeRollupColumnId, cancellationToken);
if (rollupColumn != null)
{
return Ok(rollupColumn);
}
return NotFound();
}
[HttpPost("[action]/{initiativeRollupColumnId}")]
public async Task<bool> IsNameValid([FromRoute] int initiativeRollupColumnId, [FromBody] string name, CancellationToken cancellationToken)
{
return await _initiativeRollupColumnService.IsNameValid(name, cancellationToken, initiativeRollupColumnId);
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<ActionResult<InitiativeRollupColumn>> Create(InitiativeRollupColumnCreateInfo createInfo, CancellationToken cancellationToken)
{
if (await _initiativeRollupColumnService.ContainsNameAsync(createInfo.Name, cancellationToken))
{
return BadRequest();
}
return await _initiativeRollupColumnService.CreateAsync(createInfo, cancellationToken);
}
[HttpPut("{initiativeRollupColumnId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<ActionResult<InitiativeRollupColumn>> Update([FromRoute] int initiativeRollupColumnId, [FromBody] InitiativeRollupColumnSaveInfo initiativeRollupColumnSaveInfo, CancellationToken cancellationToken)
{
if (await _initiativeRollupColumnService.ContainsNameAsync(initiativeRollupColumnSaveInfo.Name, cancellationToken, initiativeRollupColumnId))
{
return BadRequest();
}
if (!await _initiativeRollupColumnService.ContainsAsync(initiativeRollupColumnId, cancellationToken))
{
return NotFound();
}
return await _initiativeRollupColumnService.UpdateAsync(initiativeRollupColumnId, initiativeRollupColumnSaveInfo, cancellationToken);
}
[HttpDelete("{initiativeRollupColumnId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<IActionResult> Delete([FromRoute] int initiativeRollupColumnId, CancellationToken cancellationToken)
{
if (!await _initiativeRollupColumnService.ContainsAsync(initiativeRollupColumnId, cancellationToken))
{
return NotFound();
}
await _initiativeRollupColumnService.DeleteAsync(initiativeRollupColumnId, cancellationToken);
return NoContent();
}
[HttpGet("{initiativeRollupColumnId}/values")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IEnumerable<InitiativeRollupValue>>> GetInitiativeRollupValuesAsync([FromRoute] int initiativeRollupColumnId, CancellationToken cancellationToken)
{
var values = await _initiativeRollupColumnService.GetByRollupValuesById(initiativeRollupColumnId, cancellationToken);
if (values != null)
{
return Ok(values);
}
return NotFound();
}
[HttpPut("{initiativeRollupColumnId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<ActionResult<InitiativeRollupValue>> AddRollupValueAsync([FromRoute] int initiativeRollupColumnId, [FromBody] string value, CancellationToken cancellationToken)
{
var values = await _initiativeRollupColumnService.GetByRollupValuesById(initiativeRollupColumnId, cancellationToken);
if (values == null) { return NotFound(); }
if (values.Select(v => v.Value).Contains(value)) { return BadRequest(); }
return await _initiativeRollupColumnService.AddRollupValue(initiativeRollupColumnId, value, cancellationToken);
}
}
}
@@ -0,0 +1,643 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Extensions;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.All.Opportunities;
using Strata.ContinuousImprovement.Biz.ChargeCode.Initiatives;
using Strata.ContinuousImprovement.Biz.ChargeCode.Jobs;
using Strata.ContinuousImprovement.Biz.ChargeCode.Opportunities;
using Strata.ContinuousImprovement.Biz.Encounter.Initiatives;
using Strata.ContinuousImprovement.Biz.Encounter.Opportunities;
using Strata.ContinuousImprovement.Biz.FreeForm.Initiatives;
using Strata.ContinuousImprovement.Biz.FreeForm.Opportunities;
using Strata.ContinuousImprovement.Biz.GeneralLedger.Initiatives;
using Strata.ContinuousImprovement.Biz.GeneralLedger.Opportunities;
using Strata.ContinuousImprovement.Biz.Generics;
using Strata.ContinuousImprovement.Biz.InitiativeRollupColumns;
using Strata.ContinuousImprovement.Biz.Initiatives;
using Strata.ContinuousImprovement.Biz.Initiatives.Filters;
using Strata.ContinuousImprovement.Biz.Mappers;
using Strata.ContinuousImprovement.Biz.Notification;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.Payroll.Initiatives;
using Strata.ContinuousImprovement.Biz.Payroll.Opportunities;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.CoreLib.Claims.Extensions;
using Strata.FeatureFlags.Client;
using Strata.Hangfire.Jazz.Client;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class InitiativesController : ControllerBase
{
private readonly IInitiativeService _initiativeService;
private readonly ICCOpportunityService _ccOpportunityService;
private readonly ICCInitiativeService _ccInitiativeService;
private readonly IENOpportunityService _enOpportunityService;
private readonly IENInitiativeService _enInitiativeService;
private readonly IFFOpportunityService _ffOpportunityService;
private readonly IFFInitiativeService _ffInitiativeService;
private readonly IGLOpportunityService _glOpportunityService;
private readonly IGLInitiativeService _glInitiativeService;
private readonly IPROpportunityService _prOpportunityService;
private readonly IPRInitiativeService _prInitiativeService;
private readonly IStakeholderUser _stakeholderUser;
private readonly IHubContext<NotificationHub, INotificationHub> _notificationHubContext;
private readonly ILogger<InitiativesController> _logger;
private readonly ICCInitiativeJob _ccInitiativeJob;
private readonly IJazzHangfireServiceClient _hangfireService;
private readonly IInitiativeRollupColumnService _initiativeRollupColumnService;
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
private readonly IAllOpportunityService _allOpportunityService;
#pragma warning disable S107 // Constructor has NN parameters, which is greater than the 7 authorized
public InitiativesController(IInitiativeService initiativeService,
ICCOpportunityService ccOpportunityService,
ICCInitiativeService ccInitiativeService,
IENOpportunityService enOpportunityService,
IENInitiativeService enInitiativeService,
IFFOpportunityService ffOpportunityService,
IFFInitiativeService ffInitiativeService,
IGLOpportunityService glOpportunityService,
IGLInitiativeService glInitiativeService,
IPROpportunityService prOpportunityService,
IPRInitiativeService prInitiativeService,
IStakeholderUser stakeholderUser,
IHubContext<NotificationHub, INotificationHub> notificationHubContext,
ILogger<InitiativesController> logger,
ICCInitiativeJob ccInitiativeJob,
IJazzHangfireServiceClient hangfireService,
IInitiativeRollupColumnService initiativeRollupColumnService,
IFeatureFlagServiceClient featureFlagServiceClient,
IAllOpportunityService allOpportunityService)
#pragma warning restore S107 // Constructor has NN parameters, which is greater than the 7 authorized
{
_initiativeService = initiativeService;
_ccOpportunityService = ccOpportunityService;
_ccInitiativeService = ccInitiativeService;
_enOpportunityService = enOpportunityService;
_enInitiativeService = enInitiativeService;
_ffOpportunityService = ffOpportunityService;
_ffInitiativeService = ffInitiativeService;
_glOpportunityService = glOpportunityService;
_glInitiativeService = glInitiativeService;
_prOpportunityService = prOpportunityService;
_prInitiativeService = prInitiativeService;
_stakeholderUser = stakeholderUser;
_notificationHubContext = notificationHubContext;
_logger = logger;
_ccInitiativeJob = ccInitiativeJob;
_hangfireService = hangfireService;
_initiativeRollupColumnService = initiativeRollupColumnService;
_featureFlagServiceClient = featureFlagServiceClient;
_allOpportunityService = allOpportunityService;
}
[HttpPost()]
public async Task<IEnumerable<InitiativeDetailTreeGridModel>> GetInitiatives(InitiativeFilters filters, CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
var initiatives = await _initiativeService.GetInitiatives(filters, cancellationToken);
await _initiativeService.SetFilterSettings(userGuid, filters, cancellationToken);
return initiatives;
}
[HttpGet("{initiativeGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<InitiativeDetail>> GetById(Guid initiativeGuid, CancellationToken cancellationToken)
{
var initiative = await _initiativeService.GetById(initiativeGuid, cancellationToken);
return initiative.MapToDetail();
}
[HttpGet("{initiativeGuid}/[action]")]
public async Task<InitiativeInfo> GetInitiativeInfo(Guid initiativeGuid, CancellationToken cancellationToken)
=> await _initiativeService.GetInitiativeInfoAsync(initiativeGuid, cancellationToken);
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> Export(bool exportAll, [FromBody] InitiativeExportOptions filters, CancellationToken cancellationToken)
{
var workbook = await _initiativeService.ExportAsync(exportAll, User.GetUserGuid(), filters.SortOptions, filters.Filters, cancellationToken);
return new ExcelContentResult(workbook, "Initiatives");
}
[HttpGet("[action]")]
public async Task<IEnumerable<WorkstreamDto>> GetWorkstreams(CancellationToken cancellationToken)
{
return await _initiativeService.GetWorkstreams(cancellationToken);
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<InitiativeFilterOptions> GetFilterOptions(Guid configurationGuid, CancellationToken cancellationToken)
{
return await _initiativeService.GetFilterOptions(configurationGuid, cancellationToken);
}
[HttpGet("[action]")]
public async Task<InitiativeFilters> GetFilterSettings(CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
return await _initiativeService.GetFilterSettings(userGuid, cancellationToken);
}
[HttpPost("[action]")]
public async Task<InitiativeFilters> SetFilterSettings(InitiativeFilters filters, CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
return await _initiativeService.SetFilterSettings(userGuid, filters, cancellationToken);
}
[HttpPut("[action]")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> SetRampupInfo(InitiativeDto initiative, CancellationToken cancellationToken)
{
try
{
var found = await _initiativeService.GetById(initiative.InitiativeGuid, cancellationToken);
if (found == null)
{
return NotFound();
}
await _initiativeService.SetRampupInfo(initiative, cancellationToken);
return NoContent();
}
catch (Exception ex)
{
_logger.LogServiceError(ex, info: new Dictionary<string, object> { { nameof(initiative), initiative } });
return BadRequest();
}
}
[HttpPut("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<IActionResult> SetInitiativeRollupValue(InitiativeRollupValueInfo initiativeRollupValue, CancellationToken cancellationToken)
{
if (!await _initiativeService.ContainsAsync(initiativeRollupValue.InitiativeGuid, cancellationToken))
{
return NotFound();
}
if (!await _initiativeRollupColumnService.ContainsAsync(initiativeRollupValue.RollupId, cancellationToken))
{
return BadRequest();
}
await _initiativeService.SetRollupValue(initiativeRollupValue.InitiativeGuid, initiativeRollupValue.RollupId, initiativeRollupValue.RollupValueId, cancellationToken);
return Ok();
}
[ExcludeFromCodeCoverage]
public class InitiativeRollupValueInfo
{
public Guid InitiativeGuid { get; set; }
public int RollupId { get; set; }
public int RollupValueId { get; set; }
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult> CreateChargeCode(CCInitiativeDto initiative, CancellationToken cancellationToken)
{
var hub = _notificationHubContext.Clients.Group(initiative.Initiative.Name);
try
{
var dbGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
var isNameValid = await _allOpportunityService.NameIsValid(initiative.Initiative.ConfigurationGuid, (int)OpportunityTypes.ChargeCode, initiative.Initiative.Name,
initiative.Initiative.OpportunityGuid, cancellationToken);
if (dbGuid == Guid.Empty || !isNameValid)
{
return BadRequest();
}
await hub.SendOpportunityCreate(initiative.Initiative.Name);
if (initiative.Initiative.OpportunityGuid == Guid.Empty)
{
await _ccInitiativeJob.CreateChargeCodeInitiative(initiative.Initiative, initiative.NetworkOpportunityId, dbGuid, cancellationToken);
}
else
{
var opportunity = await _ccOpportunityService.GetById(initiative.Initiative.OpportunityGuid, cancellationToken);
// Save Initiative
var newInitiative = initiative.Initiative.MapToInitiative(_stakeholderUser);
var initiativeGuid = await _ccInitiativeService.CreateChargeCodeInitiative(dbGuid, newInitiative, opportunity.OpportunityGuid, cancellationToken);
return Ok(initiativeGuid);
}
return Ok();
}
catch (Exception ex)
{
_logger.LogServiceError(ex, info: new Dictionary<string, object> { { "initiativeName", initiative.Initiative.Name } });
await hub.SendOpportunityCreateError(initiative.Initiative.Name, ex.Message);
return BadRequest();
}
}
[ExcludeFromCodeCoverage]
public class CCInitiativeDto
{
public ChargeCodeInitiativeDto Initiative { get; set; }
public int? NetworkOpportunityId { get; set; }
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> UpdateChargeCode(CCInitiativeDto initiativeOpportunity, CancellationToken cancellationToken)
{
var hub = _notificationHubContext.Clients.Group(initiativeOpportunity.Initiative.Name);
try
{
var dbGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
var isNameValid = await _allOpportunityService.NameIsValid(initiativeOpportunity.Initiative.ConfigurationGuid, (int)OpportunityTypes.ChargeCode, initiativeOpportunity.Initiative.Name,
initiativeOpportunity.Initiative.OpportunityGuid, cancellationToken);
if (dbGuid == Guid.Empty || !isNameValid)
{
return BadRequest();
}
await hub.SendOpportunityUpdate(initiativeOpportunity.Initiative.Name);
await _ccInitiativeJob.UpdateChargeCodeInitiative(initiativeOpportunity.Initiative, initiativeOpportunity.NetworkOpportunityId, dbGuid, cancellationToken);
return Ok();
}
catch (Exception ex)
{
_logger.LogServiceError(ex, info: new Dictionary<string, object> { { "initiativeName", initiativeOpportunity.Initiative.Name } });
await hub.SendOpportunityCreateError(initiativeOpportunity.Initiative.Name, ex.Message);
return BadRequest();
}
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> CreateEncounter(EncounterInitiativeDto initiative, CancellationToken cancellationToken)
{
var hub = _notificationHubContext.Clients.Group(initiative.Name);
try
{
var dbGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
var isNameValid = await _allOpportunityService.NameIsValid(initiative.ConfigurationGuid, (int)OpportunityTypes.Encounter, initiative.Name,
Guid.Empty, cancellationToken);
if (dbGuid == Guid.Empty || !isNameValid)
{
return BadRequest();
}
await hub.SendOpportunityCreate(initiative.Name);
var opportunityWithFilters = initiative.MapToDataModel(User);
await _enOpportunityService.SetOpportunityFilters(opportunityWithFilters, cancellationToken);
var newOpportunity = opportunityWithFilters.MapToDataModel(User);
var isTempoPage = await _featureFlagServiceClient.IsEnabledAsync("enableencounteropportunityintempo", dbGuid, cancellationToken: cancellationToken);
var jobId = await _enOpportunityService.CreateAsync(newOpportunity, dbGuid, isTempoPage, cancellationToken);
await hub.SendInitiativeCreate(initiative.Name);
Initiative newInitiative = initiative.MapToInitiative(_stakeholderUser);
await _enInitiativeService.CreateInitiative(dbGuid, newInitiative, newOpportunity.OpportunityGuid, cancellationToken);
return newOpportunity.OpportunityGuid;
}
catch (Exception ex)
{
_logger.LogServiceError(ex, ex.Message);
return Guid.Empty;
}
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<EncounterInitiativeDto>> UpdateEncounter(EncounterInitiativeDto initiativeDto, CancellationToken cancellationToken)
{
var hub = _notificationHubContext.Clients.Group(initiativeDto.Name);
try
{
var dbGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
var isNameValid = await _allOpportunityService.NameIsValid(initiativeDto.ConfigurationGuid, (int)OpportunityTypes.Encounter, initiativeDto.Name,
initiativeDto.OpportunityGuid, cancellationToken);
if (dbGuid == Guid.Empty || !isNameValid)
{
return BadRequest();
}
var initiative = initiativeDto.MapToInitiative(_stakeholderUser);
if (initiativeDto.InitiativeGuid == Guid.Empty)
{
await hub.SendInitiativeCreate(initiativeDto.Name);
var newInitiativeGuid = await _enInitiativeService.CreateInitiative(dbGuid, initiative, initiativeDto.OpportunityGuid, cancellationToken);
initiativeDto.InitiativeGuid = newInitiativeGuid;
}
else
{
var updatedOpportunity = initiativeDto.MapToDataModel(User, false);
var jobId = await _enOpportunityService.UpdateAsync(dbGuid, updatedOpportunity, cancellationToken);
await _enInitiativeService.UpdateInitiative(initiative, updatedOpportunity.OpportunityGuid, cancellationToken);
}
return Ok(initiativeDto);
}
catch (Exception ex)
{
_logger.LogServiceError(ex, ex.Message);
return BadRequest();
}
}
/// <summary>
/// Hangfire job helper to wait for job completion and check success.
/// </summary>
/// <param name="jobId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<bool> JobFinishedSuccessfully(Guid jobId, CancellationToken cancellationToken)
{
// Wait for the job to finish
var result = await _hangfireService.GetJobInfoAsync(jobId, cancellationToken);
while (result.IsRunningStatus())
{
await Task.Delay(500, cancellationToken);
result = await _hangfireService.GetJobInfoAsync(jobId, cancellationToken);
}
return result.IsSuccessful();
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> CreateFreeForm(FreeFormInitiativeDto initiative, CancellationToken cancellationToken)
{
try
{
var dbGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
if (dbGuid == Guid.Empty)
{
return BadRequest();
}
var newOpportunity = initiative.MapToDataModel(User);
var opportunityGuid = await _ffOpportunityService.CreateAsync(newOpportunity, cancellationToken);
var newInitiative = initiative.MapToInitiative(_stakeholderUser);
await _ffInitiativeService.CreateInitiative(dbGuid, newInitiative, opportunityGuid, cancellationToken);
return newOpportunity.OpportunityGuid;
}
catch (Exception ex)
{
_logger.LogServiceError(ex, ex.Message);
return Guid.Empty;
}
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<FreeFormInitiativeDto>> UpdateFreeForm(FreeFormInitiativeDto initiativeDto, CancellationToken cancellationToken)
{
try
{
var dbGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
if (dbGuid == Guid.Empty)
{
return BadRequest();
}
var initiative = initiativeDto.MapToInitiative(_stakeholderUser);
if (initiativeDto.InitiativeGuid == Guid.Empty)
{
var newInitiativeGuid = await _ffInitiativeService.CreateInitiative(dbGuid, initiative, initiativeDto.OpportunityGuid, cancellationToken);
initiativeDto.InitiativeGuid = newInitiativeGuid;
}
else
{
var updatedOpportunity = initiativeDto.MapToOpportunity();
await _ffOpportunityService.UpdateAsync(updatedOpportunity, cancellationToken);
await _ffInitiativeService.UpdateAsync(initiative, cancellationToken);
}
return Ok(initiativeDto);
}
catch (Exception ex)
{
_logger.LogServiceError(ex, ex.Message);
return BadRequest(default(FreeFormInitiativeDto));
}
}
[HttpGet("{initiativeGuid}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult> Finalize(Guid initiativeGuid, CancellationToken cancellationToken)
{
try
{
await _initiativeService.FinalizeInitiative(initiativeGuid, cancellationToken);
if (!(await RunTracking(initiativeGuid, cancellationToken)))
{
throw new Exception($"Initiative tracking job did not complete successfully for initiative {initiativeGuid}.");
}
return Ok();
}
catch (Exception ex)
{
_logger.LogServiceError(ex);
return BadRequest();
}
}
[HttpPost("[action]")]
[ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> CreateGeneralLedger(GeneralLedgerInitiativeDto initiative, CancellationToken cancellationToken)
{
var opportunityGuid = await CreateOpportunityAndInitiativeWithJob(
initiative.MapToDataModel(User),
initiative.MapToInitiative(_stakeholderUser),
_glOpportunityService.CreateAsync,
_glInitiativeService.CreateInitiative,
OpportunityTypes.GeneralLedger.GetDisplayName(),
cancellationToken);
return CreatedAtAction(nameof(CreateGeneralLedger), null, opportunityGuid);
}
[HttpPost("[action]")]
[ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> CreatePayroll(PayrollInitiativeDto initiative, CancellationToken cancellationToken)
{
var opportunityGuid = await CreateOpportunityAndInitiativeWithJob(
initiative.MapToDataModel(User),
initiative.MapToInitiative(_stakeholderUser),
_prOpportunityService.CreateAsync,
_prInitiativeService.CreateInitiative,
OpportunityTypes.Payroll.GetDisplayName(),
cancellationToken);
return CreatedAtAction(nameof(CreatePayroll), null, opportunityGuid);
}
private async Task<ActionResult<Guid>> CreateOpportunityAndInitiativeWithJob<TOpportunityDataModel>(
TOpportunityDataModel newOpportunity,
Initiative initiative,
Func<TOpportunityDataModel, CancellationToken, Task<Guid>> createOpportunityFunc,
Func<Guid, Initiative, Guid, CancellationToken, Task<Guid>> createInitiativeFunc,
string opportunityType,
CancellationToken cancellationToken)
where TOpportunityDataModel : IOpportunity
{
ArgumentNullException.ThrowIfNull(initiative);
var dbGuid = User.GetDatabaseGuid();
if (dbGuid == Guid.Empty)
{
throw new Exception("Invalid database context.");
}
// Use the opportunity GUID from the data model
var opportunityGuid = newOpportunity.OpportunityGuid;
var jobId = await createOpportunityFunc(newOpportunity, cancellationToken);
// Result is a job ID - wait for completion
var jobFinishedSuccessfully = await JobFinishedSuccessfully(jobId, cancellationToken);
if (!jobFinishedSuccessfully)
{
throw new Exception($"{opportunityType} Opportunity creation job did not complete successfully.");
}
var initiativeGuid = await createInitiativeFunc(dbGuid, initiative, opportunityGuid, cancellationToken);
if (initiativeGuid == Guid.Empty)
{
throw new Exception($"{opportunityType} Initiative creation failed for opportunity {opportunityGuid}.");
}
return opportunityGuid;
}
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<IEnumerable<InitiativeCriteriaDto>> GetCriteria(CancellationToken cancellationToken)
{
return await _initiativeService.GetCriteria(cancellationToken);
}
[HttpGet("[action]/{initiativeGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<InitiativeScoresDto> GetScores(Guid initiativeGuid, CancellationToken cancellationToken)
{
return await _initiativeService.GetScores(initiativeGuid, cancellationToken);
}
[HttpPut("set-scores")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<bool> SetScores(InitiativeScoresDto scores, CancellationToken cancellationToken)
{
var result = await _initiativeService.SetScores(scores, cancellationToken);
return result;
}
/// <summary>
/// Runs tracking for an initiative and waits for completion.
/// </summary>
/// <param name="initiativeGuid"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<bool> RunTracking(Guid initiativeGuid, CancellationToken cancellationToken)
{
var jobId = await _initiativeService.RunTracking(initiativeGuid, cancellationToken);
var jobFinishedSuccessfully = await JobFinishedSuccessfully(jobId, cancellationToken);
if (!jobFinishedSuccessfully)
{
throw new Exception($"Initiative tracking job did not complete successfully for initiative {initiativeGuid}.");
}
return jobFinishedSuccessfully;
}
private static readonly List<int> SupportedOpportunityTypesForNameRetrieval =
[
(int)OpportunityTypes.GeneralLedger,
(int)OpportunityTypes.Payroll
];
/// <summary>
/// Retrieves the opportunity ID for a given initiative or opportunity name.
/// </summary>
/// <param name="name">The name to search for.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The opportunity GUID if found.</returns>
[HttpGet("[action]")]
[ProducesResponseType(typeof(Guid), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Guid>> GetOpportunityGuidByName([FromQuery] Guid configurationGuid,
[FromQuery] int opportunityType, [FromQuery] string name, CancellationToken cancellationToken)
{
if (configurationGuid == Guid.Empty) { return BadRequest("Invalid configuration GUID."); }
if (!SupportedOpportunityTypesForNameRetrieval.Contains(opportunityType)) { return BadRequest("Invalid opportunity type."); }
if (string.IsNullOrWhiteSpace(name)) { return BadRequest("Name parameter is required."); }
try
{
var initiative = await _initiativeService.FindInitiativeByPartialName(configurationGuid, name, cancellationToken);
if (initiative == null)
{
var opp = await _allOpportunityService.GetOpportunityByNamePrefix(configurationGuid, (OpportunityTypes)opportunityType, name, cancellationToken);
return opp == null
? throw new Exception($"No Initiative/Opportunity found with name: {name}")
: (ActionResult<Guid>)Ok(opp.OpportunityGuid);
}
var opportunity = await _allOpportunityService.GetByInitiativeId(initiative.InitiativeGUID, configurationGuid, (OpportunityTypes)opportunityType, cancellationToken);
return opportunity == null
? throw new Exception($"Opportunity not found for initiative: {initiative.InitiativeGUID}")
: (ActionResult<Guid>)Ok(opportunity.OpportunityGuid);
}
catch (Exception ex)
{
_logger.LogServiceError(ex, ex.Message);
return NotFound();
}
}
[ExcludeFromCodeCoverage]
public class InitiativeExportOptions
{
public SortOptions SortOptions { get; set; }
public InitiativeFilters Filters { get; set; }
}
}
}
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.QualityVariation.QVI;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers.JazzQvi
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/quality-variation-savings/non-qvi")]
public class NonQviSavingsController : ControllerBase
{
private readonly INonQviSavingsService _nonQviSavingsService;
public NonQviSavingsController(INonQviSavingsService nonQviSavingsService)
{
_nonQviSavingsService = nonQviSavingsService;
}
[HttpGet("case-type-families")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<NonQviCaseTypeFamilyInfo>>> GetCaseTypeFamilies(
[FromQuery] Guid configurationGuid,
[FromQuery] bool isBaseline = false,
CancellationToken cancellationToken = default)
{
if (configurationGuid == Guid.Empty)
{
return BadRequest("configurationGuid is required.");
}
var result = await _nonQviSavingsService.GetCaseTypeFamilyInfoAsync(
configurationGuid,
isBaseline,
cancellationToken);
return Ok(result);
}
[HttpGet("drg-types")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<NonQviDrgTypeInfo>>> GetDrgTypes(
[FromQuery] Guid configurationGuid,
[FromQuery] bool isBaseline = false,
CancellationToken cancellationToken = default)
{
if (configurationGuid == Guid.Empty)
{
return BadRequest("configurationGuid is required.");
}
var result = await _nonQviSavingsService.GetDrgTypeInfoAsync(
configurationGuid,
isBaseline,
cancellationToken);
return Ok(result);
}
}
}
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.QualityVariation.QVI;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers.JazzQvi
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/quality-variation-encounters")]
public class QviEncountersController : ControllerBase
{
private readonly IQviEncounterService _qviEncounterService;
public QviEncountersController(IQviEncounterService qviEncounterService)
{
_qviEncounterService = qviEncounterService;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<EncounterInfo>>> Get(
[FromQuery] Guid configurationGuid,
[FromQuery] DateTime startDate,
[FromQuery] DateTime endDate,
CancellationToken cancellationToken = default)
{
if (configurationGuid == Guid.Empty)
{
return BadRequest("configurationGuid is required.");
}
var result = await _qviEncounterService.GetEncounterInfoAsync(
configurationGuid,
startDate,
endDate,
cancellationToken);
return Ok(result);
}
}
}
@@ -0,0 +1,91 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.QualityVariation.QVI;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers.JazzQvi
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/quality-variation-savings/line-items")]
public class QviLineItemSavingsController : ControllerBase
{
private readonly IQviLineItemSavingsService _qviLineItemSavingsService;
public QviLineItemSavingsController(IQviLineItemSavingsService qviLineItemSavingsService)
{
_qviLineItemSavingsService = qviLineItemSavingsService;
}
[HttpGet("case-type-families")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<QviLineItemCaseTypeFamilyInfo>>> GetCaseTypeFamilies(
[FromQuery] Guid configurationGuid,
[FromQuery] bool isBaseline = false,
CancellationToken cancellationToken = default)
{
if (configurationGuid == Guid.Empty)
{
return BadRequest("configurationGuid is required.");
}
var result = await _qviLineItemSavingsService.GetCaseTypeFamilyInfoAsync(
configurationGuid,
isBaseline,
cancellationToken);
return Ok(result);
}
[HttpGet("drg-types")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<QviLineItemDrgTypeInfo>>> GetDrgTypes(
[FromQuery] Guid configurationGuid,
[FromQuery] bool isBaseline = false,
CancellationToken cancellationToken = default)
{
if (configurationGuid == Guid.Empty)
{
return BadRequest("configurationGuid is required.");
}
var result = await _qviLineItemSavingsService.GetDrgTypeInfoAsync(
configurationGuid,
isBaseline,
cancellationToken);
return Ok(result);
}
[HttpGet("encounter-savings")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<QviLineItemEncounterSavingInfo>>> GetEncounterSavings(
[FromQuery] Guid configurationGuid,
[FromQuery] Guid? opportunityGuid = null,
[FromQuery] bool? isBaseline = null,
CancellationToken cancellationToken = default)
{
if (configurationGuid == Guid.Empty)
{
return BadRequest("configurationGuid is required.");
}
var result = await _qviLineItemSavingsService.GetEncounterSavingInfoAsync(
configurationGuid,
opportunityGuid,
isBaseline,
cancellationToken);
return Ok(result);
}
}
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.MappedColumns;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class MappedColumnsController(
IMappedColumnService mappedColumnService
) : ControllerBase
{
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<MappedColumn>>> GetColumnList(string name, CancellationToken cancellationToken)
{
var result = await mappedColumnService.GetColumnList(name, cancellationToken);
return Ok(result);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> SetColumnList(ColumnListRequest columnListRequest, CancellationToken cancellationToken)
{
await mappedColumnService.SetColumnList(columnListRequest.Name, columnListRequest.Columns, cancellationToken);
return NoContent();
}
public class ColumnListRequest
{
public string Name { get; set; }
public IEnumerable<MappedColumn> Columns { get; set; }
}
}
}
@@ -0,0 +1,118 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Biz.Encounter.Opportunities.Filters;
using Strata.ContinuousImprovement.Biz.Network;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class NetworkOpportunitiesController : ControllerBase
{
private readonly INetworkOpportunityService _networkOpportunityService;
private readonly List<string> _creators;
private readonly ILogger<NetworkOpportunitiesController> _logger;
public NetworkOpportunitiesController(INetworkOpportunityService networkOpportunityService,
IOptions<NetworkOpportunitySection> sectionAccessor,
ILogger<NetworkOpportunitiesController> logger)
{
_networkOpportunityService = networkOpportunityService;
var networkOpportunity = sectionAccessor.Value ?? throw new ArgumentNullException(nameof(NetworkOpportunitySection));
_creators = networkOpportunity.Creators?.Distinct().ToList() ?? new List<string>();
_logger = logger;
var dict = new Dictionary<string, object> { { "service", nameof(NetworkOpportunitiesController) }, { "creators", _creators } };
using (_logger.BeginScope(dict))
_logger.LogInformation("Network Opportunity Creators");
}
[HttpGet("[action]")]
public async Task<bool> GetEnabled(CancellationToken cancellationToken)
{
var dict = new Dictionary<string, object> {
{ "service", nameof(NetworkOpportunitiesController) },
{ "creators", _creators },
{ "isSdtEmployee", User.IsSdtEmployee() }
};
using (_logger.BeginScope(dict))
_logger.LogInformation("Network Opportunity Enabled");
if (User.IsSdtEmployee()) { return true; }
var databaseGuid = ClaimsPrincipalExtensions.GetDatabaseGuid(User);
return await _networkOpportunityService.GetEnabledAsync(databaseGuid, cancellationToken);
}
[HttpGet("[action]")]
public async Task<bool> IsCreator()
{
var user = User?.Identity?.Name ?? "";
var isCreator = _creators.Any() && (_creators.Contains(user) || _creators.Any(x => x == "*"));
var dict = new Dictionary<string, object> {
{ "service", nameof(NetworkOpportunitiesController) },
{ "creators", _creators },
{ "user", user },
{ "isCreator", isCreator }
};
using (_logger.BeginScope(dict))
_logger.LogInformation("Network Opportunity IsCreator");
return isCreator;
}
[HttpPost("[action]")]
public async Task<PagedApiResponse<NetworkOpportunitySummary>> GetOpportunities(NetworkOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await _networkOpportunityService.GetOpportunitiesAsync(User, loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<NetworkOpportunity>> Create([FromBody] NetworkOpportunitySaveModel opportunitySaveModel, CancellationToken cancellationToken)
{
return await _networkOpportunityService.CreateAsync(opportunitySaveModel, cancellationToken);
}
[HttpPut("{networkOpportunityId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Update(int networkOpportunityId, NetworkOpportunitySaveModel opportunitySaveModel, CancellationToken cancellationToken)
{
if (networkOpportunityId != opportunitySaveModel.NetworkOpportunityId)
{
return BadRequest();
}
await _networkOpportunityService.UpdateAsync(opportunitySaveModel, cancellationToken);
return NoContent();
}
[HttpDelete("{networkOpportunityId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> Delete(int networkOpportunityId, CancellationToken cancellationToken)
{
return Ok(await _networkOpportunityService.DeleteAsync(networkOpportunityId, cancellationToken));
}
}
[ExcludeFromCodeCoverage]
public class NetworkOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public NetworkOpportunityFilters Filters { get; set; }
}
}
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.OpportunityTypeWorkbooks;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class OpportunityTypeWorkbooksController : ControllerBase
{
private readonly IOpportunityTypeWorkbookService _opportunityTypeWorkbookService;
public OpportunityTypeWorkbooksController(IOpportunityTypeWorkbookService opportunityTypeWorkbook)
{
_opportunityTypeWorkbookService = opportunityTypeWorkbook;
}
[HttpGet("{opportunityType}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<OpportunityTypeWorkbook>>> GetOpportunityTypeWorkbooks([FromRoute] int opportunityType, CancellationToken cancellationToken)
{
return Ok(await _opportunityTypeWorkbookService.GetOpportunityTypeWorkbooksAsync(opportunityType, cancellationToken));
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<Guid>> CreateOpportunityTypeWorkbooks([FromBody] IEnumerable<OpportunityTypeWorkbook> opportunityTypeWorkbooks, CancellationToken cancellationToken)
{
return Ok(await _opportunityTypeWorkbookService.CreateOpportunityTypeWorkbooksAsync(opportunityTypeWorkbooks, cancellationToken));
}
[HttpDelete("{opportunityType}/workbook/{workbookId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete([FromRoute] int opportunityType, [FromRoute] int workbookId, CancellationToken cancellationToken)
{
if (!await _opportunityTypeWorkbookService.ContainsAsync(opportunityType, workbookId, cancellationToken))
{
return NotFound();
}
await _opportunityTypeWorkbookService.DeleteAsync(opportunityType, workbookId, cancellationToken);
return NoContent();
}
}
}
@@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.OpportunityWorkbooks;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Dtos;
using Strata.CoreLib.Claims.Extensions;
using Strata.FeatureFlags.Client;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class OpportunityWorkbooksController : ControllerBase
{
private readonly IOpportunityWorkbookService _opportunityWorkbookService;
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
public OpportunityWorkbooksController(IOpportunityWorkbookService opportunityWorkbook, IFeatureFlagServiceClient featureFlagServiceClient)
{
_opportunityWorkbookService = opportunityWorkbook;
_featureFlagServiceClient = featureFlagServiceClient;
}
[HttpGet("{opportunityGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<OpportunityWorkbook>>> GetOpportunityWorkbooks([FromRoute] Guid opportunityGuid, CancellationToken cancellationToken)
{
return Ok(await _opportunityWorkbookService.GetOpportunityWorkbooksAsync(opportunityGuid, cancellationToken));
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<Guid>> CreateOpportunityWorkbooks([FromBody] IEnumerable<OpportunityWorkbook> opportunityWorkbooks, CancellationToken cancellationToken)
{
return Ok(await _opportunityWorkbookService.CreateOpportunityWorkbooksAsync(opportunityWorkbooks, cancellationToken));
}
[HttpDelete("{opportunityGuid}/workbook/{workbookId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete([FromRoute] Guid opportunityGuid, [FromRoute] int workbookId, CancellationToken cancellationToken)
{
if (!await _opportunityWorkbookService.ContainsAsync(opportunityGuid, workbookId, cancellationToken))
{
return NotFound();
}
await _opportunityWorkbookService.DeleteAsync(opportunityGuid, workbookId, cancellationToken);
return NoContent();
}
[HttpGet("strategic/{opportunityId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<StrategicOpportunityWorkbookDto>>> GetStrategicOpportunityWorkbooks([FromRoute] long opportunityId, CancellationToken cancellationToken)
{
var workbooks = await _opportunityWorkbookService.GetStrategicOpportunityWorkbooksAsync(opportunityId, cancellationToken);
if (workbooks == null)
{
return NotFound();
}
return Ok(workbooks);
}
[HttpPost("strategic")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<StrategicOpportunityWorkbookDto>>> CreateStrategicOpportunityWorkbooks([FromBody] IEnumerable<StrategicOpportunityWorkbook> opportunityWorkbooks, CancellationToken cancellationToken)
{
return Ok(await _opportunityWorkbookService.CreateStrategicOpportunityWorkbooksAsync(opportunityWorkbooks, cancellationToken));
}
[HttpDelete("strategic/{opportunityId}/workbook/{workbookId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete([FromRoute] long opportunityId, [FromRoute] int workbookId, CancellationToken cancellationToken)
{
if (!await _opportunityWorkbookService.ContainsStrategicAsync(opportunityId, workbookId, cancellationToken))
{
return NotFound();
}
await _opportunityWorkbookService.DeleteStrategicOpportunityAsync(opportunityId, workbookId, cancellationToken);
return NoContent();
}
[HttpGet("[action]")]
public async Task<bool> OpportunityWorkbooksEnabled(CancellationToken cancellationToken)
=> await _featureFlagServiceClient.IsEnabledAsync("ciopportunityworkbooks", User?.GetDatabaseGuid() ?? Guid.Empty, cancellationToken: cancellationToken);
}
}
@@ -0,0 +1,118 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.Dtos;
using Strata.ContinuousImprovement.Biz.Initiatives;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.Payroll.Opportunities;
using Strata.ContinuousImprovement.Biz.Payroll.Opportunities.Filters;
using Strata.CoreLib.Claims.Extensions;
using Strata.FeatureFlags.Client;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class PayrollOpportunitiesController : OpportunityController<PROpportunity>
{
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
public PayrollOpportunitiesController(IPROpportunityService opportunityService, IFeatureFlagServiceClient featureFlagServiceClient)
: base(opportunityService)
{
_featureFlagServiceClient = featureFlagServiceClient;
}
[HttpPost("")]
public async Task<PagedApiResponse<PROpportunity>> GetOpportunities(PROpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(PROpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((IPROpportunityService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "PR_Opportunities");
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<PRFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await ((IPROpportunityService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Guid>> Create(PROpportunitySaveModel opportunity, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
opportunity.AuthorGuid = User.GetUserGuid();
opportunity.AuthorName = User.Identity.Name;
var newOpportunityId = await ((IPROpportunityService)OpportunityService).CreateAsync(opportunity, cancellationToken);
return Ok(newOpportunityId);
}
[HttpDelete("{id}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<bool> Delete(Guid id, CancellationToken cancellationToken)
=> await OpportunityService.DeleteAsync<PROpportunityDataModel>(id, cancellationToken);
[HttpGet("{opportunityGuid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult<PROpportunityDto>> GetById(Guid opportunityGuid, CancellationToken cancellationToken)
{
var opportunity = await ((IPROpportunityService)OpportunityService).GetPROpportunityById(opportunityGuid, cancellationToken);
if (opportunity == null)
{
return NotFound();
}
return Ok(opportunity.Mapped());
}
[HttpGet("{opportunityGuid}/[action]")]
public async Task<ActionResult<InitiativeInfo>> GetInitiativeInfo(Guid opportunityGuid, CancellationToken cancellationToken)
{
return await ((IPROpportunityService)OpportunityService).GetInitiativeInfoAsync(opportunityGuid, cancellationToken);
}
[HttpGet("[action]")]
public async Task<bool> TempoPageEnabled(CancellationToken cancellationToken)
{
return await _featureFlagServiceClient.IsEnabledAsync("enablepayrollopportunityintempo", User?.GetDatabaseGuid() ?? Guid.Empty, cancellationToken: cancellationToken);
}
}
[ExcludeFromCodeCoverage]
public class PROpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public PRFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class PROpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public PRFilters Filters { get; set; }
}
}
@@ -0,0 +1,115 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.PeerGroups;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class PeerGroupController : ControllerBase
{
private readonly IPeerGroupService _peerGroupService;
public PeerGroupController(IPeerGroupService peerGroupService)
{
_peerGroupService = peerGroupService;
}
[HttpGet]
public async Task<IEnumerable<PeerGroupDto>> GetPeerGroupsAsync(CancellationToken cancellationToken)
{
return await _peerGroupService.GetAllAsync(cancellationToken);
}
[HttpGet("{peerGroupId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<PeerGroupDto>> GetByIdAsync([FromRoute] int peerGroupId, CancellationToken cancellationToken)
{
var peerGroup = await _peerGroupService.GetByIdAsync(peerGroupId, cancellationToken);
if (peerGroup != null)
{
return Ok(peerGroup);
}
return NotFound();
}
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public ActionResult<PeerGroupDropdownItems> GetDropdownItems()
{
var dropdownItems = _peerGroupService.GetDropdownItems();
if (dropdownItems != null)
{
return Ok(dropdownItems);
}
return NotFound();
}
[HttpPost("[action]/{peerGroupId}")]
public async Task<bool> IsNameValid([FromRoute] int peerGroupId, [FromBody] string name, CancellationToken cancellationToken)
{
return await _peerGroupService.IsNameValid(name, cancellationToken, peerGroupId);
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<ActionResult<bool>> Create(PeerGroupDto createInfo, CancellationToken cancellationToken)
{
if (!await _peerGroupService.IsNameValid(createInfo.Name, cancellationToken))
{
return BadRequest();
}
return await _peerGroupService.CreateAsync(createInfo, cancellationToken);
}
[HttpPut("{peerGroupId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<ActionResult<bool>> Update([FromRoute] int peerGroupId, [FromBody] PeerGroupDto dto, CancellationToken cancellationToken)
{
if (!await _peerGroupService.IsNameValid(dto.Name, cancellationToken, peerGroupId))
{
return BadRequest();
}
if (!await _peerGroupService.ContainsAsync(peerGroupId, cancellationToken))
{
return NotFound();
}
return await _peerGroupService.UpdateAsync(peerGroupId, dto, cancellationToken);
}
[HttpDelete("{peerGroupId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.IsAdmin)]
public async Task<IActionResult> Delete([FromRoute] int peerGroupId, CancellationToken cancellationToken)
{
if (!await _peerGroupService.ContainsAsync(peerGroupId, cancellationToken))
{
return NotFound();
}
await _peerGroupService.DeleteAsync(peerGroupId, cancellationToken);
return NoContent();
}
}
}
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.PickerItems;
using Strata.ContinuousImprovement.Biz.Population;
using Strata.ContinuousImprovement.Biz.Shared;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class PickerItemsController : ControllerBase
{
private readonly IPickerItemService _pickerItemService;
public PickerItemsController(IPickerItemService pickerItemService)
{
_pickerItemService = pickerItemService;
}
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<PickerItem>>> GetExplorations(CancellationToken cancellationToken)
{
var items = await _pickerItemService.GetItems<ExplorationPopulation>((item) => new PickerItem() { Text = item.Name, Value = item.ExplorationPopulationId }, cancellationToken);
return Ok(items);
}
}
}
@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.QualityVariation.Breakdowns;
using Strata.ContinuousImprovement.Biz.QualityVariation.Breakdowns.Filters;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class QualityVariationItemBreakdownsController : ControllerBase
{
private readonly IQVIBreakdownService _qviBreakdownService;
public QualityVariationItemBreakdownsController(IQVIBreakdownService qviBreakdownService)
{
_qviBreakdownService = qviBreakdownService;
}
[HttpPost("")]
public async Task<PagedApiResponse<QVIBreakdown>> GetBreakdowns(QVIBreakdownsLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await _qviBreakdownService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<QVIBreakdownFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await _qviBreakdownService.GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
}
[ExcludeFromCodeCoverage]
public class QVIBreakdownsLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public QVIBreakdownFilters Filters { get; set; }
}
}
@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.QualityVariation.Events;
using Strata.ContinuousImprovement.Biz.QualityVariation.Events.Filters;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class QualityVariationItemEventsController : ControllerBase
{
private readonly IQVIEventService _qviEventService;
public QualityVariationItemEventsController(IQVIEventService qviEventService)
{
_qviEventService = qviEventService;
}
[HttpPost("")]
public async Task<PagedApiResponse<QVIEvent>> GetEvents(QVIEventsLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await _qviEventService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<QVIEventFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await _qviEventService.GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
}
[ExcludeFromCodeCoverage]
public class QVIEventsLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public QVIEventFilters Filters { get; set; }
}
}
@@ -0,0 +1,83 @@
using Hangfire;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.QualityVariation.Jobs;
using Strata.ContinuousImprovement.Biz.QualityVariation.Opportunities;
using Strata.ContinuousImprovement.Biz.QualityVariation.Opportunities.Filters;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class QualityVariationItemOpportunitiesController : OpportunityController<QVIOpportunity>
{
private readonly IBackgroundJobClient _backgroundJobClient;
public QualityVariationItemOpportunitiesController(
IQVIOpportunityService opportunityService,
IBackgroundJobClient backgroundJobClient)
: base(opportunityService)
{
_backgroundJobClient = backgroundJobClient;
}
[HttpPost("")]
public async Task<PagedApiResponse<QVIOpportunity>> GetOpportunities(QVIOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(QVIOpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((IQVIOpportunityService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "QVI_Opportunities");
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<QVIFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await ((IQVIOpportunityService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public IActionResult PopulateQviMetrics(Guid configurationGuid, bool isBaseline, CancellationToken cancellationToken)
{
if (configurationGuid == Guid.Empty)
{
return BadRequest("Invalid request. ConfigurationGuid is required.");
}
var jobId = _backgroundJobClient.Enqueue<QviOpportunityMetricsJob>(job =>
job.PopulateQviMetricsAsync(configurationGuid, isBaseline, cancellationToken));
return Ok(jobId);
}
}
[ExcludeFromCodeCoverage]
public class QVIOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public QVIFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class QVIOpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public QVIFilters Filters { get; set; }
}
}
@@ -0,0 +1,99 @@
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.DbContexts.Reports;
using Strata.ContinuousImprovement.Biz.Reports;
using Strata.CoreLib.Claims.Extensions;
using Strata.Stratasphere.Biz.DbContexts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ReportController : Controller
{
private readonly IReportService _reportService;
public ReportController(IReportService reportService)
{
_reportService = reportService;
}
[HttpGet("{sourceOfSavings}/[action]")]
public async Task<IEnumerable<REReport>> GetByModule(int sourceOfSavings, CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
return await _reportService.GetReportsByModule(sourceOfSavings, userGuid, User.IsSdtEmployee(), cancellationToken);
}
[HttpGet("{opportunityGuid:guid}/[action]")]
public async Task<IEnumerable<REReport>> GetByOpportunity(Guid opportunityGuid, CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
return await _reportService.GetReportsByOpportunity(opportunityGuid, userGuid, User.IsSdtEmployee(), cancellationToken);
}
[HttpPost("[action]")]
public async Task<IEnumerable<REReport>> GetReports(ReportRequest request, CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
var moduleRpts = await _reportService.GetReportsByModule(request.SourceOfSavings, userGuid, User.IsSdtEmployee(), cancellationToken);
var opportunityRpts = await _reportService.GetReportsByOpportunity(request.OpportunityGuid, userGuid, User.IsSdtEmployee(), cancellationToken);
return moduleRpts.Union(opportunityRpts);
}
[HttpGet("[action]")]
public async Task<IEnumerable<REReport>> GetReports(CancellationToken cancellationToken)
{
var userGuid = User?.GetUserGuid() ?? Guid.Empty;
return await _reportService.GetReports(userGuid, User.IsSdtEmployee(), cancellationToken);
}
[HttpPost("[action]")]
public async Task AddByOpportunity(ReportByOpportunity reportRequest, CancellationToken cancellationToken)
=> await _reportService.AddReportByOpportunity(reportRequest.OpportunityGuid, reportRequest.ReportGuid, cancellationToken);
[HttpPost("[action]")]
public async Task AddByModule(ReportByModule reportRequest, CancellationToken cancellationToken)
=> await _reportService.AddReportByModule(reportRequest.SourceOfSavings, reportRequest.ReportGuid, cancellationToken);
[HttpDelete("[action]")]
public async Task RemoveByOpportunity(ReportByOpportunity reportRequest, CancellationToken cancellationToken)
=> await _reportService.DeleteReportByOpportunity(reportRequest.OpportunityGuid, reportRequest.ReportGuid, cancellationToken);
[HttpDelete("[action]")]
public async Task RemoveByModule(ReportByModule reportRequest, CancellationToken cancellationToken)
=> await _reportService.DeleteReportByModule(reportRequest.SourceOfSavings, reportRequest.ReportGuid, cancellationToken);
[HttpGet("[action]")]
public async Task<FolderTree> GetFolders(CancellationToken cancellationToken)
=> await _reportService.GetReportFolders(cancellationToken);
[HttpGet("{folderGuid:guid}[action]")]
public async Task<IEnumerable<REReport>> GetFolderReports(Guid folderGuid, CancellationToken cancellationToken)
=> await _reportService.GetFolderReports(folderGuid, cancellationToken);
public class ReportRequest
{
public Guid OpportunityGuid { get; set; }
public int SourceOfSavings { get; set; }
}
public class ReportByOpportunity
{
public Guid ReportGuid { get; set; }
public Guid OpportunityGuid { get; set; }
}
public class ReportByModule
{
public Guid ReportGuid { get; set; }
public int SourceOfSavings { get; set; }
}
}
}
@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.Authorization;
using Strata.ContinuousImprovement.Biz.Security;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class SecurityController : ControllerBase
{
private readonly ISimpleSecurityService _securityService;
private readonly IAuthorizationServiceClient _authorizationServiceClient;
private readonly Guid CIGeneralAccessLootId = Guid.Parse("33F10831-E42E-4D48-A6FA-A110798B4F63");
public SecurityController(ISimpleSecurityService securityService,
IAuthorizationServiceClient authorizationServiceClient)
{
_securityService = securityService;
_authorizationServiceClient = authorizationServiceClient;
}
[HttpGet("[action]")]
public async Task<bool> GetGeneralAccessPermission(CancellationToken cancellationToken)
=> (await _authorizationServiceClient.GetPermissionAsync(CIGeneralAccessLootId, cancellationToken)).CanRead;
[HttpGet("{lootId}")]
public async Task<Permission> GetUserPermissions(Guid lootId, CancellationToken cancellationToken)
=> await _authorizationServiceClient.GetPermissionAsync(lootId, cancellationToken);
[HttpGet("[action]")]
public async Task<IEnumerable<IdentityNode>> GetIdentities(CancellationToken cancellationToken)
=> await _securityService.GetAllIdentities(cancellationToken);
[HttpGet("{lootId}/[action]")]
public async Task<IEnumerable<Permission>> GetSimpleSecurities(Guid lootId, CancellationToken cancellationToken)
=> await _authorizationServiceClient.GetUserPermissionsAsync(lootId, cancellationToken);
[HttpPut("{lootId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<bool>> SetSimpleSecurities(Guid lootId, [FromBody] IEnumerable<Permission> permissionSettings, CancellationToken cancellationToken)
{
if (permissionSettings.Any(ps => !ps.LootId.HasValue || ps.LootId.Value != lootId)) { return BadRequest(); }
var isSecurable = await _authorizationServiceClient.IsSecurable(lootId, cancellationToken);
var userGuid = User?.GetUserGuid() ?? Guid.NewGuid();
if (!User.IsSdtEmployee() && !isSecurable) return Ok(false);
await _authorizationServiceClient.ReplaceUserPermissionsAsync(lootId, permissionSettings, cancellationToken);
return Ok(true);
}
}
}
@@ -0,0 +1,158 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Strata.ContinuousImprovement.Biz.ChargeCode;
using Strata.ContinuousImprovement.Biz.FiscalMonths;
using Strata.ContinuousImprovement.Biz.PerformanceEngine;
using Strata.ContinuousImprovement.Biz.Settings;
using Strata.ContinuousImprovement.Biz.Shared;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.CoreLib.Claims.Extensions;
using Strata.DataSchema.Client;
using Strata.FeatureFlags.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class SettingsController : Controller
{
private readonly ISettingsService _settingsService;
private ChargeCodeOpportunityValidationSection _validationSection;
private readonly IDataSchemaService _dataSchemaService;
private readonly IFeatureFlagServiceClient _featureFlagServiceClient;
public SettingsController(ISettingsService settingsService,
IOptions<ChargeCodeOpportunityValidationSection> sectionAccessor,
IDataSchemaService dataSchemaService,
IFeatureFlagServiceClient featureFlagServiceClient)
{
_settingsService = settingsService;
_validationSection = sectionAccessor.Value ?? throw new ArgumentNullException(nameof(ChargeCodeOpportunityValidationSection));
_dataSchemaService = dataSchemaService;
_featureFlagServiceClient = featureFlagServiceClient;
}
[HttpGet("[action]")]
public async Task<Dictionary<string, bool>> GetFeatureFlagsAsync()
{
var databaseGuid = User?.GetDatabaseGuid() ?? Guid.Empty;
var featureFlags = await _featureFlagServiceClient.GetFeatureFlagClientStatesByArea(databaseGuid, "CI", CancellationToken.None);
return featureFlags.ToDictionary(key => key.Key.Replace("-", ""), val => val.Enabled);
}
[HttpPost("[action]")]
public async Task<Dictionary<string, string>> GetSystemSettings([FromBody] List<string> settings, CancellationToken cancellationToken)
{
var systemSettings = await _settingsService.GetSystemSettings(settings, cancellationToken);
return systemSettings;
}
[HttpGet("[action]")]
public int GetOpportunityLevelLimit()
{
return _validationSection.MaxOpportunityLevelSelections;
}
[HttpGet("[action]")]
public int GetChargeCodeLimit()
{
return _validationSection.MaxChargeCodeSelections;
}
[HttpGet("[action]")]
public PerformanceEngingeCalculationData GetPerformanceEngineCalculations()
{
// this is stubbed with test data taken from the Performance Engine tests to populate the calculations page
// we will need to modify this to take inputs from the wizards and return a PerformanceTrackingItem
var currentYear = DateTime.Now.Year;
double[] goalUnits = { 13221, 17628, 22035, 22035, 8814, 26442, 8814, 13221, 22035, 17628, 30849, 44070 };
double[] baselineUnits = { 12231, 16788, 21605, 21305, 8430, 24648, 8172, 11847, 24535, 16704, 26159, 45000 };
double[] baselineStats = { 3, 4, 5, 5, 2, 6, 2, 3, 5, 4, 7, 10 };
double[] trackingUnits = { 54314, 8798, 31680, 21415, 25895, 0, 0, 0, 0, 0, 0, 0 };
double[] trackingStats = { 13, 2, 8, 5, 5, 0, 0, 0, 0, 0, 0, 0 };
MonthlyRampUp[] rampUps =
{
new MonthlyRampUp(1, currentYear, 100), new MonthlyRampUp(2, currentYear, 100), new MonthlyRampUp(3, currentYear, 100), new MonthlyRampUp(4, currentYear, 100),
new MonthlyRampUp(5, currentYear, 100),new MonthlyRampUp(6, currentYear, 100),new MonthlyRampUp(7, currentYear, 100),new MonthlyRampUp(8, currentYear, 100),
new MonthlyRampUp(9, currentYear, 100),new MonthlyRampUp(10, currentYear, 100),new MonthlyRampUp(11, currentYear, 100),new MonthlyRampUp(12, currentYear, 100)
};
var trackingYearMonths = Enumerable.Range(1, 12).ToList()
.ConvertAll(i =>
{
var ym = new TrackingYearMonth(currentYear, currentYear,
new FiscalMonth(Guid.NewGuid(), (byte)i, (byte)i, $"{i}", "", "", 0, 0, $"Month{i:00}", 0, 0), true, true, true);
return ym;
}).ToList();
var performanceBaselineData = PerformanceComparisonData.SetupBaselineDataList(baselineUnits, baselineStats, currentYear - 1);
var performanceTrackingData = PerformanceComparisonData.SetupTrackingDataList(goalUnits, trackingUnits, trackingStats, currentYear);
var group = new PerformanceGroup(1, "testCalculations");
var options = new PerformanceOptions(false, true, true);
var performanceTrackingItem = new PerformanceTrackingItem(group, rampUps, options, performanceBaselineData, performanceTrackingData, trackingYearMonths);
var data = new PerformanceEngingeCalculationData(performanceTrackingItem);
return data;
}
}
public class PerformanceEngingeCalculationData
{
// inputs
public string entity { get; set; }
public string caseTypeEntity { get; set; }
public int serviceLineID { get; set; }
public int physicianGroupingAffiliationId { get; set; }
public string primaryPhysician { get; set; }
public string chargeCodeRollup { get; set; }
public string costDriver { get; set; }
public string baselineStartDate { get; set; }
public string baselineEndDate { get; set; }
// numbers fed into calcs
public double totalVolume { get; set; }
public double totalGrossCharges { get; set; }
public double totalVariableDirectCost { get; set; }
public double totalVariableCost { get; set; }
// calculated results
public double goalUnitVariance { get; set; }
public double actualUnitVariance { get; set; }
public double committedUnits { get; set; }
public double committedStats { get; set; }
public double baselineAverage { get; set; }
public PerformanceEngingeCalculationData(PerformanceTrackingItem item)
{
entity = "n/a";
caseTypeEntity = "n/a";
serviceLineID = -1;
physicianGroupingAffiliationId = -1;
primaryPhysician = "n/a";
chargeCodeRollup = "n/a";
costDriver = "n/a";
baselineStartDate = DateTime.Now.ToShortDateString();
baselineEndDate = DateTime.Now.ToShortDateString();
totalVolume = -1;
totalGrossCharges = -1;
totalVariableDirectCost = -1;
totalVariableCost = -1;
goalUnitVariance = item.MonthlyTrackingData.Sum(month => month.GoalUnitVariance);
actualUnitVariance = item.MonthlyTrackingData.Sum(month => month.ActualUnitVariance);
committedUnits = item.MonthlyTrackingData.Sum(month => month.CommittedUnits);
committedStats = item.MonthlyTrackingData.Sum(month => month.CommittedStats);
baselineAverage = item.MonthlyTrackingData.Average(month => month.BaselineAverage);
}
}
}
@@ -0,0 +1,132 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Dtos;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class StrategicChargeCodeController : ControllerBase
{
private readonly IStrategicOpportunityService _strategicOpportunityService;
private readonly IStrategicChargeCodeOpportunityDetailService _strategicChargeCodeOpportunityDetailService;
public StrategicChargeCodeController(IStrategicOpportunityService strategicOpportunityService, IStrategicChargeCodeOpportunityDetailService strategicChargeCodeOpportunityDetailService)
{
_strategicOpportunityService = strategicOpportunityService;
_strategicChargeCodeOpportunityDetailService = strategicChargeCodeOpportunityDetailService;
}
[HttpGet("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityChargeCodeEditData>> GetOpportunityChargeCode(long opportunityId, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.GetStrategicOpportunityChargeCode(opportunityId, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<long>> CreateOpportunityChargeCode([FromBody] StrategicOpportunityChargeCodeDto opportunity, CancellationToken cancellationToken)
{
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, cancellationToken);
if (opportunityId == 0) return BadRequest();
return Ok(opportunityId);
}
[HttpPut("{opportunityId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdateOpportunityChargeCode(long opportunityId, [FromBody] StrategicOpportunityChargeCodeUpdateDto opportunity, CancellationToken cancellationToken)
{
var strategicOpportunityId = await _strategicOpportunityService.UpdateOpportunityChargeCode(opportunityId, opportunity, cancellationToken);
return strategicOpportunityId != 0 ? Ok(true) : NotFound();
}
[HttpDelete("{opportunityId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> DeleteOpportunityChargeCode(long opportunityId, CancellationToken cancellationToken)
{
if (!await _strategicOpportunityService.ContainsAsync(opportunityId, cancellationToken))
return NotFound();
var isDeleted = await _strategicOpportunityService.DeleteChargeCodeOpportunityAsync(opportunityId, cancellationToken);
return isDeleted ? NoContent() : BadRequest();
}
[HttpPost("{opportunityId}/goal-detail")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityDetailData>> GetGoalDetail(long opportunityId, [FromBody] StrategicOpportunityGetDetailData getData, CancellationToken cancellationToken)
{
var result = await _strategicChargeCodeOpportunityDetailService.GetStrategicOpportunityDetailData(opportunityId, false, getData, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpPost("{opportunityId}/export")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ExportInitiative(long opportunityId, [FromBody] StrategicOpportunityGetDetailData getData, CancellationToken cancellationToken)
{
var workbook = await _strategicChargeCodeOpportunityDetailService.ExportInitiativeAsync(opportunityId, true, getData, cancellationToken);
if (workbook == null)
return NotFound();
return new ExcelContentResult(workbook, $"Strategic Initiative Charge Code {opportunityId} Export");
}
[HttpPost("{opportunityId}/export-opportunity")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ExportOpportunity(long opportunityId, [FromBody] StrategicOpportunityGetDetailData getData, CancellationToken cancellationToken)
{
var workbook = await _strategicChargeCodeOpportunityDetailService.ExportOpportunityAsync(opportunityId, getData, cancellationToken);
if (workbook == null)
return NotFound();
return new ExcelContentResult(workbook, $"Strategic Opportunity {opportunityId} Export");
}
[HttpPost("{opportunityId}/tracking-detail")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityDetailData>> GetTrackingDetail(long opportunityId, [FromBody] StrategicOpportunityGetDetailData getData, CancellationToken cancellationToken)
{
var result = await _strategicChargeCodeOpportunityDetailService.GetStrategicOpportunityDetailData(opportunityId, true, getData, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpPut("{opportunityId}/details")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult> UpdateDetails(long opportunityId, [FromBody] UpdateDetailsDto dto, CancellationToken cancellationToken)
{
if (dto == null || dto.Adjustments?.Any() != true || dto.Rows?.Any() != true)
return BadRequest();
await _strategicChargeCodeOpportunityDetailService.UpdateStrategicOpportunityDetailsData(opportunityId, dto.Adjustments, dto.Rows, cancellationToken);
return Ok();
}
public class UpdateDetailsDto
{
public IEnumerable<StrategicOpportunityChargeCodeAdjustment> Adjustments { get; set; }
public IEnumerable<string> Rows { get; set; }
}
}
}
@@ -0,0 +1,102 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Dtos;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class StrategicGeneralLedgerController : ControllerBase
{
private readonly IStrategicOpportunityService _strategicOpportunityService;
private readonly IStrategicGLOpportunityDetailService _strategicGLOpportunityDetailService;
public StrategicGeneralLedgerController(IStrategicOpportunityService strategicOpportunityService, IStrategicGLOpportunityDetailService strategicGLOpportunityDetailService)
{
_strategicOpportunityService = strategicOpportunityService;
_strategicGLOpportunityDetailService = strategicGLOpportunityDetailService;
}
[HttpGet("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityGLEditData>> GetOpportunityGL(long opportunityId, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.GetStrategicOpportunityGL(opportunityId, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<long>> CreateOpportunityGeneralLedger([FromBody] StrategicOpportunityGLDto opportunity, CancellationToken cancellationToken)
{
var opportunityId = await _strategicOpportunityService.CreateOpportunityGeneralLedgerAsync(opportunity, cancellationToken);
if (opportunityId == 0) return BadRequest();
return Ok(opportunityId);
}
[HttpPut("{opportunityId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdateOpportunityGeneralLedger(long opportunityId, [FromBody] StrategicOpportunityGLUpdateDto opportunity, CancellationToken cancellationToken)
{
var strategicOpportunityId = await _strategicOpportunityService.UpdateOpportunityGeneralLedger(opportunityId, opportunity, cancellationToken);
return strategicOpportunityId != 0 ? Ok(true) : NotFound();
}
[HttpDelete("{opportunityId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> DeleteOpportunityGeneralLedger(long opportunityId, CancellationToken cancellationToken)
{
if (!await _strategicOpportunityService.ContainsGLAsync(opportunityId, cancellationToken))
return NotFound();
var isDeleted = await _strategicOpportunityService.DeleteGeneralLedgerOpportunityAsync(opportunityId, cancellationToken);
return isDeleted ? NoContent() : BadRequest();
}
[HttpPost("{opportunityId}/detail")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityGLDetailData>> GetGLDetail(long opportunityId, [FromBody] string[] dimensions, CancellationToken cancellationToken)
{
var result = await _strategicGLOpportunityDetailService.GetStrategicOpportunityGLDetailData(opportunityId, false, dimensions, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpPost("{opportunityId}/tracking-detail")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityGLDetailData>> GetGLTrackingDetail(long opportunityId, [FromBody] string[] dimensions, CancellationToken cancellationToken)
{
var result = await _strategicGLOpportunityDetailService.GetStrategicOpportunityGLDetailData(opportunityId, true, dimensions, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpPut("{opportunityId}/details")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public async Task<ActionResult> UpdateGLDetails(long opportunityId, [FromBody] UpdateGLDetailsDto dto, CancellationToken cancellationToken)
{
if (dto == null || dto.Adjustments?.Any() != true || dto.Rows?.Any() != true)
return BadRequest();
await _strategicGLOpportunityDetailService.UpdateStrategicOpportunityGLDetailsData(opportunityId, dto.Adjustments, dto.Rows, cancellationToken);
return Ok();
}
}
}
@@ -0,0 +1,190 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Dtos;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class StrategicOpportunityController : ControllerBase
{
private readonly IStrategicOpportunityService _strategicOpportunityService;
public StrategicOpportunityController(IStrategicOpportunityService strategicOpportunityService)
{
_strategicOpportunityService = strategicOpportunityService;
}
[HttpGet("")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<List<StrategicOpportunityItem>> GetOpportunities(CancellationToken cancellationToken)
{
return await _strategicOpportunityService.GetStrategicOpportunitiesAsync(cancellationToken);
}
[HttpGet("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityGeneralInfo>> GetGeneralInfo(long opportunityId, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.GetStrategicOpportunityGeneralInfo(opportunityId, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpPost("[action]")]
public async Task<bool> IsNameValid([FromBody] string name, CancellationToken cancellationToken)
{
return await _strategicOpportunityService.NameIsValidAsync(name, cancellationToken);
}
[HttpGet("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityLayout>> GetLayout([FromRoute] long opportunityId, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.GetStrategicOpportunityLayout(opportunityId, cancellationToken);
return result;
}
[HttpPut("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdateLayout(long opportunityId, [FromBody] StrategicOpportunityLayout layout, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.UpdateStrategicOpportunityLayout(opportunityId, layout, cancellationToken);
return result ? Ok(result) : NotFound();
}
[HttpPut("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> SetStatus(long opportunityId, [FromBody] StrategicOpportunityUpdateStatusDto updateStatusDto, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.UpdateStrategicOpportunityStatus(opportunityId, updateStatusDto, cancellationToken);
return result ? Ok(result) : NotFound();
}
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<List<StrategicInitiativeItem>> GetInitiatives(CancellationToken cancellationToken)
{
return await _strategicOpportunityService.GetStrategicInitiativesAsync(cancellationToken);
}
[HttpPut("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdateInitiative(long opportunityId, [FromBody] StrategicInitiativeUpdateDto updateInitiativeDto, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.UpdateStrategicInitiative(opportunityId, updateInitiativeDto, cancellationToken);
return result ? Ok(result) : NotFound();
}
[HttpPut("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdateRoles(long opportunityId, [FromBody] StrategicRolesUpdate updateRolesDto, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.UpdateRoles(opportunityId, updateRolesDto, cancellationToken);
return result ? Ok(result) : NotFound();
}
[HttpPut("{opportunityId}/ramp-ups")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> UpdateRampUps(long opportunityId, [FromBody] List<StrategicOpportunityRampUpDto> rampUps, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.UpdateStrategicOpportunityRampUpsAsync(opportunityId, rampUps, cancellationToken);
return result ? Ok(result) : NotFound();
}
[HttpGet("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<StrategicOpportunityRampUpDto>>> GetRampUps(long opportunityId, CancellationToken cancellationToken)
{
var rampUps = await _strategicOpportunityService.GetStrategicOpportunityRampUpsAsync(opportunityId, cancellationToken);
return Ok(rampUps);
}
[HttpGet("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> RunTrackingTask(long opportunityId, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.RunTrackingTask(opportunityId, cancellationToken);
return result ? Ok(result) : NotFound();
}
[HttpPut("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<bool>> CompleteInitiative(long opportunityId, [FromBody] StrategicOpportunityCompleteInitiativeData completeInitiativeData, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.CompleteInitiative(opportunityId, completeInitiativeData, cancellationToken);
return result ? Ok(result) : NotFound();
}
[HttpGet("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<StrategicOpportunityMeasureDto>>> GetMeasures(long opportunityId, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.GetStrategicOpportunityMeasures(opportunityId, cancellationToken);
return result.Any() ? Ok(result) : NotFound();
}
[HttpGet("{opportunityId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityMeasureDto>> GetPrimaryMeasure(long opportunityId, CancellationToken cancellationToken)
{
var primaryMeasure = await _strategicOpportunityService.GetStrategicOpportunityPrimaryMeasure(opportunityId, cancellationToken);
return Ok(primaryMeasure);
}
[HttpGet("{measureId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<StrategicOpportunityMeasureDetail>>> GetMeasureDetails(long measureId, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.GetStrategicOpportunityMeasureDetails(measureId, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpGet("{opportunityId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StrategicOpportunityDrawerDetails>> GetDrawerDetails(long opportunityId, CancellationToken cancellationToken)
{
var result = await _strategicOpportunityService.GetStrategicOpportunityDrawerDetails(opportunityId, cancellationToken);
return result != null ? Ok(result) : NotFound();
}
[HttpPost("{opportunityId}/copy")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<long>> Copy(long opportunityId, [FromBody] string opportunityName, CancellationToken cancellationToken)
{
var newOpportunityId = await _strategicOpportunityService.CopyStrategicOpportunity(opportunityId, opportunityName, cancellationToken);
return newOpportunityId == 0 ? BadRequest() : Ok(newOpportunityId);
}
}
}
@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.OpportunityRoleAssignment;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities;
using Strata.CoreLib.Claims.Extensions;
using System.Collections.Generic;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class StrategicOpportunitySecurityController : ControllerBase
{
private readonly IStrategicOpportunityService _strategicOpportunityService;
private readonly IOpportunityRoleAssignmentService _opportunityRoleAssignmentService;
public StrategicOpportunitySecurityController(IStrategicOpportunityService strategicOpportunityService, IOpportunityRoleAssignmentService opportunityRoleAssignmentService)
{
_strategicOpportunityService = strategicOpportunityService;
_opportunityRoleAssignmentService = opportunityRoleAssignmentService;
}
[HttpGet("{lootId}")]
public async Task<RoleAssignmentView> GetUserPermissions(long lootId, CancellationToken cancellationToken)
=> await _opportunityRoleAssignmentService.GetUserRoleAssignmentsAsync(lootId, cancellationToken);
[HttpGet("{lootId}/[action]")]
public async Task<IEnumerable<OpportunityRoleAssignmentView>> GetSimpleSecurities(long lootId, CancellationToken cancellationToken)
=> await _opportunityRoleAssignmentService.GetRoleAssignmentsAsync(lootId, cancellationToken);
[HttpPut("{lootId}/[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<bool>> SetSimpleSecurities(long lootId, [FromBody] List<OpportunityRoleAssignmentView> permissionSettings, CancellationToken cancellationToken)
{
if (permissionSettings.Any(ps => ps.LootId != lootId)) { return BadRequest(); }
var roleAssignments = await _opportunityRoleAssignmentService.GetUserRoleAssignmentsAsync(lootId, cancellationToken);
var isSecurable = roleAssignments.CanSecure;
var userGuid = User?.GetUserGuid() ?? Guid.NewGuid();
if (!User.IsSdtEmployee() && !isSecurable) return Ok(false);
await _opportunityRoleAssignmentService.SaveRoleAssignmentsAsync(lootId, permissionSettings, cancellationToken);
return Ok(true);
}
}
}
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.UserSetting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class UserSettingsController : ControllerBase
{
private readonly IUserSettingService _userSettingService;
public UserSettingsController(IUserSettingService userSettingService)
{
_userSettingService = userSettingService;
}
[HttpGet("{userGuid}")]
public async Task<CciUserSetting> GetUserSetting(Guid userGuid, CancellationToken cancellationToken)
{
return await _userSettingService.GetById(userGuid, cancellationToken);
}
[HttpPost()]
public async Task<CciUserSetting> SetUserSetting([FromBody] CciUserSetting userSetting, CancellationToken cancellationToken)
{
return await _userSettingService.SetUserSetting(userSetting, cancellationToken);
}
}
}
@@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.UserService;
using Strata.Users.Client.UserGroups.Models;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService users)
{
_userService = users;
}
[HttpGet("[action]")]
public async Task<IEnumerable<CIUser>> GetUsers(CancellationToken cancellationToken)
=> await GetUsersByGlobalId("CCI - Basic User", cancellationToken);
[HttpGet("[action]")]
public async Task<IEnumerable<CIUser>> GetOwners(CancellationToken cancellationToken)
=> await GetUsers(cancellationToken);
[HttpGet("[action]")]
public async Task<IEnumerable<CIUser>> GetExecutiveSponsors(CancellationToken cancellationToken)
=> await GetUsersByGlobalId("CCI - Exec Sponsors", cancellationToken);
[HttpGet("[action]")]
public async Task<IEnumerable<CIUser>> GetCostLeaders(CancellationToken cancellationToken)
=> await GetUsersByGlobalId("CCI - Cost Leader", cancellationToken);
[HttpGet("[action]")]
public async Task<IEnumerable<CIUser>> GetAdministrators(CancellationToken cancellationToken)
=> await GetUsersByGlobalId("CCI - System Administrator", cancellationToken);
[HttpGet("[action]")]
public async Task<IEnumerable<CIUser>> GetJazzUsers(CancellationToken cancellationToken)
=> await _userService.GetJazzUsers(cancellationToken);
[HttpGet("[action]")]
public async Task<IEnumerable<UserGroup>> GetUserGroups(CancellationToken cancellationToken)
=> await _userService.GetUserGroups(cancellationToken);
[HttpGet("all-ci-users")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<IEnumerable<CIUser>> GetAllCIUsers(CancellationToken cancellationToken)
=> await _userService.GetAllCIUsers(cancellationToken);
private async Task<IEnumerable<CIUser>> GetUsersByGlobalId(string globalId, CancellationToken cancellationToken)
=> await _userService.GetUsersByGlobalId(globalId, cancellationToken);
[HttpGet("{userGuid}/[action]")]
public async Task<CIUser> GetUser(Guid userGuid, CancellationToken cancellationToken)
=> await _userService.GetUser(userGuid, cancellationToken);
}
}
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.UtilizationVariation.Opportunities.Filters;
using Strata.ContinuousImprovement.Biz.UtilizationVariation.VariationCaseTypes;
using Strata.ContinuousImprovement.Biz.UtilizationVariation.VariationCaseTypes.Filters;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class VariationCaseTypesController : ControllerBase
{
private readonly IUVCaseTypeService _variationCaseTypeService;
public VariationCaseTypesController(IUVCaseTypeService variationCaseTypeService)
{
_variationCaseTypeService = variationCaseTypeService;
}
[HttpPost("")]
public async Task<PagedApiResponse<UVCaseType>> GetCaseTypes(UVCaseTypeLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await _variationCaseTypeService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<UVFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await _variationCaseTypeService.GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
[HttpPost("{configurationGuid}/[action]")]
public async Task<bool> AddPatientPopulationCaseTypes(Guid configurationGuid, List<string> patientPopulations, CancellationToken cancellationToken)
{
return await _variationCaseTypeService.AddPatientPopulationCaseTypes(configurationGuid, User?.Identity?.Name ?? "", patientPopulations, cancellationToken);
}
}
[ExcludeFromCodeCoverage]
public class UVCaseTypeLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public UVCaseTypeFilters Filters { get; set; }
}
}
@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.ApiLib.Standard.Models;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.Pagination;
using Strata.ContinuousImprovement.Biz.UtilizationVariation.Opportunities;
using Strata.ContinuousImprovement.Biz.UtilizationVariation.Opportunities.Filters;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class VariationOpportunitiesController : OpportunityController<UVOpportunity>
{
public VariationOpportunitiesController(IUVOpportunityService opportunityService)
: base(opportunityService)
{
}
[HttpPost("")]
public async Task<PagedApiResponse<UVOpportunity>> GetOpportunities(UVOpportunityLoadOptions loadOptions, CancellationToken cancellationToken)
{
return await OpportunityService.GetPagedAsync(loadOptions.PagingOptions, loadOptions.Filters, cancellationToken);
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Export(UVOpportunityExportOptions exportOptions, CancellationToken cancellationToken)
{
var workbook = await ((IUVOpportunityService)OpportunityService).ExportAsync(exportOptions.SortOptions, exportOptions.Filters, cancellationToken);
return new ExcelContentResult(workbook, "UV_Opportunities");
}
[HttpGet("{configurationGuid}/[action]")]
public async Task<UVFilterOptions> GetFilterOptionsAsync(Guid configurationGuid, CancellationToken cancellationToken)
{
return await ((IUVOpportunityService)OpportunityService).GetFilterOptionsAsync(configurationGuid, cancellationToken);
}
}
[ExcludeFromCodeCoverage]
public class UVOpportunityLoadOptions
{
public PagingOptions PagingOptions { get; set; }
public UVFilters Filters { get; set; }
}
[ExcludeFromCodeCoverage]
public class UVOpportunityExportOptions
{
public SortOptions SortOptions { get; set; }
public UVFilters Filters { get; set; }
}
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Strata.ContinuousImprovement.Api.Extensions
{
public static class ControllerBaseExtensions
{
public static ObjectResult Problem(this ControllerBase controller, ProblemDetails problemDetails)
=> controller.Problem(title: problemDetails.Title,
detail: problemDetails.Detail,
statusCode: problemDetails.Status,
instance: problemDetails.Instance,
type: problemDetails.Type);
public static ObjectResult NotFoundProblem(this ControllerBase controller, ProblemDetails problemDetails = default)
=> controller.Problem(title: problemDetails.Title,
detail: problemDetails.Detail,
statusCode: problemDetails?.Status ?? StatusCodes.Status404NotFound,
instance: problemDetails.Instance,
type: problemDetails.Type);
public static ObjectResult NotFoundProblem(this ControllerBase controller, string title, string detail)
{
var problemDetails = new ProblemDetails
{
Title = title,
Detail = detail,
Status = StatusCodes.Status404NotFound
};
return controller.Problem(problemDetails);
}
public static ObjectResult NoContentProblem(this ControllerBase controller, ProblemDetails problemDetails = default)
=> controller.Problem(title: problemDetails?.Title,
detail: problemDetails?.Detail,
statusCode: problemDetails?.Status ?? StatusCodes.Status204NoContent,
instance: problemDetails?.Instance,
type: problemDetails?.Type);
}
}
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Extensions
{
internal static class MultipartFileControllerExtensions
{
public static async Task<FileMultipartSection> GetFileFromMultipartUpload(this HttpRequest request, CancellationToken cancellationToken = default)
{
// validation of Content-Type
// 1. first, it must be a form-data request
// 2. a boundary should be found in the Content-Type
if (!request.HasFormContentType ||
!MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaTypeHeader) ||
string.IsNullOrEmpty(mediaTypeHeader.Boundary.Value))
{
return null;
}
var boundary = HeaderUtilities.RemoveQuotes(mediaTypeHeader.Boundary.Value).Value ?? string.Empty;
var reader = new MultipartReader(boundary, request.Body);
var section = await reader.ReadNextSectionAsync(cancellationToken);
var fileSection = section?.AsFileSection();
return fileSection is null
? null
: section.AsFileSection();
}
}
}
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc;
using Strata.ContinuousImprovement.Biz.Generics;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.Generics
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class OpportunityController<T> : ControllerBase where T : class, IOpportunity
{
protected readonly IOpportunityService<T> OpportunityService;
public OpportunityController(IOpportunityService<T> opportunityService)
{
OpportunityService = opportunityService;
}
[HttpPut("{opportunityGuid}/[action]")]
public async Task SetHiddenAsync(Guid opportunityGuid, [FromBody] bool isHidden, CancellationToken cancellationToken)
{
await OpportunityService.SetHiddenAsync(opportunityGuid, isHidden, cancellationToken);
}
[HttpPut("{opportunityGuid}/[action]")]
public virtual async Task SetNoteAsync(Guid opportunityGuid, [FromBody] string note, CancellationToken cancellationToken)
{
await OpportunityService.SetNoteAsync(opportunityGuid, note, cancellationToken);
}
}
}
@@ -0,0 +1,8 @@
namespace Strata.ContinuousImprovement.Api.Generics
{
public class Policies
{
public const string StrataOnly = "StrataOnly";
public const string IsAdmin = "IsAdmin";
}
}
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics.CodeAnalysis;
namespace Strata.ContinuousImprovement.Api.Generics
{
[ExcludeFromCodeCoverage]
public class ProcessingResult : StatusCodeResult
{
public ProcessingResult()
: base(StatusCodes.Status102Processing)
{
}
}
}
@@ -0,0 +1,110 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Strata.Configuration.Client.DependencyInjection;
using Strata.ContinuousImprovement.Biz.DbContexts;
using Strata.healthchecks.Biz;
using Strata.Logging.DependencyInjection;
using Strata.Snowflake.Migrations.Client;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api
{
[ExcludeFromCodeCoverage]
public class Program
{
public static void Main(string[] args)
{
try
{
var commandLineApplication = new CommandLineApplication(false);
var doMigrate = commandLineApplication.Option(
"--ef-migrate",
"Apply entity framework migrations and exit",
CommandOptionType.NoValue);
commandLineApplication.HelpOption("-? | -h | --help");
commandLineApplication.OnExecute(async () =>
{
await ExecuteApp(args, doMigrate);
return 0;
});
commandLineApplication.Execute(args);
}
catch (Exception ex)
{
Log.Error(ex, "There was an unhandled exception that caused the program to crash");
//This is for serilog to ensure that all of the cached logs are flushed when the app crashes
//This is useful so we can capture logs when the application crashes during startup:
Log.CloseAndFlush();
}
}
public static async Task ExecuteApp(string[] args, CommandOption doMigrate)
{
var webHost = CreateWebHostBuilder(args).Build();
if (doMigrate.HasValue())
{
using var scope = webHost.Services.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
try
{
// Snowflake Migrations
logger.LogInformation("Uploading and applying Snowflake migrations");
var snowflakeMigrationService = scope.ServiceProvider.GetRequiredService<ISnowflakeMigrationService>();
var snowflakeMigrationDirectory = Path.Combine(Environment.CurrentDirectory, "SnowflakeMigrations");
var success = await snowflakeMigrationService.PackageMigrations(snowflakeMigrationDirectory);
if (!success)
{
logger.LogError("Failed to package migrations.");
Environment.Exit(1);
}
logger.LogInformation("Snowflake migrations successfully uploaded to S3. Begin apply migrations.");
success = await snowflakeMigrationService.ApplyClientMigrations();
if (!success)
{
logger.LogError("Failed to apply migrations.");
Environment.Exit(1);
}
// EF Migrations
logger.LogInformation("Applying Entity Framework migrations");
using var context = scope.ServiceProvider.GetService<CentralDbContext>();
var hostEnv = scope.ServiceProvider.GetService<IHostEnvironment>();
context.Database.SetCommandTimeout(TimeSpan.FromMinutes(10));
context.Database.Migrate();
logger.LogInformation("All done, closing app");
Environment.Exit(Environment.ExitCode);
}
catch (Exception ex)
{
logger.LogError(ex, "There was an unhandled exception that caused the program to crash");
Environment.Exit(1);
}
}
// no flags provided, so just run the webhost
webHost.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStrataConfiguration()
.UseStrataLogging()
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "Strata.healthchecks.Jazz")
.UseStrataHealthChecks()
.UseStartup<Startup>();
}
}
@@ -0,0 +1,16 @@
{
"profiles": {
"Strata.ContinuousImprovement.Api": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "",
"applicationUrl": "https://localhost:8443",
"environmentVariables": {
"Hangfire__RunLocally": "true",
"ASPNETCORE_ENVIRONMENT": "Development",
"AWS_S3_US_EAST_1_REGIONAL_ENDPOINT": "regional",
"AWS_Profile": "sdt-continuous-improvement-service-role"
}
}
}
}
@@ -0,0 +1,124 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.DecisionSupport.Biz.Parameters;
using Strata.DecisionSupport.Biz.Services;
using Strata.DecisionSupport.Biz.Utilities;
using Strata.DecisionSupport.Models.Encounters;
using Strata.DecisionSupport.Models.Jazz;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.SnowflakeControllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class EncountersController : Controller
{
private readonly IEncounterQueryService _encounterQueryService;
public EncountersController(IEncounterQueryService encounterQueryService)
{
_encounterQueryService = encounterQueryService;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
#pragma warning disable S4457
public async Task<ActionResult<IEnumerable<IJazzEncounterInfo>>> GetAsync(
[FromBody] EncounterQueryInfo queryInfo, CancellationToken cancellationToken)
#pragma warning restore
{
if (queryInfo.EntityIDs.IsEmpty())
return BadRequest(queryInfo);
return Ok(await _encounterQueryService.RunEncounterInfoQuery(queryInfo, cancellationToken));
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
#pragma warning disable S4457
public async Task<ActionResult<IEnumerable<EncounterCostInfo>>> GetCostInfoAsync(
[FromBody] EncounterCostQueryInfo queryInfo, CancellationToken cancellationToken)
#pragma warning restore
{
if (queryInfo.EncounterIds.IsEmpty())
return BadRequest(queryInfo);
return Ok(await _encounterQueryService.RunEncounterCostInfoQuery(queryInfo, cancellationToken));
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
#pragma warning disable S4457
public async Task<ActionResult<IEnumerable<DepartmentEncounterUnitOfService>>> GetDepartmentUnitOfServiceInfoAsync(
[FromBody] DepartmentEncounterUnitOfServiceQueryInfo queryInfo, CancellationToken cancellationToken)
#pragma warning restore
{
if (queryInfo.EncounterIds.IsEmpty())
return BadRequest(queryInfo);
return Ok(await _encounterQueryService.RunDepartmentEncounterUnitOfServiceQuery(queryInfo, cancellationToken));
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
#pragma warning disable S4457
public async Task<ActionResult<IEnumerable<EncounterUnitOfService>>> GetUnitOfServiceInfoAsync(
[FromBody] EncounterUnitOfServiceQueryInfo queryInfo, CancellationToken cancellationToken)
#pragma warning restore
{
if (queryInfo.EncounterIds.IsEmpty())
return BadRequest(queryInfo);
return Ok(await _encounterQueryService.RunEncounterUnitOfServiceQuery(queryInfo, cancellationToken));
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
#pragma warning disable S4457
public async Task<ActionResult<IEnumerable<EncounterChargeInfo>>> GetEncounterChargeInfoAsync(
[FromBody] EncounterChargeQueryInfo queryInfo, CancellationToken cancellationToken)
#pragma warning restore
{
if (queryInfo.OpportunityGuid.Equals(Guid.Empty))
return BadRequest(queryInfo);
return Ok(await _encounterQueryService.RunEncounterChargeQuery(queryInfo, cancellationToken));
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
#pragma warning disable S4457
public async Task<ActionResult<IEnumerable<VariationEncounterChargeInfo>>> GetVariationEncounterChargeInfoAsync(
[FromBody] VariationEncounterChargeQueryInfo queryInfo, CancellationToken cancellationToken)
#pragma warning restore
{
if (queryInfo.OpportunityGuids.IsEmpty())
return BadRequest(queryInfo);
return Ok(await _encounterQueryService.RunVariationEncounterChargeQuery(queryInfo, cancellationToken));
}
[HttpPost("los-opportunity-info")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<IEnumerable<LOSOpportunityInfo>>> GetLOSOpportunityInfoAsync(
[FromBody] LOSOpportunityQueryInfo queryInfo, CancellationToken cancellationToken)
{
if (queryInfo.OpportunityGuid.Equals(Guid.Empty))
return BadRequest(queryInfo);
return Ok(await _encounterQueryService.RunLOSOpportunityInfoQuery(queryInfo, cancellationToken));
}
}
}
@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.DecisionSupport.Biz.Parameters;
using Strata.DecisionSupport.Biz.Services;
using Strata.DecisionSupport.Biz.Utilities;
using Strata.DecisionSupport.Models.Jazz;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.ContinuousImprovement.Api.SnowflakeControllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class UtilizationVariationController : Controller
{
private readonly IUtilizationVariationService _utilizationVariationService;
public UtilizationVariationController(IUtilizationVariationService utilizationVariationService)
{
_utilizationVariationService = utilizationVariationService;
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<IEnumerable<AllChargeInfo>>> GetAllChargeInfoAsync(
[FromBody] AllChargeQueryInfo queryInfo, CancellationToken cancellationToken)
{
if (queryInfo.OpportunityGuid.Equals(Guid.Empty))
return BadRequest(queryInfo);
if (queryInfo.ChargeCodeIds.IsEmpty())
return BadRequest(queryInfo);
return Ok(await _utilizationVariationService.RunAllChargeInfoQuery(queryInfo, cancellationToken));
}
[HttpPost("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<IEnumerable<ChargeInfo>>> GetChargeInfoAsync(
[FromBody] ChargeQueryInfo queryInfo, CancellationToken cancellationToken)
{
if (queryInfo.ConfigurationGuid.Equals(Guid.Empty))
return BadRequest(queryInfo);
if (queryInfo.ChargeCodeIds.IsEmpty())
return BadRequest(queryInfo);
return Ok(await _utilizationVariationService.RunChargeInfoQuery(queryInfo, cancellationToken));
}
}
}
@@ -0,0 +1,6 @@
--Author: sdt-cloveland
--Forward
CREATE SCHEMA IF NOT EXISTS cci;
--Rollback
--ILB
@@ -0,0 +1,43 @@
--Author: tdebolt
--Forward
CREATE OR REPLACE DYNAMIC TABLE cci.encounterchargecodecosts
TARGET_LAG = DOWNSTREAM --important to set to downstream for creation, manually change to schedule as we enable for clients
WAREHOUSE = 'APP_CI_DYNAMICTABLE_WH'
COMMENT = 'Populate Charge Data by Encounter and Charge Code for use in CI'
REFRESH_MODE = AUTO
INITIALIZE = ON_SCHEDULE
AS
WITH pblidGrouping (pblidrowid, unitsofservice, fixeddirectcost, fixedindirectcost, variabledirectcost, variableindirectcost, totalcost) AS (
SELECT
cd.pblidrowid,
min(unitsofservice) AS unitsofservice,
sum(fixeddirectcost) as fixeddirectcost,
sum(fixedindirectcost) as fixedindirectcost,
sum(variabledirectcost) as variabledirectcost,
sum(variableindirectcost) as variableindirectcost,
sum(fixeddirectcost) + sum(fixedindirectcost) + sum(variabledirectcost) + sum(variableindirectcost) as totalcost
FROM dss.factpatientcostdetail cd
GROUP BY cd.pblidrowid
ORDER BY cd.pblidrowid
)
SELECT
d.encounterid,
d.chargecodeid,
sum(d.charge) as charge,
sum(g.unitsofservice) as unitsofservice,
sum(g.fixeddirectcost) as fixeddirectcost,
sum(g.fixedindirectcost) as fixedindirectcost,
sum(g.variabledirectcost) as variabledirectcost,
sum(g.variableindirectcost) as variableindirectcost,
sum(d.fixeddirectunitcost) as fixeddirectunitcost,
sum(d.fixedindirectunitcost) as fixedindirectunitcost,
sum(d.variabledirectunitcost) as variabledirectunitcost,
sum(d.variableindirectunitcost) as variableindirectunitcost,
sum(g.totalcost) as totalcost,
FROM dss.factpatientbillinglineitemdetail d
LEFT JOIN pblidGrouping g on g.pblidrowid = d.rowid
GROUP BY d.encounterid, d.chargecodeid
;
--Rollback
DROP DYNAMIC TABLE IF EXISTS cci.encounterchargecodecosts;
@@ -0,0 +1,19 @@
--Author: sdt-cloveland
--Forward
CREATE SCHEMA IF NOT EXISTS cci;
CREATE OR REPLACE TABLE cci.StrategicOpportunityDetail (
OpportunityId BIGINT,
ChargeDepartmentID INT NOT NULL,
EncounterID INT NOT NULL,
ChargeCodeID INT NOT NULL,
EntityID INT NOT NULL,
PhysicianID INT NOT NULL,
PrimaryServiceLineID INT NOT NULL,
PhysicianSpecialtyID INT NOT NULL,
CostDriver STRING NOT NULL,
UnitsOfService FLOAT NOT NULL
);
--Rollback
DROP TABLE cci.StrategicOpportunityDetail;
@@ -0,0 +1,13 @@
--Author: sdt-dmoroziuk
--Forward
CREATE OR REPLACE TABLE cci.DimExplorationPopulation (
ExplorationPopulationId INT NOT NULL,
Name STRING NOT NULL,
CaseTypeFamilyId INT NOT NULL,
CostDriver STRING NOT NULL,
TotalEncounters INT NOT NULL,
TotalVariation FLOAT NOT NULL
);
--Rollback
DROP TABLE cci.DimExplorationPopulation;
@@ -0,0 +1,9 @@
--Author: sdt-dmoroziuk
--Forward
CREATE OR REPLACE TABLE cci.FactExplorationPopulationEncounter (
ExplorationPopulationId INT NOT NULL,
EncounterId INT NOT NULL
);
--Rollback
DROP TABLE cci.FactExplorationPopulationEncounter;
@@ -0,0 +1,6 @@
--Author: sdt-dmoroziuk
--Forward
CREATE OR REPLACE STAGE cci.POSTGRES_REPLICATION_DATA;
--Rollback
DROP STAGE IF EXISTS cci.POSTGRES_REPLICATION_DATA;
@@ -0,0 +1,70 @@
--Author: sdt-cloveland
--Forward
ALTER TABLE cci.StrategicOpportunityDetail
ADD COLUMN IF NOT EXISTS BaselineCost FLOAT NOT NULL;
ALTER TABLE cci.StrategicOpportunityDetail
ADD COLUMN IF NOT EXISTS GoalUnits FLOAT NOT NULL;
ALTER TABLE cci.StrategicOpportunityDetail
ADD COLUMN IF NOT EXISTS GoalCost FLOAT NOT NULL;
BEGIN
IF (
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = CURRENT_DATABASE()
AND TABLE_SCHEMA = 'CCI'
AND TABLE_NAME = 'STRATEGICOPPORTUNITYDETAIL'
AND COLUMN_NAME = 'UNITSOFSERVICE'
)
AND NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = CURRENT_DATABASE()
AND TABLE_SCHEMA = 'CCI'
AND TABLE_NAME = 'STRATEGICOPPORTUNITYDETAIL'
AND COLUMN_NAME = 'BASELINEUNITS'
)
)
THEN
ALTER TABLE cci.StrategicOpportunityDetail
RENAME COLUMN UnitsOfService TO BaselineUnits;
END IF;
END;
--Rollback
ALTER TABLE cci.StrategicOpportunityDetail
DROP COLUMN BaselineCost;
ALTER TABLE cci.StrategicOpportunityDetail
DROP COLUMN GoalCost;
ALTER TABLE cci.StrategicOpportunityDetail
DROP COLUMN GoalUnits;
BEGIN
IF (
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = CURRENT_DATABASE()
AND TABLE_SCHEMA = 'CCI'
AND TABLE_NAME = 'STRATEGICOPPORTUNITYDETAIL'
AND COLUMN_NAME = 'BASELINEUNITS'
)
AND NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = CURRENT_DATABASE()
AND TABLE_SCHEMA = 'CCI'
AND TABLE_NAME = 'STRATEGICOPPORTUNITYDETAIL'
AND COLUMN_NAME = 'UNITSOFSERVICE'
)
)
THEN
ALTER TABLE cci.StrategicOpportunityDetail
RENAME COLUMN BaselineUnits TO UnitsOfService;
END IF;
END;
@@ -0,0 +1,82 @@
--Author: tdebolt
--Forward
CREATE OR REPLACE DYNAMIC TABLE cci.encounterchargecodecosts
TARGET_LAG = DOWNSTREAM --important to set to downstream for creation, manually change to schedule as we enable for clients
WAREHOUSE = 'APP_CI_DYNAMICTABLE_WH'
COMMENT = 'Populate Charge Data by Encounter and Charge Code for use in CI'
REFRESH_MODE = AUTO
INITIALIZE = ON_SCHEDULE
AS
WITH pblidGrouping (pblidrowid, unitsofservice, fixeddirectcost, fixedindirectcost, variabledirectcost, variableindirectcost, totalcost) AS (
SELECT
cd.pblidrowid,
min(unitsofservice) AS unitsofservice,
sum(fixeddirectcost) as fixeddirectcost,
sum(fixedindirectcost) as fixedindirectcost,
sum(variabledirectcost) as variabledirectcost,
sum(variableindirectcost) as variableindirectcost,
sum(fixeddirectcost) + sum(fixedindirectcost) + sum(variabledirectcost) + sum(variableindirectcost) as totalcost
FROM dss.factpatientcostdetail cd
GROUP BY cd.pblidrowid
ORDER BY cd.pblidrowid
)
SELECT
d.encounterid,
d.chargecodeid,
d.rowid,
sum(d.charge) as charge,
sum(g.unitsofservice) as unitsofservice,
sum(g.fixeddirectcost) as fixeddirectcost,
sum(g.fixedindirectcost) as fixedindirectcost,
sum(g.variabledirectcost) as variabledirectcost,
sum(g.variableindirectcost) as variableindirectcost,
sum(d.fixeddirectunitcost) as fixeddirectunitcost,
sum(d.fixedindirectunitcost) as fixedindirectunitcost,
sum(d.variabledirectunitcost) as variabledirectunitcost,
sum(d.variableindirectunitcost) as variableindirectunitcost,
sum(g.totalcost) as totalcost
FROM dss.factpatientbillinglineitemdetail d
LEFT JOIN pblidGrouping g on g.pblidrowid = d.rowid
GROUP BY d.encounterid, d.chargecodeid, d.rowid
;
--Rollback
CREATE OR REPLACE DYNAMIC TABLE cci.encounterchargecodecosts
TARGET_LAG = DOWNSTREAM --important to set to downstream for creation, manually change to schedule as we enable for clients
WAREHOUSE = 'APP_CI_DYNAMICTABLE_WH'
COMMENT = 'Populate Charge Data by Encounter and Charge Code for use in CI'
REFRESH_MODE = AUTO
INITIALIZE = ON_SCHEDULE
AS
WITH pblidGrouping (pblidrowid, unitsofservice, fixeddirectcost, fixedindirectcost, variabledirectcost, variableindirectcost, totalcost) AS (
SELECT
cd.pblidrowid,
min(unitsofservice) AS unitsofservice,
sum(fixeddirectcost) as fixeddirectcost,
sum(fixedindirectcost) as fixedindirectcost,
sum(variabledirectcost) as variabledirectcost,
sum(variableindirectcost) as variableindirectcost,
sum(fixeddirectcost) + sum(fixedindirectcost) + sum(variabledirectcost) + sum(variableindirectcost) as totalcost
FROM dss.factpatientcostdetail cd
GROUP BY cd.pblidrowid
ORDER BY cd.pblidrowid
)
SELECT
d.encounterid,
d.chargecodeid,
sum(d.charge) as charge,
sum(g.unitsofservice) as unitsofservice,
sum(g.fixeddirectcost) as fixeddirectcost,
sum(g.fixedindirectcost) as fixedindirectcost,
sum(g.variabledirectcost) as variabledirectcost,
sum(g.variableindirectcost) as variableindirectcost,
sum(d.fixeddirectunitcost) as fixeddirectunitcost,
sum(d.fixedindirectunitcost) as fixedindirectunitcost,
sum(d.variabledirectunitcost) as variabledirectunitcost,
sum(d.variableindirectunitcost) as variableindirectunitcost,
sum(g.totalcost) as totalcost
FROM dss.factpatientbillinglineitemdetail d
LEFT JOIN pblidGrouping g on g.pblidrowid = d.rowid
GROUP BY d.encounterid, d.chargecodeid
@@ -0,0 +1,51 @@
--Author: sdt-dmoroziuk
--Forward
BEGIN
IF (
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = CURRENT_DATABASE()
AND TABLE_SCHEMA = 'CCI'
AND TABLE_NAME = 'STRATEGICOPPORTUNITYDETAIL'
AND COLUMN_NAME = 'ChargeDepartmentID'
)
AND NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = CURRENT_DATABASE()
AND TABLE_SCHEMA = 'CCI'
AND TABLE_NAME = 'STRATEGICOPPORTUNITYDETAIL'
AND COLUMN_NAME = 'DepartmentId'
)
)
THEN
ALTER TABLE cci.StrategicOpportunityDetail
RENAME COLUMN ChargeDepartmentID TO DepartmentId;
END IF;
END;
--Rollback
BEGIN
IF (
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = CURRENT_DATABASE()
AND TABLE_SCHEMA = 'CCI'
AND TABLE_NAME = 'STRATEGICOPPORTUNITYDETAIL'
AND COLUMN_NAME = 'DepartmentId'
)
AND NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = CURRENT_DATABASE()
AND TABLE_SCHEMA = 'CCI'
AND TABLE_NAME = 'STRATEGICOPPORTUNITYDETAIL'
AND COLUMN_NAME = 'ChargeDepartmentID'
)
)
THEN
ALTER TABLE cci.StrategicOpportunityDetail
RENAME COLUMN DepartmentId TO ChargeDepartmentID;
END IF;
END;
@@ -0,0 +1,8 @@
--Author: yrenz
--Forward
ALTER TABLE cci.StrategicOpportunityDetail
ADD COLUMN IF NOT EXISTS CaseTypeFamilyID INT NOT NULL DEFAULT(0);
--Rollback
ALTER TABLE cci.StrategicOpportunityDetail
DROP COLUMN CaseTypeFamilyID;
@@ -0,0 +1,9 @@
--Author: yrenz
--Forward
CREATE OR ALTER VIEW cci.viewEncounterPatientTypeRollup AS
SELECT fpes.encounterid, ptr.Type as PatientTypeRollup
FROM clientdss.FactPatientEncounterSummary fpes
INNER JOIN fw.DimPatientType pt ON fpes.PatientTypeID = pt.PatientTypeID
INNER JOIN fw.DimPatientTypeRollup ptr ON pt.PatientTypeRollupID = ptr.PatientTypeRollupID;
--Rollback
DROP VIEW IF EXISTS cci.viewEncounterPatientTypeRollup;
@@ -0,0 +1,44 @@
--Author: sdt-mvasylovskyi
--Forward
CREATE OR REPLACE TABLE cci.FactStrategicChargeCodeDetail (
RowId BIGINT AUTOINCREMENT START 1 INCREMENT 1 PRIMARY KEY,
IsBaseline BOOLEAN NOT NULL DEFAULT FALSE,
DischargeFiscalYearId SMALLINT NOT NULL DEFAULT 0,
DischargeFiscalMonthId TINYINT NOT NULL DEFAULT 0,
OpportunityId BIGINT,
EntityId INT NOT NULL,
PrimaryPhysicianId INT NOT NULL,
PrimaryServiceLineId INT NOT NULL,
DepartmentId INT NOT NULL,
PhysicianSpecialtyId INT NOT NULL,
ChargeCodeId INT NOT NULL,
CaseTypeFamilyId INT NOT NULL,
CostDriver VARCHAR(1024) NOT NULL COLLATE 'en-ci',
Units FLOAT NOT NULL,
Cost FLOAT NOT NULL,
GoalUnits FLOAT NOT NULL,
GoalCost FLOAT NOT NULL
);
DROP TABLE IF EXISTS cci.StrategicOpportunityDetail;
--Rollback
CREATE OR REPLACE TABLE cci.StrategicOpportunityDetail (
OpportunityId BIGINT,
EncounterId INT NOT NULL,
EntityId INT NOT NULL,
PhysicianId INT NOT NULL,
PrimaryServiceLineId INT NOT NULL,
DepartmentId INT NOT NULL,
PhysicianSpecialtyId INT NOT NULL,
ChargeCodeId INT NOT NULL,
CaseTypeFamilyId INT NOT NULL,
CostDriver VARCHAR(1024) NOT NULL COLLATE 'en-ci',
BaselineUnits FLOAT NOT NULL,
BaselineCost FLOAT NOT NULL,
GoalUnits FLOAT NOT NULL,
GoalCost FLOAT NOT NULL
);
DROP TABLE IF EXISTS cci.FactStrategicChargeCodeDetail;
@@ -0,0 +1,63 @@
--Author: sdt-mvasylovskyi
--Forward
BEGIN
IF (
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA ILIKE 'cci'
AND TABLE_NAME ILIKE 'FactStrategicChargeCodeDetail'
)
)
THEN
IF (
NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA ILIKE 'cci'
AND TABLE_NAME ILIKE 'FactStrategicChargeCodeDetail'
AND COLUMN_NAME ILIKE 'DistributedUnits'
)
)
THEN
ALTER TABLE cci.FactStrategicChargeCodeDetail
ADD COLUMN DistributedUnits FLOAT NOT NULL DEFAULT(0);
END IF;
IF (
NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA ILIKE 'cci'
AND TABLE_NAME ILIKE 'FactStrategicChargeCodeDetail'
AND COLUMN_NAME ILIKE 'DistributedCost'
)
)
THEN
ALTER TABLE cci.FactStrategicChargeCodeDetail
ADD COLUMN DistributedCost FLOAT NOT NULL DEFAULT(0);
END IF;
END IF;
END;
--Rollback
BEGIN
IF (
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA ILIKE 'cci'
AND TABLE_NAME ILIKE 'FactStrategicChargeCodeDetail'
)
)
THEN
ALTER TABLE cci.FactStrategicChargeCodeDetail
DROP COLUMN IF EXISTS DistributedUnits;
ALTER TABLE cci.FactStrategicChargeCodeDetail
DROP COLUMN IF EXISTS DistributedCost;
END IF;
END;
@@ -0,0 +1,12 @@
--Author: sdt-ydzoba
--Forward
CREATE OR REPLACE TABLE cci.StrategicOpportunityMeasureDetail (
MeasureId BIGINT NOT NULL,
YearId SMALLINT NOT NULL,
MonthId TINYINT NOT NULL,
Value FLOAT NOT NULL DEFAULT 0,
PRIMARY KEY (MeasureId, YearId, MonthId)
);
--Rollback
DROP TABLE IF EXISTS cci.StrategicOpportunityMeasureDetail;
@@ -0,0 +1,36 @@
--Author: sdt-tdebolt
--Forward
CREATE TABLE IF NOT EXISTS cci.dimstrategicitem (
strategic_item_id NUMBER(38,0) NOT NULL,
name STRING,
rollup_one_id NUMBER(38,0),
rollup_two_id NUMBER(38,0),
rollup_three_id NUMBER(38,0),
baseline_type STRING,
strategic_item_type STRING,
opportunity_type STRING,
status STRING,
baseline_distribution STRING,
baseline_start_date DATE,
baseline_end_date DATE,
estimated_tracking_duration NUMBER(38,0),
tracking_start_date DATE,
tracking_end_date DATE,
estimated_goal NUMBER(19,4),
total_improvement NUMBER(19,4),
cost_leader STRING,
executive_sponsor STRING,
owner STRING,
stakeholders STRING,
team_members STRING,
last_run_tracking_date DATE,
last_run_tracking_status STRING,
CONSTRAINT pk_dimstrategicitem
PRIMARY KEY (strategic_item_id)
);
--Rollback
DROP TABLE IF EXISTS cci.dimstrategicitem;
@@ -0,0 +1,22 @@
--Author: sdt-mleitch
--Forward
CREATE TABLE IF NOT EXISTS cci.FactStrategicGLDetail (
RowId BIGINT AUTOINCREMENT START 1 INCREMENT 1 PRIMARY KEY,
IsBaseline BOOLEAN NOT NULL DEFAULT FALSE,
FiscalYearId SMALLINT NOT NULL DEFAULT 0,
FiscalMonthId TINYINT NOT NULL DEFAULT 0,
OpportunityId BIGINT NOT NULL,
EntityId INT NOT NULL,
DepartmentId INT NOT NULL,
AccountId INT NOT NULL,
AccountType TINYINT,
Amount FLOAT NOT NULL DEFAULT 0,
GoalAmount FLOAT NOT NULL DEFAULT 0,
DistributedAmount FLOAT NOT NULL DEFAULT 0
);
--Rollback
DROP TABLE IF EXISTS cci.FactStrategicGLDetail;
@@ -0,0 +1,122 @@
using Amazon.SQS;
using AutoMapper;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Prometheus;
using Strata.ApiLib.Core.Cors.Extensions;
using Strata.ApiLib.Core.ExceptionHandling.DependencyInjection.Bootstrappers;
using Strata.ApiLib.Core.Logging.Bootstrappers;
using Strata.ApiLib.Core.StrataAuthentication.Bootstrappers;
using Strata.ApiLib.Core.Transformers;
using Strata.ContinuousImprovement.Api.Generics;
using Strata.ContinuousImprovement.Biz.BackgroundServices;
using Strata.ContinuousImprovement.Biz.Configurations;
using Strata.ContinuousImprovement.Biz.Configurations.Hangfire;
using Strata.CoreLib.Claims.Extensions;
using Strata.DecisionSupport.Biz.Configurations;
using Strata.healthchecks.Biz;
using Strata.SignalR.Configuration;
using Strata.SwaggerExtensions;
using System.Diagnostics.CodeAnalysis;
using System.Net.Mime;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Strata.ContinuousImprovement.Api
{
[ExcludeFromCodeCoverage]
public class Startup
{
private readonly IConfiguration _configuration;
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddGlobalExceptionMiddleware(_configuration);
services.AddControllers(options =>
{
//Prevent browsers from caching API responses (this addresses an IE11 issue):
options.Filters.Add(
new ResponseCacheAttribute()
{
Location = ResponseCacheLocation.None,
NoStore = false,
Duration = -1
});
options.ReturnHttpNotAcceptable = true;
options.Filters.Add(new ProducesAttribute(MediaTypeNames.Application.Json));
options.Filters.Add(new ConsumesAttribute(MediaTypeNames.Application.Json));
options.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()));
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddAuthorization(options =>
{
options.AddPolicy(Policies.IsAdmin,
policy => policy.RequireAssertion(context => context.User.IsInRole("CI - System Administrator") || context.User.IsSdtEmployee()));
options.AddPolicy(Policies.StrataOnly,
policy => policy.RequireAssertion(context => context.User.IsSdtEmployee()));
});
services.AddStrataAuthentication(_configuration);
services.AddStrataHangfire();
services.AddContinuousImprovementServices(_configuration);
services.AddDecisionSupportServices(_configuration);
services.AddRouting();
services.AddSwagger(_configuration);
services.AddStrataSignalR();
services.AddStrataHealthChecks(_configuration);
services.AddSnowflakeMigrationServiceClient();
services.AddStrataCors(_configuration);
services.AddHealthChecks();
services.AddTransient<IAmazonSQS, AmazonSQSClient>();
services.AddHostedService<ReplicationBackgroundService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
app.UseGlobalExceptionMiddleware();
app.UseRouting();
app.UseStrataCorsPolicy(env);
if (!env.IsDevelopment())
{
app.UseHsts();
app.UseHttpsRedirection();
}
app.UseMetricServer();
app.UseHttpMetrics();
app.UseAccessTokenQueryStringMiddleware();
app.UseStrataAuthentication();
app.UseClaimsLogging();
app.ConfigureSwagger(env, provider);
app.UseEndpoints(endpoints => endpoints.MapControllers().RequireAuthorization());
app.UseEndpoints(endpoints => endpoints.MapHangfireHub());
app.UseEndpoints(endpoints => endpoints.MapNotificationHub());
app.UseStrataHealthChecks(env);
}
}
}

Some files were not shown because too many files have changed in this diff Show More