diff --git a/package-lock.json b/package-lock.json index ed6e2890..90e5c0c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "rxjs-compat": "^6.6.7", "tslib": "^2.4.1", "uuid": "^14.0.0", + "xlsx": "^0.18.5", "zone.js": "~0.15.1" }, "devDependencies": { @@ -64,6 +65,7 @@ "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^8.41.0", "@typescript-eslint/parser": "^8.41.0", + "cross-env": "^10.1.0", "eslint": "^9.34.0", "eslint-plugin-import": "^2.32.0", "jasmine-core": "~3.7.1", @@ -2747,6 +2749,13 @@ "node": ">=14.17.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -7533,6 +7542,15 @@ "node": ">=8.9.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -8371,6 +8389,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8578,6 +8609,15 @@ "node": ">=0.10.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -8873,6 +8913,18 @@ } } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -8880,6 +8932,24 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -10403,6 +10473,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -15614,6 +15693,18 @@ "wbuf": "^1.7.3" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ssri": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", @@ -17559,6 +17650,24 @@ "dev": true, "license": "MIT" }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17758,6 +17867,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 90116b12..ad0ae1c3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint -c .eslintrc.js --ext .ts src/", "e2e": "ng e2e", "swagger:gen": "curl --insecure http://localhost:4300/swagger/v1/swagger.json --output swagger.json && node_modules/@openapitools/openapi-generator-cli/bin/openapi-generator generate -i ./swagger.json -g typescript-angular -o src/app/generated/blueprint-api --additional-properties ngVersion=21 --additional-properties useRxJS6=true --additional-properties modelPropertyNaming=original --type-mappings=DateTime=Date --skip-validate-spec", - "swagger:gen-docker": "docker run --rm -v \"$(pwd)\":/local openapitools/openapi-generator-cli:v7.15.0 generate -i http://host.docker.internal:4724/swagger/v1/swagger.json -g typescript-angular -o /local/src/app/generated/blueprint.api --additional-properties ngVersion=21 --additional-properties useRxJS6=true --additional-properties modelPropertyNaming=original --type-mappings=DateTime=Date --skip-validate-spec", + "swagger:gen-docker": "cross-env-shell docker run --rm -v $INIT_CWD:/local openapitools/openapi-generator-cli:v7.15.0 generate -i http://host.docker.internal:4724/swagger/v1/swagger.json -g typescript-angular -o /local/src/app/generated/blueprint.api --additional-properties ngVersion=21 --additional-properties useRxJS6=true --additional-properties modelPropertyNaming=original --type-mappings=DateTime=Date --skip-validate-spec", "docker:init": "docker network create identity --driver=overlay", "docker:stack": "npm run docker:stack:identity && npm run docker:stack:api", "docker:stack:api": "docker stack deploy -c docker-compose.yml -c docker/compose/api.yml blueprint_stack", @@ -59,10 +59,12 @@ "rxjs-compat": "^6.6.7", "tslib": "^2.4.1", "uuid": "^14.0.0", + "xlsx": "^0.18.5", "zone.js": "~0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^21.2.1", + "cross-env": "^10.1.0", "@angular-eslint/builder": "21.3.0", "@angular-eslint/eslint-plugin": "21.3.0", "@angular-eslint/eslint-plugin-template": "21.3.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 4aede5ac..431f81fa 100755 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -15,6 +15,7 @@ import { JoinComponent } from './components/landing/join/join.component'; import { LaunchComponent } from './components/landing/launch/launch.component'; import { ManageComponent } from './components/landing/manage/manage.component'; import { StarterComponent } from './components/starter/starter.component'; +import { AssessorPageComponent } from './components/assessor-page/assessor-page.component'; import { EventDetailPageComponent } from './components/event-detail-page/event-detail-page.component'; import { IntegrationInProgressGuard } from './services/integration-in-progress.guard'; @@ -50,6 +51,11 @@ export const ROUTES: Routes = [ component: StarterComponent, canActivate: [ComnAuthGuardService], }, + { + path: 'assess', + component: AssessorPageComponent, + canActivate: [ComnAuthGuardService], + }, { path: 'msel/:mselid/view', component: MselViewComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7194e107..b381b657 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -64,6 +64,14 @@ import { AdminCatalogListComponent } from './components/admin/admin-catalog-list import { AdminContainerComponent } from './components/admin/admin-container/admin-container.component'; import { AdminInjectTypesComponent } from './components/admin/admin-inject-types/admin-inject-types.component'; import { AdminInjectTypeEditDialogComponent } from './components/admin/admin-inject-type-edit-dialog/admin-inject-type-edit-dialog.component'; +import { AdminCompetencyFrameworksComponent } from './components/admin/admin-competency-frameworks/admin-competency-frameworks.component'; +import { AdminCompetencyFrameworkEditDialogComponent } from './components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component'; +import { AdminCompetencyEditDialogComponent } from './components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component'; +import { AdminCompetencyDetailDialogComponent } from './components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component'; +import { AdminProficiencyScaleEditDialogComponent } from './components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component'; +import { AdminProficiencyLevelEditDialogComponent } from './components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component'; +import { AdminCompetencyFrameworkImportDialogComponent } from './components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component'; +import { AdminProficiencyScalesComponent } from './components/admin/admin-proficiency-scales/admin-proficiency-scales.component'; import { AdminUnitsComponent } from './components/admin/admin-units/admin-units.component'; import { AdminUnitEditDialogComponent } from './components/admin/admin-unit-edit-dialog/admin-unit-edit-dialog.component'; import { AdminUnitUsersComponent } from './components/admin/admin-unit-users/admin-unit-users.component'; @@ -86,7 +94,11 @@ import { CiteDutyListComponent } from './components/cite-duty-list/cite-duty-lis import { DashboardComponent } from './components/landing/dashboard/dashboard.component'; import { DataFieldEditDialogComponent } from './components/data-field-edit-dialog/data-field-edit-dialog.component'; import { DataFieldListComponent } from './components/data-field-list/data-field-list.component'; +import { CompetencyOptionsDialogComponent } from './components/competency-options-dialog/competency-options-dialog.component'; +import { TeamCompetencyPropagateDialogComponent } from './components/team-competency-propagate-dialog/team-competency-propagate-dialog.component'; import { DataOptionEditDialogComponent } from './components/data-option-edit-dialog/data-option-edit-dialog.component'; +import { DataOptionImportDialogComponent } from './components/data-option-import-dialog/data-option-import-dialog.component'; +import { DataOptionListDialogComponent } from './components/data-option-list-dialog/data-option-list-dialog.component'; import { HomeAppComponent } from './components/home-app/home-app.component'; import { InjectEditDialogComponent } from './components/inject-edit-dialog/inject-edit-dialog.component'; import { InjectListComponent } from './components/inject-list/inject-list.component'; @@ -104,6 +116,9 @@ import { MselInfoComponent } from './components/msel-info/msel-info.component'; import { IntegrationStatusComponent } from './components/integration-status/integration-status.component'; import { MselListComponent } from './components/msel-list/msel-list.component'; import { MselPageComponent } from './components/msel-page/msel-page.component'; +import { MselCompetenciesComponent } from './components/msel-competencies/msel-competencies.component'; +import { AssessorViewComponent } from './components/assessor-view/assessor-view.component'; +import { AssessorPageComponent } from './components/assessor-page/assessor-page.component'; import { MselTeamsComponent } from './components/msel-teams/msel-teams.component'; import { MselViewComponent } from './components/msel-view/msel-view.component'; import { OrganizationEditDialogComponent } from './components/organization-edit-dialog/organization-edit-dialog.component'; @@ -179,7 +194,11 @@ export const appConfig: ApplicationConfig = { DashboardComponent, DataFieldEditDialogComponent, DataFieldListComponent, + CompetencyOptionsDialogComponent, + TeamCompetencyPropagateDialogComponent, DataOptionEditDialogComponent, + DataOptionImportDialogComponent, + DataOptionListDialogComponent, HomeAppComponent, InjectEditDialogComponent, InjectListComponent, @@ -197,6 +216,9 @@ export const appConfig: ApplicationConfig = { IntegrationStatusComponent, MselListComponent, MselPageComponent, + MselCompetenciesComponent, + AssessorViewComponent, + AssessorPageComponent, MselTeamsComponent, MselViewComponent, OrganizationEditDialogComponent, @@ -220,6 +242,14 @@ export const appConfig: ApplicationConfig = { AdminContainerComponent, AdminInjectTypesComponent, AdminInjectTypeEditDialogComponent, + AdminCompetencyFrameworksComponent, + AdminCompetencyFrameworkEditDialogComponent, + AdminCompetencyEditDialogComponent, + AdminCompetencyDetailDialogComponent, + AdminProficiencyScaleEditDialogComponent, + AdminProficiencyLevelEditDialogComponent, + AdminCompetencyFrameworkImportDialogComponent, + AdminProficiencyScalesComponent, AdminUnitsComponent, AdminUnitEditDialogComponent, AdminUnitUsersComponent, diff --git a/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.html b/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.html new file mode 100644 index 00000000..7113e569 --- /dev/null +++ b/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.html @@ -0,0 +1,36 @@ + +
+
+ Competency Detail +   + +
+
+
+ ID + {{ data.competency.idNumber }} +
+
+ Name + {{ data.competency.shortName }} +
+ @if (getType()) { +
+ Type + {{ getType() }} +
+ } + @if (data.competency.description) { +
+ Description + {{ data.competency.description }} +
+ } +
+
diff --git a/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.scss b/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.scss new file mode 100644 index 00000000..d0b47745 --- /dev/null +++ b/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.scss @@ -0,0 +1,20 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.close-button { + float: right; +} + +.detail-row { + display: flex; + flex-direction: column; + margin-bottom: 12px; +} + +.detail-label { + font-weight: bold; + font-size: 12px; + color: var(--mat-sys-on-surface-variant); + margin-bottom: 2px; +} diff --git a/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.ts b/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.ts new file mode 100644 index 00000000..6ef8d970 --- /dev/null +++ b/src/app/components/admin/admin-competency-detail-dialog/admin-competency-detail-dialog.component.ts @@ -0,0 +1,35 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Competency } from 'src/app/generated/blueprint.api'; + +@Component({ + selector: 'app-admin-competency-detail-dialog', + templateUrl: './admin-competency-detail-dialog.component.html', + styleUrls: ['./admin-competency-detail-dialog.component.scss'], + standalone: false +}) +export class AdminCompetencyDetailDialogComponent { + competencyTypeMap: Map; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { + competency: Competency; + competencyTypeMap: Map; + } + ) { + this.competencyTypeMap = data.competencyTypeMap; + } + + getType(): string { + return this.competencyTypeMap?.get(this.data.competency.id) || ''; + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.html b/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.html new file mode 100644 index 00000000..e7623cbc --- /dev/null +++ b/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.html @@ -0,0 +1,93 @@ + +
+
+ @if (data.competency.id) { + Edit + } + @if (!data.competency.id) { + Add + } + {{ typeHint || 'Competency' }} +   + +
+ @if (!!data && data.competency !== undefined && data.competency !== null) { +
+ @if (availableTypes.length > 0) { +
+ + Type + + @for (t of availableTypes; track t) { + {{ t }} + } + + +
+ } + @if (availableParents.length > 0) { +
+ + Category + + None + @for (p of availableParents; track p.id) { + {{ p.label }} + } + + +
+ } +
+ + ID Number + + +
+
+ + Short Name + + +
+
+ + Description + + +
+
+
+ +
+
+ +
+
+
+ } diff --git a/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.scss b/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.scss new file mode 100644 index 00000000..9e3ab68a --- /dev/null +++ b/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.scss @@ -0,0 +1,28 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.full-width { + width: 90%; +} + +.add-margin { + margin-top: 8px; + margin-bottom: 8px; + margin-right: 65px; +} + +.bottom-button { + margin-bottom: 10px; + margin-top: 10px; +} + +.delete-button { + margin-top: 10px; + margin-right: 50px; +} + +.close-button { + float: right; +} + diff --git a/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.ts b/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.ts new file mode 100644 index 00000000..3dd23110 --- /dev/null +++ b/src/app/components/admin/admin-competency-edit-dialog/admin-competency-edit-dialog.component.ts @@ -0,0 +1,76 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, EventEmitter, Inject, Output } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; + +@Component({ + selector: 'app-admin-competency-edit-dialog', + templateUrl: './admin-competency-edit-dialog.component.html', + styleUrls: ['./admin-competency-edit-dialog.component.scss'], + standalone: false +}) +export class AdminCompetencyEditDialogComponent { + @Output() editComplete = new EventEmitter(); + typeHint = ''; + typeHintLocked = false; + availableTypes: string[] = []; + availableParents: { id: string; label: string }[] = []; + private typePrefixMap: Record = { + 'Work Role': 'WRL-', + 'Task': 'T', + 'Knowledge': 'K', + 'Skill': 'S', + 'Ability': 'A', + }; + + constructor( + dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) { + dialogRef.disableClose = true; + this.typeHint = this.data.typeHint || ''; + this.typeHintLocked = !!this.data.typeHint; + this.availableTypes = this.data.availableTypes || []; + this.availableParents = this.data.availableParents || []; + if (this.typeHintLocked && !this.data.competency.id) { + this.onTypeChange(this.typeHint); + } + } + + onTypeChange(type: string) { + const oldPrefix = this.typePrefixMap[this.typeHint] || ''; + this.typeHint = type; + const newPrefix = this.typePrefixMap[type] || ''; + const current = this.data.competency.idNumber || ''; + if (!current) { + // Empty — set new prefix + this.data.competency.idNumber = newPrefix; + } else if (oldPrefix && newPrefix && current.startsWith(oldPrefix)) { + // Both types have prefixes — swap old for new + this.data.competency.idNumber = newPrefix + current.substring(oldPrefix.length); + } else if (!oldPrefix && newPrefix && !current) { + // Old type had no prefix, new does, field empty — set prefix + this.data.competency.idNumber = newPrefix; + } + // Otherwise leave the ID as-is + } + + errorFree() { + return this.data.competency.idNumber || this.data.competency.shortName; + } + + handleEditComplete(saveChanges: boolean): void { + if (!saveChanges) { + this.editComplete.emit({ saveChanges: false, competency: null }); + } else { + if (this.errorFree()) { + this.editComplete.emit({ + saveChanges: true, + competency: this.data.competency, + }); + } + } + } +} diff --git a/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.html b/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.html new file mode 100644 index 00000000..173a90a7 --- /dev/null +++ b/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.html @@ -0,0 +1,89 @@ + +
+
+ @if (data.element.id) { + Edit + } + @if (!data.element.id) { + Create + } + Competency Element +   + +
+ @if (!!data && data.element !== undefined && data.element !== null) { +
+
+
+ + Identifier + + +
+
+
+
+ + Type + + +
+
+
+
+ + Name + + +
+
+
+
+ + Description + + +
+
+
+
+ +
+
+ +
+
+
+ } diff --git a/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.scss b/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.scss new file mode 100644 index 00000000..9879e956 --- /dev/null +++ b/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.scss @@ -0,0 +1,27 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.full-width { + width: 90%; +} + +.add-margin { + margin-top: 8px; + margin-bottom: 8px; + margin-right: 65px; +} + +.bottom-button { + margin-bottom: 10px; + margin-top: 10px; +} + +.delete-button { + margin-top: 10px; + margin-right: 50px; +} + +.close-button { + float: right; +} diff --git a/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.ts b/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.ts new file mode 100644 index 00000000..c65ef028 --- /dev/null +++ b/src/app/components/admin/admin-competency-element-edit-dialog/admin-competency-element-edit-dialog.component.ts @@ -0,0 +1,43 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, EventEmitter, Inject, Output } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { DialogService } from 'src/app/services/dialog/dialog.service'; + +@Component({ + selector: 'app-admin-competency-element-edit-dialog', + templateUrl: './admin-competency-element-edit-dialog.component.html', + styleUrls: ['./admin-competency-element-edit-dialog.component.scss'], + standalone: false +}) + +export class AdminCompetencyElementEditDialogComponent { + @Output() editComplete = new EventEmitter(); + + constructor( + public dialogService: DialogService, + dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) { + dialogRef.disableClose = true; + } + + errorFree() { + return this.data.element.name; + } + + handleEditComplete(saveChanges: boolean): void { + if (!saveChanges) { + this.editComplete.emit({ saveChanges: false, element: null }); + } else { + if (this.errorFree()) { + this.editComplete.emit({ + saveChanges: saveChanges, + element: this.data.element, + }); + } + } + } +} diff --git a/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.html b/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.html new file mode 100644 index 00000000..ca466f3f --- /dev/null +++ b/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.html @@ -0,0 +1,103 @@ + +
+
+ @if (data.competencyFramework.id) { + Edit + } + @if (!data.competencyFramework.id) { + Create + } + Competency Framework +   + +
+ @if (!!data && data.competencyFramework !== undefined && data.competencyFramework !== null) { +
+
+
+ + Name + + +
+
+
+
+ + Version + + +
+
+
+
+ + Source + + +
+
+
+
+ + Description + + +
+
+
+
+ + Proficiency Scale + + None + @for (scale of proficiencyScales; track scale.id) { + {{ scale.name }} + } + + +
+
+
+
+ +
+
+ +
+
+
+ } diff --git a/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.scss b/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.scss new file mode 100644 index 00000000..9879e956 --- /dev/null +++ b/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.scss @@ -0,0 +1,27 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.full-width { + width: 90%; +} + +.add-margin { + margin-top: 8px; + margin-bottom: 8px; + margin-right: 65px; +} + +.bottom-button { + margin-bottom: 10px; + margin-top: 10px; +} + +.delete-button { + margin-top: 10px; + margin-right: 50px; +} + +.close-button { + float: right; +} diff --git a/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.ts b/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.ts new file mode 100644 index 00000000..b16d025d --- /dev/null +++ b/src/app/components/admin/admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component.ts @@ -0,0 +1,78 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core'; +import { + UntypedFormControl, + FormGroupDirective, + NgForm, + Validators, +} from '@angular/forms'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { DialogService } from 'src/app/services/dialog/dialog.service'; +import { ProficiencyScale, ProficiencyScaleService } from 'src/app/generated/blueprint.api'; +import { take } from 'rxjs/operators'; + +/** Error when invalid control is dirty, touched, or submitted. */ +export class UserErrorStateMatcher implements ErrorStateMatcher { + isErrorState( + control: UntypedFormControl | null, + form: FormGroupDirective | NgForm | null + ): boolean { + const isSubmitted = form && form.submitted; + return !!(control && control.invalid && (control.dirty || isSubmitted)); + } +} + +@Component({ + selector: 'app-admin-competency-framework-edit-dialog', + templateUrl: './admin-competency-framework-edit-dialog.component.html', + styleUrls: ['./admin-competency-framework-edit-dialog.component.scss'], + standalone: false +}) + +export class AdminCompetencyFrameworkEditDialogComponent implements OnInit { + @Output() editComplete = new EventEmitter(); + isChanged = false; + proficiencyScales: ProficiencyScale[] = []; + + constructor( + public dialogService: DialogService, + dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + private proficiencyScaleService: ProficiencyScaleService + ) { + dialogRef.disableClose = true; + } + + ngOnInit() { + this.proficiencyScaleService.getProficiencyScales() + .pipe(take(1)) + .subscribe(scales => { + this.proficiencyScales = scales; + }); + } + + errorFree() { + return this.data.competencyFramework.name; + } + + /** + * Closes the edit screen + */ + handleEditComplete(saveChanges: boolean): void { + if (!saveChanges) { + this.editComplete.emit({ saveChanges: false, competencyFramework: null }); + } else { + if (this.errorFree) { + this.editComplete.emit({ + saveChanges: saveChanges, + competencyFramework: this.data.competencyFramework, + }); + } + } + } + +} diff --git a/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.html b/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.html new file mode 100644 index 00000000..54dd60d2 --- /dev/null +++ b/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.html @@ -0,0 +1,105 @@ + + +
Import Competency Framework
+ +
+
+

+ Supported formats: Moodle CSV (lpimportcsv), NICE Framework JSON, DCWF XLSX +

+ + + @if (fileName) { + {{ fileName }} + } +
+ + @if (parseError) { +
{{ parseError }}
+ } + + @if (successMessage) { +
+ + {{ successMessage }} +
+ } + + @if (selectedFile) { +
+ + Source + + + + Version + + + + @if (frameworkName) { +
+ Framework Name: {{ frameworkName }} +
+ } + + @if (!isProcessing && totalElements > 0) { +
+
Competencies to import: {{ totalElements }}
+ @if (totalRelationships > 0) { +
Relationships: {{ totalRelationships }}
+ } + @if (elementTypeCounts.length > 0) { + + + + + + + + + @for (tc of elementTypeCounts; track tc.type) { + + + + + } + +
TypeCount
{{ tc.type }}{{ tc.count }}
+ } +
+ } + + @if (isProcessing) { +
+ + Processing file... +
+ } +
+ } +
+ +
+ @if (importSucceeded) { + + } @else { + + + } +
diff --git a/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.scss b/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.scss new file mode 100644 index 00000000..051e8df0 --- /dev/null +++ b/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.scss @@ -0,0 +1,123 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.upload-section { + margin-bottom: 16px; +} + +.instructions { + font-size: 13px; + color: var(--mat-sys-on-surface-variant); + margin-bottom: 8px; +} + +.file-name { + margin-left: 12px; + font-size: 14px; +} + +.error-message { + color: var(--mat-sys-error); + padding: 8px 0; +} + +.success-banner { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + margin: 16px 0; + background: #e8f5e9 !important; + color: #1b5e20 !important; + border: 1px solid #81c784 !important; + border-radius: 8px; + font-weight: 500; + font-size: 15px; + + mat-icon { + font-size: 24px !important; + width: 24px !important; + height: 24px !important; + line-height: 24px !important; + color: #2e7d32 !important; + flex-shrink: 0; + } + + span { + color: #1b5e20 !important; + } +} + +.preview-section { + margin-top: 8px; +} + +.preview-header { + font-weight: bold; + font-size: 14px; + margin-bottom: 8px; +} + +.preview-field { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.preview-label { + font-weight: 500; + min-width: 70px; + font-size: 13px; +} + +.preview-input { + flex: 1; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 4px; + padding: 4px 8px; + font-size: 13px; + background: transparent; + color: inherit; +} + +.element-summary { + margin-top: 16px; +} + +.type-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th, td { + text-align: left; + padding: 4px 8px; + border-bottom: 1px solid var(--mat-sys-outline-variant); + } + + th { + font-weight: 500; + } + + .skipped { + opacity: 0.5; + text-decoration: line-through; + } +} + +.processing-message { + display: flex; + align-items: center; + gap: 12px; + margin-top: 16px; + padding: 12px; + background: var(--mat-sys-surface-container-low); + border-radius: 4px; + + span { + font-size: 13px; + color: var(--mat-sys-on-surface-variant); + } +} diff --git a/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.ts b/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.ts new file mode 100644 index 00000000..2941d769 --- /dev/null +++ b/src/app/components/admin/admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component.ts @@ -0,0 +1,169 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, EventEmitter, Output } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { CompetencyFramework, Competency, CompetencyFrameworkService } from 'src/app/generated/blueprint.api'; +import { take } from 'rxjs/operators'; + +interface ElementTypeCount { + type: string; + count: number; +} + +export interface ImportResult { + type: 'csv' | 'json' | 'xlsx'; + file?: File; + source: string; + version: string; +} + +@Component({ + selector: 'app-admin-competency-framework-import-dialog', + templateUrl: './admin-competency-framework-import-dialog.component.html', + styleUrls: ['./admin-competency-framework-import-dialog.component.scss'], + standalone: false +}) +export class AdminCompetencyFrameworkImportDialogComponent { + @Output() importComplete = new EventEmitter(); + fileName = ''; + parseError = ''; + successMessage = ''; + importSucceeded = false; + source = ''; + version = ''; + selectedFile: File | null = null; + fileType: 'csv' | 'json' | 'xlsx' | null = null; + elementTypeCounts: ElementTypeCount[] = []; + totalElements = 0; + totalRelationships = 0; + frameworkName = ''; + isProcessing = false; + + constructor( + public dialogRef: MatDialogRef, + private competencyFrameworkService: CompetencyFrameworkService + ) { + dialogRef.disableClose = true; + } + + onFileSelected(event: any) { + const file = event.target.files[0]; + if (!file) return; + this.fileName = file.name; + this.parseError = ''; + this.elementTypeCounts = []; + this.totalElements = 0; + this.totalRelationships = 0; + this.frameworkName = ''; + this.selectedFile = file; + this.isProcessing = true; + + if (file.name.endsWith('.csv')) { + this.fileType = 'csv'; + // Extract source/version from filename + const versionMatch = file.name.match(/_v([\d.]+)/i); + if (versionMatch) this.version = versionMatch[1]; + const sourceMatch = file.name.match(/^([A-Z]+)/i); + if (sourceMatch) this.source = sourceMatch[1].toUpperCase(); + + // Call preview API + this.competencyFrameworkService.previewCompetencyFrameworkCsv(this.source, this.version, file) + .pipe(take(1)) + .subscribe({ + next: (preview) => this.handlePreview(preview), + error: (err) => { + this.parseError = `Error previewing CSV: ${err.error?.title || err.message || 'Unknown error'}`; + this.isProcessing = false; + } + }); + } else if (file.name.endsWith('.json')) { + this.fileType = 'json'; + // Call preview API + this.competencyFrameworkService.previewCompetencyFrameworkJson(file) + .pipe(take(1)) + .subscribe({ + next: (preview) => this.handlePreview(preview), + error: (err) => { + this.parseError = `Error previewing JSON: ${err.error?.title || err.message || 'Unknown error'}`; + this.isProcessing = false; + } + }); + } else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { + this.fileType = 'xlsx'; + // Extract version from filename + const versionMatch = file.name.match(/_v([\d.]+)\./i); + this.source = 'DCWF'; + this.version = versionMatch ? versionMatch[1] : ''; + + // Call preview API + this.competencyFrameworkService.previewCompetencyFrameworkXlsx(this.source, this.version, file) + .pipe(take(1)) + .subscribe({ + next: (preview) => this.handlePreview(preview), + error: (err) => { + this.parseError = `Error previewing XLSX: ${err.error?.title || err.message || 'Unknown error'}`; + this.isProcessing = false; + } + }); + } else { + this.parseError = 'Supported formats: .csv (Moodle), .json (NICE), .xlsx (DCWF)'; + this.selectedFile = null; + this.fileType = null; + this.isProcessing = false; + } + } + + private handlePreview(preview: any): void { + console.log('Preview response:', preview); + if (preview.error) { + this.parseError = preview.error; + } else { + this.source = preview.source || this.source; + this.version = preview.version || this.version; + this.frameworkName = preview.frameworkName || ''; + this.elementTypeCounts = preview.elementTypeCounts || []; + this.totalElements = preview.totalElements || 0; + this.totalRelationships = preview.totalRelationships || 0; + } + this.isProcessing = false; + } + + + get ready(): boolean { + if (this.fileType === 'csv') return !!this.selectedFile && !!this.source && !!this.version; + if (this.fileType === 'json') return !!this.selectedFile; // Allow import without preview + if (this.fileType === 'xlsx') return !!this.selectedFile && !!this.source && !!this.version; + return false; + } + + handleImport(): void { + if (this.fileType === 'csv' && this.selectedFile) { + this.importComplete.emit({ + type: 'csv', + file: this.selectedFile, + source: this.source, + version: this.version, + }); + } else if (this.fileType === 'json' && this.selectedFile) { + this.importComplete.emit({ + type: 'json', + file: this.selectedFile, + source: this.source, + version: this.version, + }); + } else if (this.fileType === 'xlsx' && this.selectedFile) { + this.importComplete.emit({ + type: 'xlsx', + file: this.selectedFile, + source: this.source, + version: this.version, + }); + } + } + + handleCancel(): void { + this.importComplete.emit(null); + } +} diff --git a/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.html b/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.html new file mode 100644 index 00000000..cfe77240 --- /dev/null +++ b/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.html @@ -0,0 +1,557 @@ + + +
+
+ +
+ + + @if (filterString) { + + } + + @if (canEdit) { + + } + @if (importError) { + {{ importError }} + } +
+ +
+
+ +
+ + + + + @if (canEdit) { + + }   + + +
+ @if (canEdit) { + + + + } +
+
+
+ + + + Name + + + {{ element.name }} + + + + + + Version + + + {{ element.version }} + + + + + + Source + + + {{ element.source }} + + + + + + Scale + + + {{ getScaleName(element) }} + + + + + + Description + + + {{ element.description }} + + + + + +
+ @if (expandedElementId === element.id) { + + @if (workRoles.length > 0 || canEdit) { + + + + + Work Roles ({{ workRoles.length }}) + + +
+ + + @if (workRoleFilterString) { + + } + + @if (workRoleCategories.length > 0) { + + Category + + All + @for (cat of workRoleCategories; track cat) { + {{ cat }} + } + + + } +
+ +
+
+
+ + + + + @if (canEdit) { + + } + + + @if (canEdit) { + + + } + + + + + ID + {{ c.idNumber }} + + + + Name + {{ c.shortName }} + + + + Category + {{ getWorkRoleCategory(c) }} + + + + Description + {{ c.description }} + + + + + @if (expandedCompetencyId === c.id) { + + + } + + + + + + + + @if (workRoleDataSource.filteredData.length === 0) { +
No work roles found
+ } +
+
+ } + + + + + + Competencies ({{ expandedCompetencies.length }}) + + +
+ + + @if (competencyFilterString) { + + } + + @if (competencyTypes.length > 1) { + + Type + + All + @for (t of competencyTypes; track t) { + {{ t }} + } + + + } +
+ +
+
+
+ + + + + @if (canEdit) { + + } + + + @if (canEdit) { + + + } + + + + + ID + {{ c.idNumber }} + + + + Type + {{ getCompetencyType(c) }} + + + + Short Name + {{ c.shortName }} + + + + Description + {{ c.description }} + + + + + @if (expandedCompetencyId === c.id) { + + + } + + + + + + + + @if (competencyDataSource.filteredData.length === 0) { +
No competencies found
+ } +
+
+ } +
+
+
+ + + + + + +
+ @if (competencyFrameworkDataSource?.filteredData.length === 0) { +
No Competency Frameworks found
+ } +
diff --git a/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.scss b/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.scss new file mode 100644 index 00000000..f1daf79e --- /dev/null +++ b/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.scss @@ -0,0 +1,395 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +@use "@angular/material" as mat; + +:host { + display: flex; + flex-direction: column; + height: calc(100vh - 44px); +} + +.cssLayoutRowStartCenter { + min-height: 76px; + flex-shrink: 0; +} + +.button-end { + margin-left: auto; +} + +.section-panel { + margin-bottom: 12px; + background-color: transparent; + width: 100%; + min-width: 660px; +} + +.panel-icon { + margin-right: 8px; + color: var(--mat-sys-primary); +} + +.table { + font-size: small; + width: 100%; + overflow: none; + background: transparent; +} + +.header-row { + min-height: 18px; + background: transparent; +} + +.row { + min-height: 18px; +} + +.row, +.header-row { + display: flex; + border-bottom-width: 1px; + border-bottom-style: solid; + align-items: center; + box-sizing: border-box; +} + +.header-cell { + font-weight: bold; +} + +.cell, +.header-cell { + overflow: hidden; + word-wrap: break-word; + text-align: left; +} + +.element-row { + position: relative; + cursor: pointer; +} + +.element-row:hover { + background-color: var(--mat-sys-surface-variant); +} + +.detail-row { + height: 0; + min-height: 0; + overflow: hidden; + --mat-table-row-item-outline-width: 0px; +} + +::ng-deep .detail-row .mat-mdc-cell.expanded-detail-cell { + padding: 0 !important; + min-height: 0; + border-bottom-width: 0; + flex: 0 0 100%; + width: 100%; + max-width: 100%; +} + +.element-row td { + border-bottom-width: 0; +} + +.column-name { + flex: 0 0 20%; + padding: 2px; +} + +.column-action { + flex: 0 0 150px; + width: 150px; +} + +.action-div { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; +} + +.column-version { + flex: 0 0 10%; + padding: 2px; +} + +.column-source { + flex: 0 0 10%; + padding: 2px; +} + +.column-scale { + flex: 0 0 12%; + padding: 2px; +} + +.column-description { + flex: 0 0 calc(48% - 150px); + padding: 2px; +} + +.expanded-detail-cell { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + min-width: 660px; + padding: 0; + gap: 8px; +} + +.row:has(.expanded-detail-div) { + border-bottom: none; +} + +.expanded-detail-div { + width: 100%; + min-width: 660px; + max-height: 500px; + overflow: auto; +} + +.scrolling-region { + height: calc(100% - 40px); + overflow: auto; + width: 100%; +} + +.no-results { + margin-top: 20px; + margin-left: 150px; +} + +.no-results-inline { + padding: 8px 0; + color: var(--mat-sys-on-surface-variant); + font-style: italic; +} + +// Sub-table column widths (scales and elements) +.sub-col-action { + flex: 0 0 100px; + width: 100px; +} + +.sub-col-name { + flex: 1 1 25%; +} + +.sub-col-desc { + flex: 2 1 45%; +} + +.sub-col-levels { + flex: 0 0 80px; +} + +.el-col-action { + flex: 0 0 100px; + width: 100px; +} + +.el-col-id { + flex: 0 0 120px; + white-space: nowrap; +} + +.el-col-type { + flex: 0 0 10%; +} + +.el-col-name { + flex: 1 1 25%; +} + +.el-col-desc { + flex: 2 1 45%; +} + +// Level sub-table column widths +.lv-col-name { + flex: 1 1 20%; +} + +.lv-col-value { + flex: 0 0 80px; +} + +.lv-col-order { + flex: 0 0 80px; +} + +.lv-col-desc { + flex: 2 1 50%; +} + +// Levels detail section +.levels-detail { + margin: 8px 0 8px 16px; + padding: 8px; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 4px; +} + +.levels-title { + font-weight: bold; + font-size: 13px; + margin-bottom: 4px; +} + +.element-row-expanded { + background-color: var(--mat-sys-surface-variant); +} + +// Competency (Moodle-imported) sub-table column widths +.comp-col-action { + flex: 0 0 140px; + width: 140px; +} + +.comp-col-id { + flex: 0 0 120px; + white-space: nowrap; +} + +.comp-col-type { + flex: 0 0 10%; +} + +.comp-col-name { + flex: 1 1 20%; +} + +.comp-col-desc { + flex: 2 1 40%; +} + +.comp-col-category { + flex: 0 0 15%; +} + +.comp-col-related { + flex: 1 1 15%; +} + + +.element-toolbar { + display: flex; + align-items: center; + min-height: 56px; + flex-shrink: 0; +} + +// Sticky header background for sub-tables +::ng-deep .expanded-detail-div .mat-mdc-header-row { + background-color: var(--mat-sys-surface); + border-bottom: 1px solid var(--mat-sys-outline-variant); +} + +::ng-deep .expanded-detail-div .mat-mdc-row { + border-bottom: 1px solid var(--mat-sys-outline-variant); +} + +// Competency row expand +.comp-row { + cursor: pointer; + border-bottom: 1px solid var(--mat-sys-outline-variant); +} + +.comp-row:hover { + background-color: var(--mat-sys-surface-variant); +} + +.comp-row-expanded { + background-color: var(--mat-sys-surface-variant); +} + +// Related detail cell +::ng-deep .detail-row .mat-mdc-cell.related-detail-cell { + display: flex; + flex-direction: column; + align-items: stretch; + padding: 0 !important; + min-height: 0; + border-bottom-width: 0; + flex: 0 0 100%; + width: 100%; + max-width: 100%; +} + +.related-hint { + padding: 8px 4px 0; + font-size: 13px; + color: var(--mat-sys-on-surface-variant); +} + +// Card-teams-style two-table layout +.related-enchilada { + display: flex; + flex-direction: row; + align-items: flex-start; + width: 100%; + padding: 8px 0; + gap: 4%; +} + +.related-list-container { + display: flex; + flex-direction: column; + width: 48%; + min-width: 250px; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 4px; +} + +.related-toolbar { + background: transparent; + overflow: visible; + + @include mat.form-field-density(-5); + + p { + margin-left: 8px; + } +} + +::ng-deep .related-toolbar .mat-toolbar-row { + overflow: visible; +} + +.header-icon { + margin-right: 10px; + color: var(--mat-sys-primary); +} + +.related-search { + width: 40%; +} + +.related-type-filter { + width: 140px; + margin-left: 8px; +} + +::ng-deep .related-type-filter .mat-mdc-select-panel { + min-width: 200px; +} + +.related-table-scroll { + max-height: 600px; + overflow: auto; +} + +.related-col-btn { + flex: 0 0 48px; + width: 48px; + justify-content: center; +} + + + diff --git a/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.ts b/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.ts new file mode 100644 index 00000000..1539a4b7 --- /dev/null +++ b/src/app/components/admin/admin-competency-frameworks/admin-competency-frameworks.component.ts @@ -0,0 +1,815 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. +import { Component, Input, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'; +import { UntypedFormControl } from '@angular/forms'; +import { MatTableDataSource, MatTable } from '@angular/material/table'; +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { Subject, Subscription } from 'rxjs'; +import { take, takeUntil } from 'rxjs/operators'; +import { + Competency, + CompetencyFramework, + CompetencyFrameworkService, + ProficiencyScaleService, +} from 'src/app/generated/blueprint.api'; +import { Sort } from '@angular/material/sort'; +import { MatPaginator } from '@angular/material/paginator'; +import { CompetencyFrameworkDataService } from 'src/app/data/competency-framework/competency-framework-data.service'; +import { CompetencyFrameworkQuery } from 'src/app/data/competency-framework/competency-framework.query'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from 'src/app/services/dialog/dialog.service'; +import { AdminCompetencyFrameworkEditDialogComponent } from '../admin-competency-framework-edit-dialog/admin-competency-framework-edit-dialog.component'; +import { AdminCompetencyFrameworkImportDialogComponent, ImportResult } from '../admin-competency-framework-import-dialog/admin-competency-framework-import-dialog.component'; +import { AdminCompetencyEditDialogComponent } from '../admin-competency-edit-dialog/admin-competency-edit-dialog.component'; +import { AdminCompetencyDetailDialogComponent } from '../admin-competency-detail-dialog/admin-competency-detail-dialog.component'; +import { v4 as uuidv4 } from 'uuid'; + +@Component({ + selector: 'app-admin-competency-frameworks', + templateUrl: './admin-competency-frameworks.component.html', + styleUrls: ['./admin-competency-frameworks.component.scss'], + animations: [ + trigger('detailExpand', [ + state('collapsed', style({ height: '0px', minHeight: '0', visibility: 'hidden' })), + state('expanded', style({ height: '*', visibility: 'visible' })), + transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), + ]), + ], + standalone: false +}) +export class AdminCompetencyFrameworksComponent implements OnDestroy, AfterViewInit { + @Input() loggedInUserId: string; + @Input() canEdit: boolean; + @ViewChild('competencyFrameworkTable', { static: false }) competencyFrameworkTable: MatTable; + @ViewChild('paginator') paginator: MatPaginator; + @ViewChild('competencyPaginator') competencyPaginator: MatPaginator; + @ViewChild('workRolePaginator') workRolePaginator: MatPaginator; + adminCompetencyFrameworks: CompetencyFramework[] = []; + filterControl = new UntypedFormControl(); + filterString = ''; + sort: Sort = { active: 'name', direction: 'asc' }; + competencyFrameworkDataSource = new MatTableDataSource(new Array()); + displayedColumns: string[] = ['action', 'name', 'version', 'source', 'scale', 'description']; + private scaleMap = new Map(); + private frameworkDeleteCheckMap = new Map(); + private unsubscribe$ = new Subject(); + isExpansionDetailRow = (i: number, row: Object) => (row as CompetencyFramework).id === this.expandedElementId; + expandedElementId = ''; + // Competency data sources + expandedCompetencies: Competency[] = []; + competencyDataSource = new MatTableDataSource([]); + competencyDisplayedColumns: string[] = ['action', 'idNumber', 'type', 'shortName', 'description']; + competencyFilterControl = new UntypedFormControl(); + competencyFilterString = ''; + competencySort: Sort = { active: 'idNumber', direction: 'asc' }; + competencyTypes: string[] = []; + selectedCompetencyType = ''; + private taxonomyLevels: string[] = []; + private competencyTypeMap = new Map(); + private competencyById = new Map(); + // Work role data sources + workRoles: Competency[] = []; + workRoleDataSource = new MatTableDataSource([]); + workRoleDisplayedColumns: string[] = ['action', 'idNumber', 'shortName', 'category', 'description']; + workRoleFilterControl = new UntypedFormControl(); + workRoleFilterString = ''; + workRoleSort: Sort = { active: 'idNumber', direction: 'asc' }; + workRoleCategories: string[] = []; + selectedWorkRoleCategory = ''; + // Inline related competency management + expandedCompetencyId = ''; + availableRelatedDataSource = new MatTableDataSource([]); + relatedDataSource = new MatTableDataSource([]); + availableRelatedColumns: string[] = ['name', 'view', 'add']; + relatedColumns: string[] = ['name', 'view', 'remove']; + relatedFilterControl = new UntypedFormControl(); + relatedSideFilterControl = new UntypedFormControl(); + availableTypeFilter = ''; + relatedTypeFilter = ''; + availableTypes: string[] = []; + relatedTypes: string[] = []; + @ViewChild('availablePaginator') availablePaginator: MatPaginator; + @ViewChild('relatedPaginator') relatedPaginator: MatPaginator; + private expandedComp: Competency = null; + private currentRelatedIdNumbers: string[] = []; + private relatedFilterSub: Subscription; + private relatedSideFilterSub: Subscription; + + importing = false; + importError = ''; + + constructor( + private competencyFrameworkDataService: CompetencyFrameworkDataService, + private competencyFrameworkQuery: CompetencyFrameworkQuery, + private competencyFrameworkService: CompetencyFrameworkService, + private proficiencyScaleService: ProficiencyScaleService, + public dialog: MatDialog, + public dialogService: DialogService + ) { + this.proficiencyScaleService.getProficiencyScales() + .pipe(take(1)) + .subscribe(scales => { + scales.forEach(s => this.scaleMap.set(s.id, s.name)); + }); + this.competencyFrameworkQuery.selectAll().pipe(takeUntil(this.unsubscribe$)).subscribe(competencyFrameworks => { + this.adminCompetencyFrameworks = competencyFrameworks; + this.checkAllFrameworksForDelete(); + this.sortChanged(this.sort); + }); + this.filterControl.valueChanges + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((term) => { + this.filterString = term; + this.sortChanged(this.sort); + }); + this.competencyFilterControl.valueChanges + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((term) => { + this.competencyFilterString = term; + this.applyCompetencyFilter(); + }); + this.workRoleFilterControl.valueChanges + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((term) => { + this.workRoleFilterString = term; + this.applyWorkRoleFilter(); + }); + } + + getSortedCompetencyFrameworks(competencyFrameworks: CompetencyFramework[]) { + if (competencyFrameworks) { + competencyFrameworks.sort((a, b) => this.sortCompetencyFrameworks(a, b, this.sort.active, this.sort.direction)); + } + return competencyFrameworks; + } + + addOrEditCompetencyFramework(competencyFramework: CompetencyFramework) { + if (!competencyFramework) { + competencyFramework = {}; + } + const dialogRef = this.dialog.open(AdminCompetencyFrameworkEditDialogComponent, { + minWidth: '500px', + maxWidth: '90vw', + width: '700px', + data: { + competencyFramework: { ...competencyFramework }, + }, + }); + dialogRef.componentInstance.editComplete.subscribe((result) => { + if (result.saveChanges && result.competencyFramework) { + this.saveCompetencyFramework(result.competencyFramework); + } + dialogRef.close(); + }); + } + + saveCompetencyFramework(competencyFramework: CompetencyFramework) { + if (competencyFramework.id) { + this.competencyFrameworkDataService.update(competencyFramework); + } else { + competencyFramework.id = uuidv4(); + this.competencyFrameworkDataService.add(competencyFramework); + } + } + + checkAllFrameworksForDelete(): void { + this.adminCompetencyFrameworks.forEach(fw => { + this.competencyFrameworkService.checkCanDeleteCompetencyFramework(fw.id) + .pipe(take(1)) + .subscribe({ + next: (check) => { + this.frameworkDeleteCheckMap.set(fw.id, { + canDelete: check.canDelete, + inUseByMsels: check.affectedMsels?.map(m => m.name) || [] + }); + }, + error: () => { + this.frameworkDeleteCheckMap.set(fw.id, { canDelete: true, inUseByMsels: [] }); + } + }); + }); + } + + canDeleteFramework(frameworkId: string): boolean { + return this.frameworkDeleteCheckMap.get(frameworkId)?.canDelete ?? true; + } + + getDeleteTooltip(frameworkId: string): string { + const check = this.frameworkDeleteCheckMap.get(frameworkId); + if (!check || check.canDelete) { + return 'Delete framework'; + } + const mselCount = check.inUseByMsels.length; + const mselList = check.inUseByMsels.slice(0, 3).join(', '); + const more = mselCount > 3 ? ` and ${mselCount - 3} more` : ''; + return `In use by ${mselCount} MSEL(s): ${mselList}${more}`; + } + + downloadFramework(competencyFramework: CompetencyFramework): void { + this.competencyFrameworkService.getCompetencyFramework(competencyFramework.id) + .pipe(take(1)) + .subscribe({ + next: (fw) => { + const json = JSON.stringify(fw, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + const filename = `${fw.name}-${fw.version || 'export'}.json`.replace(/[^a-z0-9.-]/gi, '_'); + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, + error: (err) => { + this.importError = 'Download failed: ' + (err.error?.title || err.message || 'Unknown error'); + } + }); + } + + deleteCompetencyFramework(competencyFramework: CompetencyFramework): void { + this.dialogService + .confirm( + 'Delete Competency Framework', + 'Are you sure that you want to delete ' + competencyFramework.name + '? This will delete ' + + (competencyFramework.competencies?.length || 0) + ' competencies.' + ) + .subscribe((result) => { + if (result['confirm']) { + this.competencyFrameworkDataService.delete(competencyFramework.id); + this.expandedElementId = ''; + } + }); + } + + importFramework(): void { + const dialogRef = this.dialog.open(AdminCompetencyFrameworkImportDialogComponent, { + width: '600px', + maxWidth: '90vw', + }); + dialogRef.componentInstance.importComplete.subscribe((result: ImportResult | null) => { + if (!result) { + dialogRef.close(); + return; + } + + dialogRef.componentInstance.isProcessing = true; + dialogRef.componentInstance.parseError = ''; + + if (result.type === 'csv' && result.file) { + this.competencyFrameworkService.importCompetencyFramework(result.source, result.version, result.file) + .pipe(take(1)) + .subscribe({ + next: (created) => { + this.competencyFrameworkDataService.updateStore(created); + dialogRef.componentInstance.isProcessing = false; + dialogRef.componentInstance.importSucceeded = true; + dialogRef.componentInstance.successMessage = `Successfully imported ${created.name}`; + }, + error: (err) => { + dialogRef.componentInstance.isProcessing = false; + dialogRef.componentInstance.parseError = 'Import failed: ' + (err.error?.title || err.message || 'Unknown error'); + } + }); + } else if (result.type === 'json' && result.file) { + this.competencyFrameworkService.importCompetencyFrameworkJson(result.file) + .pipe(take(1)) + .subscribe({ + next: (created) => { + this.competencyFrameworkDataService.updateStore(created); + dialogRef.componentInstance.isProcessing = false; + dialogRef.componentInstance.importSucceeded = true; + dialogRef.componentInstance.successMessage = `Successfully imported ${created.name}`; + }, + error: (err) => { + dialogRef.componentInstance.isProcessing = false; + dialogRef.componentInstance.parseError = 'Import failed: ' + (err.error?.title || err.message || 'Unknown error'); + } + }); + } else if (result.type === 'xlsx' && result.file) { + this.competencyFrameworkService.importCompetencyFrameworkXlsx(result.source, result.version, result.file) + .pipe(take(1)) + .subscribe({ + next: (created) => { + this.competencyFrameworkDataService.updateStore(created); + dialogRef.componentInstance.isProcessing = false; + dialogRef.componentInstance.importSucceeded = true; + dialogRef.componentInstance.successMessage = `Successfully imported ${created.name}`; + }, + error: (err) => { + dialogRef.componentInstance.isProcessing = false; + dialogRef.componentInstance.parseError = 'Import failed: ' + (err.error?.title || err.message || 'Unknown error'); + } + }); + } + }); + } + + sortChanged(sort: Sort) { + this.sort = sort; + this.competencyFrameworkDataSource.data = this.getSortedCompetencyFrameworks(this.getFilteredCompetencyFrameworks(this.adminCompetencyFrameworks)); + } + + ngAfterViewInit() { + this.competencyFrameworkDataSource.paginator = this.paginator; + } + + ngOnDestroy() { + this.unsubscribe$.next(null); + this.unsubscribe$.complete(); + } + + getFilteredCompetencyFrameworks(competencyFrameworks: CompetencyFramework[]): CompetencyFramework[] { + let filtered: CompetencyFramework[] = []; + if (competencyFrameworks) { + competencyFrameworks.forEach(cf => { + filtered.push({ ...cf }); + }); + if (filtered && filtered.length > 0 && this.filterString) { + const filterString = this.filterString?.toLowerCase(); + filtered = filtered.filter(cf => + cf.name?.toLowerCase().includes(filterString) || + cf.source?.toLowerCase().includes(filterString) || + cf.version?.toLowerCase().includes(filterString)); + } + } + return filtered; + } + + private sortCompetencyFrameworks( + a: CompetencyFramework, + b: CompetencyFramework, + column: string, + direction: string + ) { + const isAsc = direction !== 'desc'; + switch (column) { + case 'name': + return (((a.name || '').toLowerCase() < (b.name || '').toLowerCase() ? -1 : 1) * (isAsc ? 1 : -1)); + case 'version': + return (((a.version || '').toLowerCase() < (b.version || '').toLowerCase() ? -1 : 1) * (isAsc ? 1 : -1)); + case 'source': + return (((a.source || '').toLowerCase() < (b.source || '').toLowerCase() ? -1 : 1) * (isAsc ? 1 : -1)); + case 'scale': + const aScale = (this.scaleMap.get(a.defaultProficiencyScaleId) || '').toLowerCase(); + const bScale = (this.scaleMap.get(b.defaultProficiencyScaleId) || '').toLowerCase(); + return ((aScale < bScale ? -1 : aScale > bScale ? 1 : 0) * (isAsc ? 1 : -1)); + case 'description': + return (((a.description || '').toLowerCase() < (b.description || '').toLowerCase() ? -1 : 1) * (isAsc ? 1 : -1)); + default: + return 0; + } + } + + rowClicked(row: CompetencyFramework) { + if (this.expandedElementId === row.id) { + this.collapseCompetencyDetail(); + this.expandedElementId = ''; + this.expandedCompetencies = []; + this.competencyDataSource.data = []; + this.workRoles = []; + this.workRoleDataSource.data = []; + } else { + this.expandedElementId = row.id; + this.loadCompetencies(row.id); + } + this.competencyFrameworkTable.renderRows(); + } + + getScaleName(fw: CompetencyFramework): string { + return this.scaleMap.get(fw.defaultProficiencyScaleId) || ''; + } + + getRowClass(id: string) { + return this.expandedElementId === id + ? 'element-row element-row-expanded' + : 'element-row element-row-not-expanded'; + } + + // --- Competencies --- + + loadCompetencies(frameworkId: string) { + this.competencyFilterControl.setValue(''); + this.workRoleFilterControl.setValue(''); + this.selectedCompetencyType = ''; + this.selectedWorkRoleCategory = ''; + this.competencyFrameworkService.getCompetencyFramework(frameworkId) + .pipe(take(1)) + .subscribe(fw => { + const allComps = fw.competencies || []; + this.buildTypeMap(fw); + // Separate work roles from other competencies + this.workRoles = allComps.filter(c => this.competencyTypeMap.get(c.id) === 'Work Role'); + this.expandedCompetencies = allComps.filter(c => this.competencyTypeMap.get(c.id) !== 'Work Role'); + this.workRoleCategories = [...new Set(this.workRoles.map(wr => this.getWorkRoleCategory(wr)).filter(c => c))].sort(); + this.applyWorkRoleFilter(); + this.applyCompetencyFilter(); + // Refresh expanded row's related data with updated inverse relationships + if (this.expandedCompetencyId && this.competencyById.has(this.expandedCompetencyId)) { + const fresh = this.competencyById.get(this.expandedCompetencyId); + this.expandedComp = fresh; + this.currentRelatedIdNumbers = [...(fresh.relatedIdNumbers || [])]; + + this.updateRelatedDataSources(); + } + setTimeout(() => { + if (this.competencyPaginator) { + this.competencyDataSource.paginator = this.competencyPaginator; + } + if (this.workRolePaginator) { + this.workRoleDataSource.paginator = this.workRolePaginator; + } + }); + }); + } + + private buildTypeMap(fw: CompetencyFramework) { + this.competencyTypeMap.clear(); + this.competencyById.clear(); + const comps = fw.competencies || []; + const byId = new Map(); + comps.forEach(c => { + byId.set(c.id, c); + this.competencyById.set(c.id, c); + }); + + const hasHierarchy = comps.some(c => c.parentId && byId.has(c.parentId)); + + // Build set of IDs that participate in the hierarchy + const parentIds = new Set(); + const childIds = new Set(); + if (hasHierarchy) { + for (const c of comps) { + if (c.parentId && byId.has(c.parentId)) { + childIds.add(c.id); + parentIds.add(c.parentId); + } + } + } + + this.taxonomyLevels = (fw.taxonomies || '') + .split(',') + .map(t => t.trim()) + .filter(t => t.length > 0); + + for (const c of comps) { + // Always try ID-pattern first — it's the most reliable signal + const idType = this.deriveTypeFromId(c.idNumber); + if (idType !== 'Other') { + this.competencyTypeMap.set(c.id, idType); + } else if (hasHierarchy && this.taxonomyLevels.length > 0) { + // Fall back to taxonomy depth for items without a recognizable ID pattern + const depth = this.getDepth(c, byId); + this.competencyTypeMap.set(c.id, this.taxonomyLevels[Math.min(depth, this.taxonomyLevels.length - 1)]); + } else if (hasHierarchy) { + // No taxonomy labels, no known ID — use hierarchy position + const isRoot = !c.parentId || !byId.has(c.parentId); + this.competencyTypeMap.set(c.id, isRoot ? 'Category' : 'Other'); + } else { + this.competencyTypeMap.set(c.id, 'Other'); + } + } + + // Build sorted unique types list (exclude Work Role — shown in separate panel) + this.competencyTypes = [...new Set(this.competencyTypeMap.values())].filter(t => t !== 'Work Role').sort(); + } + + private deriveTypeFromId(idNumber: string): string { + if (!idNumber) return 'Other'; + // DCWF/NICE 2.x: WRL in ID → Work Role + if (idNumber.includes('WRL')) return 'Work Role'; + // TKSA prefix: starts with T/K/S/A followed by digit or dash (T0001, T-401, K0055, etc.) + if (/^[TKSA][\d-]/.test(idNumber)) { + const prefixMap: Record = { + 'T': 'Task', 'K': 'Knowledge', 'S': 'Skill', 'A': 'Ability', + }; + return prefixMap[idNumber.charAt(0)] || 'Other'; + } + // NICE 2017: XX-YYY-NNN pattern (3 hyphenated parts) → Work Role + if (/^[A-Z]{2}-[A-Z]{3}-\d+$/.test(idNumber)) return 'Work Role'; + // 3-letter code → Specialty Area (e.g. DEV, MGT, ASA) + if (/^[A-Z]{3}$/.test(idNumber)) return 'Specialty Area'; + // 2-letter code → Category (e.g. PD, IO, AN) + if (/^[A-Z]{2}$/.test(idNumber)) return 'Category'; + return 'Other'; + } + + private getDepth(comp: Competency, byId: Map): number { + let depth = 0; + let current = comp; + while (current.parentId && byId.has(current.parentId)) { + depth++; + current = byId.get(current.parentId); + } + return depth; + } + + getCompetencyType(comp: Competency): string { + return this.competencyTypeMap.get(comp.id) || ''; + } + + onTypeFilterChange(type: string) { + this.selectedCompetencyType = type; + this.applyCompetencyFilter(); + } + + competencySortChanged(sort: Sort) { + this.competencySort = sort; + this.applyCompetencyFilter(); + } + + applyCompetencyFilter() { + let filtered = [...this.expandedCompetencies]; + if (this.selectedCompetencyType) { + filtered = filtered.filter(c => this.competencyTypeMap.get(c.id) === this.selectedCompetencyType); + } + if (this.competencyFilterString) { + const fs = this.competencyFilterString.toLowerCase(); + filtered = filtered.filter(c => + c.idNumber?.toLowerCase().includes(fs) || + c.shortName?.toLowerCase().includes(fs) || + c.description?.toLowerCase().includes(fs)); + } + const col = this.competencySort.active; + const isAsc = this.competencySort.direction !== 'desc'; + filtered.sort((a, b) => { + let aVal: string, bVal: string; + if (col === 'type') { + aVal = (this.competencyTypeMap.get(a.id) || '').toLowerCase(); + bVal = (this.competencyTypeMap.get(b.id) || '').toLowerCase(); + } else { + aVal = (a[col] || '').toString().toLowerCase(); + bVal = (b[col] || '').toString().toLowerCase(); + } + return (aVal < bVal ? -1 : aVal > bVal ? 1 : 0) * (isAsc ? 1 : -1); + }); + this.competencyDataSource.data = filtered; + } + + getWorkRoleCategory(workRole: Competency): string { + let current = workRole; + while (current.parentId && this.competencyById.has(current.parentId)) { + current = this.competencyById.get(current.parentId); + } + if (current.id === workRole.id) return ''; + return current.shortName || current.idNumber || ''; + } + + onWorkRoleCategoryFilterChange(category: string): void { + this.selectedWorkRoleCategory = category; + this.applyWorkRoleFilter(); + } + + applyWorkRoleFilter() { + let filtered = [...this.workRoles]; + if (this.selectedWorkRoleCategory) { + filtered = filtered.filter(c => this.getWorkRoleCategory(c) === this.selectedWorkRoleCategory); + } + if (this.workRoleFilterString) { + const fs = this.workRoleFilterString.toLowerCase(); + filtered = filtered.filter(c => + c.idNumber?.toLowerCase().includes(fs) || + c.shortName?.toLowerCase().includes(fs) || + c.description?.toLowerCase().includes(fs)); + } + const col = this.workRoleSort.active; + const isAsc = this.workRoleSort.direction !== 'desc'; + filtered.sort((a, b) => { + let aVal: string, bVal: string; + if (col === 'category') { + aVal = this.getWorkRoleCategory(a).toLowerCase(); + bVal = this.getWorkRoleCategory(b).toLowerCase(); + } else { + aVal = (a[col] || '').toString().toLowerCase(); + bVal = (b[col] || '').toString().toLowerCase(); + } + return (aVal < bVal ? -1 : aVal > bVal ? 1 : 0) * (isAsc ? 1 : -1); + }); + this.workRoleDataSource.data = filtered; + } + + workRoleSortChanged(sort: Sort) { + this.workRoleSort = sort; + this.applyWorkRoleFilter(); + } + + toggleCompetencyExpand(comp: Competency): void { + if (this.expandedCompetencyId === comp.id) { + this.collapseCompetencyDetail(); + } else { + this.expandCompetencyDetail(comp); + } + } + + private expandCompetencyDetail(comp: Competency): void { + this.collapseCompetencyDetail(); + this.expandedCompetencyId = comp.id; + this.expandedComp = comp; + this.currentRelatedIdNumbers = [...(comp.relatedIdNumbers || [])]; + this.relatedFilterControl.setValue(''); + this.relatedSideFilterControl.setValue(''); + this.availableTypeFilter = ''; + this.relatedTypeFilter = ''; + this.updateRelatedDataSources(); + this.availableTypes = [...new Set( + this.availableRelatedDataSource.data.map(c => this.competencyTypeMap.get(c.id) || '').filter(t => t) + )].sort(); + this.relatedTypes = [...new Set( + this.relatedDataSource.data.map(c => this.competencyTypeMap.get(c.id) || '').filter(t => t) + )].sort(); + this.availableRelatedDataSource.filterPredicate = (c: Competency, filter: string): boolean => { + if (this.availableTypeFilter) { + const type = this.competencyTypeMap?.get(c.id) || ''; + if (type !== this.availableTypeFilter) return false; + } + const term = (this.relatedFilterControl.value || '').toLowerCase(); + if (!term) return true; + return c.idNumber?.toLowerCase().includes(term) || + c.shortName?.toLowerCase().includes(term) || + c.description?.toLowerCase().includes(term); + }; + this.relatedDataSource.filterPredicate = (c: Competency, filter: string): boolean => { + if (this.relatedTypeFilter) { + const type = this.competencyTypeMap?.get(c.id) || ''; + if (type !== this.relatedTypeFilter) return false; + } + const term = (this.relatedSideFilterControl.value || '').toLowerCase(); + if (!term) return true; + return c.idNumber?.toLowerCase().includes(term) || + c.shortName?.toLowerCase().includes(term) || + c.description?.toLowerCase().includes(term); + }; + this.relatedFilterSub?.unsubscribe(); + this.relatedFilterSub = this.relatedFilterControl.valueChanges.subscribe(() => { + this.applyAvailableFilter(); + }); + this.relatedSideFilterSub?.unsubscribe(); + this.relatedSideFilterSub = this.relatedSideFilterControl.valueChanges.subscribe(() => { + this.applyRelatedFilter(); + }); + setTimeout(() => { + if (this.availablePaginator) { + this.availableRelatedDataSource.paginator = this.availablePaginator; + } + if (this.relatedPaginator) { + this.relatedDataSource.paginator = this.relatedPaginator; + } + }); + } + + private collapseCompetencyDetail(): void { + this.expandedCompetencyId = ''; + this.expandedComp = null; + } + + private updateRelatedDataSources(): void { + const relatedSet = new Set(this.currentRelatedIdNumbers); + const selfId = this.expandedComp?.idNumber; + const sortByIdNumber = (a: Competency, b: Competency) => + (a.idNumber || '').localeCompare(b.idNumber || ''); + this.relatedDataSource.data = [...this.competencyById.values()] + .filter(c => relatedSet.has(c.idNumber)) + .sort(sortByIdNumber); + this.availableRelatedDataSource.data = [...this.competencyById.values()] + .filter(c => c.idNumber !== selfId && !relatedSet.has(c.idNumber)) + .sort(sortByIdNumber); + } + + onAvailableTypeFilterChange(type: string): void { + this.availableTypeFilter = type; + this.applyAvailableFilter(); + } + + private applyAvailableFilter(): void { + const term = (this.relatedFilterControl.value || '').toLowerCase(); + // Trigger re-evaluation — use term or type or space to force filter + this.availableRelatedDataSource.filter = term || this.availableTypeFilter || ' '; + if (!term && !this.availableTypeFilter) { + this.availableRelatedDataSource.filter = ''; + } + } + + onRelatedTypeFilterChange(type: string): void { + this.relatedTypeFilter = type; + this.applyRelatedFilter(); + } + + private applyRelatedFilter(): void { + const term = (this.relatedSideFilterControl.value || '').toLowerCase(); + this.relatedDataSource.filter = term || this.relatedTypeFilter || ' '; + if (!term && !this.relatedTypeFilter) { + this.relatedDataSource.filter = ''; + } + } + + addRelatedCompetency(comp: Competency): void { + if (!this.currentRelatedIdNumbers.includes(comp.idNumber)) { + this.currentRelatedIdNumbers.push(comp.idNumber); + this.updateRelatedDataSources(); + this.saveCurrentRelated(); + } + } + + removeRelatedCompetency(comp: Competency): void { + this.currentRelatedIdNumbers = this.currentRelatedIdNumbers.filter(id => id !== comp.idNumber); + this.updateRelatedDataSources(); + this.saveCurrentRelated(); + } + + private saveCurrentRelated(): void { + if (this.expandedComp) { + const updated = { ...this.expandedComp, relatedIdNumbers: this.currentRelatedIdNumbers }; + this.saveCompetency(updated); + } + } + + viewCompetencyDetail(comp: Competency): void { + this.dialog.open(AdminCompetencyDetailDialogComponent, { + width: '600px', + data: { competency: comp, competencyTypeMap: this.competencyTypeMap }, + }); + } + + getRelatedCompetencies(comp: Competency): Competency[] { + if (!comp.relatedIdNumbers || comp.relatedIdNumbers.length === 0) return []; + const relatedSet = new Set(comp.relatedIdNumbers); + return [...this.competencyById.values()].filter(c => relatedSet.has(c.idNumber)); + } + + getChildCompetencies(comp: Competency): Competency[] { + return [...this.competencyById.values()].filter(c => c.parentId === comp.id); + } + + addOrEditCompetency(competency: Competency, typeHint?: string) { + if (!competency) { + competency = { competencyFrameworkId: this.expandedElementId }; + } + // Build available parents: all competencies in this framework except self + const availableParents = [...this.competencyById.values()] + .filter(c => c.id !== competency.id) + .map(c => ({ id: c.id, label: (c.idNumber ? c.idNumber + ' — ' : '') + (c.shortName || '') })) + .sort((a, b) => a.label.localeCompare(b.label)); + const dialogRef = this.dialog.open(AdminCompetencyEditDialogComponent, { + minWidth: '500px', + maxWidth: '90vw', + width: '600px', + data: { + competency: { ...competency }, + typeHint: typeHint || '', + availableTypes: [...new Set(['Category', 'Work Role', 'Task', 'Knowledge', 'Skill', 'Ability', ...this.competencyTypes])], + availableParents, + }, + }); + dialogRef.componentInstance.editComplete.subscribe((result: any) => { + if (result.saveChanges && result.competency) { + this.saveCompetency(result.competency); + } + dialogRef.close(); + }); + } + + saveCompetency(competency: Competency) { + if (competency.id) { + this.competencyFrameworkService.updateCompetency(competency.id, competency) + .pipe(take(1)) + .subscribe({ + next: () => this.loadCompetencies(this.expandedElementId), + error: (err: any) => { + this.importError = 'Save failed: ' + (err.error?.title || err.message || 'Unknown error'); + } + }); + } else { + this.competencyFrameworkService.createCompetency(this.expandedElementId, competency) + .pipe(take(1)) + .subscribe({ + next: () => this.loadCompetencies(this.expandedElementId), + error: (err: any) => { + this.importError = 'Save failed: ' + (err.error?.title || err.message || 'Unknown error'); + } + }); + } + } + + deleteCompetency(competency: Competency): void { + this.dialogService + .confirm( + 'Delete Competency', + 'Are you sure you want to delete ' + (competency.idNumber || competency.shortName) + '?' + ) + .subscribe((result) => { + if (result['confirm']) { + this.competencyFrameworkService.deleteCompetency(competency.id) + .pipe(take(1)) + .subscribe({ + next: () => this.loadCompetencies(this.expandedElementId), + error: (err: any) => { + this.importError = 'Delete failed: ' + (err.error?.title || err.message || 'Unknown error'); + } + }); + } + }); + } + +} diff --git a/src/app/components/admin/admin-container/admin-container.component.html b/src/app/components/admin/admin-container/admin-container.component.html index efcaf10f..348c13c4 100755 --- a/src/app/components/admin/admin-container/admin-container.component.html +++ b/src/app/components/admin/admin-container/admin-container.component.html @@ -5,6 +5,92 @@ --> + @if (sideNavCollapsed) { + + + + + + + + @if (canViewUnits) { + + + + } + @if (canViewDataFields) { + + + + } + @if (canViewInjectTypes) { + + + + } + @if (canViewCatalogs) { + + + + } + @if (canViewOrganizations) { + + + + } + @if (canViewGalleryCards) { + + + + } + @if (canViewCiteActions) { + + + + } + @if (canViewCiteDuties) { + + + + } + @if (canViewCompetencyFrameworks) { + + + + + + + } + @if (canViewUsers) { + + + + } + @if (canViewRoles) { + + + + } + @if (canViewGroups) { + + + + } + +
+ + @if (!hideTopbar) { + + } +
+
+ } @else {
@@ -103,6 +189,26 @@

Administration

} + @if (canViewCompetencyFrameworks) { + + + + {{ competencyFrameworksSidebarText }} + + + + + + {{ proficiencyScalesText }} + + + } @if (canViewUsers) { @@ -151,6 +257,7 @@

Administration

}
+ } @if (!hideTopbar) { Administration [canEdit]="canManageInjectTypes"> } + @if (canViewCompetencyFrameworks && (selectedTab === competencyFrameworksText)) { + + } + @if (canViewCompetencyFrameworks && (selectedTab === proficiencyScalesText)) { + + + + } @if (canViewCatalogs && (selectedTab === catalogsText)) { diff --git a/src/app/components/admin/admin-container/admin-container.component.scss b/src/app/components/admin/admin-container/admin-container.component.scss index c46fd2e1..4d9c9ed6 100755 --- a/src/app/components/admin/admin-container/admin-container.component.scss +++ b/src/app/components/admin/admin-container/admin-container.component.scss @@ -1,124 +1,164 @@ -// Copyright 2022 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the -// project root for license information. - -@use "@angular/material" as mat; -@include mat.elevation-classes(); -@include mat.app-background(); - -:host { - flex: 1; - overflow: hidden; - min-height: 0; -} - -.sidenav-content-wrapper { - display: flex; - flex-direction: column; - height: 100%; -} - -.appitems-container { - flex: 1; -} - -.appitems-container .mat-list-item { - height: auto; -} - -.appslist { - margin-top: 10px; - margin-bottom: 20px; -} - -.mat-list-item-icon { - color: var(--mat-sys-primary) !important; -} - -.nolink { - text-decoration: none; - color: inherit; - cursor: pointer; -} - -.appcontent-container { - height: 100%; - width: 100%; - overflow: hidden; - ::ng-deep .mat-sidenav-content { - overflow-y: hidden; - } -} - -.appbarmenu-container { - width: 250px; - border-right: solid var(--mat-sys-surface-container); -} - -.sidenav-header { - height: 40px; - border-bottom: 2px solid var(--mat-sys-surface-container); -} - -.pull-right { - position: absolute; - right: 0px; - margin-top: -8px; -} - -.showhand { - cursor: pointer; -} - -.crucible-icon-blueprint { - color: var(--mat-sys-primary); -} - -#wrapper { - display: flex; - flex-direction: row; - width: 100%; -} - -#rightcontext { - width: 30px; - font-size: 75%; -} - -#content { - text-align: left; - vertical-align: middle; - width: 160px; - font-size: 90%; - padding: 0.5em; -} - -.crucible-logo { - align-self: flex-start; -} - -.app-versions { - width: 100%; - padding-bottom: 5px; - padding-top: 5px; - text-align: center; - font-size: x-small; - border-bottom: 2px solid var(--mat-sys-surface-container); - border-top: 2px solid var(--mat-sys-surface-container); - font-weight: 400; -} - -.icon-text { - margin-left: 10px; - font-weight: bold; -} - -.selected-item { - background-color: var(--mat-sys-surface-container); - cursor: pointer; -} - -.non-selected-item { - background-color: var(--mat-sys-background); - cursor: pointer; -} +// Copyright 2022 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +@use "@angular/material" as mat; +@include mat.elevation-classes(); +@include mat.app-background(); + +:host { + flex: 1; + overflow: hidden; + min-height: 0; +} + +.sidenav-content-wrapper { + display: flex; + flex-direction: column; + height: 100%; +} + +.appitems-container { + flex: 1; +} + +.appitems-container .mat-list-item { + height: auto; +} + +.appslist { + margin-top: 10px; + margin-bottom: 20px; +} + +.mat-list-item-icon { + color: var(--mat-sys-primary) !important; +} + +.nolink { + text-decoration: none; + color: inherit; + cursor: pointer; +} + +.appcontent-container { + height: 100%; + width: 100%; + overflow: hidden; + ::ng-deep .mat-sidenav-content { + overflow-y: hidden; + } +} + +.appbarmenu-container { + width: 250px; + border-right: solid var(--mat-sys-surface-container); +} + +.appbarmenu-container-collapsed { + width: 50px; + border-right: solid var(--mat-sys-surface-container); + overflow: hidden; +} + +::ng-deep .appbarmenu-container-collapsed .mat-drawer-inner-container { + overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + height: 100% !important; +} + +.appbarmenu-container-collapsed .appitems-container { + width: 50px; + height: calc(100% - 142px); + overflow-y: auto; + overflow-x: hidden; +} + +.appbarmenu-container-collapsed .sidenav-header { + justify-content: center; + padding: 0; +} + +.appbarmenu-container-collapsed .bottom-div { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + padding-bottom: 10px; + height: 80px; +} + +.appbarmenu-container-collapsed .crucible-logo { + width: 40px; + height: auto; + margin-top: 5px; +} + +.sidenav-header { + height: 40px; + border-bottom: 2px solid var(--mat-sys-surface-container); +} + +.pull-right { + position: absolute; + right: 0px; + margin-top: -8px; +} + +.showhand { + cursor: pointer; +} + +.crucible-icon-blueprint { + color: var(--mat-sys-primary); +} + +#wrapper { + display: flex; + flex-direction: row; + width: 100%; +} + +#rightcontext { + width: 30px; + font-size: 75%; +} + +#content { + text-align: left; + vertical-align: middle; + width: 160px; + font-size: 90%; + padding: 0.5em; +} + +.crucible-logo { + align-self: flex-start; +} + +.app-versions { + width: 100%; + padding-bottom: 5px; + padding-top: 5px; + text-align: center; + font-size: x-small; + border-bottom: 2px solid var(--mat-sys-surface-container); + border-top: 2px solid var(--mat-sys-surface-container); + font-weight: 400; +} + +.icon-text { + margin-left: 10px; + font-weight: bold; +} + +.selected-item { + background-color: var(--mat-sys-surface-container); + cursor: pointer; +} + +.non-selected-item { + background-color: var(--mat-sys-background); + cursor: pointer; +} diff --git a/src/app/components/admin/admin-container/admin-container.component.ts b/src/app/components/admin/admin-container/admin-container.component.ts index adf22bd4..d487cf64 100755 --- a/src/app/components/admin/admin-container/admin-container.component.ts +++ b/src/app/components/admin/admin-container/admin-container.component.ts @@ -22,6 +22,7 @@ import { environment } from 'src/environments/environment'; import { HealthCheckService, SystemPermission, User } from 'src/app/generated/blueprint.api'; import { UIDataService } from 'src/app/data/ui/ui-data.service'; import { InjectTypeDataService } from 'src/app/data/inject-type/inject-type-data.service'; +import { CompetencyFrameworkDataService } from 'src/app/data/competency-framework/competency-framework-data.service'; @Component({ selector: 'app-admin-container', @@ -42,10 +43,29 @@ export class AdminContainerComponent implements OnDestroy, OnInit { galleryCardsText = 'Gallery Cards'; citeActionsText = 'CITE Actions'; citeDutiesText = 'CITE Duties'; + competencyFrameworksText = 'Competency Frameworks'; + competencyFrameworksSidebarText = 'Competencies'; + proficiencyScalesText = 'Proficiency Scales'; selectedTab = 'Organizations'; displayedSection = ''; exitSection = ''; isSidebarOpen = true; + sideNavCollapsed = false; + fontIconMap = new Map([ + ['Units', 'mdi-account-group'], + ['Data Fields', 'mdi-view-column-outline'], + ['Inject Types', 'mdi-format-list-group'], + ['Catalogs', 'mdi-book-open-outline'], + ['Organizations', 'mdi-bank'], + ['Gallery Cards', 'mdi-view-grid-outline'], + ['CITE Actions', 'mdi-clipboard-check-outline'], + ['CITE Duties', 'mdi-clipboard-account-outline'], + ['Competency Frameworks', 'mdi-certificate-outline'], + ['Proficiency Scales', 'mdi-tune-variant'], + ['Users', 'mdi-account'], + ['Roles', 'mdi-shield-account'], + ['Groups', 'mdi-account-multiple'], + ]); loggedInUserId = ''; username = ''; canAccessAdminSection = false; @@ -72,6 +92,8 @@ export class AdminContainerComponent implements OnDestroy, OnInit { canManageCiteDuties = false; canViewDataFields = false; canManageDataFields = false; + canViewCompetencyFrameworks = false; + canManageCompetencyFrameworks = false; hideTopbar = false; TopbarView = TopbarView; topbarImage = this.settingsService.settings.AppTopBarImage; @@ -96,6 +118,7 @@ export class AdminContainerComponent implements OnDestroy, OnInit { private signalRService: SignalRService, private uiDataService: UIDataService, private injectTypeDataService: InjectTypeDataService, + private competencyFrameworkDataService: CompetencyFrameworkDataService, ) { this.theme$ = this.authQuery.userTheme$; this.hideTopbar = this.uiDataService.inIframe(); @@ -153,8 +176,10 @@ export class AdminContainerComponent implements OnDestroy, OnInit { this.canManageCiteDuties = this.permissionDataService.hasPermission(SystemPermission.ManageCiteDuties); this.canViewDataFields = this.permissionDataService.hasPermission(SystemPermission.ViewDataFields); this.canManageDataFields = this.permissionDataService.hasPermission(SystemPermission.ManageDataFields); + this.canViewCompetencyFrameworks = this.permissionDataService.hasPermission(SystemPermission.ViewCompetencyFrameworks); + this.canManageCompetencyFrameworks = this.permissionDataService.hasPermission(SystemPermission.ManageCompetencyFrameworks); // Update canAccessAdminSection based on having any admin permission - this.canAccessAdminSection = this.canViewUsers || this.canViewRoles || this.canViewGroups || this.canViewUnits || this.canViewInjectTypes || this.canViewCatalogs || this.canViewOrganizations || this.canViewCiteActions || this.canViewCiteDuties || this.canViewDataFields; + this.canAccessAdminSection = this.canViewUsers || this.canViewRoles || this.canViewGroups || this.canViewUnits || this.canViewInjectTypes || this.canViewCatalogs || this.canViewOrganizations || this.canViewCiteActions || this.canViewCiteDuties || this.canViewDataFields || this.canViewCompetencyFrameworks; // Load additional data if user has permissions if (this.canAccessAdminSection) { this.unitDataService.load(); @@ -162,6 +187,8 @@ export class AdminContainerComponent implements OnDestroy, OnInit { }); // load injectTypes this.injectTypeDataService.load(); + // load competencyFrameworks + this.competencyFrameworkDataService.load(); // Start SignalR connection this.signalRService .startConnection(ApplicationArea.admin) @@ -192,6 +219,15 @@ export class AdminContainerComponent implements OnDestroy, OnInit { } } + setCollapsed(value: boolean) { + this.sideNavCollapsed = value; + } + + getSidebarLabel(section: string): string { + if (section === this.competencyFrameworksText) return this.competencyFrameworksSidebarText; + return section; + } + logout() { this.authService.logout(); } diff --git a/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.html b/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.html new file mode 100644 index 00000000..5c00e3e7 --- /dev/null +++ b/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.html @@ -0,0 +1,89 @@ + +
+
+ @if (data.level.id) { + Edit + } + @if (!data.level.id) { + Create + } + Proficiency Level +   + +
+ @if (!!data && data.level !== undefined && data.level !== null) { +
+
+
+ + Name + + +
+
+
+
+ + Value + + +
+
+
+
+ + Display Order + + +
+
+
+
+ + Description + + +
+
+
+
+ +
+
+ +
+
+
+ } diff --git a/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.scss b/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.scss new file mode 100644 index 00000000..9879e956 --- /dev/null +++ b/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.scss @@ -0,0 +1,27 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.full-width { + width: 90%; +} + +.add-margin { + margin-top: 8px; + margin-bottom: 8px; + margin-right: 65px; +} + +.bottom-button { + margin-bottom: 10px; + margin-top: 10px; +} + +.delete-button { + margin-top: 10px; + margin-right: 50px; +} + +.close-button { + float: right; +} diff --git a/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.ts b/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.ts new file mode 100644 index 00000000..d4031c20 --- /dev/null +++ b/src/app/components/admin/admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component.ts @@ -0,0 +1,43 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, EventEmitter, Inject, Output } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { DialogService } from 'src/app/services/dialog/dialog.service'; + +@Component({ + selector: 'app-admin-proficiency-level-edit-dialog', + templateUrl: './admin-proficiency-level-edit-dialog.component.html', + styleUrls: ['./admin-proficiency-level-edit-dialog.component.scss'], + standalone: false +}) + +export class AdminProficiencyLevelEditDialogComponent { + @Output() editComplete = new EventEmitter(); + + constructor( + public dialogService: DialogService, + dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) { + dialogRef.disableClose = true; + } + + errorFree() { + return this.data.level.name; + } + + handleEditComplete(saveChanges: boolean): void { + if (!saveChanges) { + this.editComplete.emit({ saveChanges: false, level: null }); + } else { + if (this.errorFree()) { + this.editComplete.emit({ + saveChanges: saveChanges, + level: this.data.level, + }); + } + } + } +} diff --git a/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.html b/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.html new file mode 100644 index 00000000..39296c3d --- /dev/null +++ b/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.html @@ -0,0 +1,63 @@ + +
+
+ @if (data.scale.id) { + Edit + } + @if (!data.scale.id) { + Create + } + Proficiency Scale +   + +
+ @if (!!data && data.scale !== undefined && data.scale !== null) { +
+
+
+ + Name + + +
+
+
+
+ + Description + + +
+
+
+
+ +
+
+ +
+
+
+ } diff --git a/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.scss b/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.scss new file mode 100644 index 00000000..9879e956 --- /dev/null +++ b/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.scss @@ -0,0 +1,27 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.full-width { + width: 90%; +} + +.add-margin { + margin-top: 8px; + margin-bottom: 8px; + margin-right: 65px; +} + +.bottom-button { + margin-bottom: 10px; + margin-top: 10px; +} + +.delete-button { + margin-top: 10px; + margin-right: 50px; +} + +.close-button { + float: right; +} diff --git a/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.ts b/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.ts new file mode 100644 index 00000000..1bcb9ebf --- /dev/null +++ b/src/app/components/admin/admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component.ts @@ -0,0 +1,43 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, EventEmitter, Inject, Output } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { DialogService } from 'src/app/services/dialog/dialog.service'; + +@Component({ + selector: 'app-admin-proficiency-scale-edit-dialog', + templateUrl: './admin-proficiency-scale-edit-dialog.component.html', + styleUrls: ['./admin-proficiency-scale-edit-dialog.component.scss'], + standalone: false +}) + +export class AdminProficiencyScaleEditDialogComponent { + @Output() editComplete = new EventEmitter(); + + constructor( + public dialogService: DialogService, + dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) { + dialogRef.disableClose = true; + } + + errorFree() { + return this.data.scale.name; + } + + handleEditComplete(saveChanges: boolean): void { + if (!saveChanges) { + this.editComplete.emit({ saveChanges: false, scale: null }); + } else { + if (this.errorFree()) { + this.editComplete.emit({ + saveChanges: saveChanges, + scale: this.data.scale, + }); + } + } + } +} diff --git a/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.html b/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.html new file mode 100644 index 00000000..b01d3774 --- /dev/null +++ b/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.html @@ -0,0 +1,140 @@ + + +
+
+ +
+ + + @if (filterString) { + + } + +
+ +
+
+ +
+ + + + + @if (canEdit) { + + }   + + + @if (canEdit) { + + + } + + + + + Name + {{ scale.name }} + + + + Description + {{ scale.description }} + + + + Levels + {{ scale.proficiencyLevels?.length || 0 }} + + + + +
+ @if (expandedScaleId === scale.id) { +
+
+ Levels for "{{ scale.name }}" +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ @if (canEdit) { + + }   + + @if (canEdit) { + + + } + Name{{ level.name }}Value{{ level.value }}Order{{ level.displayOrder }}Description{{ level.description }}
+ @if (scale.proficiencyLevels?.length === 0) { +
No levels defined
+ } +
+ } +
+ + + + + + + + + + + @if (scaleDataSource.data.length === 0) { +
No proficiency scales defined
+ } + diff --git a/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.scss b/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.scss new file mode 100644 index 00000000..8c494eea --- /dev/null +++ b/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.scss @@ -0,0 +1,134 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +:host { + display: flex; + flex-direction: column; + height: calc(100vh - 44px); +} + +.cssLayoutRowStartCenter { + min-height: 76px; + flex-shrink: 0; +} + +.button-end { + margin-left: auto; +} + +.scrolling-region { + height: calc(100% - 94px); + overflow: auto; + width: 100%; +} + +.element-row { + position: relative; + cursor: pointer; +} + +.element-row:hover { + background-color: var(--mat-sys-surface-variant); +} + +.element-row-expanded { + background-color: var(--mat-sys-surface-variant); +} + +.detail-row { + height: 0; + min-height: 0; + overflow: hidden; +} + +::ng-deep .detail-row .mat-mdc-cell.expanded-detail-cell { + padding: 0 !important; + min-height: 0; + border-bottom-width: 0; + flex: 0 0 100%; + width: 100%; + max-width: 100%; +} + +.element-row td { + border-bottom-width: 0; +} + +.expanded-detail-cell { + display: flex; + flex-direction: column; + width: 100%; + padding: 8px 10px; +} + +.sub-col-action { + flex: 0 0 100px; + width: 100px; +} + +.sub-col-name { + flex: 1 1 25%; +} + +.sub-col-desc { + flex: 2 1 45%; +} + +.sub-col-levels { + flex: 0 0 80px; +} + +.lv-col-action { + flex: 0 0 100px; + width: 100px; + white-space: nowrap; +} + +.lv-col-name { + flex: 2 1 20%; + min-width: 100px; +} + +.lv-col-value { + flex: 0 0 80px; +} + +.lv-col-order { + flex: 0 0 80px; +} + +.lv-col-desc { + flex: 4 1 40%; + min-width: 150px; +} + +.levels-detail { + margin: 8px 0; + padding: 8px 16px; + width: 100%; + box-sizing: border-box; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 4px; + + table { + width: 100%; + } +} + +.levels-title { + font-weight: bold; + font-size: 13px; + margin-bottom: 4px; +} + +.no-results { + margin-top: 20px; + margin-left: 150px; +} + +.no-results-inline { + padding: 8px 0; + color: var(--mat-sys-on-surface-variant); + font-style: italic; +} diff --git a/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.ts b/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.ts new file mode 100644 index 00000000..e808aaf6 --- /dev/null +++ b/src/app/components/admin/admin-proficiency-scales/admin-proficiency-scales.component.ts @@ -0,0 +1,238 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { UntypedFormControl } from '@angular/forms'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatTable, MatTableDataSource } from '@angular/material/table'; +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { Subject } from 'rxjs'; +import { take, takeUntil } from 'rxjs/operators'; +import { + ProficiencyScale, + ProficiencyLevel, + ProficiencyScaleService, + ProficiencyLevelService, +} from 'src/app/generated/blueprint.api'; +import { Sort } from '@angular/material/sort'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from 'src/app/services/dialog/dialog.service'; +import { AdminProficiencyScaleEditDialogComponent } from '../admin-proficiency-scale-edit-dialog/admin-proficiency-scale-edit-dialog.component'; +import { AdminProficiencyLevelEditDialogComponent } from '../admin-proficiency-level-edit-dialog/admin-proficiency-level-edit-dialog.component'; + +@Component({ + selector: 'app-admin-proficiency-scales', + templateUrl: './admin-proficiency-scales.component.html', + styleUrls: ['./admin-proficiency-scales.component.scss'], + animations: [ + trigger('detailExpand', [ + state('collapsed', style({ height: '0px', minHeight: '0', visibility: 'hidden' })), + state('expanded', style({ height: '*', visibility: 'visible' })), + transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), + ]), + ], + standalone: false +}) +export class AdminProficiencyScalesComponent implements OnInit, OnDestroy { + @Input() loggedInUserId: string; + @Input() canEdit: boolean; + scales: ProficiencyScale[] = []; + scaleDataSource = new MatTableDataSource([]); + scaleDisplayedColumns: string[] = ['action', 'name', 'description', 'levels']; + levelDisplayedColumns: string[] = ['action', 'name', 'value', 'displayOrder', 'description']; + scaleSort: Sort = { active: 'name', direction: 'asc' }; + levelSort: Sort = { active: 'displayOrder', direction: 'asc' }; + @ViewChild('scaleTable', { static: false }) scaleTable: MatTable; + @ViewChild('paginator') paginator: MatPaginator; + expandedScaleId = ''; + filterControl = new UntypedFormControl(); + filterString = ''; + private unsubscribe$ = new Subject(); + + constructor( + private proficiencyScaleService: ProficiencyScaleService, + private proficiencyLevelService: ProficiencyLevelService, + public dialog: MatDialog, + public dialogService: DialogService + ) {} + + ngOnInit() { + this.loadScales(); + this.filterControl.valueChanges + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((term) => { + this.filterString = term; + this.applySorting(); + }); + } + + loadScales() { + this.proficiencyScaleService.getProficiencyScales() + .pipe(take(1)) + .subscribe(scales => { + this.scales = scales; + this.applySorting(); + }); + } + + applySorting() { + const col = this.scaleSort.active; + const isAsc = this.scaleSort.direction !== 'desc'; + let filtered = [...this.scales]; + if (this.filterString) { + const term = this.filterString.toLowerCase(); + filtered = filtered.filter(s => + s.name?.toLowerCase().includes(term) || + s.description?.toLowerCase().includes(term)); + } + this.scaleDataSource.data = filtered.sort((a, b) => { + let aVal: string; + let bVal: string; + if (col === 'levels') { + aVal = (a.proficiencyLevels?.length || 0).toString(); + bVal = (b.proficiencyLevels?.length || 0).toString(); + return (Number(aVal) - Number(bVal)) * (isAsc ? 1 : -1); + } + aVal = (a[col] || '').toString().toLowerCase(); + bVal = (b[col] || '').toString().toLowerCase(); + return (aVal < bVal ? -1 : aVal > bVal ? 1 : 0) * (isAsc ? 1 : -1); + }); + if (this.paginator) { + this.scaleDataSource.paginator = this.paginator; + } + } + + scaleSortChanged(sort: Sort) { + this.scaleSort = sort; + this.applySorting(); + } + + toggleScaleExpand(row: ProficiencyScale) { + this.expandedScaleId = this.expandedScaleId === row.id ? '' : row.id; + this.scaleTable.renderRows(); + } + + getLevelDataSource(scale: ProficiencyScale): MatTableDataSource { + const levels = scale.proficiencyLevels || []; + const col = this.levelSort.active; + const isAsc = this.levelSort.direction !== 'desc'; + const sorted = [...levels].sort((a, b) => { + const aVal = (a[col] ?? '').toString().toLowerCase(); + const bVal = (b[col] ?? '').toString().toLowerCase(); + return (aVal < bVal ? -1 : aVal > bVal ? 1 : 0) * (isAsc ? 1 : -1); + }); + return new MatTableDataSource(sorted); + } + + levelSortChanged(sort: Sort) { + this.levelSort = sort; + } + + addOrEditScale(scale: ProficiencyScale) { + if (!scale) { + scale = {}; + } + const dialogRef = this.dialog.open(AdminProficiencyScaleEditDialogComponent, { + minWidth: '400px', + maxWidth: '90vw', + width: 'auto', + data: { + scale: { ...scale }, + }, + }); + dialogRef.componentInstance.editComplete.subscribe((result) => { + if (result.saveChanges && result.scale) { + this.saveScale(result.scale); + } + dialogRef.close(); + }); + } + + saveScale(scale: ProficiencyScale) { + if (scale.id) { + this.proficiencyScaleService.updateProficiencyScale(scale.id, scale) + .pipe(take(1)) + .subscribe(() => { + this.loadScales(); + }); + } else { + this.proficiencyScaleService.createProficiencyScale(scale) + .pipe(take(1)) + .subscribe(() => { + this.loadScales(); + }); + } + } + + deleteScale(scale: ProficiencyScale) { + this.dialogService + .confirm('Delete Scale', 'Are you sure you want to delete ' + scale.name + '?') + .subscribe((result) => { + if (result['confirm']) { + this.proficiencyScaleService.deleteProficiencyScale(scale.id) + .pipe(take(1)) + .subscribe(() => { + this.scales = this.scales.filter(s => s.id !== scale.id); + this.applySorting(); + if (this.expandedScaleId === scale.id) { + this.expandedScaleId = ''; + } + }); + } + }); + } + + addOrEditLevel(scale: ProficiencyScale, level: ProficiencyLevel) { + if (!level) { + level = { + proficiencyScaleId: scale.id, + }; + } + const dialogRef = this.dialog.open(AdminProficiencyLevelEditDialogComponent, { + minWidth: '400px', + maxWidth: '90vw', + width: 'auto', + data: { + level: { ...level }, + }, + }); + dialogRef.componentInstance.editComplete.subscribe((result) => { + if (result.saveChanges && result.level) { + this.saveLevel(scale, result.level); + } + dialogRef.close(); + }); + } + + saveLevel(scale: ProficiencyScale, level: ProficiencyLevel) { + if (level.id) { + this.proficiencyLevelService.updateProficiencyLevel(level.id, level) + .pipe(take(1)) + .subscribe(() => { + this.loadScales(); + }); + } else { + this.proficiencyLevelService.createProficiencyLevel(level) + .pipe(take(1)) + .subscribe(l => { + if (!scale.proficiencyLevels) { + scale.proficiencyLevels = []; + } + scale.proficiencyLevels.push(l); + }); + } + } + + deleteLevel(scale: ProficiencyScale, level: ProficiencyLevel) { + this.proficiencyLevelService.deleteProficiencyLevel(level.id) + .pipe(take(1)) + .subscribe(() => { + scale.proficiencyLevels = scale.proficiencyLevels.filter(l => l.id !== level.id); + }); + } + + ngOnDestroy() { + this.unsubscribe$.next(null); + this.unsubscribe$.complete(); + } +} diff --git a/src/app/components/assessor-page/assessor-page.component.html b/src/app/components/assessor-page/assessor-page.component.html new file mode 100644 index 00000000..38b893c1 --- /dev/null +++ b/src/app/components/assessor-page/assessor-page.component.html @@ -0,0 +1,29 @@ + +
+ + + @if (selectedMselId) { + @if (!canViewAssessorPage) { +
+ +

Access Denied

+

You need at least Editor role on this MSEL to view the assessor page.

+
+ } @else { + + } + } +
diff --git a/src/app/components/assessor-page/assessor-page.component.scss b/src/app/components/assessor-page/assessor-page.component.scss new file mode 100644 index 00000000..7a41ad93 --- /dev/null +++ b/src/app/components/assessor-page/assessor-page.component.scss @@ -0,0 +1,33 @@ +// Copyright 2024 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +:host { + flex: 1; + overflow: hidden; + min-height: 0; +} + +.no-access-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + text-align: center; + gap: 16px; + + mat-icon { + color: #ff9800; + } + + h3 { + margin: 0; + font-size: 24px; + } + + p { + margin: 0; + color: rgba(0, 0, 0, 0.6); + } +} diff --git a/src/app/components/assessor-page/assessor-page.component.ts b/src/app/components/assessor-page/assessor-page.component.ts new file mode 100644 index 00000000..b2e56d0d --- /dev/null +++ b/src/app/components/assessor-page/assessor-page.component.ts @@ -0,0 +1,186 @@ +// Copyright 2024 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { Subject, Observable } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { + ComnSettingsService, + ComnAuthQuery, +} from '@cmusei/crucible-common'; +import { Msel, MselRole, UserMselRole, SystemPermission } from 'src/app/generated/blueprint.api'; +import { DataFieldDataService } from 'src/app/data/data-field/data-field-data.service'; +import { DataOptionDataService } from 'src/app/data/data-option/data-option-data.service'; +import { DataValueDataService } from 'src/app/data/data-value/data-value-data.service'; +import { MoveDataService } from 'src/app/data/move/move-data.service'; +import { MselDataService } from 'src/app/data/msel/msel-data.service'; +import { MselQuery } from 'src/app/data/msel/msel.query'; +import { ScenarioEventDataService } from 'src/app/data/scenario-event/scenario-event-data.service'; +import { TeamDataService } from 'src/app/data/team/team-data.service'; +import { UserDataService } from 'src/app/data/user/user-data.service'; +import { CurrentUserQuery } from 'src/app/data/user/user.query'; +import { UserMselRoleDataService } from 'src/app/data/user-msel-role/user-msel-role-data.service'; +import { UserMselRoleQuery } from 'src/app/data/user-msel-role/user-msel-role.query'; +import { PermissionDataService } from 'src/app/data/permission/permission-data.service'; +import { TopbarView } from '../shared/top-bar/topbar.models'; + +@Component({ + selector: 'app-assessor-page', + templateUrl: './assessor-page.component.html', + styleUrls: ['./assessor-page.component.scss'], + standalone: false +}) +export class AssessorPageComponent implements OnDestroy, OnInit { + private unsubscribe$ = new Subject(); + private msel: Msel = {}; + selectedMselId = ''; + loggedInUserId: string; + userTheme$ = this.authQuery.userTheme$; + topbarColor = '#ef3a47'; + topbarTextColor = '#FFFFFF'; + topbarImage = this.settingsService.settings.AppTopBarImage; + TopbarView = TopbarView; + topbarTextBase = 'Set AppTopBarText in Settings'; + topbarText = 'blank'; + appTitle = ''; + userMselRole: UserMselRole | null = null; + canEditAssessorPage = false; + canViewAssessorPage = false; + isSystemAdmin = false; + + constructor( + private activatedRoute: ActivatedRoute, + private router: Router, + private dataFieldDataService: DataFieldDataService, + private dataOptionDataService: DataOptionDataService, + private dataValueDataService: DataValueDataService, + private moveDataService: MoveDataService, + private mselDataService: MselDataService, + private mselQuery: MselQuery, + private scenarioEventDataService: ScenarioEventDataService, + private teamDataService: TeamDataService, + private userDataService: UserDataService, + private currentUserQuery: CurrentUserQuery, + private authQuery: ComnAuthQuery, + private settingsService: ComnSettingsService, + private titleService: Title, + private userMselRoleDataService: UserMselRoleDataService, + private userMselRoleQuery: UserMselRoleQuery, + private permissionDataService: PermissionDataService + ) { + this.activatedRoute.queryParamMap + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((params) => { + this.topbarText = this.topbarTextBase; + const mselId = params.get('msel'); + if (mselId && this.selectedMselId !== mselId) { + this.mselDataService.loadById(mselId); + this.mselDataService.setActive(mselId); + this.moveDataService.loadByMsel(mselId); + this.teamDataService.loadByMsel(mselId); + this.dataFieldDataService.loadByMsel(mselId); + this.dataOptionDataService.loadByMsel(mselId); + this.dataValueDataService.loadByMsel(mselId); + this.scenarioEventDataService.loadByMsel(mselId); + this.userMselRoleDataService.loadByMsel(mselId); + this.selectedMselId = mselId; + } + }); + + (this.mselQuery.selectActive() as Observable) + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((msel) => { + if (msel) { + this.msel = msel; + const prefix = this.appTitle + ' - '; + this.topbarText = prefix + msel.name; + this.titleService.setTitle(prefix + msel.name); + } else { + this.msel = {}; + } + }); + + this.appTitle = + this.settingsService.settings.AppTitle || 'Set AppTitle in Settings'; + this.titleService.setTitle(this.appTitle); + this.topbarTextBase = + this.settingsService.settings.AppTopBarText || this.topbarTextBase; + this.topbarText = this.topbarTextBase; + } + + ngOnInit() { + this.userDataService.setCurrentUser(); + this.currentUserQuery + .select() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((cu) => { + this.loggedInUserId = cu.id; + this.updateRolePermissions(); + }); + + // Load permissions and check system admin + this.permissionDataService.load() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(() => { + this.isSystemAdmin = this.permissionDataService.hasPermission(SystemPermission.CreateMsels); + this.updateRolePermissions(); + }); + + // Watch for user MSEL role changes + this.userMselRoleQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((roles) => { + this.updateRolePermissions(); + }); + } + + private updateRolePermissions() { + if (!this.loggedInUserId || !this.selectedMselId) { + this.canViewAssessorPage = false; + this.canEditAssessorPage = false; + return; + } + + // System admins have full access + if (this.isSystemAdmin) { + this.canViewAssessorPage = true; + this.canEditAssessorPage = true; + return; + } + + // Find user's role for this MSEL + const roles = this.userMselRoleQuery.getAll(); + this.userMselRole = roles.find(r => + r.userId === this.loggedInUserId && r.mselId === this.selectedMselId + ) || null; + + if (!this.userMselRole) { + this.canViewAssessorPage = false; + this.canEditAssessorPage = false; + return; + } + + // Editor or higher can view assessor page + const viewRoles: MselRole[] = ['Owner', 'Editor', 'Approver', 'Evaluator']; + this.canViewAssessorPage = viewRoles.includes(this.userMselRole.role as MselRole); + + // Only Evaluator, Owner can edit (check/uncheck checkboxes) + const editRoles: MselRole[] = ['Owner', 'Evaluator']; + this.canEditAssessorPage = editRoles.includes(this.userMselRole.role as MselRole); + } + + goToUrl(url): void { + if (url !== '/') { + this.router.navigate([url], { + queryParamsHandling: 'merge', + }); + } + } + + ngOnDestroy() { + this.unsubscribe$.next(null); + this.unsubscribe$.complete(); + } +} diff --git a/src/app/components/assessor-view/assessor-view.component.html b/src/app/components/assessor-view/assessor-view.component.html new file mode 100644 index 00000000..ff527515 --- /dev/null +++ b/src/app/components/assessor-view/assessor-view.component.html @@ -0,0 +1,762 @@ + +
+ @if (msel) { + @if (assessorDataFields.length === 0) { +
+ +

No data fields are marked as assessor-visible.

+

Go to the Data Fields tab and enable "Assessor View" on the fields you want assessors to see.

+
+ } @else { +
+ + + @if (showSearch) { + + + + } + + + + + + + @if (msel.showMoveOnAssessorView) { + + } + + @if (msel.showGroupOnAssessorView) { + + } + + @if (msel.showTimeOnAssessorView) { + + } + + @if (msel.showIntegrationTargetOnAssessorView) { + + } + + @for (df of assessorDataFields; track df.id) { + @if (sortableDataTypes.includes(df.dataType)) { + + } @else { + + } + } + + + + @for (row of displayRows; track $index) { + + @if (row.type === 'move') { + + + + + @if (expandedMoveNumbers.has(row.moveNumber)) { + + + + } + } + + @if (row.type === 'group') { + + + + + @if (expandedGroupKeys.has(row.groupKey)) { + + + + } + } + + @if (row.type === 'event') { + + + + + + + @if (msel.showMoveOnAssessorView) { + + } + + @if (msel.showGroupOnAssessorView) { + + } + + @if (msel.showTimeOnAssessorView) { + + } + + @if (msel.showIntegrationTargetOnAssessorView) { + + } + + @for (df of assessorDataFields; track df.id) { + + } + + + @if (expandedEventIds.has(row.event.id)) { + + + + } + } + } + +
+
+ + Search + + @if (filterString) { + + } + + @for (df of filterableFields; track df.id) { + + {{ df.name }} + + @for (val of getFieldDistinctValues(df); track val) { + {{ val }} + } + + + } + @if (hasActiveTopFilters) { + + } +
+
+
+ @if (!showSearch) { + + } + @if (showSearch) { + + } + + +
+
+
Move
+
+
Group
+
+
+ @if (!showRealTime) { + + } + @if (showRealTime) { + + } + Execution Time +
+
+
Integration Target
+
+
{{ df.name }}
+
+
{{ df.name }}
+
+ + + +
+ + + {{ getMoveDescription(row.moveNumber) }} +
+
+
+
+
+ + xAPI Evidence — Move {{ row.moveNumber }} + @if (getMoveStatements(row.moveNumber).length > 0) { + {{ getFilteredMoveStatements(row.moveNumber).length }} / {{ getMoveStatements(row.moveNumber).length }} + } +
+ @if (teamList.length > 0) { + + Team + + @for (team of teamList; track team.id) { + {{ team.shortName }} + } + + + } + + Source + + Blueprint + CITE + Steamfitter + Player + Gallery + + + + Verb + + @for (verb of availableVerbs; track verb) { + {{ verb }} + } + + +
+ +
+ @if (loadingStatements.has('_all')) { + + } @else if (getMoveStatements(row.moveNumber).length === 0) { +
No xAPI statements found for this move.
+ } @else { +
+ @for (stmt of getFilteredMoveStatements(row.moveNumber); track $index) { +
+
+ + {{ formatTimestamp(stmt.timestamp) }} + {{ getMovePart(stmt) }} + {{ getGroupPart(stmt) }} + {{ stmt.context?.platform || '?' }} + {{ stmt.actor?.name || stmt.actor?.account?.name || 'Unknown' }} + {{ stmt.context?.team?.name || '' }} + {{ stmt.verb?.display?.['en-US'] || stmt.verb?.id?.split('/').pop() || '?' }} + {{ stmt.object?.definition?.name?.['en-US'] || stmt.object?.id || '?' }} +
+ @if (expandedStatementIds.has(stmt.id)) { +
+ @for (section of getStatementSections(stmt); track section.key) { +
+ {{ isJsonSectionExpanded(stmt.id, section.key) ? '▼' : '▶' }} + "{{ section.key }}": + @if (!isJsonSectionExpanded(stmt.id, section.key)) { + {{ section.preview || '{...}' }} + } @else { +
{{ formatJson(section.value) }}
+ } +
+ } +
+ } +
+ } +
+ } +
+ @if (getMoveCompetencies(row.moveNumber).length > 0) { +
+
+ + Move {{ row.moveNumber }} — Competency Assessment +
+ @if (teamList.length > 0) { + + Assess Team + + None + @for (team of getTeamsForContext('move-' + row.moveNumber); track team.id) { + {{ team.shortName }} + } + + + } +
+ @for (comp of getMoveCompetencies(row.moveNumber); track comp.id) { +
+
{{ comp.idNumber }}
+ + Rating + + @for (level of proficiencyLevels; track level.id) { + {{ level.name }} ({{ level.value }}) + } + + + + Comment + + +
+ + @if (isRatingSubmitted('move-' + row.moveNumber, comp.id)) { + + } +
+
+ } +
+ @if (hasAnyRatings('move-' + row.moveNumber)) { +
+ +
+ } +
+ } @else { +
+
+ + Competency Assessment +
+
No competencies in this move.
+
+ } +
+
+ + + +
+ + +
+
+
+
+
+ + xAPI Evidence — Group {{ row.groupNumber }} + @if (getGroupStatements(row.groupKey).length > 0) { + {{ getFilteredGroupStatements(row.groupKey).length }} / {{ getGroupStatements(row.groupKey).length }} + } +
+ @if (teamList.length > 0) { + + Team + + @for (team of teamList; track team.id) { + {{ team.shortName }} + } + + + } + + Source + + Blueprint + CITE + Steamfitter + Player + Gallery + + + + Verb + + @for (verb of availableVerbs; track verb) { + {{ verb }} + } + + +
+ +
+ @if (loadingStatements.has('_all')) { + + } @else if (getGroupStatements(row.groupKey).length === 0) { +
No xAPI statements found for this group.
+ } @else { +
+ @for (stmt of getFilteredGroupStatements(row.groupKey); track $index) { +
+
+ + {{ formatTimestamp(stmt.timestamp) }} + {{ getMovePart(stmt) }} + {{ getGroupPart(stmt) }} + {{ stmt.context?.platform || '?' }} + {{ stmt.actor?.name || stmt.actor?.account?.name || 'Unknown' }} + {{ stmt.context?.team?.name || '' }} + {{ stmt.verb?.display?.['en-US'] || stmt.verb?.id?.split('/').pop() || '?' }} + {{ stmt.object?.definition?.name?.['en-US'] || stmt.object?.id || '?' }} +
+ @if (expandedStatementIds.has(stmt.id)) { +
+ @for (section of getStatementSections(stmt); track section.key) { +
+ {{ isJsonSectionExpanded(stmt.id, section.key) ? '▼' : '▶' }} + "{{ section.key }}": + @if (!isJsonSectionExpanded(stmt.id, section.key)) { + {{ section.preview || '{...}' }} + } @else { +
{{ formatJson(section.value) }}
+ } +
+ } +
+ } +
+ } +
+ } +
+ @if (getGroupCompetencies(row.groupKey).length > 0) { +
+
+ + Group {{ row.groupNumber }} — Competency Assessment +
+ @if (teamList.length > 0) { + + Assess Team + + None + @for (team of getTeamsForContext('group-' + row.groupKey); track team.id) { + {{ team.shortName }} + } + + + } +
+ @for (comp of getGroupCompetencies(row.groupKey); track comp.id) { +
+
{{ comp.idNumber }}
+ + Rating + + @for (level of proficiencyLevels; track level.id) { + {{ level.name }} ({{ level.value }}) + } + + + + Comment + + +
+ + @if (isRatingSubmitted('group-' + row.groupKey, comp.id)) { + + } +
+
+ } +
+ @if (hasAnyRatings('group-' + row.groupKey)) { +
+ +
+ } +
+ } @else { +
+
+ + Competency Assessment +
+
No competencies in this group.
+
+ } +
+
+ + + + {{ row.rowIndex }} + +
+ {{ moveAndGroupNumbers[row.event.id] ? moveAndGroupNumbers[row.event.id][0] : '' }} +
+
+
+ {{ moveAndGroupNumbers[row.event.id] ? moveAndGroupNumbers[row.event.id][1] : '' }} +
+
+
+ +
+
+
+ {{ row.event.integrationTarget || '' }} +
+
+ + +
+
+ +
+
+ + xAPI Evidence + @if (getStatements(row.event.id).length > 0) { + {{ getFilteredStatements(row.event.id).length }} / {{ getStatements(row.event.id).length }} + } +
+ @if (teamList.length > 0) { + + Team + + @for (team of teamList; track team.id) { + {{ team.shortName }} + } + + + } + + Source + + Blueprint + CITE + Steamfitter + Player + Gallery + + + + Verb + + @for (verb of availableVerbs; track verb) { + {{ verb }} + } + + +
+ +
+ @if (isLoading(row.event.id)) { + + } @else if (getStatements(row.event.id).length === 0) { +
No xAPI statements found for this event.
+ } @else { +
+ @for (stmt of getFilteredStatements(row.event.id); track $index) { +
+
+ + {{ formatTimestamp(stmt.timestamp) }} + {{ getMovePart(stmt) }} + {{ getGroupPart(stmt) }} + {{ stmt.context?.platform || '?' }} + {{ stmt.actor?.name || stmt.actor?.account?.name || 'Unknown' }} + {{ stmt.context?.team?.name || '' }} + {{ stmt.verb?.display?.['en-US'] || stmt.verb?.id?.split('/').pop() || '?' }} + {{ stmt.object?.definition?.name?.['en-US'] || stmt.object?.id || '?' }} +
+ @if (expandedStatementIds.has(stmt.id)) { +
+ @for (section of getStatementSections(stmt); track section.key) { +
+ {{ isJsonSectionExpanded(stmt.id, section.key) ? '▼' : '▶' }} + "{{ section.key }}": + @if (!isJsonSectionExpanded(stmt.id, section.key)) { + {{ section.preview || '{...}' }} + } @else { +
{{ formatJson(section.value) }}
+ } +
+ } +
+ } +
+ } +
+ } +
+ + @if (getEventCompetencies(row.event.id).length > 0) { +
+
+ + Competency Assessment +
+ @if (teamList.length > 0) { + + Assess Team + + None + @for (team of getTeamsForContext(row.event.id); track team.id) { + {{ team.shortName }} + } + + + } +
+ @for (comp of getEventCompetencies(row.event.id); track comp.id) { +
+
{{ comp.idNumber }}
+ + Rating + + @for (level of proficiencyLevels; track level.id) { + {{ level.name }} ({{ level.value }}) + } + + + + Comment + + +
+ + @if (isRatingSubmitted(row.event.id, comp.id)) { + + } +
+
+ } +
+ @if (hasAnyRatings(row.event.id)) { +
+ +
+ } +
+ } @else { +
+
+ + Competency Assessment +
+
No competencies on this event.
+
+ } +
+
+
+ } + } +
diff --git a/src/app/components/assessor-view/assessor-view.component.scss b/src/app/components/assessor-view/assessor-view.component.scss new file mode 100644 index 00000000..94723219 --- /dev/null +++ b/src/app/components/assessor-view/assessor-view.component.scss @@ -0,0 +1,603 @@ +// Copyright 2024 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +:host { + flex: 1; + overflow: hidden; + min-height: 0; +} + +.container { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: auto; + background-color: var(--mat-sys-background); + color: var(--mat-sys-on-background); +} + +.tableFixHead thead th { + position: sticky; + top: 0px; + z-index: 50; + background-color: var(--mat-sys-background); +} + +table { + border-collapse: collapse; + width: 100%; + background-color: var(--mat-sys-background); + color: var(--mat-sys-on-background); +} + +tr { + border-bottom: 1pt solid var(--mat-sys-outline-variant); + width: 100%; +} + +th, +td { + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + text-align: left; + word-wrap: normal; + color: var(--mat-sys-on-background); +} + +.constrainer { + max-height: 300px; +} + +.time-span { + min-width: 90px; +} + +.expand-col { + width: 120px; + min-width: 120px; + max-width: 120px; +} + +.row-num-col { + text-align: right; +} + +.row-index { + width: 15px; + text-align: right; + margin-right: 20px; +} + +.center-self { + align-self: center; +} + +.row-start-buttons { + display: flex; + flex-shrink: 0; +} + +.header-button { + width: 30px !important; + height: 30px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + padding: 0 !important; +} + + +.scenario-event-button { + width: 30px !important; + height: 30px !important; + align-self: center !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + padding: 0 !important; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + color: var(--mat-sys-on-surface-variant); + text-align: center; + + p { + margin: 4px 0; + } +} + +.move-header-row { + td { + padding: 6px 8px; + background-color: var(--mat-sys-surface-container); + } + border-bottom: 2px solid var(--mat-sys-outline); +} + +.group-header-row { + td { + padding: 4px 8px; + background-color: var(--mat-sys-surface-container-low); + } + border-bottom: 1px solid var(--mat-sys-outline-variant); +} + +.section-header-content { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + color: var(--mat-sys-on-surface); +} + +.section-label { + font-weight: 600; + color: var(--mat-sys-on-surface); +} + +.section-description { + color: var(--mat-sys-on-surface); + font-weight: 400; +} + +.detail-row { + td { + padding: 0 !important; + } +} + +.event-detail { + padding: 12px 16px; + background-color: var(--mat-sys-surface-container-lowest); + border-top: 1px solid var(--mat-sys-outline-variant); +} + +.statements-header { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + margin-bottom: 8px; + min-height: 40px; + color: var(--mat-sys-on-surface); +} + +.no-statements { + font-size: 13px; + color: var(--mat-sys-on-surface-variant); + font-style: italic; + padding: 8px 0; +} + +.statements-list { + display: flex; + flex-direction: column; + gap: 2px; +} + +.statement-entry { + border-radius: 2px; +} + +.statement-row { + display: flex; + gap: 12px; + padding: 4px 8px; + font-size: 13px; + border-radius: 2px; + cursor: pointer; + + &:hover { + background-color: var(--mat-sys-surface-container); + } +} + +.statement-expanded { + background-color: var(--mat-sys-surface-container); +} + +.statement-json { + margin: 0; + padding: 8px 12px; + font-size: 11px; + font-family: monospace; + background-color: var(--mat-sys-surface-container-high); + border-radius: 0 0 4px 4px; + overflow-x: auto; + max-height: 400px; + overflow-y: auto; + color: var(--mat-sys-on-surface); +} + +.json-section { + padding: 2px 0; + cursor: pointer; + display: flex; + align-items: baseline; + gap: 4px; + + &:hover { + background-color: var(--mat-sys-surface-container-highest); + } +} + +.json-toggle { + font-size: 9px; + width: 12px; + flex-shrink: 0; + color: var(--mat-sys-on-surface-variant); +} + +.json-key { + color: var(--mat-sys-on-surface); + font-weight: 600; + flex-shrink: 0; +} + +.json-preview { + color: var(--mat-sys-on-surface-variant); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.json-value { + margin: 4px 0 4px 16px; + padding: 0; + font-size: 11px; + font-family: monospace; + white-space: pre-wrap; + word-break: break-all; + color: var(--mat-sys-on-surface); +} + +.statement-time { + flex-shrink: 0; + color: var(--mat-sys-on-surface-variant); + font-family: monospace; + font-size: 12px; + width: 180px; +} + +.stmt-move { + flex-shrink: 0; + font-family: monospace; + font-weight: 600; + font-size: 12px; + color: var(--mat-sys-primary); + width: 30px; + text-align: center; +} + +.stmt-group { + flex-shrink: 0; + font-family: monospace; + font-weight: 600; + font-size: 12px; + color: var(--mat-sys-primary); + width: 30px; + text-align: center; +} + +.stmt-actor { + flex-shrink: 0; + font-weight: 500; + color: var(--mat-sys-on-surface); + width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stmt-team { + flex-shrink: 0; + color: var(--mat-sys-on-surface-variant); + font-size: 12px; + width: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stmt-verb { + flex-shrink: 0; + font-weight: 600; + color: var(--mat-sys-tertiary); + width: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stmt-object { + color: var(--mat-sys-on-surface); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stmt-object-type { + flex-shrink: 0; + color: var(--mat-sys-on-surface-variant); + font-size: 11px; + font-style: italic; + width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.copy-button { + width: 22px !important; + height: 22px !important; + padding: 0 !important; + flex-shrink: 0; + opacity: 0.4; + &:hover { + opacity: 1; + } +} + +.filter-row th { + padding: 4px 8px !important; +} + +.filter-bar { + display: flex; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; + padding: 4px 0; +} + +.search-field { + min-width: 180px; + max-width: 280px; + font-size: 13px; +} + +.top-filter-field { + min-width: 120px; + max-width: 180px; + font-size: 13px; +} + +.clear-filters-button { + width: 28px !important; + height: 28px !important; + padding: 0 !important; + align-self: center; +} + +.statement-count { + font-size: 12px; + font-weight: 500; + background-color: var(--mat-sys-primary); + color: var(--mat-sys-on-primary); + border-radius: 10px; + padding: 1px 8px; + min-width: 20px; + text-align: center; +} + +.evidence-filters { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +} + +.evidence-filter-field { + min-width: 120px; + max-width: 200px; + font-size: 12px; +} + +.refresh-button { + width: 28px !important; + height: 28px !important; + padding: 0 !important; +} + +.platform-badge { + flex-shrink: 0; + font-size: 11px; + font-weight: 600; + padding: 1px 6px; + border-radius: 3px; + text-transform: uppercase; + width: 90px; + text-align: center; +} + +.platform-blueprint { + background-color: #1565c0; + color: #fff; +} + +.platform-cite { + background-color: #6a1b9a; + color: #fff; +} + +.platform-steamfitter { + background-color: #e65100; + color: #fff; +} + +.platform-player { + background-color: #2e7d32; + color: #fff; +} + +.platform-gallery { + background-color: #00838f; + color: #fff; +} + +.platform-unknown { + background-color: var(--mat-sys-surface-container-high); + color: var(--mat-sys-on-surface); +} + +// Side-by-side event detail layout +.event-detail-split { + display: flex; + gap: 1px; + background-color: var(--mat-sys-outline-variant); + min-height: 120px; +} + +.evidence-panel { + flex: 6; + min-width: 0; + padding: 12px 16px; + background-color: var(--mat-sys-surface-container-lowest); +} + +.evidence-panel-full { + flex: 1; +} + +.assessment-panel-spacer { + flex: 4; + min-width: 300px; + background-color: var(--mat-sys-surface-container-lowest); +} + +.assessment-panel { + flex: 4; + min-width: 300px; + padding: 12px 16px; + background-color: var(--mat-sys-surface-container-lowest); +} + +.assessment-header { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + margin-bottom: 8px; + min-height: 40px; + color: var(--mat-sys-on-surface); +} + +.assessment-team-field { + width: 100%; + font-size: 13px; + margin-bottom: 4px; +} + +.competency-ratings { + display: flex; + flex-direction: column; + gap: 8px; +} + +.rating-card { + padding: 8px 10px; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 6px; + background-color: var(--mat-sys-surface-container); +} + +.rating-submitted { + border-color: var(--mat-sys-primary); + background-color: var(--mat-sys-primary-container); +} + +.rating-comp-id { + font-family: monospace; + font-size: 11px; + color: var(--mat-sys-on-surface-variant); + line-height: 1.2; +} + +.rating-comp-name { + font-size: 13px; + font-weight: 500; + color: var(--mat-sys-on-surface); + margin-bottom: 6px; + line-height: 1.3; +} + +.rating-level-field { + width: 100%; + font-size: 12px; +} + +.rating-comment-field { + width: 100%; + font-size: 12px; +} + +.rating-submit-button { + width: 28px !important; + height: 28px !important; + padding: 0 !important; + float: right; +} + +.rating-actions { + display: flex; + align-items: center; + gap: 4px; + margin-top: 2px; +} + +.rating-submitted-indicator { + display: flex; + align-items: center; + gap: 4px; + color: var(--mat-sys-primary); + font-size: 11px; + font-weight: 500; +} + +.assessment-panel-empty { + color: var(--mat-sys-on-surface-variant); +} + +.assessment-header-inactive { + color: var(--mat-sys-on-surface-variant); +} + +.no-competencies { + font-size: 13px; + color: var(--mat-sys-on-surface-variant); + font-style: italic; + padding: 4px 0; +} + +.assessment-actions { + margin-top: 8px; + display: flex; + justify-content: flex-end; +} + +.submit-all-button { + font-size: 13px; +} + +@media (max-width: 900px) { + .event-detail-split { + flex-direction: column; + } + + .assessment-panel { + width: 100%; + min-width: unset; + max-height: none; + border-top: 1px solid var(--mat-sys-outline-variant); + } +} diff --git a/src/app/components/assessor-view/assessor-view.component.ts b/src/app/components/assessor-view/assessor-view.component.ts new file mode 100644 index 00000000..4f397f6c --- /dev/null +++ b/src/app/components/assessor-view/assessor-view.component.ts @@ -0,0 +1,1664 @@ +// Copyright 2024 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. +import { Component, Input, OnDestroy, HostListener } from '@angular/core'; +import { Subject, Subscription, Observable, of } from 'rxjs'; +import { takeUntil, debounceTime, distinctUntilChanged, mergeMap, delay } from 'rxjs/operators'; +import { Sort } from '@angular/material/sort'; +import { HttpClient } from '@angular/common/http'; +import { ComnSettingsService, Theme } from '@cmusei/crucible-common'; +import { + Card, + Competency, + CompetencyFramework, + DataField, + DataValue, + Move, + Msel, + MselCompetency, + Organization, + ProficiencyLevel, + ProficiencyScale, + ScenarioEvent, + Team, + TeamCompetency, + User, +} from 'src/app/generated/blueprint.api'; +import { MselDataService, MselPlus } from 'src/app/data/msel/msel-data.service'; +import { MselQuery } from 'src/app/data/msel/msel.query'; +import { DataFieldQuery } from 'src/app/data/data-field/data-field.query'; +import { DataValueQuery } from 'src/app/data/data-value/data-value.query'; +import { DataValueDataService } from 'src/app/data/data-value/data-value-data.service'; +import { MoveQuery } from 'src/app/data/move/move.query'; +import { CardQuery } from 'src/app/data/card/card.query'; +import { OrganizationQuery } from 'src/app/data/organization/organization.query'; +import { TeamQuery } from 'src/app/data/team/team.query'; +import { MselCompetencyQuery } from 'src/app/data/msel-competency/msel-competency.query'; +import { MselCompetencyDataService } from 'src/app/data/msel-competency/msel-competency-data.service'; +import { TeamCompetencyQuery } from 'src/app/data/team-competency/team-competency.query'; +import { TeamCompetencyDataService } from 'src/app/data/team-competency/team-competency-data.service'; +import { CompetencyFrameworkService, ProficiencyScaleService } from 'src/app/generated/blueprint.api/api/api'; +import { + DataValuePlus, + ScenarioEventDataService, + ScenarioEventView, + ScenarioEventViewIndexing, +} from 'src/app/data/scenario-event/scenario-event-data.service'; +import { ScenarioEventQuery } from 'src/app/data/scenario-event/scenario-event.query'; +import { UIDataService } from 'src/app/data/ui/ui-data.service'; +import { AngularEditorConfig } from '@kolkov/angular-editor'; + +export interface XApiStatement { + id?: string; + actor?: { name?: string; account?: { name?: string } }; + verb?: { id?: string; display?: { 'en-US'?: string } }; + object?: { id?: string; definition?: { name?: { 'en-US'?: string }; type?: string; extensions?: Record } }; + result?: { score?: { raw?: number }; completion?: boolean; success?: boolean; response?: string }; + timestamp?: string; + context?: { + team?: { name?: string; account?: { name?: string } }; + platform?: string; + extensions?: Record; + contextActivities?: { + grouping?: { id?: string; definition?: { name?: { 'en-US'?: string }; type?: string } }[]; + }; + }; +} + +@Component({ + selector: 'app-assessor-view', + templateUrl: './assessor-view.component.html', + styleUrls: ['./assessor-view.component.scss'], + standalone: false +}) +export class AssessorViewComponent implements OnDestroy, ScenarioEventView { + @Input() loggedInUserId: string; + @Input() userTheme: Theme; + @Input() canEditCheckboxes = false; + msel = new MselPlus(); + + // ScenarioEventView fields + mselScenarioEvents: ScenarioEvent[] = []; + filterString = ''; + sort: Sort = { active: '', direction: '' }; + displayedScenarioEvents: ScenarioEvent[] = []; + assessorDataFields: DataField[] = []; + dataValues: DataValue[] = []; + cardList: Card[] = []; + mselUsers: User[] = []; + viewIndex = new ScenarioEventViewIndexing(); + + moveList: Move[] = []; + teamList: Team[] = []; + organizationList: Organization[] = []; + moveAndGroupNumbers: Record[] = []; + blankDataValue = { + id: '', + scenarioEventId: '', + dataFieldId: '', + value: '', + valueArray: [], + } as DataValuePlus; + darkThemeTint = this.settingsService.settings.DarkThemeTint + ? this.settingsService.settings.DarkThemeTint + : 0.7; + lightThemeTint = this.settingsService.settings.LightThemeTint + ? this.settingsService.settings.LightThemeTint + : 0.4; + viewConfig: AngularEditorConfig = { + editable: false, + height: 'auto', + minHeight: '1200px', + width: '100%', + minWidth: '0', + translate: 'yes', + enableToolbar: false, + showToolbar: false, + placeholder: '', + defaultParagraphSeparator: '', + defaultFontName: '', + defaultFontSize: '', + sanitize: false, + }; + + private static FILTERABLE_TYPES = new Set([ + 'Organization', 'Team', 'TeamsMultiple', 'Status', 'Card', + 'SourceType', 'Move', 'IntegrationTarget', 'Checkbox', 'Competency', + ]); + + sortableDataTypes = this.scenarioEventDataService.sortableDataTypes; + showRealTime = false; + showSearch = false; + selectedMoveNumber: number | null = null; + topFieldFilters = new Map(); + sectionTeamFilter = new Map(); + sectionSourceFilter = new Map(); + sectionVerbFilter = new Map(); + sectionTeamInitialized = new Set(); + availableVerbs: string[] = []; + keyUp = new Subject(); + private subscription: Subscription; + expandedEventIds = new Set(); + expandedMoveNumbers = new Set(); + expandedGroupKeys = new Set(); + eventStatements: Map = new Map(); + moveStatements: Map = new Map(); + groupStatements: Map = new Map(); + loadingStatements: Set = new Set(); + + // Competency assessment + mselCompetencies: MselCompetency[] = []; + teamCompetencies: TeamCompetency[] = []; + proficiencyScale: ProficiencyScale | null = null; + proficiencyLevels: ProficiencyLevel[] = []; + eventRatings = new Map>(); + submittedAssertions = new Set(); + submittingAssertions = new Set(); + assertionTeam = new Map(); + + // Precomputed competency caches — rebuilt on data change, not per change detection + private baseEventCompetencies = new Map(); + private baseMoveCompetencies = new Map(); + private baseGroupCompetencies = new Map(); + private teamCompIdSets = new Map>(); + private statementSectionsCache = new Map(); + private competencyFrameworkCache = new Map(); + private competencyTypeCache = new Map(); + + private apiUrl: string; + private unsubscribe$ = new Subject(); + + constructor( + private mselQuery: MselQuery, + private dataFieldQuery: DataFieldQuery, + private dataValueQuery: DataValueQuery, + private dataValueDataService: DataValueDataService, + private moveQuery: MoveQuery, + private cardQuery: CardQuery, + private organizationQuery: OrganizationQuery, + private teamQuery: TeamQuery, + private scenarioEventDataService: ScenarioEventDataService, + private scenarioEventQuery: ScenarioEventQuery, + private uiDataService: UIDataService, + private http: HttpClient, + private settingsService: ComnSettingsService, + private mselCompetencyQuery: MselCompetencyQuery, + private mselCompetencyDataService: MselCompetencyDataService, + private teamCompetencyQuery: TeamCompetencyQuery, + private teamCompetencyDataService: TeamCompetencyDataService, + private competencyFrameworkService: CompetencyFrameworkService, + private proficiencyScaleService: ProficiencyScaleService + ) { + this.apiUrl = this.settingsService.settings.ApiUrl; + this.showRealTime = this.uiDataService.useRealTime(); + + (this.mselQuery.selectActive() as Observable) + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(msel => { + if (msel) { + const mselChanged = this.msel.id !== msel.id; + this.msel = { ...msel } as MselPlus; + if (mselChanged && msel.id) { + this.mselCompetencyDataService.loadByMsel(msel.id); + this.teamCompetencyDataService.loadByMsel(msel.id); + } + } + }); + + this.dataFieldQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(dataFields => { + this.allDataFields = dataFields; + this.competencyFieldIds = dataFields + .filter(df => df.dataType?.toString() === 'Competency') + .map(df => df.id); + this.rebuildCompetencyCaches(); + this.assessorDataFields = dataFields + .filter(df => df.isAssessorVisible) + .sort((a, b) => (+a.displayOrder > +b.displayOrder ? 1 : -1)); + this.scenarioEventDataService.updateScenarioEventViewDataFields(this); + this.scenarioEventDataService.updateScenarioEventViewDataValues(this); + this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents(this); + }); + + this.dataValueQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(dataValues => { + this.dataValues = []; + dataValues.forEach(dv => { + this.dataValues.push({ ...dv }); + }); + this.scenarioEventDataService.updateScenarioEventViewDataValues(this); + this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents(this); + this.rebuildCompetencyCaches(); + }); + + this.scenarioEventQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(scenarioEvents => { + this.scenarioEventDataService.refreshScenarioEventViewEvents(this, scenarioEvents); + if (scenarioEvents && scenarioEvents.length > 0) { + this.moveAndGroupNumbers = this.scenarioEventDataService.getMoveAndGroupNumbers( + this.mselScenarioEvents, this.moveList + ); + this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents(this); + this.rebuildCompetencyCaches(); + } + }); + + this.cardQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(cards => { + this.cardList = cards; + }); + this.scenarioEventDataService.updateScenarioEventViewCards(this); + this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents(this); + + this.moveQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(moves => { + this.moveList = moves.sort((a, b) => + +a.moveNumber < +b.moveNumber ? -1 : 1 + ); + this.moveAndGroupNumbers = this.scenarioEventDataService.getMoveAndGroupNumbers( + this.mselScenarioEvents, this.moveList + ); + this.rebuildCompetencyCaches(); + }); + + this.organizationQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(organizations => { + this.organizationList = organizations.filter( + org => !org.isTemplate && org.mselId === this.msel.id + ); + }); + + this.teamQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(teams => { + this.teamList = teams; + }); + + this.mselCompetencyQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(mcs => { + this.mselCompetencies = mcs; + this.loadProficiencyScale(); + this.loadCompetencyFrameworks(); + this.rebuildCompetencyCaches(); + }); + + this.teamCompetencyQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(tcs => { + this.teamCompetencies = tcs; + this.rebuildTeamCompIdSets(); + }); + + this.subscription = this.keyUp + .pipe( + debounceTime(250), + distinctUntilChanged(), + mergeMap(search => of(search).pipe(delay(250))) + ) + .subscribe(event => { + this.applyFilter(this.filterString); + }); + } + + // ScenarioEventView interface + get dataFields(): DataField[] { + return this.assessorDataFields; + } + + get userList(): User[] { + return this.mselUsers; + } + + get showHiddenEvents(): boolean { + return false; + } + + getDataValue(scenarioEvent: ScenarioEvent, dataFieldName: string): DataValuePlus { + if (!(this.msel && scenarioEvent && scenarioEvent.id)) { + return this.blankDataValue; + } + return this.scenarioEventDataService.getDataValueFromView(this, scenarioEvent, dataFieldName); + } + + getSortedOrganizationOptions(): string[] { + let orgs: string[] = []; + this.organizationList.forEach(o => { + orgs.push(o.shortName); + }); + this.msel.teams?.forEach(t => { + orgs.push(t.shortName); + }); + orgs = orgs.sort((a, b) => (a < b ? -1 : 1)); + return orgs; + } + + getRowStyle(scenarioEvent: ScenarioEvent) { + if (!scenarioEvent || !scenarioEvent.rowMetadata) { + return ''; + } + const rowMetadata = scenarioEvent.rowMetadata + ? scenarioEvent.rowMetadata.split(',') + : []; + const color = + rowMetadata.length >= 4 + ? rowMetadata[1] + ', ' + rowMetadata[2] + ', ' + rowMetadata[3] + : ''; + const tint = + this.userTheme === 'dark-theme' + ? this.darkThemeTint + : this.lightThemeTint; + const style = color + ? { 'background-color': 'rgba(' + color + ', ' + tint + ')' } + : {}; + return style; + } + + getStyle(dataField: DataField): string { + if (dataField && dataField.columnMetadata) { + const width = Math.trunc(+dataField.columnMetadata * 7); + return 'text-align: left; width: ' + width.toString() + 'px;'; + } else { + return 'text-align: left; width: 90%; min-width: 40px;'; + } + } + + sortChanged(sort: Sort) { + this.sort = sort; + this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents(this); + } + + applyFilter(filterValue: string) { + this.filterString = filterValue; + this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents(this); + } + + setSearch(value: boolean) { + if (!value) { + this.applyFilter(''); + } + this.showSearch = value; + } + + setRealTime(value: boolean) { + this.showRealTime = value; + this.uiDataService.setUseRealTime(value); + } + + filterByMove(moveNumber: number | null) { + this.selectedMoveNumber = moveNumber; + } + + saveDataValue(event: ScenarioEvent, dataField: DataField, newValue: any) { + const dataValue = this.getDataValue(event, dataField.name); + if (dataValue.value === newValue) { + return; // No change + } + dataValue.value = newValue; + this.dataValueDataService.updateDataValue(dataValue); + } + + filterByTeams(sectionKey: string, teamIds: string[]) { + this.sectionTeamFilter.set(sectionKey, teamIds); + this.sectionTeamInitialized.add(sectionKey); + } + + filterBySources(sectionKey: string, sources: string[]) { + this.sectionSourceFilter.set(sectionKey, sources); + } + + filterByVerbs(sectionKey: string, verbs: string[]) { + this.sectionVerbFilter.set(sectionKey, verbs); + } + + getSectionTeams(sectionKey: string): string[] { + return this.sectionTeamFilter.get(sectionKey) ?? []; + } + + getSectionSources(sectionKey: string): string[] { + return this.sectionSourceFilter.get(sectionKey) ?? []; + } + + getSectionVerbs(sectionKey: string): string[] { + return this.sectionVerbFilter.get(sectionKey) ?? []; + } + + initEventTeamDefault(eventId: string) { + const sectionKey = `event-${eventId}`; + if (this.sectionTeamInitialized.has(sectionKey)) return; + this.sectionTeamInitialized.add(sectionKey); + + const event = this.mselScenarioEvents.find(e => e.id === eventId); + if (!event) return; + + const orgField = this.assessorDataFields.find(df => + df.dataType?.toString().toLowerCase() === 'organization' + ); + if (!orgField) return; + + const dv = this.getDataValue(event, orgField.name); + const orgValue = (dv.value || '').trim(); + if (!orgValue || orgValue.toUpperCase() === 'ALL') return; + + const orgNames = orgValue.split(',').map(s => s.trim().toLowerCase()).filter(s => s); + const matchedIds = this.teamList + .filter(t => + orgNames.includes((t.shortName || '').toLowerCase()) || + orgNames.includes((t.name || '').toLowerCase()) + ) + .map(t => t.id); + + if (matchedIds.length > 0) { + this.sectionTeamFilter.set(sectionKey, matchedIds); + } + } + + + refreshStatements() { + if (this.statementsLoading || !this.msel.id) return; + this.statementsLoading = true; + this.loadingStatements.add('_all'); + + const baseUrl = this.apiUrl.endsWith('/') ? this.apiUrl : this.apiUrl + '/'; + const params: any = { mselId: this.msel.id }; + if (this.allMselStatements.length > 0) { + const latest = this.allMselStatements[this.allMselStatements.length - 1]; + if (latest.timestamp) { + params.since = latest.timestamp; + } + } + this.http.get(`${baseUrl}api/xapi/statements`, { params }) + .subscribe({ + next: (response) => { + const statements = response?.statements || response || []; + const newStmts: XApiStatement[] = Array.isArray(statements) ? statements : []; + const existingIds = new Set(this.allMselStatements.map(s => s.id)); + const added = newStmts.filter(s => !s.id || !existingIds.has(s.id)); + if (added.length > 0) { + this.allMselStatements.push(...added); + this.allMselStatements.sort((a, b) => + (a.timestamp || '').localeCompare(b.timestamp || '') + ); + this.distributeStatements(); + this.buildVerbList(); + } + this.statementsLoading = false; + this.loadingStatements.delete('_all'); + }, + error: () => { + this.statementsLoading = false; + this.loadingStatements.delete('_all'); + } + }); + } + + getPlatformClass(stmt: XApiStatement): string { + const platform = (stmt.context?.platform || '').toLowerCase(); + return `platform-${platform || 'unknown'}`; + } + + trackByFn(index, item) { + return item.id; + } + + private buildVerbList() { + const verbs = new Set(); + for (const stmt of this.allMselStatements) { + const verb = stmt.verb?.display?.['en-US'] || stmt.verb?.id?.split('/').pop() || ''; + if (verb) verbs.add(verb); + } + this.availableVerbs = Array.from(verbs).sort(); + } + + // xAPI evidence + private allMselStatements: XApiStatement[] = []; + private statementsLoaded = false; + statementsLoading = false; + + toggleEvent(eventId: string) { + if (this.expandedEventIds.has(eventId)) { + this.expandedEventIds.delete(eventId); + } else { + this.expandedEventIds.add(eventId); + this.initEventTeamDefault(eventId); + if (!this.statementsLoaded) { + this.loadAllStatements(); + } + } + } + + toggleMoveExpand(moveNumber: number) { + if (this.expandedMoveNumbers.has(moveNumber)) { + this.expandedMoveNumbers.delete(moveNumber); + } else { + this.expandedMoveNumbers.add(moveNumber); + if (!this.statementsLoaded) { + this.loadAllStatements(); + } + } + } + + toggleGroupExpand(groupKey: string) { + if (this.expandedGroupKeys.has(groupKey)) { + this.expandedGroupKeys.delete(groupKey); + } else { + this.expandedGroupKeys.add(groupKey); + if (!this.statementsLoaded) { + this.loadAllStatements(); + } + } + } + + expandAll() { + // Expand all moves, groups, and events + const rows = this.displayRows; + rows.forEach(row => { + if (row.type === 'move' && row.moveNumber !== undefined) { + this.expandedMoveNumbers.add(row.moveNumber); + } else if (row.type === 'group' && row.groupKey) { + this.expandedGroupKeys.add(row.groupKey); + } else if (row.type === 'event' && row.event?.id) { + this.expandedEventIds.add(row.event.id); + this.initEventTeamDefault(row.event.id); + } + }); + if (!this.statementsLoaded) { + this.loadAllStatements(); + } + } + + collapseAll() { + this.expandedMoveNumbers.clear(); + this.expandedGroupKeys.clear(); + this.expandedEventIds.clear(); + } + + get isFullyExpanded(): boolean { + const rows = this.displayRows; + return rows.every(row => { + if (row.type === 'move' && row.moveNumber !== undefined) { + return this.expandedMoveNumbers.has(row.moveNumber); + } else if (row.type === 'group' && row.groupKey) { + return this.expandedGroupKeys.has(row.groupKey); + } else if (row.type === 'event' && row.event?.id) { + return this.expandedEventIds.has(row.event.id); + } + return true; + }); + } + + get isFullyCollapsed(): boolean { + return this.expandedMoveNumbers.size === 0 && + this.expandedGroupKeys.size === 0 && + this.expandedEventIds.size === 0; + } + + expandAllInMove(moveNumber: number) { + // Expand the move itself + this.expandedMoveNumbers.add(moveNumber); + // Expand all groups and events in this move + const rows = this.displayRows; + rows.forEach(row => { + if (row.type === 'group' && row.moveNumber === moveNumber && row.groupKey) { + this.expandedGroupKeys.add(row.groupKey); + } else if (row.type === 'event' && row.moveNumber === moveNumber && row.event?.id) { + this.expandedEventIds.add(row.event.id); + this.initEventTeamDefault(row.event.id); + } + }); + if (!this.statementsLoaded) { + this.loadAllStatements(); + } + } + + collapseAllInMove(moveNumber: number) { + // Collapse all groups and events in this move + const rows = this.displayRows; + rows.forEach(row => { + if (row.type === 'group' && row.moveNumber === moveNumber && row.groupKey) { + this.expandedGroupKeys.delete(row.groupKey); + } else if (row.type === 'event' && row.moveNumber === moveNumber && row.event?.id) { + this.expandedEventIds.delete(row.event.id); + } + }); + // Collapse the move itself + this.expandedMoveNumbers.delete(moveNumber); + } + + expandAllInGroup(moveNumber: number, groupOrder: number) { + const groupKey = `${moveNumber}-${groupOrder}`; + // Expand the group itself + this.expandedGroupKeys.add(groupKey); + // Expand all events in this group + const rows = this.displayRows; + rows.forEach(row => { + if (row.type === 'event' && row.moveNumber === moveNumber && row.groupNumber === groupOrder && row.event?.id) { + this.expandedEventIds.add(row.event.id); + this.initEventTeamDefault(row.event.id); + } + }); + if (!this.statementsLoaded) { + this.loadAllStatements(); + } + } + + collapseAllInGroup(moveNumber: number, groupOrder: number) { + // Collapse all events in this group + const rows = this.displayRows; + rows.forEach(row => { + if (row.type === 'event' && row.moveNumber === moveNumber && row.groupNumber === groupOrder && row.event?.id) { + this.expandedEventIds.delete(row.event.id); + } + }); + // Collapse the group itself + const groupKey = `${moveNumber}-${groupOrder}`; + this.expandedGroupKeys.delete(groupKey); + } + + @HostListener('window:keydown', ['$event']) + handleKeyboardEvent(event: KeyboardEvent) { + // Ctrl+Shift+E = Expand All + if (event.ctrlKey && event.shiftKey && event.key === 'E') { + event.preventDefault(); + this.expandAll(); + } + // Ctrl+Shift+C = Collapse All + if (event.ctrlKey && event.shiftKey && event.key === 'C') { + event.preventDefault(); + this.collapseAll(); + } + } + + getMoveStatements(moveNumber: number): XApiStatement[] { + return this.moveStatements.get(moveNumber) || []; + } + + getGroupStatements(groupKey: string): XApiStatement[] { + return this.groupStatements.get(groupKey) || []; + } + + getFilteredMoveStatements(moveNumber: number): XApiStatement[] { + return this.applyStatementFilters(this.getMoveStatements(moveNumber), `move-${moveNumber}`); + } + + getFilteredGroupStatements(groupKey: string): XApiStatement[] { + return this.applyStatementFilters(this.getGroupStatements(groupKey), `group-${groupKey}`); + } + + getMoveDescription(moveNumber: number): string { + const move = this.moveList.find(m => +m.moveNumber === moveNumber); + return move?.description || `Move ${moveNumber}`; + } + + getGroupDescription(moveNumber: number, groupNumber: number): string { + return `Group ${groupNumber}`; + } + + get filterableFields(): DataField[] { + return this.assessorDataFields.filter(df => + AssessorViewComponent.FILTERABLE_TYPES.has(df.dataType?.toString() || '') + || (df.isChosenFromList && df.dataOptions?.length > 0) + ); + } + + getFieldDistinctValues(df: DataField): string[] { + if (df.dataOptions?.length > 0) { + return df.dataOptions.map(o => o.optionName).filter(n => !!n).sort(); + } + if (df.dataType === 'Checkbox') { + return ['Unchecked', 'Checked']; + } + const vals = new Set(); + for (const event of this.mselScenarioEvents) { + const dv = this.getDataValue(event, df.name); + const v = (dv.value || '').trim(); + if (v) vals.add(v); + } + return Array.from(vals).sort(); + } + + getTopFieldFilter(fieldId: string): string[] { + return this.topFieldFilters.get(fieldId) ?? []; + } + + setTopFieldFilter(fieldId: string, values: string[]) { + this.topFieldFilters.set(fieldId, values); + } + + clearAllTopFilters() { + this.topFieldFilters.clear(); + this.filterString = ''; + this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents(this); + } + + get hasActiveTopFilters(): boolean { + for (const vals of this.topFieldFilters.values()) { + if (vals.length > 0) return true; + } + return !!this.filterString; + } + + private applyTopFieldFilters(events: ScenarioEvent[]): ScenarioEvent[] { + for (const [fieldId, selected] of this.topFieldFilters) { + if (selected.length === 0) continue; + const df = this.assessorDataFields.find(d => d.id === fieldId); + if (!df) continue; + events = events.filter(event => { + const dv = this.getDataValue(event, df.name); + let val = (dv.value || '').trim(); + if (df.dataType === 'Checkbox') { + val = val === 'true' ? 'true' : 'false'; + const mappedSelected = selected.map(s => s === 'Checked' ? 'true' : s === 'Unchecked' ? 'false' : s); + return mappedSelected.some(s => val.toLowerCase() === s.toLowerCase()); + } + if (!val) return false; + if (val.toUpperCase() === 'ALL') return true; + return selected.some(s => val.toLowerCase().includes(s.toLowerCase())); + }); + } + return events; + } + + get displayRows(): { type: 'move' | 'group' | 'event'; moveNumber?: number; groupNumber?: number; groupKey?: string; event?: ScenarioEvent; rowIndex?: number }[] { + const filtered = this.applyTopFieldFilters(this.displayedScenarioEvents); + const rows: { type: 'move' | 'group' | 'event'; moveNumber?: number; groupNumber?: number; groupKey?: string; event?: ScenarioEvent; rowIndex?: number }[] = []; + let lastMove = -1; + let lastGroup = ''; + let rowIndex = 0; + for (const event of filtered) { + const nums = this.moveAndGroupNumbers[event.id]; + const moveNum = nums ? +nums[0] : 0; + const groupNum = nums ? +nums[1] : 0; + const groupKey = `${moveNum}-${groupNum}`; + + if (moveNum !== lastMove) { + rows.push({ type: 'move', moveNumber: moveNum }); + lastMove = moveNum; + lastGroup = ''; + } + if (groupKey !== lastGroup) { + rows.push({ type: 'group', moveNumber: moveNum, groupNumber: groupNum, groupKey }); + lastGroup = groupKey; + } + rows.push({ type: 'event', event, rowIndex: ++rowIndex, moveNumber: moveNum, groupNumber: groupNum, groupKey }); + } + return rows; + } + + private applyStatementFilters(stmts: XApiStatement[], sectionKey?: string): XApiStatement[] { + if (sectionKey) { + const sources = this.sectionSourceFilter.get(sectionKey); + if (sources && sources.length > 0) { + stmts = stmts.filter(s => + sources.includes((s.context?.platform || '').toLowerCase()) + ); + } + const verbs = this.sectionVerbFilter.get(sectionKey); + if (verbs && verbs.length > 0) { + stmts = stmts.filter(s => { + const v = s.verb?.display?.['en-US'] || s.verb?.id?.split('/').pop() || ''; + return verbs.includes(v); + }); + } + const teamIds = this.sectionTeamFilter.get(sectionKey); + if (teamIds && teamIds.length > 0) { + const teamNames = new Set(); + for (const id of teamIds) { + const team = this.teamList.find(t => t.id === id); + if (team) { + if (team.shortName) teamNames.add(team.shortName); + if (team.name) teamNames.add(team.name); + } + } + stmts = stmts.filter(s => teamNames.has(s.context?.team?.name || '')); + } + } + return stmts; + } + + private loadAllStatements() { + if (this.statementsLoading || !this.msel.id) return; + this.statementsLoading = true; + this.loadingStatements.add('_all'); + + const baseUrl = this.apiUrl.endsWith('/') ? this.apiUrl : this.apiUrl + '/'; + this.http.get(`${baseUrl}api/xapi/statements`, { params: { mselId: this.msel.id } }) + .subscribe({ + next: (response) => { + const statements = response?.statements || response || []; + this.allMselStatements = Array.isArray(statements) ? statements : []; + this.allMselStatements.sort((a, b) => + (a.timestamp || '').localeCompare(b.timestamp || '') + ); + this.statementsLoaded = true; + this.statementsLoading = false; + this.loadingStatements.delete('_all'); + this.filterToCurrentSession(); + this.distributeStatements(); + this.buildVerbList(); + }, + error: () => { + this.allMselStatements = []; + this.statementsLoaded = true; + this.statementsLoading = false; + this.loadingStatements.delete('_all'); + } + }); + } + + private filterToCurrentSession() { + let launchTimestamp = ''; + for (const stmt of this.allMselStatements) { + const verb = stmt.verb?.id || ''; + const platform = (stmt.context?.platform || '').toLowerCase(); + if (platform === 'blueprint' && verb.endsWith('/launched') && (stmt.timestamp || '') > launchTimestamp) { + launchTimestamp = stmt.timestamp || ''; + } + } + if (launchTimestamp) { + this.allMselStatements = this.allMselStatements.filter(s => + (s.timestamp || '') >= launchTimestamp + ); + } + } + + private distributeStatements() { + this.eventStatements.clear(); + this.moveStatements.clear(); + this.groupStatements.clear(); + for (const event of this.mselScenarioEvents) { + this.eventStatements.set(event.id, []); + } + if (this.mselScenarioEvents.length === 0 || this.allMselStatements.length === 0) return; + + const moveGroupToEvents = new Map(); + const knownMoveNumbers = new Set(); + + for (const event of this.mselScenarioEvents) { + const nums = this.moveAndGroupNumbers[event.id]; + if (!nums) continue; + const moveNum = +nums[0]; + const groupNum = +nums[1]; + knownMoveNumbers.add(moveNum); + + const mgKey = `${moveNum}-${groupNum}`; + if (!moveGroupToEvents.has(mgKey)) moveGroupToEvents.set(mgKey, []); + moveGroupToEvents.get(mgKey).push(event); + } + + const matchedTimeline: { timestamp: string; moveNumber: number }[] = []; + const directRouted = new Set(); + + for (const stmt of this.allMselStatements) { + const grouping = stmt.context?.contextActivities?.grouping || []; + const eventGrouping = grouping.find(g => (g.id || '').includes('scenarioevents/')); + if (eventGrouping) { + const eventId = eventGrouping.id.split('scenarioevents/').pop(); + if (this.eventStatements.has(eventId)) { + this.eventStatements.get(eventId).push(stmt); + directRouted.add(stmt.id); + continue; + } + } + + const moveInfo = this.extractMoveFromStatement(stmt); + + // Resolve move number (explicit, by name, or null) + let moveNum = moveInfo.number; + if (moveNum == null && moveInfo.name) { + const move = this.moveList.find(m => m.description?.toLowerCase() === moveInfo.name); + if (move) moveNum = +move.moveNumber; + } + + if (moveNum != null && knownMoveNumbers.has(moveNum)) { + matchedTimeline.push({ timestamp: stmt.timestamp || '', moveNumber: moveNum }); + + // If we have a group, add to group bucket only (not move) + if (moveInfo.group != null) { + const gk = `${moveNum}-${moveInfo.group}`; + let matchedEvent = false; + + const stmtPlatform = (stmt.context?.platform || '').toLowerCase(); + const stmtObjectName = (stmt.object?.definition?.name?.['en-US'] || '').toLowerCase(); + if (moveGroupToEvents.has(gk)) { + for (const event of moveGroupToEvents.get(gk)) { + const eventTarget = (event.integrationTarget || '').toLowerCase(); + if (eventTarget && stmtPlatform && eventTarget === stmtPlatform) { + const groupEvents = moveGroupToEvents.get(gk).filter(e => + (e.integrationTarget || '').toLowerCase() === stmtPlatform + ); + if (groupEvents.length === 1) { + this.eventStatements.get(event.id).push(stmt); + matchedEvent = true; + } else if (stmtObjectName && this.eventNameContains(event, stmtObjectName)) { + this.eventStatements.get(event.id).push(stmt); + matchedEvent = true; + } + } + } + } + + if (!matchedEvent) { + if (!this.groupStatements.has(gk)) this.groupStatements.set(gk, []); + this.groupStatements.get(gk).push(stmt); + } + } else { + // Move-only (no group) — add to move bucket + if (!this.moveStatements.has(moveNum)) this.moveStatements.set(moveNum, []); + this.moveStatements.get(moveNum).push(stmt); + } + } + } + + // Second pass: statements with no move context — infer move by timestamp + if (matchedTimeline.length > 0) { + matchedTimeline.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); + for (const stmt of this.allMselStatements) { + const moveInfo = this.extractMoveFromStatement(stmt); + let moveNum = moveInfo.number; + if (moveNum == null && moveInfo.name) { + const move = this.moveList.find(m => m.description?.toLowerCase() === moveInfo.name); + if (move) moveNum = +move.moveNumber; + } + if (moveNum != null) continue; // already handled + if (directRouted.has(stmt.id)) continue; + + const ts = stmt.timestamp || ''; + let inferredMove = matchedTimeline[0].moveNumber; + for (let i = matchedTimeline.length - 1; i >= 0; i--) { + if (matchedTimeline[i].timestamp <= ts) { + inferredMove = matchedTimeline[i].moveNumber; + break; + } + } + if (!this.moveStatements.has(inferredMove)) this.moveStatements.set(inferredMove, []); + this.moveStatements.get(inferredMove).push(stmt); + } + } + + for (const bucket of this.eventStatements.values()) { + bucket.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || '')); + } + for (const bucket of this.moveStatements.values()) { + bucket.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || '')); + } + for (const bucket of this.groupStatements.values()) { + bucket.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || '')); + } + + this.syncAssertionsFromStatements(); + } + + private syncAssertionsFromStatements() { + if (this.mselCompetencies.length === 0 || this.proficiencyLevels.length === 0) return; + + for (const stmt of this.allMselStatements) { + const verbId = stmt.verb?.id || ''; + if (verbId !== 'https://w3id.org/xapi/tla/verbs/asserted' && verbId !== 'https://w3id.org/xapi/dod-isd/verbs/asserted') continue; + const objType = stmt.object?.definition?.type || ''; + if (objType !== 'https://w3id.org/xapi/tla/activity-types/competency' && objType !== 'http://adlnet.gov/expapi/activities/competency') continue; + + const grouping = stmt.context?.contextActivities?.grouping || []; + + let contextKey: string | null = null; + const eventGrouping = grouping.find(g => (g.id || '').includes('scenarioevents/')); + if (eventGrouping) { + contextKey = eventGrouping.id.split('scenarioevents/').pop(); + if (!this.eventStatements.has(contextKey)) contextKey = null; + } + if (!contextKey) { + const groupGrouping = grouping.find(g => (g.id || '').includes('/groups/')); + const moveGrouping = grouping.find(g => (g.id || '').includes('/moves/') && !(g.id || '').includes('/groups/')); + if (groupGrouping) { + const parts = groupGrouping.id.match(/moves\/(\d+)\/groups\/(\d+)/); + if (parts) contextKey = `group-${parts[1]}-${parts[2]}`; + } else if (moveGrouping) { + const parts = moveGrouping.id.match(/moves\/(\d+)/); + if (parts) contextKey = `move-${parts[1]}`; + } + } + if (!contextKey) continue; + + const objectId = stmt.object?.id || ''; + const compIdentifier = stmt.object?.definition?.extensions?.['https://w3id.org/xapi/tla/extensions/competency-identifier'] || ''; + const mc = this.mselCompetencies.find(mc => { + const comp = mc.competency; + if (!comp) return false; + if (compIdentifier && comp.idNumber === compIdentifier) return true; + if (comp.idNumber?.startsWith('http') && objectId === comp.idNumber) return true; + if (objectId.includes('competencies/' + comp.id)) return true; + return false; + }); + if (!mc?.competency) continue; + + const scoreRaw = stmt.result?.score?.raw; + const matchedLevel = scoreRaw != null + ? this.proficiencyLevels.find(pl => pl.value === scoreRaw) + : null; + + const teamId = stmt.context?.team?.account?.name || ''; + const assertKey = `${contextKey}:${mc.competency.id}:${teamId}`; + + if (this.submittedAssertions.has(assertKey)) continue; + + const ratingData = { + levelId: matchedLevel?.id || '', + comment: stmt.result?.response || '', + }; + + const keysToPopulate = [contextKey]; + if (!contextKey.startsWith('move-') && !contextKey.startsWith('group-')) { + const nums = this.moveAndGroupNumbers[contextKey]; + if (nums) { + keysToPopulate.push(`move-${+nums[0]}`); + keysToPopulate.push(`group-${+nums[0]}-${+nums[1]}`); + } + } + + for (const ck of keysToPopulate) { + const ratingMapKey = teamId ? `${ck}::${teamId}` : ck; + if (!this.eventRatings.has(ratingMapKey)) this.eventRatings.set(ratingMapKey, new Map()); + const existing = this.eventRatings.get(ratingMapKey).get(mc.competency.id); + if (!existing || !existing.levelId) { + this.eventRatings.get(ratingMapKey).set(mc.competency.id, ratingData); + } + const ak = `${ck}:${mc.competency.id}:${teamId}`; + this.submittedAssertions.add(ak); + } + + if (teamId) { + for (const ck of keysToPopulate) { + this.assertionTeam.set(ck, teamId); + } + } + } + } + + private extractMoveFromStatement(stmt: XApiStatement): { name: string | null; number: number | null; group: number | null } { + const grouping = stmt.context?.contextActivities?.grouping || []; + let moveNumber: number | null = null; + let moveName: string | null = null; + let groupNumber: number | null = null; + + for (const g of grouping) { + const id = g.id || ''; + const name = g.definition?.name?.['en-US'] || ''; + if (id.includes('/group/')) { + const numMatch = id.match(/\/group\/(\d+)$/); + if (numMatch) groupNumber = parseInt(numMatch[1], 10); + } else if (id.includes('/inject/')) { + const numMatch = id.match(/\/inject\/(\d+)$/); + if (numMatch) groupNumber = parseInt(numMatch[1], 10); + } else if (id.includes('/move/')) { + const numMatch = id.match(/\/move\/(\d+)$/); + if (numMatch) { + moveNumber = parseInt(numMatch[1], 10); + moveName = name.toLowerCase() || null; + } else { + const uuidMatch = id.match(/\/move\/([0-9a-f-]{36})$/i); + const move = (uuidMatch && this.moveList.find(m => m.id === uuidMatch[1])) + || this.moveList.find(m => m.description?.toLowerCase() === name.toLowerCase()); + if (move) { + moveNumber = +move.moveNumber; + moveName = (move.description || '').toLowerCase() || null; + } else { + moveName = name.toLowerCase() || null; + } + } + } + } + if (moveNumber != null || moveName != null) { + return { name: moveName, number: moveNumber, group: groupNumber }; + } + + const objectName = stmt.object?.definition?.name?.['en-US'] || ''; + const prefixMatch = objectName.match(/^(\d+)-(\d+)\s/); + if (prefixMatch) { + return { name: null, number: parseInt(prefixMatch[1], 10), group: parseInt(prefixMatch[2], 10) }; + } + return { name: null, number: null, group: null }; + } + + private eventNameContains(event: ScenarioEvent, needle: string): boolean { + for (const df of this.assessorDataFields) { + const dv = this.getDataValue(event, df.name); + if (dv.value && dv.value.toLowerCase().includes(needle)) return true; + } + return false; + } + + getStatements(eventId: string): XApiStatement[] { + return this.eventStatements.get(eventId) || []; + } + + getFilteredStatements(eventId: string): XApiStatement[] { + let stmts = this.getStatements(eventId); + stmts = this.applyStatementFilters(stmts, `event-${eventId}`); + if (this.selectedMoveNumber != null) { + const move = this.moveList.find(m => +m.moveNumber === +this.selectedMoveNumber); + const targetName = move?.description?.toLowerCase(); + const targetNum = +this.selectedMoveNumber; + stmts = stmts.filter(s => { + const info = this.extractMoveFromStatement(s); + return (targetName && info.name === targetName) || (info.number != null && info.number === targetNum); + }); + } + return stmts; + } + + isLoading(eventId: string): boolean { + return this.loadingStatements.has('_all'); + } + + expandedStatementIds = new Set(); + + toggleStatement(stmtId: string) { + if (this.expandedStatementIds.has(stmtId)) { + this.expandedStatementIds.delete(stmtId); + } else { + this.expandedStatementIds.add(stmtId); + } + } + + getMovePart(stmt: XApiStatement): string { + const grouping = stmt.context?.contextActivities?.grouping || []; + for (const g of grouping) { + const id = g.id || ''; + const name = g.definition?.name?.['en-US'] || ''; + if (id.includes('/move/') && !id.includes('/group/') && !id.includes('/inject/')) { + const numMatch = id.match(/\/move\/(\d+)$/); + if (numMatch) return `M${numMatch[1]}`; + const uuidMatch = id.match(/\/move\/([0-9a-f-]{36})$/i); + const move = (uuidMatch && this.moveList.find(m => m.id === uuidMatch[1])) + || this.moveList.find(m => m.description?.toLowerCase() === name.toLowerCase()); + return move ? `M${move.moveNumber}` : name; + } + } + const objectName = stmt.object?.definition?.name?.['en-US'] || ''; + const prefixMatch = objectName.match(/^(\d+)-\d+\s/); + if (prefixMatch) return `M${parseInt(prefixMatch[1], 10)}`; + return ''; + } + + getGroupPart(stmt: XApiStatement): string { + const grouping = stmt.context?.contextActivities?.grouping || []; + for (const g of grouping) { + const id = g.id || ''; + if (id.includes('/group/')) { + const numMatch = id.match(/\/group\/(\d+)$/); + if (numMatch) return `G${numMatch[1]}`; + } else if (id.includes('/inject/')) { + const numMatch = id.match(/\/inject\/(\d+)$/); + if (numMatch) return `G${numMatch[1]}`; + } + } + const objectName = stmt.object?.definition?.name?.['en-US'] || ''; + const prefixMatch = objectName.match(/^\d+-(\d+)\s/); + if (prefixMatch) return `G${parseInt(prefixMatch[1], 10)}`; + return ''; + } + + getObjectType(stmt: XApiStatement): string { + return stmt.object?.definition?.type?.split('/').pop() || ''; + } + + copyStatement(stmt: XApiStatement, event: MouseEvent) { + event.stopPropagation(); + navigator.clipboard.writeText(JSON.stringify(stmt, null, 2)); + } + + getStatementSections(stmt: XApiStatement): { key: string; value: any; preview: string }[] { + if (stmt.id && this.statementSectionsCache.has(stmt.id)) { + return this.statementSectionsCache.get(stmt.id); + } + const sections: { key: string; value: any; preview: string }[] = []; + const keyOrder = ['actor', 'verb', 'object', 'result', 'context', 'timestamp', 'id']; + const raw = stmt as Record; + for (const key of keyOrder) { + if (raw[key] === undefined) continue; + const val = raw[key]; + let preview = ''; + if (typeof val === 'string') { + preview = val; + } else if (key === 'actor') { + preview = val?.name || val?.account?.name || ''; + } else if (key === 'verb') { + preview = val?.display?.['en-US'] || val?.id?.split('/').pop() || ''; + } else if (key === 'object') { + preview = val?.definition?.name?.['en-US'] || val?.id || ''; + } else if (key === 'result') { + const parts: string[] = []; + if (val?.score?.raw != null) parts.push(`score: ${val.score.raw}`); + if (val?.response) parts.push(`"${val.response.substring(0, 40)}${val.response.length > 40 ? '...' : ''}"`); + if (val?.completion != null) parts.push(val.completion ? 'complete' : 'incomplete'); + preview = parts.join(', '); + } else if (key === 'context') { + const parts: string[] = []; + if (val?.platform) parts.push(val.platform); + if (val?.team?.name) parts.push(val.team.name); + preview = parts.join(', '); + } + sections.push({ key, value: val, preview }); + } + for (const key of Object.keys(raw)) { + if (!keyOrder.includes(key)) { + sections.push({ key, value: raw[key], preview: '' }); + } + } + if (stmt.id) { + this.statementSectionsCache.set(stmt.id, sections); + } + return sections; + } + + expandedJsonSections = new Map>(); + + isJsonSectionExpanded(stmtId: string, key: string): boolean { + return this.expandedJsonSections.get(stmtId)?.has(key) || false; + } + + toggleJsonSection(stmtId: string, key: string) { + if (!this.expandedJsonSections.has(stmtId)) this.expandedJsonSections.set(stmtId, new Set()); + const set = this.expandedJsonSections.get(stmtId); + if (set.has(key)) set.delete(key); else set.add(key); + } + + formatJson(value: any): string { + return JSON.stringify(value, null, 2); + } + + formatTimestamp(timestamp: string): string { + if (!timestamp) return ''; + const d = new Date(timestamp); + const day = String(d.getDate()).padStart(2, '0'); + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const month = months[d.getMonth()]; + const year = d.getFullYear(); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + return `${day} ${month} ${year} ${hours}:${minutes} ${this.getTimezoneAbbr()}`; + } + + private getTimezoneAbbr(): string { + try { + const date = new Date(); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; + const formatted = date.toLocaleTimeString('en-US', { timeZoneName: 'short', timeZone }); + const parts = formatted.split(' '); + return parts[parts.length - 1] || 'UTC'; + } catch { + return 'UTC'; + } + } + + // --- Competency assessment methods --- + + private loadProficiencyScale() { + if (this.mselCompetencies.length === 0 || this.proficiencyScale) return; + const firstComp = this.mselCompetencies[0]?.competency; + if (!firstComp?.competencyFrameworkId) return; + + this.competencyFrameworkService.getCompetencyFramework(firstComp.competencyFrameworkId) + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(fw => { + if (fw?.defaultProficiencyScaleId) { + this.proficiencyScaleService.getProficiencyScale(fw.defaultProficiencyScaleId) + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(scale => { + this.proficiencyScale = scale; + this.proficiencyLevels = (scale?.proficiencyLevels || []) + .sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)); + }); + } + }); + } + + private loadCompetencyFrameworks() { + // Get unique framework IDs from competencies + const frameworkIds = new Set(); + for (const mc of this.mselCompetencies) { + if (mc.competency?.competencyFrameworkId) { + frameworkIds.add(mc.competency.competencyFrameworkId); + } + } + + // Load frameworks that aren't already cached + for (const fwId of frameworkIds) { + if (!this.competencyFrameworkCache.has(fwId)) { + this.competencyFrameworkService.getCompetencyFramework(fwId) + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(fw => { + if (fw) { + this.competencyFrameworkCache.set(fwId, fw); + } + }); + } + } + } + + private allDataFields: DataField[] = []; + private competencyFieldIds: string[] = []; + private static EMPTY_COMPETENCIES: Competency[] = []; + + private rebuildCompetencyCaches() { + this.baseEventCompetencies.clear(); + this.baseMoveCompetencies.clear(); + this.baseGroupCompetencies.clear(); + + if (this.competencyFieldIds.length === 0 || this.mselCompetencies.length === 0) return; + + const idNumberToComp = new Map(); + for (const mc of this.mselCompetencies) { + if (mc.competency?.idNumber) { + idNumberToComp.set(mc.competency.idNumber, mc.competency); + } + } + + for (const event of this.mselScenarioEvents) { + const comps: Competency[] = []; + const seen = new Set(); + for (const fieldId of this.competencyFieldIds) { + const dv = this.dataValues.find(v => + v.scenarioEventId === event.id && v.dataFieldId === fieldId + ); + if (dv?.value) { + for (const raw of dv.value.split(',')) { + const idNum = raw.trim(); + if (idNum && !seen.has(idNum)) { + seen.add(idNum); + const comp = idNumberToComp.get(idNum); + if (comp) comps.push(comp); + } + } + } + } + if (comps.length > 0) { + this.baseEventCompetencies.set(event.id, comps); + } + } + + const moveComps = new Map>(); + const groupComps = new Map>(); + + for (const event of this.mselScenarioEvents) { + const eventComps = this.baseEventCompetencies.get(event.id); + if (!eventComps) continue; + const nums = this.moveAndGroupNumbers[event.id]; + if (!nums) continue; + const moveNum = +nums[0]; + const groupKey = `${moveNum}-${+nums[1]}`; + + if (!moveComps.has(moveNum)) moveComps.set(moveNum, new Map()); + if (!groupComps.has(groupKey)) groupComps.set(groupKey, new Map()); + + for (const comp of eventComps) { + moveComps.get(moveNum).set(comp.id, comp); + groupComps.get(groupKey).set(comp.id, comp); + } + } + + for (const [moveNum, compMap] of moveComps) { + this.baseMoveCompetencies.set(moveNum, Array.from(compMap.values())); + } + for (const [groupKey, compMap] of groupComps) { + this.baseGroupCompetencies.set(groupKey, Array.from(compMap.values())); + } + } + + private rebuildTeamCompIdSets() { + this.teamCompIdSets.clear(); + for (const tc of this.teamCompetencies) { + if (!this.teamCompIdSets.has(tc.teamId)) this.teamCompIdSets.set(tc.teamId, new Set()); + this.teamCompIdSets.get(tc.teamId).add(tc.competencyId); + } + } + + private filterByTeam(comps: Competency[], contextKey: string): Competency[] { + const teamId = this.getAssertionTeam(contextKey); + if (!teamId || this.teamCompIdSets.size === 0) return comps; + const compIds = this.teamCompIdSets.get(teamId); + if (!compIds) return comps; + const filtered = comps.filter(c => compIds.has(c.id)); + return filtered.length > 0 ? filtered : comps; + } + + getEventCompetencies(eventId: string): Competency[] { + const base = this.baseEventCompetencies.get(eventId); + if (!base) return AssessorViewComponent.EMPTY_COMPETENCIES; + return this.filterByTeam(base, eventId); + } + + getMoveCompetencies(moveNumber: number): Competency[] { + const base = this.baseMoveCompetencies.get(moveNumber); + if (!base) return AssessorViewComponent.EMPTY_COMPETENCIES; + return this.filterByTeam(base, 'move-' + moveNumber); + } + + getGroupCompetencies(groupKey: string): Competency[] { + const base = this.baseGroupCompetencies.get(groupKey); + if (!base) return AssessorViewComponent.EMPTY_COMPETENCIES; + return this.filterByTeam(base, 'group-' + groupKey); + } + + getCompetencyTooltip(comp: Competency): string { + return comp?.shortName || ''; + } + + private getCompetencyType(comp: Competency): string { + if (!comp || !comp.id) return ''; + + // Check cache first + if (this.competencyTypeCache.has(comp.id)) { + return this.competencyTypeCache.get(comp.id) || ''; + } + + // Derive from ID pattern + const idNumber = comp.idNumber || ''; + let type = ''; + + if (idNumber.includes('WRL')) { + type = 'Work Role'; + } else if (/^[TKSA][\d-]/.test(idNumber)) { + const prefixMap: Record = { + 'T': 'Task', 'K': 'Knowledge', 'S': 'Skill', 'A': 'Ability', + }; + type = prefixMap[idNumber.charAt(0)] || ''; + } else if (/^[A-Z]{2}-[A-Z]{3}-\d+$/.test(idNumber)) { + type = 'Work Role'; + } else if (/^[A-Z]{3}$/.test(idNumber)) { + type = 'Specialty Area'; + } else if (/^[A-Z]{2}$/.test(idNumber)) { + type = 'Category'; + } + + this.competencyTypeCache.set(comp.id, type); + return type; + } + + private ratingKey(contextKey: string): string { + const teamId = this.getAssertionTeam(contextKey); + return teamId ? `${contextKey}::${teamId}` : contextKey; + } + + getRating(contextKey: string, competencyId: string): { levelId: string; comment: string } { + const key = this.ratingKey(contextKey); + const map = this.eventRatings.get(key); + return map?.get(competencyId) ?? { levelId: '', comment: '' }; + } + + setRatingLevel(contextKey: string, competencyId: string, levelId: string) { + const key = this.ratingKey(contextKey); + if (!this.eventRatings.has(key)) this.eventRatings.set(key, new Map()); + const existing = this.getRating(contextKey, competencyId); + this.eventRatings.get(key).set(competencyId, { ...existing, levelId }); + } + + setRatingComment(contextKey: string, competencyId: string, comment: string) { + const key = this.ratingKey(contextKey); + if (!this.eventRatings.has(key)) this.eventRatings.set(key, new Map()); + const existing = this.getRating(contextKey, competencyId); + this.eventRatings.get(key).set(competencyId, { ...existing, comment }); + } + + getAssertionTeam(eventId: string): string { + if (!this.assertionTeam.has(eventId)) { + const teamIds = this.getSectionTeams('event-' + eventId); + if (teamIds.length === 1) { + this.assertionTeam.set(eventId, teamIds[0]); + } + } + return this.assertionTeam.get(eventId) ?? ''; + } + + setAssertionTeam(eventId: string, teamId: string) { + this.assertionTeam.set(eventId, teamId); + } + + getTeamsForContext(contextKey: string): Team[] { + // Get competencies for this context + let competencies: Competency[] = []; + if (contextKey.startsWith('move-')) { + const moveNumber = parseInt(contextKey.replace('move-', ''), 10); + competencies = this.baseMoveCompetencies.get(moveNumber) || []; + } else if (contextKey.startsWith('group-')) { + const groupKey = contextKey.replace('group-', ''); + competencies = this.baseGroupCompetencies.get(groupKey) || []; + } else { + competencies = this.baseEventCompetencies.get(contextKey) || []; + } + + // If no team assignments exist or no competencies in context, return empty + if (this.teamCompIdSets.size === 0 || competencies.length === 0) { + return []; + } + + // Get competency IDs in this context + const contextCompIds = new Set(competencies.map(c => c.id)); + + // Filter teams to only those with assignments to these competencies + const filteredTeams = this.teamList.filter(team => { + const teamCompIds = this.teamCompIdSets.get(team.id); + if (!teamCompIds) return false; + // Team has assignment if it has at least one competency from this context + for (const compId of contextCompIds) { + if (teamCompIds.has(compId)) return true; + } + return false; + }); + + // Return filtered list (empty if no teams have assignments) + return filteredTeams; + } + + assertionKey(eventId: string, competencyId: string): string { + const teamId = this.getAssertionTeam(eventId); + return `${eventId}:${competencyId}:${teamId}`; + } + + canSubmitRating(eventId: string, competencyId: string): boolean { + const rating = this.getRating(eventId, competencyId); + const teamId = this.getAssertionTeam(eventId); + return !!rating.levelId && !!teamId && !this.submittingAssertions.has(this.assertionKey(eventId, competencyId)); + } + + isRatingSubmitted(eventId: string, competencyId: string): boolean { + return this.submittedAssertions.has(this.assertionKey(eventId, competencyId)); + } + + submitRating(contextKey: string, competencyId: string) { + const rating = this.getRating(contextKey, competencyId); + if (!rating.levelId) return; + const teamId = this.getAssertionTeam(contextKey); + if (!teamId) { + alert('Please select a team before submitting a competency assertion.'); + return; + } + const key = this.assertionKey(contextKey, competencyId); + this.submittingAssertions.add(key); + + const baseUrl = this.apiUrl.endsWith('/') ? this.apiUrl : this.apiUrl + '/'; + const body: any = { + mselId: this.msel.id, + competencyId: competencyId, + teamId: teamId || null, + proficiencyLevelId: rating.levelId, + comment: rating.comment || null, + }; + + if (contextKey.startsWith('move-')) { + body.moveNumber = parseInt(contextKey.replace('move-', ''), 10); + } else if (contextKey.startsWith('group-')) { + const parts = contextKey.replace('group-', '').split('-'); + body.moveNumber = parseInt(parts[0], 10); + body.groupNumber = parseInt(parts[1], 10); + } else { + body.scenarioEventId = contextKey; + } + + this.http.post(`${baseUrl}api/xapi/assertions`, body).subscribe({ + next: () => { + this.submittingAssertions.delete(key); + this.submittedAssertions.add(key); + }, + error: () => { + this.submittingAssertions.delete(key); + } + }); + } + + submitAllRatings(contextKey: string) { + const comps = this.getContextCompetencies(contextKey); + for (const comp of comps) { + if (this.canSubmitRating(contextKey, comp.id)) { + this.submitRating(contextKey, comp.id); + } + } + } + + private getContextCompetencies(contextKey: string): Competency[] { + if (contextKey.startsWith('move-')) { + return this.getMoveCompetencies(parseInt(contextKey.replace('move-', ''), 10)); + } else if (contextKey.startsWith('group-')) { + return this.getGroupCompetencies(contextKey.replace('group-', '')); + } + return this.getEventCompetencies(contextKey); + } + + hasAnyRatings(contextKey: string): boolean { + const key = this.ratingKey(contextKey); + const map = this.eventRatings.get(key); + if (!map) return false; + for (const r of map.values()) { + if (r.levelId) return true; + } + return false; + } + + hasAnySubmittableRatings(contextKey: string): boolean { + const comps = this.getContextCompetencies(contextKey); + const teamId = this.getAssertionTeam(contextKey); + if (!teamId) return false; + for (const comp of comps) { + if (this.canSubmitRating(contextKey, comp.id)) { + return true; + } + } + return false; + } + + getLevelName(levelId: string): string { + const level = this.proficiencyLevels.find(l => l.id === levelId); + return level ? `${level.name} (${level.value})` : ''; + } + + ngOnDestroy() { + this.unsubscribe$.next(null); + this.unsubscribe$.complete(); + } +} diff --git a/src/app/components/competency-options-dialog/competency-options-dialog.component.html b/src/app/components/competency-options-dialog/competency-options-dialog.component.html new file mode 100644 index 00000000..ee941409 --- /dev/null +++ b/src/app/components/competency-options-dialog/competency-options-dialog.component.html @@ -0,0 +1,114 @@ + + +
+ Manage Competencies ({{ selectedCount }} selected) + +
+ + +
+ @if (frameworkNames.length > 1) { + + Framework + + All Frameworks + @for (fw of frameworkNames; track fw) { + {{ fw }} + } + + + } + @if (competencyTypes.length > 1) { + + Type + + All Types + @for (t of competencyTypes; track t) { + {{ t }} + } + + + } + + Search + + @if (searchText) { + + } + +
+ +@if (data.canEdit) { +
+ {{ selectedCount }} of {{ mselCompetencies.length }} selected{{ frameworkFilter || typeFilter || searchText ? ' (' + filteredCompetencies.length + ' shown)' : '' }} +
+} + + +
+ @if (mselCompetencies.length === 0) { +
No competencies in this MSEL's pool. Add competencies from the Competencies tab first.
+ } @else { + + + + @if (data.canEdit) { + + } + + + + + + + + + @for (mc of paginatedCompetencies; track mc.competency?.idNumber) { + + @if (data.canEdit) { + + } + + + + + + + } + +
+ + + IDFrameworkTypeNameDescription
+ + + {{ mc.competency?.idNumber }}{{ getFramework(mc) }}{{ getType(mc) }}{{ mc.competency?.shortName }}{{ mc.competency?.description }}
+ } +
+ + + +
+
+ +
diff --git a/src/app/components/competency-options-dialog/competency-options-dialog.component.scss b/src/app/components/competency-options-dialog/competency-options-dialog.component.scss new file mode 100644 index 00000000..06a4be66 --- /dev/null +++ b/src/app/components/competency-options-dialog/competency-options-dialog.component.scss @@ -0,0 +1,111 @@ +// Copyright 2025 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.close-button { + float: right; + margin-top: -8px; +} + +.full-width { + width: 100%; +} + +.framework-section { + padding: 0 24px; +} + +.filter-section { + display: flex; + gap: 8px; + padding: 0 24px; +} + +.search-field { + flex: 1; +} + +.type-filter-field { + width: 180px; +} + +.action-buttons { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 24px; + margin-bottom: 8px; +} + +.selection-count { + margin-left: 16px; + font-size: 13px; + color: var(--mat-sys-on-surface-variant); +} + +.table-content { + flex: 1; + overflow-y: auto; + padding: 0; +} + +.loading-message { + padding: 24px; + text-align: center; + color: var(--mat-sys-on-surface-variant); +} + +.element-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th, td { + text-align: left; + padding: 4px 8px; + border-bottom: 1px solid var(--mat-sys-outline-variant); + } + + th { + font-weight: 500; + position: sticky; + top: 0; + background: var(--mat-sys-surface); + z-index: 1; + } + + .check-column { + width: 40px; + padding: 4px; + } + + .id-column { + font-family: monospace; + font-weight: 500; + white-space: nowrap; + } + + .type-column { + white-space: nowrap; + font-size: 12px; + } +} + +.description-column { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.clickable-row { + cursor: pointer; + + &:hover { + background: var(--mat-sys-surface-variant); + } +} + +.selected-row { + background: color-mix(in srgb, var(--mat-sys-primary) 10%, transparent); +} diff --git a/src/app/components/competency-options-dialog/competency-options-dialog.component.ts b/src/app/components/competency-options-dialog/competency-options-dialog.component.ts new file mode 100644 index 00000000..b3188b58 --- /dev/null +++ b/src/app/components/competency-options-dialog/competency-options-dialog.component.ts @@ -0,0 +1,243 @@ +// Copyright 2025 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, Inject, OnDestroy } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { + CompetencyFramework, + DataOption, + MselCompetency, +} from 'src/app/generated/blueprint.api'; +import { MselCompetencyQuery } from 'src/app/data/msel-competency/msel-competency.query'; +import { CompetencyFrameworkService } from 'src/app/generated/blueprint.api/api/api'; +import { v4 as uuidv4 } from 'uuid'; + +@Component({ + selector: 'app-competency-options-dialog', + templateUrl: './competency-options-dialog.component.html', + styleUrls: ['./competency-options-dialog.component.scss'], + standalone: false +}) +export class CompetencyOptionsDialogComponent implements OnDestroy { + searchText = ''; + typeFilter = ''; + frameworkFilter = ''; + pageIndex = 0; + pageSize = 50; + mselCompetencies: MselCompetency[] = []; + competencyTypes: string[] = []; + private competencyTypeMap = new Map(); + private competencyFrameworkMap = new Map(); + frameworks: CompetencyFramework[] = []; + frameworkNames: string[] = []; + selected = new Set(); // tracks by idNumber + private unsubscribe$ = new Subject(); + + constructor( + public dialogRef: MatDialogRef, + private mselCompetencyQuery: MselCompetencyQuery, + private competencyFrameworkService: CompetencyFrameworkService, + @Inject(MAT_DIALOG_DATA) public data: { + dataFieldId: string; + dataOptions: DataOption[]; + canEdit: boolean; + } + ) { + dialogRef.disableClose = true; + // Load frameworks + this.competencyFrameworkService.getCompetencyFrameworks().subscribe(frameworks => { + this.frameworks = frameworks; + this.buildFrameworkMap(); + }); + // Subscribe to MSEL competency pool + this.mselCompetencyQuery.selectAll().pipe( + takeUntil(this.unsubscribe$) + ).subscribe(mselCompetencies => { + this.mselCompetencies = mselCompetencies; + this.buildTypeMap(); + this.buildFrameworkMap(); + }); + // Initialize selected set — only include options still in the pool + const poolIdNumbers = new Set( + this.mselCompetencies.map(mc => mc.competency?.idNumber).filter(n => n) + ); + for (const opt of this.data.dataOptions) { + if (poolIdNumbers.has(opt.optionName)) { + this.selected.add(opt.optionName); + } + } + } + + ngOnDestroy() { + this.unsubscribe$.next(null); + this.unsubscribe$.complete(); + } + + private buildTypeMap() { + this.competencyTypeMap.clear(); + for (const mc of this.mselCompetencies) { + const c = mc.competency; + if (!c) continue; + const t = this.deriveTypeFromId(c.idNumber); + this.competencyTypeMap.set(c.id, t); + } + this.competencyTypes = [...new Set(this.competencyTypeMap.values())] + .filter(t => t !== 'Other') + .sort(); + } + + private buildFrameworkMap() { + this.competencyFrameworkMap.clear(); + const frameworkMap = new Map(this.frameworks.map(f => [f.id, f.name])); + const frameworkNamesSet = new Set(); + for (const mc of this.mselCompetencies) { + const c = mc.competency; + if (!c || !c.competencyFrameworkId) continue; + const frameworkName = frameworkMap.get(c.competencyFrameworkId) || 'Unknown'; + this.competencyFrameworkMap.set(c.id, frameworkName); + frameworkNamesSet.add(frameworkName); + } + this.frameworkNames = [...frameworkNamesSet].sort(); + } + + private deriveTypeFromId(idNumber: string): string { + if (!idNumber) return 'Other'; + if (idNumber.includes('WRL')) return 'Work Role'; + if (/^[TKSA][\d-]/.test(idNumber)) { + const prefixMap: Record = { + 'T': 'Task', 'K': 'Knowledge', 'S': 'Skill', 'A': 'Ability', + }; + return prefixMap[idNumber.charAt(0)] || 'Other'; + } + if (/^[A-Z]{2}-[A-Z]{3}-\d+$/.test(idNumber)) return 'Work Role'; + if (/^[A-Z]{3}$/.test(idNumber)) return 'Specialty Area'; + if (/^[A-Z]{2}$/.test(idNumber)) return 'Category'; + return 'Other'; + } + + get filteredCompetencies(): MselCompetency[] { + let results = this.mselCompetencies; + if (this.frameworkFilter) { + results = results.filter(mc => this.competencyFrameworkMap.get(mc.competencyId) === this.frameworkFilter); + } + if (this.typeFilter) { + results = results.filter(mc => this.competencyTypeMap.get(mc.competencyId) === this.typeFilter); + } + if (this.searchText) { + const s = this.searchText.toLowerCase(); + results = results.filter(mc => + mc.competency?.idNumber?.toLowerCase().includes(s) || + mc.competency?.shortName?.toLowerCase().includes(s) || + mc.competency?.description?.toLowerCase().includes(s) + ); + } + return results; + } + + get paginatedCompetencies(): MselCompetency[] { + const start = this.pageIndex * this.pageSize; + return this.filteredCompetencies.slice(start, start + this.pageSize); + } + + onPageChanged(event: any): void { + this.pageIndex = event.pageIndex; + this.pageSize = event.pageSize; + } + + onFilterChanged(): void { + this.pageIndex = 0; + } + + getType(mc: MselCompetency): string { + return this.competencyTypeMap.get(mc.competencyId) || 'Other'; + } + + getFramework(mc: MselCompetency): string { + return this.competencyFrameworkMap.get(mc.competencyId) || ''; + } + + get selectedCount(): number { + return this.selected.size; + } + + get allFilteredSelected(): boolean { + const filtered = this.filteredCompetencies; + return filtered.length > 0 && filtered.every(mc => this.selected.has(mc.competency?.idNumber)); + } + + get someFilteredSelected(): boolean { + const filtered = this.filteredCompetencies; + const some = filtered.some(mc => this.selected.has(mc.competency?.idNumber)); + const all = filtered.every(mc => this.selected.has(mc.competency?.idNumber)); + return some && !all; + } + + isSelected(mc: MselCompetency): boolean { + return this.selected.has(mc.competency?.idNumber); + } + + toggleAll(): void { + if (this.allFilteredSelected) { + for (const mc of this.filteredCompetencies) { + this.selected.delete(mc.competency?.idNumber); + } + } else { + for (const mc of this.filteredCompetencies) { + if (mc.competency?.idNumber) { + this.selected.add(mc.competency.idNumber); + } + } + } + } + + toggleCompetency(mc: MselCompetency): void { + const idNumber = mc.competency?.idNumber; + if (!idNumber) return; + if (this.selected.has(idNumber)) { + this.selected.delete(idNumber); + } else { + this.selected.add(idNumber); + } + } + + handleClose() { + const competencyMap = new Map(); + for (const mc of this.mselCompetencies) { + if (mc.competency?.idNumber) { + competencyMap.set(mc.competency.idNumber, mc); + } + } + const existingMap = new Map(); + for (const opt of this.data.dataOptions) { + existingMap.set(opt.optionName, opt); + } + const newOptions: DataOption[] = []; + let order = 1; + for (const idNumber of this.selected) { + // Skip stale options for competencies no longer in the pool + if (!competencyMap.has(idNumber)) continue; + const existing = existingMap.get(idNumber); + if (existing) { + const mc = competencyMap.get(idNumber); + newOptions.push({ ...existing, displayOrder: order++, competencyId: mc?.competency?.id || (existing as any).competencyId } as any); + } else { + const mc = competencyMap.get(idNumber); + if (mc?.competency) { + newOptions.push({ + id: uuidv4(), + dataFieldId: this.data.dataFieldId, + optionName: mc.competency.idNumber, + optionValue: mc.competency.shortName, + optionDescription: mc.competency.description, + displayOrder: order++, + competencyId: mc.competency.id, + } as any); + } + } + } + this.dialogRef.close(newOptions); + } +} diff --git a/src/app/components/data-field-edit-dialog/data-field-edit-dialog.component.html b/src/app/components/data-field-edit-dialog/data-field-edit-dialog.component.html index deda0ee6..589f9f4b 100644 --- a/src/app/components/data-field-edit-dialog/data-field-edit-dialog.component.html +++ b/src/app/components/data-field-edit-dialog/data-field-edit-dialog.component.html @@ -48,49 +48,29 @@
+ [disabled]="(!data.canEdit && !data.isOwner) || optionListNotAllowed() || isCompetency()" class="bottom-margin"> Use Option List + @if (data.dataField.isChosenFromList && !optionListNotAllowed()) { + + @if (isCompetency()) { + {{ data.dataField.dataOptions.length > 0 ? data.dataField.dataOptions.length + ' competencies' : 'Manage' }} + } @else { + {{ data.dataField.dataOptions.length }} options + } + + }
@if (data.dataField.isChosenFromList) {
+ [disabled]="(!data.canEdit && !data.isOwner) || optionListNotAllowed() || isCompetency()" class="bottom-margin left-margin"> Multi-select
}
- @if (data.dataField.isChosenFromList) { -
- @for (dataOption of data.dataField.dataOptions; track dataOption) { -
- @if ((data.canEdit || data.isOwner) && !optionListNotAllowed()) { -
- - -
- } -
-  {{ dataOption.displayOrder }}    {{dataOption.optionName }}    - {{dataOption.optionValue }} -
-
- } - @if ((data.canEdit || data.isOwner) && !optionListNotAllowed()) { -
- -
- } -
- }
@if (!data.dataField.isTemplate) { @@ -132,6 +112,14 @@ tab
+
+
+ Assessor View
+
+
+ } + @if (!data.dataField.isTemplate) {
0 && + const base = this.data.dataField.name && this.data.dataField.name.length > 0 && this.data.dataField.displayOrder > 0; + if (this.isCompetency()) { + return base && this.data.dataField.dataOptions && this.data.dataField.dataOptions.length > 0; + } + return base; } changeDataFieldDataType(selectedDataType: string) { @@ -62,6 +69,12 @@ export class DataFieldEditDialogComponent { if (this.data.dataField.dataType !== selectedDataType) { // set the new value this.data.dataField.dataType = selectedDataType; + // Competency fields are always option-list + multi-select + if (selectedDataType === DataFieldType.Competency) { + this.data.dataField.isChosenFromList = true; + this.data.dataField.isMultiSelect = true; + this.data.dataField.isFacilitationField = true; + } } } @@ -110,16 +123,97 @@ export class DataFieldEditDialogComponent { this.data.dataField.dataOptions.splice(index, 1); } + importDataOptions(dataField: DataField) { + // If field is not saved yet, save it first + if (!dataField.id) { + if (!this.errorFree()) { + return; // Can't save if there are validation errors + } + // Save via callback if provided, otherwise emit event + if (this.data.onSave) { + const savedId = this.data.onSave(this.data.dataField); + if (savedId) { + this.data.dataField.id = savedId; + // Now open import dialog with the new ID + this.openImportDialog(this.data.dataField); + } + } + return; + } + + this.openImportDialog(dataField); + } + + private openImportDialog(dataField: DataField) { + const dialogRef = this.dialog.open(DataOptionImportDialogComponent, { + width: '800px', + maxWidth: '90vw', + data: { + dataFieldId: dataField.id, + existingOptions: this.data.dataField.dataOptions + }, + }); + dialogRef.afterClosed().subscribe((result) => { + if (result && Array.isArray(result)) { + // Add imported options to the displayed list + this.data.dataField.dataOptions.push(...result); + } + }); + } + + viewAllOptions() { + const canEdit = this.data.canEdit || this.data.isOwner; + const canAddOptions = canEdit && !this.optionListNotAllowed(); + + if (this.data.dataField.dataType === DataFieldType.Competency) { + const compDialogRef = this.dialog.open(CompetencyOptionsDialogComponent, { + width: '900px', + maxWidth: '95vw', + maxHeight: '90vh', + data: { + dataFieldId: this.data.dataField.id, + dataOptions: this.data.dataField.dataOptions, + canEdit: canAddOptions + } + }); + compDialogRef.afterClosed().subscribe((updatedOptions) => { + if (updatedOptions) { + this.data.dataField = { ...this.data.dataField, dataOptions: updatedOptions }; + } + }); + } else { + this.dialog.open(DataOptionListDialogComponent, { + width: '900px', + maxWidth: '95vw', + maxHeight: '90vh', + data: { + dataOptions: this.data.dataField.dataOptions, + canEdit: canAddOptions, + canImport: canAddOptions, + onEdit: (option: DataOption) => this.editDataOption(option), + onDelete: (option: DataOption) => this.deleteDataOption(option), + onAdd: () => this.addDataOption(this.data.dataField), + onImport: () => this.importDataOptions(this.data.dataField) + } + }); + } + } + sortedDataFieldOptions() { return this.data.dataField.dataOptions .sort((a, b) => +a.displayOrder < +b.displayOrder ? -1 : 1); } + isCompetency(): boolean { + return this.data.dataField.dataType === DataFieldType.Competency; + } + optionListNotAllowed(): boolean { return !( this.data.dataField.dataType === DataFieldType.Double || this.data.dataField.dataType === DataFieldType.Integer || - this.data.dataField.dataType === DataFieldType.String + this.data.dataField.dataType === DataFieldType.String || + this.data.dataField.dataType === DataFieldType.Competency ); } diff --git a/src/app/components/data-field-list/data-field-list.component.html b/src/app/components/data-field-list/data-field-list.component.html index fea08a34..b04947d7 100755 --- a/src/app/components/data-field-list/data-field-list.component.html +++ b/src/app/components/data-field-list/data-field-list.component.html @@ -76,9 +76,10 @@ +
{{ !showTemplates && element.displayOrder > 0 ? element.displayOrder : ' '}}
@if (element.displayOrder > 0) { - + @if (canEdit()) { diff --git a/src/app/components/data-option-import-dialog/data-option-import-dialog.component.html b/src/app/components/data-option-import-dialog/data-option-import-dialog.component.html new file mode 100644 index 00000000..59070204 --- /dev/null +++ b/src/app/components/data-option-import-dialog/data-option-import-dialog.component.html @@ -0,0 +1,114 @@ + + +
+ Import Options + +
+ +
+ +
+

+ {{ data.instructions || 'Upload a file containing options. Supported formats: JSON, CSV, or XLSX. The file should have columns for ID and Name.' }} +

+ + + @if (fileName) { + {{ fileName }} + } +
+ + + @if (parseError) { + + + {{ parseError }} + + } + + + @if (isProcessing) { +
+ + Processing... +
+ } + + + @if (previewItems.length > 0 && !isProcessing) { +
+

Preview ({{ getImportCount() }} to import, {{ getSkipCount() }} to skip)

+
+ + + + + + + @if (hasDescription) { + + } + + + + @for (item of previewItems; track item.optionName) { + + + + + @if (hasDescription) { + + } + + } + +
+ + IDNameDescription
+ + {{ item.optionName }}{{ item.optionValue }}{{ item.optionDescription }}
+
+
+ } +
+ +
+ + +
diff --git a/src/app/components/data-option-import-dialog/data-option-import-dialog.component.scss b/src/app/components/data-option-import-dialog/data-option-import-dialog.component.scss new file mode 100644 index 00000000..dcfbd6d4 --- /dev/null +++ b/src/app/components/data-option-import-dialog/data-option-import-dialog.component.scss @@ -0,0 +1,125 @@ +// Copyright 2025 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.upload-section { + margin-bottom: 20px; +} + +.instructions { + margin-bottom: 15px; + color: var(--mat-sys-on-surface-variant); + font-size: 14px; +} + +.error-message { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + margin: 16px 0; + background-color: var(--mat-sys-error-container); + color: var(--mat-sys-on-error-container); + border-radius: 4px; +} + +.processing { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + justify-content: center; +} + +.preview-section { + margin-top: 20px; + + h3 { + margin-bottom: 12px; + font-size: 16px; + font-weight: 500; + } +} + +.preview-table-container { + max-height: 400px; + overflow-y: auto; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 4px; +} + +.preview-table { + width: 100%; + border-collapse: collapse; + + thead { + position: sticky; + top: 0; + background-color: var(--mat-sys-surface-variant); + z-index: 1; + + th { + padding: 12px; + text-align: left; + font-weight: 500; + border-bottom: 2px solid var(--mat-sys-outline); + + &.checkbox-column { + width: 60px; + text-align: center; + } + } + } + + tbody { + tr { + border-bottom: 1px solid var(--mat-sys-outline-variant); + + &.exists { + opacity: 0.5; + background-color: var(--mat-sys-surface-variant); + } + + &:hover { + background-color: var(--mat-sys-surface-container-highest); + } + + td { + padding: 12px; + + &.checkbox-column { + width: 60px; + text-align: center; + } + + &:nth-child(2) { + font-family: monospace; + font-weight: 500; + width: 120px; + } + + &:nth-child(3) { + width: auto; + } + } + } + } +} + +.description-column { + max-width: 400px; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; +} + +.file-name { + margin-left: 12px; + font-size: 14px; + color: var(--mat-sys-on-surface-variant); +} + +.close-button { + float: right; + margin-top: -8px; +} diff --git a/src/app/components/data-option-import-dialog/data-option-import-dialog.component.ts b/src/app/components/data-option-import-dialog/data-option-import-dialog.component.ts new file mode 100644 index 00000000..4c217a6d --- /dev/null +++ b/src/app/components/data-option-import-dialog/data-option-import-dialog.component.ts @@ -0,0 +1,137 @@ +// Copyright 2025 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { DataOption, DataOptionService } from 'src/app/generated/blueprint.api'; +import { v4 as uuidv4 } from 'uuid'; +import { take } from 'rxjs/operators'; + +export interface ImportPreviewItem { + optionName: string; + optionValue: string; + optionDescription: string; + willImport: boolean; + exists: boolean; +} + +@Component({ + selector: 'app-data-option-import-dialog', + templateUrl: './data-option-import-dialog.component.html', + styleUrls: ['./data-option-import-dialog.component.scss'], + standalone: false +}) +export class DataOptionImportDialogComponent { + previewItems: ImportPreviewItem[] = []; + parseError: string = ''; + isProcessing = false; + fileName = ''; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { + dataFieldId: string; + existingOptions: DataOption[]; + instructions?: string; + showDescription?: boolean; + }, + private dataOptionService: DataOptionService + ) { + dialogRef.disableClose = true; + } + + get hasDescription(): boolean { + return this.data.showDescription === true; + } + + onFileSelected(event: any) { + const file = event.target.files[0]; + if (file) { + if (!this.data.dataFieldId) { + this.parseError = 'Data field ID is missing'; + return; + } + + this.isProcessing = true; + this.parseError = ''; + this.previewItems = []; + this.fileName = file.name; + + // Upload file to API for server-side parsing + this.dataOptionService.previewDataOptionImport(this.data.dataFieldId, file) + .pipe(take(1)) + .subscribe({ + next: (preview) => { + if (preview.error) { + this.parseError = preview.error; + } else { + this.previewItems = preview.items.map(item => ({ + optionName: item.optionName, + optionValue: item.optionValue, + optionDescription: item.optionDescription, + willImport: !item.exists, + exists: item.exists + })); + } + this.isProcessing = false; + }, + error: (err) => { + this.parseError = `Error processing file: ${err.error?.title || err.message || 'Unknown error'}`; + this.isProcessing = false; + } + }); + } + } + + + getImportCount(): number { + return this.previewItems.filter(item => item.willImport).length; + } + + getSkipCount(): number { + return this.previewItems.filter(item => !item.willImport).length; + } + + getAvailableCount(): number { + return this.previewItems.filter(item => !item.exists).length; + } + + areAllSelected(): boolean { + const available = this.previewItems.filter(item => !item.exists); + return available.length > 0 && available.every(item => item.willImport); + } + + isSomeSelected(): boolean { + const available = this.previewItems.filter(item => !item.exists); + const selected = available.filter(item => item.willImport); + return selected.length > 0 && selected.length < available.length; + } + + toggleAll(checked: boolean) { + this.previewItems.forEach(item => { + if (!item.exists) { + item.willImport = checked; + } + }); + } + + handleImport() { + const itemsToImport = this.previewItems + .filter(item => item.willImport) + .map((item, index) => ({ + id: uuidv4(), + dataFieldId: this.data.dataFieldId, + optionName: item.optionName, + optionValue: item.optionValue, + optionDescription: item.optionDescription || null, + displayOrder: this.data.existingOptions.length + index + 1 + })); + + this.dialogRef.close(itemsToImport); + } + + handleCancel() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/data-option-list-dialog/data-option-list-dialog.component.html b/src/app/components/data-option-list-dialog/data-option-list-dialog.component.html new file mode 100644 index 00000000..ad0bd4c3 --- /dev/null +++ b/src/app/components/data-option-list-dialog/data-option-list-dialog.component.html @@ -0,0 +1,78 @@ + + +
+ Manage Options ({{ data.dataOptions.length }}) + +
+ +
+ +
+ + Search options + + @if (searchText) { + + } + +
+ + + @if (data.canEdit) { +
+ + +
+ } + + +
+ + + + + + + + + + Order + {{ option.displayOrder }} + + + ID + {{ option.optionName }} + + + Description + {{ option.optionValue }} + + + + +
+
+ +
+
+ +
diff --git a/src/app/components/data-option-list-dialog/data-option-list-dialog.component.scss b/src/app/components/data-option-list-dialog/data-option-list-dialog.component.scss new file mode 100644 index 00000000..3b90ff70 --- /dev/null +++ b/src/app/components/data-option-list-dialog/data-option-list-dialog.component.scss @@ -0,0 +1,50 @@ +// Copyright 2025 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +.close-button { + float: right; + margin-top: -8px; +} + +.full-width { + width: 100%; +} + +.search-section { + margin-bottom: 8px; +} + +.action-buttons { + display: flex; + flex-direction: row; + margin-bottom: 8px; +} + +.options-table-container { + flex: 1; + overflow-y: auto; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 4px; +} + +.actions-column { + max-width: 100px; +} + +.order-column { + max-width: 80px; +} + +.name-column { + max-width: 150px; + font-family: monospace; + font-weight: 500; +} + +.value-column { + flex: 1; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; +} diff --git a/src/app/components/data-option-list-dialog/data-option-list-dialog.component.ts b/src/app/components/data-option-list-dialog/data-option-list-dialog.component.ts new file mode 100644 index 00000000..01378bcb --- /dev/null +++ b/src/app/components/data-option-list-dialog/data-option-list-dialog.component.ts @@ -0,0 +1,98 @@ +// Copyright 2025 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Sort } from '@angular/material/sort'; +import { DataOption } from 'src/app/generated/blueprint.api'; + +@Component({ + selector: 'app-data-option-list-dialog', + templateUrl: './data-option-list-dialog.component.html', + styleUrls: ['./data-option-list-dialog.component.scss'], + standalone: false +}) +export class DataOptionListDialogComponent { + searchText = ''; + sortActive = 'displayOrder'; + sortDirection = 'asc'; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { + dataOptions: DataOption[]; + canEdit: boolean; + canImport?: boolean; + onEdit: (option: DataOption) => void; + onDelete: (option: DataOption) => void; + onAdd: () => void; + onImport: () => void; + } + ) {} + + get displayedColumns(): string[] { + return this.data.canEdit + ? ['actions', 'displayOrder', 'optionName', 'optionValue'] + : ['displayOrder', 'optionName', 'optionValue']; + } + + get filteredOptions(): DataOption[] { + if (!this.searchText) { + return this.sortedOptions; + } + const search = this.searchText.toLowerCase(); + return this.sortedOptions.filter(option => + option.optionName?.toLowerCase().includes(search) || + option.optionValue?.toLowerCase().includes(search) + ); + } + + get sortedOptions(): DataOption[] { + const data = [...this.data.dataOptions]; + const direction = this.sortDirection === 'asc' ? 1 : -1; + return data.sort((a, b) => { + let valA: any, valB: any; + switch (this.sortActive) { + case 'optionName': + valA = (a.optionName || '').toLowerCase(); + valB = (b.optionName || '').toLowerCase(); + break; + case 'optionValue': + valA = (a.optionValue || '').toLowerCase(); + valB = (b.optionValue || '').toLowerCase(); + break; + default: + valA = +a.displayOrder; + valB = +b.displayOrder; + break; + } + return (valA < valB ? -1 : valA > valB ? 1 : 0) * direction; + }); + } + + onSortChange(sort: Sort) { + this.sortActive = sort.active; + this.sortDirection = sort.direction || 'asc'; + } + + handleEdit(option: DataOption) { + this.data.onEdit(option); + } + + handleDelete(option: DataOption) { + this.data.onDelete(option); + } + + handleAdd() { + this.data.onAdd(); + } + + handleImport() { + this.data.onImport(); + } + + handleClose() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/data-value/data-value.component.html b/src/app/components/data-value/data-value.component.html index 9d80798c..c01f7a72 100644 --- a/src/app/components/data-value/data-value.component.html +++ b/src/app/components/data-value/data-value.component.html @@ -45,12 +45,26 @@
 
} + + @if (dataField.dataType === dataType.Competency) { + + @for (id of selectedCompetencies; track id; let isLast = $last) { + {{ displayCompetency(id) }}@if (!isLast) {, } + } + + } @if (dataField.dataType !== dataType.DateTime && dataField.dataType !== dataType.Html && dataField.dataType !== dataType.Card && dataField.dataType !== dataType.Move && dataField.dataType !== dataType.User && dataField.dataType - !== dataType.Boolean && dataField.dataType !== dataType.Checkbox) { + !== dataType.Boolean && dataField.dataType !== dataType.Checkbox && dataField.dataType !== dataType.Competency) { + @if (dataField.isMultiSelect) { + @for (val of getValueArray(); track val) { + {{ val }}
+ } + } @else { {{ value }} + }
}
@@ -200,20 +214,11 @@ } @if (showValueOnly) { - - @if (teamOptions && teamOptions.length > 0) { - ALL - } - @if (teamOptions && teamOptions.length > 0) { - None - } - @for (team of teamOptions; track team.id) { - - {{ team.shortName }} - + + @for (val of getValueArray(); track val) { + {{ val }}
} -
+ } } @@ -396,8 +401,60 @@
} + +@if (dataField.dataType === dataType.Competency) { + + @if (!showValueOnly) { +
+ + + @for (id of selectedCompetencies; track id) { + + {{ displayCompetency(id) }} + @if (canEdit) { + + } + + } + + @if (canEdit) { + + Select competencies + + +
+ @for (option of filteredCompetencyOptions; track option.optionName) { + {{ option.optionName }} - {{ option.optionValue }} + } + @if (filteredCompetencyOptions.length === 0) { +
No matching competencies
+ } +
+
+ } +
+ } + @if (showValueOnly) { + + @for (id of selectedCompetencies; track id; let isLast = $last) { + {{ displayCompetency(id) }}@if (!isLast) {, } + } + + } +
+} -@if (dataField.isChosenFromList) { +@if (dataField.isChosenFromList && dataField.dataType !== dataType.Competency) { @if (!showValueOnly) { @@ -428,27 +485,12 @@ } @if (showValueOnly) { - @if (!dataField.isMultiSelect) { - - @for (dataOption of dataField.dataOptions; track $index) { - - {{ dataOption.optionName }} - - } - + {{ value }} + } @else { + @for (val of getValueArray(); track val) { + {{ val }}
} - - @if (dataField.isMultiSelect) { - - @for (dataOption of dataField.dataOptions; track $index) { - - {{ dataOption.optionName }} - - } - }
} diff --git a/src/app/components/data-value/data-value.component.scss b/src/app/components/data-value/data-value.component.scss index eb5c2fa5..9212a999 100644 --- a/src/app/components/data-value/data-value.component.scss +++ b/src/app/components/data-value/data-value.component.scss @@ -19,6 +19,26 @@ width: 300px; } +.competency-field { + width: 100%; +} + +.competency-label { + font-size: 13px; + font-weight: 400; + color: var(--mat-sys-on-surface-variant); +} + + +.competency-chip-view { + display: inline-block; + padding: 2px 8px; + margin: 2px 4px 2px 0; + border-radius: 16px; + font-size: 13px; + background: var(--mat-sys-surface-variant); +} + // overrides.scss or styles.scss // This fixes https://github.com//issues/4609 /* TODO(mdc-migration): The following rule targets internal classes of dialog that may no longer apply for the MDC version.*/ diff --git a/src/app/components/data-value/data-value.component.ts b/src/app/components/data-value/data-value.component.ts index 68a25be1..cc2f99b6 100644 --- a/src/app/components/data-value/data-value.component.ts +++ b/src/app/components/data-value/data-value.component.ts @@ -85,6 +85,58 @@ export class DataValueComponent { MselItemStatus.Archived, ]; + // --- Competency chips + search list --- + competencyFilter = ''; + + get selectedCompetencies(): string[] { + return this.value ? this.value.split(', ').filter(v => v) : []; + } + + get filteredCompetencyOptions() { + const options = this.dataField?.dataOptions || []; + if (!this.competencyFilter) return options; + const s = this.competencyFilter.toLowerCase(); + return options.filter(o => + o.optionName?.toLowerCase().includes(s) || + o.optionValue?.toLowerCase().includes(s) || + o.optionDescription?.toLowerCase().includes(s) + ); + } + + displayCompetency = (val: string): string => { + if (!val || !this.dataField?.dataOptions) return val || ''; + const opt = this.dataField.dataOptions.find(o => o.optionName === val); + return opt ? opt.optionName : val; + } + + getCompetencyTooltip = (val: string): string => { + if (!val || !this.dataField?.dataOptions) return ''; + const opt = this.dataField.dataOptions.find(o => o.optionName === val); + return opt?.optionValue || ''; + } + + isCompetencySelected(optionName: string): boolean { + return this.selectedCompetencies.includes(optionName); + } + + toggleCompetency(optionName: string) { + const current = this.selectedCompetencies; + const idx = current.indexOf(optionName); + if (idx >= 0) { + current.splice(idx, 1); + } else { + current.push(optionName); + } + this.value = current.join(', '); + this.valueChangeHandler(); + } + + removeCompetency(id: string) { + const current = this.selectedCompetencies.filter(v => v !== id); + this.value = current.join(', '); + this.valueChangeHandler(); + } + valueChangeHandler() { this.valueChange.emit(this.value); } diff --git a/src/app/components/home-app/home-app.component.html b/src/app/components/home-app/home-app.component.html index 920e4e48..3ccb2d9f 100755 --- a/src/app/components/home-app/home-app.component.html +++ b/src/app/components/home-app/home-app.component.html @@ -36,7 +36,8 @@

If the problem persists, please contact the site administrator.

@if (!apiIsSick && selectedMselId) {
} diff --git a/src/app/components/home-app/home-app.component.ts b/src/app/components/home-app/home-app.component.ts index bc3924f9..d86f4571 100755 --- a/src/app/components/home-app/home-app.component.ts +++ b/src/app/components/home-app/home-app.component.ts @@ -52,6 +52,7 @@ export class HomeAppComponent implements OnDestroy, OnInit { username = ''; canAccessAdminSection = false; canEditMsels = false; + canEditCheckboxes = false; isAuthorizedUser = false; isSidebarOpen = true; private unsubscribe$ = new Subject(); @@ -147,6 +148,8 @@ export class HomeAppComponent implements OnDestroy, OnInit { this.permissions = this.permissionDataService.permissions; this.canAccessAdminSection = this.permissions.filter(p => !p.endsWith('Msels')).length > 0; this.canEditMsels = this.permissionDataService.hasPermission(SystemPermission.EditMsels); + // Admins (ContentDevelopers/SystemAdmins) can edit checkboxes + this.canEditCheckboxes = this.permissionDataService.hasPermission(SystemPermission.CreateMsels); } ); // Start SignalR connection diff --git a/src/app/components/landing/join/join.component.ts b/src/app/components/landing/join/join.component.ts index 52cef043..2eec56f7 100644 --- a/src/app/components/landing/join/join.component.ts +++ b/src/app/components/landing/join/join.component.ts @@ -19,6 +19,7 @@ import { TopbarView } from '../../shared/top-bar/topbar.models'; import { Title } from '@angular/platform-browser'; import { ErrorService } from 'src/app/services/error/error.service'; import { UIDataService } from 'src/app/data/ui/ui-data.service'; +import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-join', @@ -52,7 +53,8 @@ export class JoinComponent implements OnDestroy, OnInit { private router: Router, private titleService: Title, private errorService: ErrorService, - private uiDataService: UIDataService + private uiDataService: UIDataService, + private http: HttpClient ) { this.hideTopbar = this.uiDataService.inIframe(); // set image @@ -79,6 +81,12 @@ export class JoinComponent implements OnDestroy, OnInit { } ngOnInit() { + // Call xAPI for join page viewed + const baseUrl = this.settingsService.settings.ApiUrl.endsWith('/') + ? this.settingsService.settings.ApiUrl + : this.settingsService.settings.ApiUrl + '/'; + this.http.post(`${baseUrl}api/xapi/viewed/joinpage`, {}).subscribe(); + // subscribe to users this.userQuery.selectAll() .pipe(takeUntil(this.unsubscribe$)) diff --git a/src/app/components/msel-competencies/msel-competencies.component.html b/src/app/components/msel-competencies/msel-competencies.component.html new file mode 100644 index 00000000..313bd049 --- /dev/null +++ b/src/app/components/msel-competencies/msel-competencies.component.html @@ -0,0 +1,362 @@ + + +@if (msel) { +
+
+ +
+ + Competency Framework + + — Select a framework — + @for (fw of frameworks; track fw.id) { + {{ fw.name }} ({{ fw.version }}) + } + + +
+
+ + + + + + Add Competencies + + +
+ @if (selectedFramework && frameworkWorkRoles.length > 0) { + + + @if (workRoleFilterString) { + + } + + + + @if (competencySearchString) { + + } + + @if (workRoleCategories.length > 0) { + + Category + + All + @for (cat of workRoleCategories; track cat) { + {{ cat }} + } + + + } +
+ +
+ } +
+ @if (selectedFramework && frameworkWorkRoles.length > 0) { +
+ + + + + @if (canEditMsel()) { + + + } + + + @if (canEditMsel()) { + + + } + + + + + ID + {{ wr.idNumber }} + + + + Name + {{ wr.shortName }} + + + + Category + {{ getWorkRoleCategory(wr) }} + + + + + @if (isBrowserExpanded(wr)) { +
+
+ @if (canEditMsel()) { + + Select All + + } + @if (browserChildTypes.length > 1) { + + Type + + All + @for (t of browserChildTypes; track t) { + {{ t }} + } + + + } + + +
+
+ @for (child of getPaginatedChildren(wr); track child.id) { +
+ @if (canEditMsel()) { + + + } + {{ child.idNumber }} + {{ getCompetencyType(child) }} + {{ child.shortName }} +
+ } @empty { +
No related competencies in framework
+ } +
+
+ } +
+
+ + + + +
+ @if (workRoleDataSource.data.length === 0) { +
No work roles found
+ } +
+ } @else if (!selectedFramework) { +
Select a framework to browse work roles and competencies
+ } +
+ + + + + + + MSEL Competencies ({{ mselCompetencyList.length }}) + + +
+ @if (canEditMsel() && selection.hasValue()) { + + } + + + @if (filterString) { + + } + + @if (competencyTypes.length > 1) { + + Type + + All + @for (t of competencyTypes; track t) { + {{ t }} + } + + + } + @if (poolFrameworks.length > 0) { + + Framework + + All + @for (fw of poolFrameworks; track fw.id) { + {{ fw.name }} ({{ fw.version }}) + } + + + } +
+ +
+
+
+ + + + + + + + + + + + + + + + + @if (canEditMsel()) { + + } + + + + + ID + {{ element.competency?.idNumber }} + + + + Type + {{ getMselCompetencyType(element) }} + + + + Framework + {{ getFrameworkName(element) }} + + + + Name + + {{ element.competency?.shortName }} + + + + + Teams + + {{ getTeamNames(element) || '—' }} + + + + + Events + {{ getEventCount(element) }} + + + + + @if (expandedCompetencyId === element.competencyId) { + + } + + + + + + + + @if (dataSource.data.length === 0) { +
No competencies associated with this MSEL.
+ } +
+
+
+} diff --git a/src/app/components/msel-competencies/msel-competencies.component.scss b/src/app/components/msel-competencies/msel-competencies.component.scss new file mode 100644 index 00000000..ad1516f5 --- /dev/null +++ b/src/app/components/msel-competencies/msel-competencies.component.scss @@ -0,0 +1,344 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +@use "@angular/material" as mat; + +:host { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-toolbar { + display: flex; + align-items: center; + min-height: 76px; + flex-shrink: 0; + padding: 0 4px; +} + +.sp-icon { + margin-left: 4px; + margin-right: 4px; +} + +.framework-select { + width: 400px; + margin-left: 10px; +} + +.content-area { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + padding: 0; + overflow-y: auto; +} + +.section-panel { + margin: 0 16px 12px; + background-color: transparent; + flex-shrink: 0; +} + +.panel-icon { + margin-right: 8px; + color: var(--mat-sys-primary); +} + +.toolbar-row { + display: flex; + align-items: center; + flex-shrink: 0; + min-height: 48px; +} + +.button-end { + margin-left: auto; +} + +table { + width: 100%; +} + +// ============================================= +// Panel 1: Add Competencies — work role browser +// ============================================= + +.browser-table-scroll { + max-height: 580px; + overflow-x: hidden; + overflow-y: auto; +} + +// Work role browser columns +.wr-col-check { + flex: 0 0 60px; + width: 60px; + padding-left: 8px !important; + padding-right: 8px !important; +} + +.wr-col-id { + flex: 0 0 120px; + white-space: nowrap; +} + +.wr-col-name { + flex: 1 1 40%; + min-width: 0; +} + +.wr-col-category { + flex: 1 1 30%; + min-width: 0; +} + +// Browser row expand +.browser-row { + cursor: pointer; +} + +.browser-row:hover { + background-color: var(--mat-sys-surface-variant); +} + +.browser-row-expanded { + background-color: var(--mat-sys-surface-variant); +} + +// Browser children (expanded detail) +.browser-children { + width: 100%; + padding: 8px 16px 12px 60px; +} + +.browser-children-toolbar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + + @include mat.form-field-density(-5); +} + +.browser-children-count { + font-size: 13px; + color: var(--mat-sys-on-surface-variant); + margin-left: auto; +} + +.browser-children-list { + max-height: 300px; + overflow-x: hidden; + overflow-y: auto; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 4px; +} + +.browser-child-row { + display: flex; + align-items: center; + padding: 4px 8px; + border-bottom: 1px solid var(--mat-sys-outline-variant); + font-size: 13px; + + &:last-child { + border-bottom: none; + } + + &:hover { + background-color: var(--mat-sys-surface-variant); + } +} + +.browser-child-check { + flex: 0 0 40px; + margin-right: 8px; + + @include mat.checkbox-density(-2); +} + +.browser-child-id { + flex: 0 0 100px; + font-weight: 500; + white-space: nowrap; +} + +.browser-child-type { + flex: 0 0 80px; + color: var(--mat-sys-on-surface-variant); + font-size: 12px; +} + +.browser-child-name { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +// ============================================= +// Panel 2: MSEL Competencies — pool +// ============================================= + +.pool-table-scroll { + max-height: calc(100vh - 400px); + overflow-x: hidden; + overflow-y: auto; + padding-bottom: 20px; +} + +// Pool table columns +.pool-col-select { + flex: 0 0 56px; + width: 56px; +} + +.pool-col-action { + flex: 0 0 72px; + width: 72px; +} + +.pool-col-id { + flex: 0 0 120px; + white-space: nowrap; +} + +.pool-col-type { + flex: 0 0 10%; +} + +.pool-col-framework { + flex: 1 1 15%; + min-width: 0; +} + +.pool-col-name { + flex: 1 1 20%; + min-width: 0; +} + +.pool-col-teams { + flex: 2 1 30%; + min-width: 0; +} + +.pool-col-events { + flex: 0 0 60px; + text-align: center; +} + +// Pool row expand +.pool-row { + cursor: pointer; +} + +.pool-row:hover { + background-color: var(--mat-sys-surface-variant); +} + +.pool-row-expanded { + background-color: var(--mat-sys-surface-variant); +} + +// ============================================= +// Shared: detail row expand +// ============================================= + +.detail-row { + height: 0; + min-height: 0; + overflow: hidden; + --mat-table-row-item-outline-width: 0px; +} + +::ng-deep .detail-row .mat-mdc-cell.expanded-detail-cell { + padding: 0 !important; + min-height: 0; + border-bottom-width: 0; + flex: 0 0 100% !important; + width: 100% !important; + max-width: 100% !important; +} + +.expanded-detail-cell { + display: flex; + flex-direction: column; + flex: 0 0 100%; + width: 100%; + max-width: 100%; + padding: 0; +} + +.no-results { + margin-top: 20px; + margin-left: 150px; +} + +.no-results-inline { + padding: 8px 0; + color: var(--mat-sys-on-surface-variant); + font-style: italic; +} + +// ============================================= +// Team assignment enchilada (Panel 2 expand) +// ============================================= + +// TODO: standardize team enchilada widths across all pages +// (card-teams, player-application-teams, competencies, etc.) +.related-enchilada { + display: flex; + flex-direction: row; + align-items: flex-start; + width: 100%; + padding: 8px 0; + gap: 4%; +} + +.related-list-container { + display: flex; + flex-direction: column; + flex: 1 1 48%; + min-width: 250px; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 4px; + + table { + width: 100%; + } +} + +.related-toolbar { + background: transparent; + overflow: visible; + + @include mat.form-field-density(-5); + + p { + margin-left: 8px; + } +} + +::ng-deep .related-toolbar .mat-toolbar-row { + overflow: visible; +} + +.header-icon { + color: var(--mat-sys-primary); +} + +.related-table-scroll { + max-height: 300px; + overflow: auto; +} + +.related-col-btn { + flex: 0 0 48px; + width: 48px; + justify-content: center; +} diff --git a/src/app/components/msel-competencies/msel-competencies.component.ts b/src/app/components/msel-competencies/msel-competencies.component.ts new file mode 100644 index 00000000..afdc31ab --- /dev/null +++ b/src/app/components/msel-competencies/msel-competencies.component.ts @@ -0,0 +1,872 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. +import { Component, Input, OnDestroy, OnInit, ViewChild, AfterViewInit } from '@angular/core'; +import { UntypedFormControl } from '@angular/forms'; +import { SelectionModel } from '@angular/cdk/collections'; +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { Subject, Observable } from 'rxjs'; +import { take, takeUntil } from 'rxjs/operators'; +import { + Competency, + CompetencyFramework, + CompetencyFrameworkService, + DataField, + DataFieldType, + DataValue, + MselCompetency, + Team, + TeamCompetency, + SystemPermission, +} from 'src/app/generated/blueprint.api'; +import { PermissionDataService } from 'src/app/data/permission/permission-data.service'; +import { MselDataService, MselPlus } from 'src/app/data/msel/msel-data.service'; +import { MselQuery } from 'src/app/data/msel/msel.query'; +import { MselCompetencyDataService } from 'src/app/data/msel-competency/msel-competency-data.service'; +import { MselCompetencyQuery } from 'src/app/data/msel-competency/msel-competency.query'; +import { TeamCompetencyDataService } from 'src/app/data/team-competency/team-competency-data.service'; +import { TeamCompetencyQuery } from 'src/app/data/team-competency/team-competency.query'; +import { CompetencyFrameworkDataService } from 'src/app/data/competency-framework/competency-framework-data.service'; +import { CompetencyFrameworkQuery } from 'src/app/data/competency-framework/competency-framework.query'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from 'src/app/services/dialog/dialog.service'; +import { TeamCompetencyPropagateDialogComponent, TeamCompetencyPropagateData } from '../team-competency-propagate-dialog/team-competency-propagate-dialog.component'; +import { DataFieldDataService } from 'src/app/data/data-field/data-field-data.service'; +import { DataFieldQuery } from 'src/app/data/data-field/data-field.query'; +import { DataValueDataService } from 'src/app/data/data-value/data-value-data.service'; +import { DataValueQuery } from 'src/app/data/data-value/data-value.query'; +import { MatTableDataSource } from '@angular/material/table'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, Sort } from '@angular/material/sort'; + +@Component({ + selector: 'app-msel-competencies', + templateUrl: './msel-competencies.component.html', + styleUrls: ['./msel-competencies.component.scss'], + animations: [ + trigger('detailExpand', [ + state('collapsed', style({ height: '0px', minHeight: '0', visibility: 'hidden' })), + state('expanded', style({ height: '*', visibility: 'visible' })), + transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), + ]), + ], + standalone: false +}) +export class MselCompetenciesComponent implements OnDestroy, OnInit, AfterViewInit { + @Input() loggedInUserId: string; + @ViewChild('competencyPaginator') competencyPaginator: MatPaginator; + @ViewChild('workRolePaginator') workRolePaginator: MatPaginator; + @ViewChild('workRoleSort') workRoleSort: MatSort; + @ViewChild('poolSort') poolSort: MatSort; + msel = new MselPlus(); + mselCompetencyList: MselCompetency[] = []; + teamCompetencyList: TeamCompetency[] = []; + private allDataFields: DataField[] = []; + private allDataValues: DataValue[] = []; + // Frameworks + frameworks: CompetencyFramework[] = []; + selectedFrameworkId = ''; + selectedFramework: CompetencyFramework = null; + frameworkWorkRoles: Competency[] = []; + private competencyTypeMap = new Map(); + private competencyById = new Map(); + + // --- Panel 1: Add Competencies (browser) --- + workRoleFilterControl = new UntypedFormControl(); + workRoleFilterString = ''; + competencySearchControl = new UntypedFormControl(); + competencySearchString = ''; + workRoleDataSource = new MatTableDataSource([]); + workRoleDisplayedColumns: string[] = ['check', 'idNumber', 'shortName', 'category']; + workRoleCategoryFilter = ''; + workRoleCategories: string[] = []; + addPanelExpanded = false; + browserExpandedId: string = null; + browserAutoExpandedIds = new Set(); + browserChildTypeFilter = ''; + browserChildTypes: string[] = []; + browserChildrenPageIndex = 0; + browserChildrenPageSize = 25; + + // --- Panel 2: MSEL Competencies (pool) --- + filterControl = new UntypedFormControl(); + filterString = ''; + dataSource = new MatTableDataSource([]); + displayedColumns: string[] = ['select', 'action', 'idNumber', 'type', 'framework', 'shortName', 'teams', 'events']; + selection = new SelectionModel(true, []); + typeFilter = ''; + competencyTypes: string[] = []; + frameworkFilter = ''; + poolFrameworks: { id: string, name: string, version: string }[] = []; + // Teams + mselTeams: Team[] = []; + teamsByCompetency = new Map(); + expandedCompetencyId: string = null; + + private unsubscribe$ = new Subject(); + + constructor( + private mselQuery: MselQuery, + private mselCompetencyDataService: MselCompetencyDataService, + private mselCompetencyQuery: MselCompetencyQuery, + private teamCompetencyDataService: TeamCompetencyDataService, + private teamCompetencyQuery: TeamCompetencyQuery, + private competencyFrameworkDataService: CompetencyFrameworkDataService, + private competencyFrameworkQuery: CompetencyFrameworkQuery, + private competencyFrameworkService: CompetencyFrameworkService, + private permissionDataService: PermissionDataService, + public dialog: MatDialog, + public dialogService: DialogService, + private dataFieldDataService: DataFieldDataService, + private dataFieldQuery: DataFieldQuery, + private dataValueDataService: DataValueDataService, + private dataValueQuery: DataValueQuery + ) { + (this.mselQuery.selectActive() as Observable).pipe(takeUntil(this.unsubscribe$)).subscribe(msel => { + if (msel) { + const isNewMsel = this.msel.id !== msel.id; + Object.assign(this.msel, msel); + this.mselTeams = msel.teams || []; + if (isNewMsel) { + this.mselCompetencyDataService.loadByMsel(msel.id); + this.teamCompetencyDataService.loadByMsel(msel.id); + } + } + }); + this.mselCompetencyQuery.selectAll().pipe(takeUntil(this.unsubscribe$)).subscribe(mselCompetencies => { + this.mselCompetencyList = mselCompetencies; + if (!this.selectedFrameworkId) { + this.addPanelExpanded = mselCompetencies.length === 0; + } + this.selection.clear(); + this.buildPoolTypes(); + this.applyFilter(); + }); + this.teamCompetencyQuery.selectAll().pipe(takeUntil(this.unsubscribe$)).subscribe(teamCompetencies => { + this.teamCompetencyList = teamCompetencies; + this.buildTeamsByCompetency(); + }); + this.competencyFrameworkQuery.selectAll().pipe(takeUntil(this.unsubscribe$)).subscribe(frameworks => { + this.frameworks = frameworks; + }); + this.dataFieldQuery.selectAll().pipe(takeUntil(this.unsubscribe$)).subscribe(fields => { + this.allDataFields = fields; + }); + this.dataValueQuery.selectAll().pipe(takeUntil(this.unsubscribe$)).subscribe(values => { + this.allDataValues = values; + }); + this.filterControl.valueChanges.pipe(takeUntil(this.unsubscribe$)).subscribe(term => { + this.filterString = term; + this.applyFilter(); + }); + this.workRoleFilterControl.valueChanges.pipe(takeUntil(this.unsubscribe$)).subscribe(term => { + this.workRoleFilterString = term; + this.applyWorkRoleFilter(); + }); + this.competencySearchControl.valueChanges.pipe(takeUntil(this.unsubscribe$)).subscribe(term => { + this.competencySearchString = term; + this.applyWorkRoleFilter(); + }); + } + + ngOnInit() { + this.permissionDataService.load().pipe(takeUntil(this.unsubscribe$)).subscribe(); + this.competencyFrameworkDataService.load(); + } + + ngAfterViewInit() { + if (this.poolSort) { + this.dataSource.sort = this.poolSort; + } + } + + canEditMsel(): boolean { + return this.permissionDataService.hasPermission(SystemPermission.ManageMsels) || + this.msel.hasRole(this.loggedInUserId, '').owner; + } + + // ============================================= + // Framework selection + // ============================================= + + onFrameworkChange(frameworkId: string) { + this.selectedFrameworkId = frameworkId; + this.browserExpandedId = null; + this.browserChildTypeFilter = ''; + if (frameworkId) { + this.addPanelExpanded = true; + } + if (!frameworkId) { + this.selectedFramework = null; + this.frameworkWorkRoles = []; + this.workRoleDataSource.data = []; + return; + } + this.competencyFrameworkService.getCompetencyFramework(frameworkId) + .pipe(take(1)) + .subscribe(fw => { + this.selectedFramework = fw; + const allComps = fw.competencies || []; + this.buildTypeMapFromComps(fw, allComps); + this.frameworkWorkRoles = allComps.filter(c => this.competencyTypeMap.get(c.id) === 'Work Role'); + this.workRoleCategories = [...new Set(this.frameworkWorkRoles.map(wr => this.getWorkRoleCategory(wr)).filter(c => c))].sort(); + this.workRoleCategoryFilter = ''; + this.workRoleFilterControl.setValue(''); + this.applyWorkRoleFilter(); + setTimeout(() => { + if (this.workRolePaginator) { + this.workRoleDataSource.paginator = this.workRolePaginator; + } + }); + }); + } + + private buildTypeMapFromComps(fw: CompetencyFramework, comps: Competency[]) { + this.competencyTypeMap.clear(); + this.competencyById.clear(); + const byId = new Map(); + comps.forEach(c => { + byId.set(c.id, c); + this.competencyById.set(c.id, c); + }); + const hasHierarchy = comps.some(c => c.parentId && byId.has(c.parentId)); + const taxonomyLevels = (fw.taxonomies || '') + .split(',') + .map(t => t.trim()) + .filter(t => t.length > 0); + + for (const c of comps) { + const idType = this.deriveTypeFromId(c.idNumber); + if (idType !== 'Other') { + this.competencyTypeMap.set(c.id, idType); + } else if (hasHierarchy && taxonomyLevels.length > 0) { + const depth = this.getDepth(c, byId); + this.competencyTypeMap.set(c.id, taxonomyLevels[Math.min(depth, taxonomyLevels.length - 1)]); + } else if (hasHierarchy) { + const isRoot = !c.parentId || !byId.has(c.parentId); + this.competencyTypeMap.set(c.id, isRoot ? 'Category' : 'Other'); + } else { + this.competencyTypeMap.set(c.id, 'Other'); + } + } + } + + private deriveTypeFromId(idNumber: string): string { + if (!idNumber) return 'Other'; + if (idNumber.includes('WRL')) return 'Work Role'; + if (/^[TKSA][\d-]/.test(idNumber)) { + const prefixMap: Record = { 'T': 'Task', 'K': 'Knowledge', 'S': 'Skill', 'A': 'Ability' }; + return prefixMap[idNumber.charAt(0)] || 'Other'; + } + if (/^[A-Z]{2}-[A-Z]{3}-\d+$/.test(idNumber)) return 'Work Role'; + if (/^[A-Z]{3}$/.test(idNumber)) return 'Specialty Area'; + if (/^[A-Z]{2}$/.test(idNumber)) return 'Category'; + return 'Other'; + } + + private getDepth(comp: Competency, byId: Map): number { + let depth = 0; + let current = comp; + while (current.parentId && byId.has(current.parentId)) { + depth++; + current = byId.get(current.parentId); + } + return depth; + } + + getWorkRoleCategory(workRole: Competency): string { + let current = workRole; + while (current.parentId && this.competencyById.has(current.parentId)) { + current = this.competencyById.get(current.parentId); + } + if (current.id === workRole.id) return ''; + return current.shortName || current.idNumber || ''; + } + + getCompetencyType(comp: Competency): string { + return this.competencyTypeMap.get(comp.id) || ''; + } + + // ============================================= + // Panel 1: Add Competencies — work role browser + // ============================================= + + isInPool(competencyId: string): boolean { + return this.mselCompetencyList.some(mc => mc.competencyId === competencyId); + } + + togglePoolItem(comp: Competency) { + if (this.isInPool(comp.id)) { + const mc = this.mselCompetencyList.find(m => m.competencyId === comp.id); + if (mc) { + const msg = this.buildRemoveMessage(comp.idNumber, comp.shortName, this.getEventCount(mc), this.getDataFieldOptionCount(comp.idNumber)); + this.dialogService.confirm('Remove Competency', msg).subscribe(result => { + if (result['confirm']) { + this.removeCompetencyReferences(comp.idNumber); + this.mselCompetencyDataService.delete(mc.id); + } + }); + } + } else { + this.mselCompetencyDataService.add({ + mselId: this.msel.id, + competencyId: comp.id, + } as MselCompetency); + } + } + + onWorkRoleCategoryChange(category: string) { + this.workRoleCategoryFilter = category; + this.applyWorkRoleFilter(); + } + + applyWorkRoleFilter() { + let filtered = [...this.frameworkWorkRoles]; + if (this.workRoleCategoryFilter) { + filtered = filtered.filter(wr => this.getWorkRoleCategory(wr) === this.workRoleCategoryFilter); + } + if (this.workRoleFilterString) { + const fs = this.workRoleFilterString.toLowerCase(); + filtered = filtered.filter(c => + c.idNumber?.toLowerCase().includes(fs) || + c.shortName?.toLowerCase().includes(fs) || + c.description?.toLowerCase().includes(fs)); + } + // Competency search: filter work roles to those with matching children, auto-expand + this.browserAutoExpandedIds.clear(); + if (this.competencySearchString) { + const cs = this.competencySearchString.toLowerCase(); + filtered = filtered.filter(wr => { + const children = this.getRelatedCompetencies(wr); + return children.some(c => + c.idNumber?.toLowerCase().includes(cs) || + c.shortName?.toLowerCase().includes(cs) || + c.description?.toLowerCase().includes(cs)); + }); + for (const wr of filtered) { + this.browserAutoExpandedIds.add(wr.id); + } + } + filtered.sort((a, b) => (a.idNumber || '').localeCompare(b.idNumber || '')); + this.workRoleDataSource.data = filtered; + this.workRoleDataSource.sortingDataAccessor = (item: Competency, property: string) => { + switch (property) { + case 'category': return this.getWorkRoleCategory(item)?.toLowerCase() || ''; + default: return (item[property] || '').toString().toLowerCase(); + } + }; + setTimeout(() => { + if (this.workRoleSort) { + this.workRoleDataSource.sort = this.workRoleSort; + } + }); + } + + // --- Browser expand: child TKSAs --- + + isBrowserExpanded(workRole: Competency): boolean { + return this.browserExpandedId === workRole.id || this.browserAutoExpandedIds.has(workRole.id); + } + + toggleBrowserExpand(workRole: Competency) { + if (this.browserAutoExpandedIds.size > 0) { + // In search mode: toggle individual within auto-expanded set + if (this.browserAutoExpandedIds.has(workRole.id)) { + this.browserAutoExpandedIds.delete(workRole.id); + } else { + this.browserAutoExpandedIds.add(workRole.id); + } + this.browserExpandedId = null; + } else if (this.browserExpandedId === workRole.id) { + this.browserExpandedId = null; + } else { + this.browserExpandedId = workRole.id; + this.browserChildTypeFilter = ''; + this.browserChildrenPageIndex = 0; + this.buildBrowserChildTypes(workRole); + } + } + + private buildBrowserChildTypes(workRole: Competency) { + const children = this.getRelatedCompetencies(workRole); + const types = new Set(); + for (const c of children) { + const t = this.competencyTypeMap.get(c.id) || this.deriveTypeFromId(c.idNumber); + if (t && t !== 'Other' && t !== 'Work Role') types.add(t); + } + this.browserChildTypes = [...types].sort(); + } + + private getRelatedCompetencies(comp: Competency): Competency[] { + const resultMap = new Map(); + for (const c of this.competencyById.values()) { + if (c.parentId === comp.id) { + resultMap.set(c.id, c); + } + } + if (comp.relatedIdNumbers?.length > 0) { + const relatedSet = new Set(comp.relatedIdNumbers); + for (const c of this.competencyById.values()) { + if (c.idNumber && relatedSet.has(c.idNumber) && c.id !== comp.id) { + resultMap.set(c.id, c); + } + } + } + return [...resultMap.values()].sort((a, b) => (a.idNumber || '').localeCompare(b.idNumber || '')); + } + + getFilteredChildren(workRole: Competency): Competency[] { + let children = this.getRelatedCompetencies(workRole); + if (this.browserChildTypeFilter) { + children = children.filter(c => { + const t = this.competencyTypeMap.get(c.id) || this.deriveTypeFromId(c.idNumber); + return t === this.browserChildTypeFilter; + }); + } + if (this.competencySearchString) { + const cs = this.competencySearchString.toLowerCase(); + children = children.filter(c => + c.idNumber?.toLowerCase().includes(cs) || + c.shortName?.toLowerCase().includes(cs) || + c.description?.toLowerCase().includes(cs)); + } + return children; + } + + getPaginatedChildren(workRole: Competency): Competency[] { + const all = this.getFilteredChildren(workRole); + const start = this.browserChildrenPageIndex * this.browserChildrenPageSize; + return all.slice(start, start + this.browserChildrenPageSize); + } + + onBrowserChildrenPage(event: any) { + this.browserChildrenPageIndex = event.pageIndex; + this.browserChildrenPageSize = event.pageSize; + } + + onBrowserChildTypeChange(type: string) { + this.browserChildTypeFilter = type; + this.browserChildrenPageIndex = 0; + } + + selectAllChildren(workRole: Competency) { + const children = this.getFilteredChildren(workRole); + for (const c of children) { + if (!this.isInPool(c.id)) { + this.mselCompetencyDataService.add({ + mselId: this.msel.id, + competencyId: c.id, + } as MselCompetency); + } + } + } + + deselectAllChildren(workRole: Competency) { + const children = this.getFilteredChildren(workRole); + const toRemove = children + .map(c => this.mselCompetencyList.find(m => m.competencyId === c.id)) + .filter(mc => mc); + if (toRemove.length === 0) return; + const totalEvents = toRemove.reduce((sum, mc) => sum + this.getEventCount(mc), 0); + const totalOptions = toRemove.reduce((sum, mc) => sum + this.getDataFieldOptionCount(mc.competency?.idNumber), 0); + let msg = `Remove ${toRemove.length} competenc${toRemove.length === 1 ? 'y' : 'ies'} from this MSEL?`; + const parts: string[] = []; + if (totalEvents > 0) parts.push(`${totalEvents} scenario event reference${totalEvents === 1 ? '' : 's'}`); + if (totalOptions > 0) parts.push(`${totalOptions} data field option${totalOptions === 1 ? '' : 's'}`); + if (parts.length > 0) { + msg += ` ${parts.join(' and ')} will also be removed.`; + } + this.dialogService.confirm('Remove Competencies', msg).subscribe(result => { + if (result['confirm']) { + for (const mc of toRemove) { + if (mc.competency?.idNumber) { + this.removeCompetencyReferences(mc.competency.idNumber); + } + this.mselCompetencyDataService.delete(mc.id); + } + } + }); + } + + allChildrenSelected(workRole: Competency): boolean { + const children = this.getFilteredChildren(workRole); + return children.length > 0 && children.every(c => this.isInPool(c.id)); + } + + someChildrenSelected(workRole: Competency): boolean { + const children = this.getFilteredChildren(workRole); + const selectedCount = children.filter(c => this.isInPool(c.id)).length; + return selectedCount > 0 && selectedCount < children.length; + } + + // --- Select all / deselect all (work role browser) --- + + isAllWorkRolesSelected(): boolean { + return this.workRoleDataSource.data.length > 0 && this.workRoleDataSource.data.every(wr => this.isInPool(wr.id)); + } + + hasWorkRoleSelection(): boolean { + return this.workRoleDataSource.data.some(wr => this.isInPool(wr.id)); + } + + toggleAllWorkRoles() { + const allSelected = this.isAllWorkRolesSelected(); + if (allSelected) { + // Remove all visible work roles + const toRemove = this.workRoleDataSource.data + .map(wr => this.mselCompetencyList.find(mc => mc.competencyId === wr.id)) + .filter(mc => mc); + if (toRemove.length === 0) return; + const totalEvents = toRemove.reduce((sum, mc) => sum + this.getEventCount(mc), 0); + const totalOptions = toRemove.reduce((sum, mc) => sum + this.getDataFieldOptionCount(mc.competency?.idNumber), 0); + let msg = `Remove ${toRemove.length} work role${toRemove.length === 1 ? '' : 's'} from this MSEL?`; + const parts: string[] = []; + if (totalEvents > 0) parts.push(`${totalEvents} scenario event reference${totalEvents === 1 ? '' : 's'}`); + if (totalOptions > 0) parts.push(`${totalOptions} data field option${totalOptions === 1 ? '' : 's'}`); + if (parts.length > 0) { + msg += ` ${parts.join(' and ')} will also be removed.`; + } + this.dialogService.confirm('Remove Work Roles', msg).subscribe(result => { + if (result['confirm']) { + for (const mc of toRemove) { + if (mc.competency?.idNumber) { + this.removeCompetencyReferences(mc.competency.idNumber); + } + this.mselCompetencyDataService.delete(mc.id); + } + } + }); + } else { + // Add all visible work roles + for (const wr of this.workRoleDataSource.data) { + if (!this.isInPool(wr.id)) { + this.mselCompetencyDataService.add({ + mselId: this.msel.id, + competencyId: wr.id, + } as MselCompetency); + } + } + } + } + + // ============================================= + // Panel 2: MSEL Competencies — pool + // ============================================= + + private buildPoolTypes() { + const types = new Set(); + const fwMap = new Map(); + for (const mc of this.mselCompetencyList) { + const t = this.competencyTypeMap.get(mc.competencyId) || this.deriveTypeFromId(mc.competency?.idNumber); + if (t && t !== 'Other') types.add(t); + const fwId = mc.competency?.competencyFrameworkId; + if (fwId && !fwMap.has(fwId)) { + const fw = this.frameworks.find(f => f.id === fwId); + if (fw) fwMap.set(fwId, { name: fw.name, version: fw.version || '' }); + } + } + this.competencyTypes = [...types].sort(); + this.poolFrameworks = [...fwMap.entries()].map(([id, fw]) => ({ id, name: fw.name, version: fw.version })).sort((a, b) => a.name.localeCompare(b.name)); + } + + onTypeFilterChange(type: string) { + this.typeFilter = type; + this.applyFilter(); + } + + onFrameworkFilterChange(frameworkId: string) { + this.frameworkFilter = frameworkId; + this.applyFilter(); + } + + applyFilter() { + let filtered = [...this.mselCompetencyList]; + if (this.frameworkFilter) { + filtered = filtered.filter(mc => mc.competency?.competencyFrameworkId === this.frameworkFilter); + } + if (this.typeFilter) { + filtered = filtered.filter(mc => { + const t = this.competencyTypeMap.get(mc.competencyId) || this.deriveTypeFromId(mc.competency?.idNumber); + return t === this.typeFilter; + }); + } + if (this.filterString) { + const term = this.filterString.toLowerCase(); + filtered = filtered.filter(mc => + mc.competency?.idNumber?.toLowerCase().includes(term) || + mc.competency?.shortName?.toLowerCase().includes(term) || + mc.competency?.description?.toLowerCase().includes(term)); + } + filtered.sort((a, b) => (a.competency?.idNumber || '').localeCompare(b.competency?.idNumber || '')); + this.dataSource.data = filtered; + this.dataSource.sortingDataAccessor = (item: MselCompetency, property: string) => { + switch (property) { + case 'idNumber': return item.competency?.idNumber?.toLowerCase() || ''; + case 'type': return this.getMselCompetencyType(item)?.toLowerCase() || ''; + case 'framework': return this.getFrameworkName(item)?.toLowerCase() || ''; + case 'shortName': return item.competency?.shortName?.toLowerCase() || ''; + case 'teams': return this.getTeamNames(item)?.toLowerCase() || ''; + case 'events': return this.getEventCount(item); + default: return ''; + } + }; + setTimeout(() => { + if (this.competencyPaginator) { + this.dataSource.paginator = this.competencyPaginator; + } + if (this.poolSort) { + this.dataSource.sort = this.poolSort; + } + }); + } + + removeMselCompetency(mc: MselCompetency) { + const idNumber = mc.competency?.idNumber || ''; + const name = mc.competency?.shortName || ''; + const msg = this.buildRemoveMessage(idNumber, name, this.getEventCount(mc), this.getDataFieldOptionCount(idNumber)); + this.dialogService.confirm('Remove Competency', msg).subscribe(result => { + if (result['confirm']) { + this.removeCompetencyReferences(idNumber); + this.mselCompetencyDataService.delete(mc.id); + } + }); + } + + getMselCompetencyType(mc: MselCompetency): string { + return this.competencyTypeMap.get(mc.competencyId) || this.deriveTypeFromId(mc.competency?.idNumber); + } + + getFrameworkName(mc: MselCompetency): string { + const fwId = mc.competency?.competencyFrameworkId; + if (!fwId) return ''; + const fw = this.frameworks.find(f => f.id === fwId); + return fw ? fw.name + ' (' + fw.version + ')' : ''; + } + + private get competencyFieldIds(): Set { + return new Set( + this.allDataFields + .filter(df => df.mselId === this.msel.id && df.dataType === DataFieldType.Competency) + .map(df => df.id) + ); + } + + getEventCount(mc: MselCompetency): number { + const idNumber = mc.competency?.idNumber; + if (!idNumber) return 0; + const fieldIds = this.competencyFieldIds; + if (fieldIds.size === 0) return 0; + const seen = new Set(); + let count = 0; + for (const dv of this.allDataValues) { + if (fieldIds.has(dv.dataFieldId) && dv.value && dv.scenarioEventId && !seen.has(dv.scenarioEventId)) { + const ids = dv.value.split(',').map(s => s.trim()); + if (ids.includes(idNumber)) { + count++; + seen.add(dv.scenarioEventId); + } + } + } + return count; + } + + private getDataFieldOptionCount(idNumber: string): number { + if (!idNumber) return 0; + return this.allDataFields + .filter(df => df.mselId === this.msel.id && df.dataType === DataFieldType.Competency) + .filter(df => (df.dataOptions || []).some(opt => opt.optionName === idNumber)) + .length; + } + + private buildRemoveMessage(idNumber: string, name: string, eventCount: number, optionCount: number): string { + const parts: string[] = []; + if (eventCount > 0) { + parts.push(`${eventCount} scenario event${eventCount === 1 ? '' : 's'}`); + } + if (optionCount > 0) { + parts.push(`${optionCount} data field${optionCount === 1 ? '' : 's'}`); + } + if (parts.length > 0) { + return `The following competency is referenced by ${parts.join(' and ')}. Those references will be removed. Continue?\n\n${idNumber} — ${name}`; + } + return `Remove the following competency from this MSEL?\n\n${idNumber} — ${name}`; + } + + private removeCompetencyReferences(idNumber: string) { + if (!idNumber) return; + const competencyFields = this.allDataFields + .filter(df => df.mselId === this.msel.id && df.dataType === DataFieldType.Competency); + // Remove from DataOptions on competency-type data fields + for (const df of competencyFields) { + const filtered = (df.dataOptions || []).filter(opt => opt.optionName !== idNumber); + if (filtered.length !== (df.dataOptions || []).length) { + this.dataFieldDataService.updateDataField({ ...df, dataOptions: filtered }); + } + } + // Remove from DataValues on scenario events + const fieldIds = new Set(competencyFields.map(df => df.id)); + for (const dv of this.allDataValues) { + if (fieldIds.has(dv.dataFieldId) && dv.value) { + const ids = dv.value.split(',').map(s => s.trim()).filter(id => id && id !== idNumber); + const newValue = ids.join(', '); + if (newValue !== dv.value) { + this.dataValueDataService.updateDataValue({ ...dv, value: newValue }); + } + } + } + } + + // --- Team mapping --- + + private buildTeamsByCompetency() { + this.teamsByCompetency.clear(); + for (const tc of this.teamCompetencyList) { + const arr = this.teamsByCompetency.get(tc.competencyId) || []; + arr.push(tc.teamId); + this.teamsByCompetency.set(tc.competencyId, arr); + } + } + + getTeamNames(mc: MselCompetency): string { + const teamIds = this.teamsByCompetency.get(mc.competencyId) || []; + if (teamIds.length === 0) return ''; + return teamIds + .map(tid => this.mselTeams.find(t => t.id === tid)?.shortName || '') + .filter(n => n) + .join(', '); + } + + // --- Pool row expand: team assignment --- + + toggleExpand(mc: MselCompetency) { + this.expandedCompetencyId = this.expandedCompetencyId === mc.competencyId ? null : mc.competencyId; + } + + getAvailableTeams(mc: MselCompetency): Team[] { + const assignedIds = this.teamsByCompetency.get(mc.competencyId) || []; + return this.mselTeams.filter(t => !assignedIds.includes(t.id)); + } + + getAssignedTeams(mc: MselCompetency): Team[] { + const assignedIds = this.teamsByCompetency.get(mc.competencyId) || []; + return this.mselTeams.filter(t => assignedIds.includes(t.id)); + } + + addTeamToCompetency(mc: MselCompetency, team: Team) { + this.teamCompetencyDataService.add({ + teamId: team.id, + competencyId: mc.competencyId, + } as TeamCompetency); + // Offer to add to related competencies on the MSEL + const children = this.getPoolChildren(mc); + const unassigned = children.filter(child => { + const assignedIds = this.teamsByCompetency.get(child.competencyId) || []; + return !assignedIds.includes(team.id); + }); + if (unassigned.length > 0) { + this.openPropagateDialog(team, 'add', unassigned); + } + } + + removeTeamFromCompetency(mc: MselCompetency, team: Team) { + const tc = this.teamCompetencyList.find(t => t.teamId === team.id && t.competencyId === mc.competencyId); + if (tc) { + this.teamCompetencyDataService.delete(tc.id); + // Offer to remove from related competencies on the MSEL + const children = this.getPoolChildren(mc); + const assigned = children.filter(child => { + const assignedIds = this.teamsByCompetency.get(child.competencyId) || []; + return assignedIds.includes(team.id); + }); + if (assigned.length > 0) { + this.openPropagateDialog(team, 'remove', assigned); + } + } + } + + private openPropagateDialog(team: Team, action: 'add' | 'remove', competencies: MselCompetency[]) { + const dialogRef = this.dialog.open(TeamCompetencyPropagateDialogComponent, { + width: '600px', + maxWidth: '95vw', + maxHeight: '80vh', + data: { + teamName: team.shortName || team.name, + action, + competencies, + } as TeamCompetencyPropagateData, + }); + dialogRef.afterClosed().subscribe((selected: MselCompetency[] | null) => { + if (!selected || selected.length === 0) return; + if (action === 'add') { + for (const child of selected) { + this.teamCompetencyDataService.add({ + teamId: team.id, + competencyId: child.competencyId, + } as TeamCompetency); + } + } else { + for (const child of selected) { + const childTc = this.teamCompetencyList.find(t => t.teamId === team.id && t.competencyId === child.competencyId); + if (childTc) { + this.teamCompetencyDataService.delete(childTc.id); + } + } + } + }); + } + + private getPoolChildren(mc: MselCompetency): MselCompetency[] { + const compId = mc.competencyId; + const relatedIds = new Set(mc.competency?.relatedIdNumbers || []); + return this.mselCompetencyList.filter(other => { + if (other.competencyId === compId) return false; + if (other.competency?.parentId === compId) return true; + if (other.competency?.idNumber && relatedIds.has(other.competency.idNumber)) return true; + return false; + }); + } + + // --- Select all / deselect all (pool) --- + + isAllSelected(): boolean { + return this.selection.selected.length === this.dataSource.data.length && this.dataSource.data.length > 0; + } + + toggleAllRows() { + if (this.isAllSelected()) { + this.selection.clear(); + } else { + this.dataSource.data.forEach(row => this.selection.select(row)); + } + } + + removeSelected() { + const selected = this.selection.selected; + if (selected.length === 0) return; + const totalEvents = selected.reduce((sum, mc) => sum + this.getEventCount(mc), 0); + const totalOptions = selected.reduce((sum, mc) => sum + this.getDataFieldOptionCount(mc.competency?.idNumber), 0); + let msg = `Remove ${selected.length} competenc${selected.length === 1 ? 'y' : 'ies'} from this MSEL?`; + const parts: string[] = []; + if (totalEvents > 0) parts.push(`${totalEvents} scenario event reference${totalEvents === 1 ? '' : 's'}`); + if (totalOptions > 0) parts.push(`${totalOptions} data field option${totalOptions === 1 ? '' : 's'}`); + if (parts.length > 0) { + msg += ` ${parts.join(' and ')} will also be removed.`; + } + this.dialogService.confirm('Remove Selected', msg).subscribe(result => { + if (result['confirm']) { + for (const mc of selected) { + if (mc.competency?.idNumber) { + this.removeCompetencyReferences(mc.competency.idNumber); + } + this.mselCompetencyDataService.delete(mc.id); + } + this.selection.clear(); + } + }); + } + + ngOnDestroy() { + this.unsubscribe$.next(null); + this.unsubscribe$.complete(); + } +} diff --git a/src/app/components/msel-info/msel-info.component.html b/src/app/components/msel-info/msel-info.component.html index 8fc548ac..bd592ecc 100755 --- a/src/app/components/msel-info/msel-info.component.html +++ b/src/app/components/msel-info/msel-info.component.html @@ -333,28 +333,144 @@ {{ starterUrl }}
+ +
+
+ Assessor View URL +
+ Use this URL for direct access to the Assessor View +
+
+ +
+ +
+
+ + Competencies + @if (competencyFrameworkNames.length > 0) { + + ({{ competencyFrameworkNames.join(', ') }}) + + } +
+
+ @if (workRoleCompetencies.length > 0) { + + + + + Work Roles ({{ workRoleCompetencies.length }}) + + +
+ @for (c of workRoleCompetencies; track c.id) { +
+ {{ c.idNumber }} +
+ } +
+
+ } + @if (nonWorkRoleCompetencies.length > 0) { + + + + + Competencies ({{ nonWorkRoleCompetencies.length }}) + + +
+ @for (c of nonWorkRoleCompetencies; track c.id) { +
+ {{ c.idNumber }} +
+ } +
+
+ } +
+
Manage on the Competencies tab
+
+ +
+ + Metadata +
+
+ + Difficulty Rating + + + @for (level of educationalLevels; track level) { + {{ level }} + } + + + + Exercise Purpose + + + @for (use of educationalUses; track use) { + {{ use }} + } + + + + Exercise Mode + + + @for (mode of courseModes; track mode) { + {{ mode }} + } + + +
+
+ + Subject + + +
+
+ + Keywords + + +
@if (canManageMsel()) {
- - Header Row Metadata (Height) when exported as an Excel file + Header Row Metadata (Height)
} -
- +
+ Date Created -
-
- + Created By - +
diff --git a/src/app/components/msel-info/msel-info.component.scss b/src/app/components/msel-info/msel-info.component.scss index 44aed21a..d2d6f6fe 100755 --- a/src/app/components/msel-info/msel-info.component.scss +++ b/src/app/components/msel-info/msel-info.component.scss @@ -81,6 +81,75 @@ a { align-self: center; } +.competency-section { + width: 100%; + margin-bottom: 16px; + text-align: left; + + .competency-header { + display: flex; + align-items: center; + gap: 4px; + font-weight: bold; + margin-bottom: 8px; + } + + .competency-title { + margin-right: 8px; + } + + .competency-count { + font-weight: normal; + font-size: 13px; + color: var(--mat-sys-on-surface-variant); + } + + .competency-panels-row { + display: flex; + gap: 16px; + align-items: flex-start; + } + + .competency-expansion { + flex: 1 1 50%; + background: transparent; + } + + .competency-list { + max-height: 300px; + overflow: auto; + } + + .competency-list-item { + display: flex; + padding: 4px 8px; + font-size: 13px; + border-bottom: 1px solid var(--mat-sys-outline-variant); + + &:last-child { + border-bottom: none; + } + } + + .competency-list-id { + flex: 0 0 100px; + font-weight: 500; + white-space: nowrap; + } + + .competency-list-name { + flex: 1 1 auto; + min-width: 0; + } + + .competency-hint { + font-size: 12px; + color: var(--mat-sys-on-surface-variant); + margin-top: 4px; + } +} + + .url-form-field { display: flex; flex-direction: column; @@ -139,6 +208,30 @@ a { width: 60%; } +.section-header { + display: flex; + align-items: center; + gap: 4px; + font-weight: bold; + margin-bottom: 8px; + text-align: left; + width: 100%; +} + +.lmt-dropdown-row { + display: flex; + gap: 16px; + width: 100%; +} + +.lmt-dropdown-width { + width: 250px; +} + +.meta-field-width { + width: 300px; +} + .half-button { margin-top: 18px; margin-left: 40px; diff --git a/src/app/components/msel-info/msel-info.component.ts b/src/app/components/msel-info/msel-info.component.ts index c29e3e7f..d62e5d14 100755 --- a/src/app/components/msel-info/msel-info.component.ts +++ b/src/app/components/msel-info/msel-info.component.ts @@ -6,8 +6,11 @@ import { Subject, Observable } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { TeamQuery } from 'src/app/data/team/team.query'; import { UserQuery } from 'src/app/data/user/user.query'; +import { UserDataService } from 'src/app/data/user/user-data.service'; import { + Competency, DataField, + MselCompetency, MselItemStatus, MselPage, MselUnit, @@ -28,6 +31,12 @@ import { DataFieldQuery } from 'src/app/data/data-field/data-field.query'; import { MselPageDataService } from 'src/app/data/msel-page/msel-page-data.service'; import { MselPageQuery } from 'src/app/data/msel-page/msel-page.query'; import { MselUnitQuery } from 'src/app/data/msel-unit/msel-unit.query'; +import { MselCompetencyDataService } from 'src/app/data/msel-competency/msel-competency-data.service'; +import { MselCompetencyQuery } from 'src/app/data/msel-competency/msel-competency.query'; +import { CompetencyFrameworkQuery } from 'src/app/data/competency-framework/competency-framework.query'; +import { CompetencyFrameworkDataService } from 'src/app/data/competency-framework/competency-framework-data.service'; +import { MatDialog } from '@angular/material/dialog'; +import { CompetencyOptionsDialogComponent } from '../competency-options-dialog/competency-options-dialog.component'; import { AngularEditorConfig } from '@kolkov/angular-editor'; import { ComnSettingsService } from '@cmusei/crucible-common'; import { HttpClient } from '@angular/common/http'; @@ -54,6 +63,13 @@ export class MselInfoComponent implements OnDestroy, OnInit { userList: User[] = []; teamList: Team[] = []; mselUnitList: MselUnit[] = []; + mselCompetencyList: MselCompetency[] = []; + workRoleCount = 0; + workRoleCompetencies: Competency[] = []; + nonWorkRoleCompetencies: Competency[] = []; + competencyFrameworkNames: string[] = []; + private competencyTypeCache = new Map(); + creatorName = 'unknown'; scoringModelList: ScoringModel[] = []; itemStatus: MselItemStatus[] = [ MselItemStatus.Pending, @@ -63,8 +79,12 @@ export class MselInfoComponent implements OnDestroy, OnInit { MselItemStatus.Complete, MselItemStatus.Archived, ]; + educationalLevels: string[] = ['Beginner', 'Intermediate', 'Advanced']; + educationalUses: string[] = ['Assessment', 'Instruction', 'Professional Support']; + courseModes: string[] = ['Online', 'Onsite', 'Blended']; viewUrl: string; starterUrl: string; + assessorUrl: string; mselPages: MselPage[] = []; newMselPage = {} as MselPage; changedMselPage = {} as MselPage; @@ -142,6 +162,7 @@ export class MselInfoComponent implements OnDestroy, OnInit { public dialogService: DialogService, private teamQuery: TeamQuery, private userQuery: UserQuery, + private userDataService: UserDataService, private dataFieldQuery: DataFieldQuery, private mselDataService: MselDataService, private mselQuery: MselQuery, @@ -150,6 +171,11 @@ export class MselInfoComponent implements OnDestroy, OnInit { private mselPageDataService: MselPageDataService, private mselPageQuery: MselPageQuery, private mselUnitQuery: MselUnitQuery, + private mselCompetencyDataService: MselCompetencyDataService, + private mselCompetencyQuery: MselCompetencyQuery, + private competencyFrameworkQuery: CompetencyFrameworkQuery, + private competencyFrameworkDataService: CompetencyFrameworkDataService, + private dialog: MatDialog, private permissionDataService: PermissionDataService, private changeDetectorRef: ChangeDetectorRef, private settingsService: ComnSettingsService, @@ -168,7 +194,9 @@ export class MselInfoComponent implements OnDestroy, OnInit { this.viewUrl = document.baseURI + 'msel/' + this.msel.id + '/view'; this.starterUrl = document.baseURI + 'starter/?msel=' + this.msel.id; + this.assessorUrl = document.baseURI + 'assess/?msel=' + this.msel.id; this.mselPageDataService.loadByMsel(msel.id); + this.mselCompetencyDataService.loadByMsel(msel.id); this.newMselPage.mselId = msel.id; } this.savedStartTime = new Date(msel.startTime); @@ -181,6 +209,7 @@ export class MselInfoComponent implements OnDestroy, OnInit { } // Fetch integration names for deployed integrations this.fetchIntegrationNames(); + this.resolveCreatorName(); } }); // subscribe to MSEL loading flag @@ -195,6 +224,7 @@ export class MselInfoComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribe$)) .subscribe((users) => { this.userList = users; + this.resolveCreatorName(); }); // subscribe to teams this.teamQuery @@ -210,6 +240,27 @@ export class MselInfoComponent implements OnDestroy, OnInit { .subscribe((mselUnits) => { this.mselUnitList = mselUnits; }); + // subscribe to mselCompetencies + this.mselCompetencyQuery + .selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((mselCompetencies) => { + this.mselCompetencyList = mselCompetencies; + const isWorkRole = (mc: MselCompetency) => { + const id = mc.competency?.idNumber || ''; + return id.includes('WRL') || /^[A-Z]{2}-[A-Z]{3}-\d+$/.test(id); + }; + this.workRoleCompetencies = mselCompetencies + .filter(mc => isWorkRole(mc) && mc.competency) + .map(mc => mc.competency) + .sort((a, b) => (a.idNumber || '').localeCompare(b.idNumber || '')); + this.nonWorkRoleCompetencies = mselCompetencies + .filter(mc => !isWorkRole(mc) && mc.competency) + .map(mc => mc.competency) + .sort((a, b) => (a.idNumber || '').localeCompare(b.idNumber || '')); + this.workRoleCount = this.workRoleCompetencies.length; + this.updateFrameworkNames(); + }); // subscribe to MselPages this.mselPageQuery .selectAll() @@ -276,6 +327,59 @@ export class MselInfoComponent implements OnDestroy, OnInit { .subscribe(() => { this.changeDetectorRef.markForCheck(); }); + this.competencyFrameworkDataService.load(); + this.competencyFrameworkQuery.selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(() => this.updateFrameworkNames()); + } + + private updateFrameworkNames() { + const fwIds = new Set(); + for (const mc of this.mselCompetencyList) { + if (mc.competency?.competencyFrameworkId) { + fwIds.add(mc.competency.competencyFrameworkId); + } + } + const frameworks = this.competencyFrameworkQuery.getAll(); + this.competencyFrameworkNames = [...fwIds] + .map(id => frameworks.find(f => f.id === id)?.name || '') + .filter(n => n) + .sort(); + } + + getCompetencyTooltip(comp: Competency): string { + return comp?.shortName || ''; + } + + private getCompetencyType(comp: Competency): string { + if (!comp || !comp.id) return ''; + + // Check cache first + if (this.competencyTypeCache.has(comp.id)) { + return this.competencyTypeCache.get(comp.id) || ''; + } + + // Derive from ID pattern + const idNumber = comp.idNumber || ''; + let type = ''; + + if (idNumber.includes('WRL')) { + type = 'Work Role'; + } else if (/^[TKSA][\d-]/.test(idNumber)) { + const prefixMap: Record = { + 'T': 'Task', 'K': 'Knowledge', 'S': 'Skill', 'A': 'Ability', + }; + type = prefixMap[idNumber.charAt(0)] || ''; + } else if (/^[A-Z]{2}-[A-Z]{3}-\d+$/.test(idNumber)) { + type = 'Work Role'; + } else if (/^[A-Z]{3}$/.test(idNumber)) { + type = 'Specialty Area'; + } else if (/^[A-Z]{2}$/.test(idNumber)) { + type = 'Category'; + } + + this.competencyTypeCache.set(comp.id, type); + return type; } getUserName(userId: string) { @@ -283,6 +387,24 @@ export class MselInfoComponent implements OnDestroy, OnInit { return user ? user.name : 'unknown'; } + private resolveCreatorName(): void { + if (!this.msel?.createdBy) { + this.creatorName = 'unknown'; + return; + } + const user = this.userList.find(u => u.id === this.msel.createdBy); + if (user) { + this.creatorName = user.name; + } else { + this.userDataService.loadById(this.msel.createdBy) + .pipe(takeUntil(this.unsubscribe$)) + .subscribe({ + next: (u) => { this.creatorName = u?.name || 'unknown'; }, + error: () => { this.creatorName = 'unknown'; } + }); + } + } + saveChanges() { this.mselDataService.updateMsel(this.msel); this.isChanged = false; @@ -318,6 +440,47 @@ export class MselInfoComponent implements OnDestroy, OnInit { this.msel.hasRole(this.loggedInUserId, '').owner; } + openCompetencyPicker(): void { + const existingIdNumbers = this.mselCompetencyList.map(mc => mc.competency?.idNumber).filter(Boolean); + const dialogRef = this.dialog.open(CompetencyOptionsDialogComponent, { + width: '900px', + maxWidth: '95vw', + maxHeight: '90vh', + data: { + dataFieldId: null, + dataOptions: existingIdNumbers.map(idNumber => ({ optionName: idNumber })), + canEdit: this.canEditMsel() + } + }); + dialogRef.afterClosed().subscribe((updatedOptions) => { + if (!updatedOptions) return; + const newIdNumbers = new Set(updatedOptions.map((o: any) => o.optionName)); + const existingMap = new Map(); + for (const mc of this.mselCompetencyList) { + if (mc.competency?.idNumber) { + existingMap.set(mc.competency.idNumber, mc); + } + } + // Remove deselected + for (const [idNumber, mc] of existingMap) { + if (!newIdNumbers.has(idNumber)) { + this.mselCompetencyDataService.delete(mc.id); + } + } + // Add newly selected — need to resolve idNumber to competencyId + // The dialog returns DataOption-shaped objects with optionName = idNumber + // We need the competency ID, which we can get from the dialog's competencies + for (const opt of updatedOptions) { + if (!existingMap.has(opt.optionName) && opt.competencyId) { + this.mselCompetencyDataService.add({ + mselId: this.msel.id, + competencyId: opt.competencyId + }); + } + } + }); + } + galleryWarningMessage() { let warningMessage = ''; if (this.msel.useGallery && !this.msel.galleryExhibitId) { diff --git a/src/app/components/msel-view/msel-view.component.html b/src/app/components/msel-view/msel-view.component.html index 5ba47cd6..788ceacc 100755 --- a/src/app/components/msel-view/msel-view.component.html +++ b/src/app/components/msel-view/msel-view.component.html @@ -149,8 +149,8 @@ @if (df.dataType.toString() === 'Checkbox') {
- +
} @@ -162,12 +162,23 @@ } + + @if (df.dataType.toString() === 'Competency') { + +
+ @for (compId of getDisplayValue(item, df.name).split(',').map(s => s.trim()).filter(s => s); track compId; let isLast = $last) { + {{ compId }}@if (!isLast) {, } + } +
+ + } @if (df.dataType.toString() !== 'Html' && df.dataType.toString() !== 'Url' && df.dataType.toString() !== 'User' && df.dataType.toString() !== 'Checkbox' && - df.dataType.toString() !== 'DateTime') { + df.dataType.toString() !== 'DateTime' && + df.dataType.toString() !== 'Competency') {
{{ getDisplayValue(item, df.name) }} diff --git a/src/app/components/msel-view/msel-view.component.ts b/src/app/components/msel-view/msel-view.component.ts index de1b14dd..9568ca67 100755 --- a/src/app/components/msel-view/msel-view.component.ts +++ b/src/app/components/msel-view/msel-view.component.ts @@ -5,6 +5,7 @@ import { Component, Input, OnDestroy, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Sort } from '@angular/material/sort'; import { Subject, Subscription, Observable, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; import { takeUntil, debounceTime, @@ -15,10 +16,13 @@ import { import { ComnSettingsService, Theme } from '@cmusei/crucible-common'; import { Card, + Competency, + CompetencyFramework, DataField, DataValue, Move, Msel, + MselCompetency, Organization, ScenarioEvent, Team, @@ -43,6 +47,10 @@ import { ScenarioEventViewIndexing, } from 'src/app/data/scenario-event/scenario-event-data.service'; import { ScenarioEventQuery } from 'src/app/data/scenario-event/scenario-event.query'; +import { MselCompetencyQuery } from 'src/app/data/msel-competency/msel-competency.query'; +import { MselCompetencyDataService } from 'src/app/data/msel-competency/msel-competency-data.service'; +import { CompetencyFrameworkQuery } from 'src/app/data/competency-framework/competency-framework.query'; +import { CompetencyFrameworkDataService } from 'src/app/data/competency-framework/competency-framework-data.service'; import { UIDataService } from 'src/app/data/ui/ui-data.service'; import { AngularEditorConfig } from '@kolkov/angular-editor'; @@ -98,6 +106,8 @@ export class MselViewComponent implements OnDestroy, ScenarioEventView { keyUp = new Subject(); private subscription: Subscription; private unsubscribe$ = new Subject(); + private competencyCache = new Map(); + private competencyTypeCache = new Map(); viewConfig: AngularEditorConfig = { editable: false, height: 'auto', @@ -116,7 +126,6 @@ export class MselViewComponent implements OnDestroy, ScenarioEventView { constructor( private activatedRoute: ActivatedRoute, - private settingsService: ComnSettingsService, private mselDataService: MselDataService, private mselQuery: MselQuery, private organizationQuery: OrganizationQuery, @@ -129,7 +138,13 @@ export class MselViewComponent implements OnDestroy, ScenarioEventView { private moveQuery: MoveQuery, private scenarioEventDataService: ScenarioEventDataService, private scenarioEventQuery: ScenarioEventQuery, - private uiDataService: UIDataService + private mselCompetencyQuery: MselCompetencyQuery, + private mselCompetencyDataService: MselCompetencyDataService, + private competencyFrameworkQuery: CompetencyFrameworkQuery, + private competencyFrameworkDataService: CompetencyFrameworkDataService, + private uiDataService: UIDataService, + private http: HttpClient, + private settingsService: ComnSettingsService ) { // subscribe to the route parameters. Used when viewing independently. this.activatedRoute.params @@ -140,6 +155,11 @@ export class MselViewComponent implements OnDestroy, ScenarioEventView { this.mselDataService.loadById(mselId); this.loadInitialData(mselId); this.mselDataService.setActive(mselId); + // Call xAPI for viewed event (only on view page, not build page) + const baseUrl = this.settingsService.settings.ApiUrl.endsWith('/') + ? this.settingsService.settings.ApiUrl + : this.settingsService.settings.ApiUrl + '/'; + this.http.post(`${baseUrl}api/xapi/viewed/msel/${mselId}`, {}).subscribe(); } }); // subscribe to the route query parameters. Used when editing the MSEL and checking the view. @@ -171,6 +191,7 @@ export class MselViewComponent implements OnDestroy, ScenarioEventView { .subscribe((dataFields) => { this.sortedDataFields = this.getSortedDataFields(dataFields); this.scenarioEventDataService.updateScenarioEventViewDataFields(this); + this.scenarioEventDataService.updateScenarioEventViewDataValues(this); this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents( this ); @@ -248,6 +269,18 @@ export class MselViewComponent implements OnDestroy, ScenarioEventView { .subscribe((teams) => { this.teamList = teams; }); + // observe the MselCompetencies for tooltip data + this.mselCompetencyQuery + .selectAll() + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((mselCompetencies) => { + this.competencyCache.clear(); + for (const mc of mselCompetencies) { + if (mc.competency && mc.competency.idNumber) { + this.competencyCache.set(mc.competency.idNumber, mc.competency); + } + } + }); // subscribe to filter string changes for debounce this.subscription = this.keyUp .pipe( @@ -285,6 +318,8 @@ export class MselViewComponent implements OnDestroy, ScenarioEventView { this.dataFieldDataService.loadByMsel(mselId); this.dataValueDataService.loadByMsel(mselId); this.scenarioEventDataService.loadByMsel(mselId); + this.mselCompetencyDataService.loadByMsel(mselId); + this.competencyFrameworkDataService.load(); } applyFilter(filterValue: string) { @@ -372,6 +407,42 @@ export class MselViewComponent implements OnDestroy, ScenarioEventView { return users; } + getCompetencyTooltip(idNumber: string): string { + const comp = this.competencyCache.get(idNumber); + return comp?.shortName || ''; + } + + private getCompetencyType(comp: Competency): string { + if (!comp || !comp.id) return ''; + + // Check cache first + if (this.competencyTypeCache.has(comp.id)) { + return this.competencyTypeCache.get(comp.id) || ''; + } + + // Derive from ID pattern + const idNumber = comp.idNumber || ''; + let type = ''; + + if (idNumber.includes('WRL')) { + type = 'Work Role'; + } else if (/^[TKSA][\d-]/.test(idNumber)) { + const prefixMap: Record = { + 'T': 'Task', 'K': 'Knowledge', 'S': 'Skill', 'A': 'Ability', + }; + type = prefixMap[idNumber.charAt(0)] || ''; + } else if (/^[A-Z]{2}-[A-Z]{3}-\d+$/.test(idNumber)) { + type = 'Work Role'; + } else if (/^[A-Z]{3}$/.test(idNumber)) { + type = 'Specialty Area'; + } else if (/^[A-Z]{2}$/.test(idNumber)) { + type = 'Category'; + } + + this.competencyTypeCache.set(comp.id, type); + return type; + } + trackByFn(index, item) { return item.id; } diff --git a/src/app/components/msel/msel.component.html b/src/app/components/msel/msel.component.html index 3a150a07..15ca3717 100755 --- a/src/app/components/msel/msel.component.html +++ b/src/app/components/msel/msel.component.html @@ -139,6 +139,9 @@ @if (selectedTab === 'CITE Duties') { } + @if (selectedTab === 'Competencies') { + + } @if (selectedTab === 'Scenario Events') { @@ -146,6 +149,10 @@ @if (selectedTab === 'Exercise View') { } + @if (selectedTab === 'Assessor View') { + + } @if (selectedTab === 'Invitations') { Invitations diff --git a/src/app/components/msel/msel.component.ts b/src/app/components/msel/msel.component.ts index 2918fb96..f0c4c68a 100755 --- a/src/app/components/msel/msel.component.ts +++ b/src/app/components/msel/msel.component.ts @@ -48,6 +48,7 @@ export class MselComponent implements OnDestroy { @Input() loggedInUserId: string; @Input() canEditMsel: boolean; @Input() canAccessAdminSection: boolean; + @Input() canEditCheckboxes = false; @Input() userTheme$: Observable; @Output() deleteThisMsel = new EventEmitter(); @ViewChild('tabGroup0', { static: false }) tabGroup0: MatTabGroup; @@ -56,8 +57,9 @@ export class MselComponent implements OnDestroy { tabList: string[] = [ 'Info', 'Contributors', - 'Teams', + 'Competencies', 'Data Fields', + 'Teams', 'Organizations', 'Moves', 'Player Apps', @@ -66,6 +68,7 @@ export class MselComponent implements OnDestroy { 'CITE Duties', 'Scenario Events', 'Exercise View', + 'Assessor View', 'MSEL Playbook', 'Invitations', ]; @@ -80,8 +83,10 @@ export class MselComponent implements OnDestroy { ['Gallery Cards', 'mdi-view-grid-outline'], ['CITE Actions', 'mdi-clipboard-check-outline'], ['CITE Duties', 'mdi-clipboard-account-outline'], + ['Competencies', 'mdi-certificate-outline'], ['Scenario Events', 'mdi-chart-timeline'], ['Exercise View', 'mdi-eye-outline'], + ['Assessor View', 'mdi-clipboard-check-multiple-outline'], ['MSEL Playbook', 'mdi-book'], ['Invitations', 'mdi-email-open-outline'], ]); diff --git a/src/app/components/scenario-event-list/scenario-event-list.component.html b/src/app/components/scenario-event-list/scenario-event-list.component.html index 4cbf1654..533311d3 100755 --- a/src/app/components/scenario-event-list/scenario-event-list.component.html +++ b/src/app/components/scenario-event-list/scenario-event-list.component.html @@ -33,6 +33,18 @@
    + @if (!showSearch) { + + } + @if (showSearch) { + + } @if ((canEditMsel() || msel.hasRole(loggedInUserId, null).owner)) { - @if (!showSearch) { - - } - @if (showSearch) { - - }
diff --git a/src/app/components/scenario-event-list/scenario-event-list.component.scss b/src/app/components/scenario-event-list/scenario-event-list.component.scss index 059a3f9b..e33c002e 100755 --- a/src/app/components/scenario-event-list/scenario-event-list.component.scss +++ b/src/app/components/scenario-event-list/scenario-event-list.component.scss @@ -5,14 +5,17 @@ @use "@angular/material" as mat; :host { - @include mat.form-field-density(-5); // Adjust -3 for this specific form field + @include mat.form-field-density(-5); + flex: 1; + overflow: hidden; + min-height: 0; } .container { display: flex; flex-direction: column; flex: 1; - height: calc(100% - 60px); + height: 100%; overflow: auto; background-color: var(--mat-sys-background); color: var(--mat-sys-on-background); diff --git a/src/app/components/scenario-event-list/scenario-event-list.component.ts b/src/app/components/scenario-event-list/scenario-event-list.component.ts index a3a4370b..49bfabb3 100755 --- a/src/app/components/scenario-event-list/scenario-event-list.component.ts +++ b/src/app/components/scenario-event-list/scenario-event-list.component.ts @@ -254,6 +254,7 @@ export class ScenarioEventListComponent this.allDataFields = dataFields; this.setSortedDataFields(); this.scenarioEventDataService.updateScenarioEventViewDataFields(this); + this.scenarioEventDataService.updateScenarioEventViewDataValues(this); this.scenarioEventDataService.updateScenarioEventViewDisplayedEvents( this ); @@ -453,7 +454,7 @@ export class ScenarioEventListComponent getMselUsers(): User[] { let users = []; - this.msel.teams.forEach((team) => { + this.msel.teams?.forEach((team) => { team.users.forEach((user) => { users.push({ ...user }); }); @@ -488,7 +489,7 @@ export class ScenarioEventListComponent this.organizationList.forEach((o) => { orgs.push(o.shortName); }); - this.msel.teams.forEach((t) => { + this.msel.teams?.forEach((t) => { orgs.push(t.shortName); }); // Remove duplicates @@ -499,7 +500,7 @@ export class ScenarioEventListComponent getSortedTeamOptions(): string[] { let teams: string[] = []; - this.msel.teams.forEach((t) => { + this.msel.teams?.forEach((t) => { teams.push(t.shortName); }); teams = teams.sort((a, b) => (a < b ? -1 : 1)); diff --git a/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.html b/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.html new file mode 100644 index 00000000..eabc6fa3 --- /dev/null +++ b/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.html @@ -0,0 +1,50 @@ + + +

{{ data.action === 'add' ? 'Add' : 'Remove' }} Team from Related

+
+

Also {{ actionVerb }} team {{ data.teamName }} {{ preposition }} the {{ data.competencies.length }} + related competenc{{ data.competencies.length === 1 ? 'y' : 'ies' }} on the MSEL?

+ + + + + + + + + + + + + + + ID + {{ mc.competency?.idNumber }} + + + + Name + + {{ mc.competency?.shortName }} + + + + + + +
+ +
+ + +
diff --git a/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.scss b/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.scss new file mode 100644 index 00000000..230b6c45 --- /dev/null +++ b/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.scss @@ -0,0 +1,34 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +.propagate-table { + max-height: 300px; + overflow: auto; + margin-top: 8px; +} + +.col-select { + max-width: 48px; + flex: 0 0 48px; +} + +.col-id { + max-width: 120px; + flex: 0 0 120px; +} + +.col-name { + flex: 1; +} + +.action-spacer { + flex: 1; +} + +mat-dialog-actions { + gap: 8px; + padding: 8px 24px 16px; +} diff --git a/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.ts b/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.ts new file mode 100644 index 00000000..235862dc --- /dev/null +++ b/src/app/components/team-competency-propagate-dialog/team-competency-propagate-dialog.component.ts @@ -0,0 +1,62 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the +// project root for license information. + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { SelectionModel } from '@angular/cdk/collections'; +import { MselCompetency } from 'src/app/generated/blueprint.api'; + +export interface TeamCompetencyPropagateData { + teamName: string; + action: 'add' | 'remove'; + competencies: MselCompetency[]; +} + +@Component({ + selector: 'app-team-competency-propagate-dialog', + templateUrl: './team-competency-propagate-dialog.component.html', + styleUrls: ['./team-competency-propagate-dialog.component.scss'], + standalone: false +}) +export class TeamCompetencyPropagateDialogComponent { + selection = new SelectionModel(true, []); + displayedColumns = ['select', 'idNumber', 'shortName']; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: TeamCompetencyPropagateData + ) { + this.dialogRef.disableClose = true; + // Pre-select all + this.selection.select(...data.competencies); + } + + get actionVerb(): string { + return this.data.action === 'add' ? 'add' : 'remove'; + } + + get preposition(): string { + return this.data.action === 'add' ? 'to' : 'from'; + } + + isAllSelected(): boolean { + return this.selection.selected.length === this.data.competencies.length; + } + + toggleAll(): void { + if (this.isAllSelected()) { + this.selection.clear(); + } else { + this.selection.select(...this.data.competencies); + } + } + + onConfirm(): void { + this.dialogRef.close(this.selection.selected); + } + + onCancel(): void { + this.dialogRef.close(null); + } +} diff --git a/src/app/data/competency-framework/competency-framework-data.service.ts b/src/app/data/competency-framework/competency-framework-data.service.ts new file mode 100644 index 00000000..fcfc8973 --- /dev/null +++ b/src/app/data/competency-framework/competency-framework-data.service.ts @@ -0,0 +1,180 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { CompetencyFrameworkStore } from './competency-framework.store'; +import { CompetencyFrameworkQuery } from './competency-framework.query'; +import { Injectable } from '@angular/core'; +import { UntypedFormControl } from '@angular/forms'; +import { PageEvent } from '@angular/material/paginator'; +import { Router, ActivatedRoute } from '@angular/router'; +import { + CompetencyFramework, + CompetencyFrameworkService, +} from 'src/app/generated/blueprint.api'; +import { map, take, tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class CompetencyFrameworkDataService { + readonly CompetencyFrameworkList: Observable; + readonly filterControl = new UntypedFormControl(); + private filterTerm: Observable; + private sortColumn: Observable; + private sortIsAscending: Observable; + private _pageEvent: PageEvent = { length: 0, pageIndex: 0, pageSize: 10 }; + readonly pageEvent = new BehaviorSubject(this._pageEvent); + private pageSize: Observable; + private pageIndex: Observable; + + constructor( + private competencyFrameworkStore: CompetencyFrameworkStore, + private competencyFrameworkQuery: CompetencyFrameworkQuery, + private competencyFrameworkService: CompetencyFrameworkService, + private router: Router, + private activatedRoute: ActivatedRoute + ) { + this.filterTerm = activatedRoute.queryParamMap.pipe( + map((params) => params.get('competencyFrameworkmask') || '') + ); + this.filterControl.valueChanges.subscribe((term) => { + this.router.navigate([], { + queryParams: { competencyFrameworkmask: term }, + queryParamsHandling: 'merge', + }); + }); + this.sortColumn = activatedRoute.queryParamMap.pipe( + map((params) => params.get('sorton') || 'name') + ); + this.sortIsAscending = activatedRoute.queryParamMap.pipe( + map((params) => (params.get('sortdir') || 'asc') === 'asc') + ); + this.pageSize = activatedRoute.queryParamMap.pipe( + map((params) => parseInt(params.get('pagesize') || '20', 10)) + ); + this.pageIndex = activatedRoute.queryParamMap.pipe( + map((params) => parseInt(params.get('pageindex') || '0', 10)) + ); + this.CompetencyFrameworkList = combineLatest([ + this.competencyFrameworkQuery.selectAll(), + this.filterTerm, + this.sortColumn, + this.sortIsAscending, + this.pageSize, + this.pageIndex, + ]).pipe( + map( + ([ + items, + filterTerm, + sortColumn, + sortIsAscending, + pageSize, + pageIndex, + ]) => + items + ? (items as CompetencyFramework[]) + .filter( + (cf) => + cf.name?.toLowerCase().includes(filterTerm.toLowerCase()) || + cf.source?.toLowerCase().includes(filterTerm.toLowerCase()) || + cf.version?.toLowerCase().includes(filterTerm.toLowerCase())) + : [] + ) + ); + } + + load() { + this.competencyFrameworkStore.setLoading(true); + this.competencyFrameworkService + .getCompetencyFrameworks() + .pipe( + tap(() => { + this.competencyFrameworkStore.setLoading(false); + }), + take(1) + ) + .subscribe( + (templates) => { + this.competencyFrameworkStore.upsertMany(templates); + }, + (error) => { } + ); + } + + loadById(id: string) { + this.competencyFrameworkStore.setLoading(true); + return this.competencyFrameworkService + .getCompetencyFramework(id) + .pipe( + tap(() => { + this.competencyFrameworkStore.setLoading(false); + }), + take(1) + ) + .subscribe((s) => { + this.competencyFrameworkStore.upsert(s.id, { ...s }); + }); + } + + unload() { + this.competencyFrameworkStore.set([]); + } + + add(competencyFramework: CompetencyFramework) { + this.competencyFrameworkStore.setLoading(true); + this.competencyFrameworkService + .createCompetencyFramework(competencyFramework) + .pipe( + tap(() => { + this.competencyFrameworkStore.setLoading(false); + }), + take(1) + ) + .subscribe((s) => { + this.competencyFrameworkStore.add(s); + }); + } + + update(competencyFramework: CompetencyFramework) { + this.competencyFrameworkStore.setLoading(true); + this.competencyFrameworkService + .updateCompetencyFramework(competencyFramework.id, competencyFramework) + .pipe( + tap(() => { + this.competencyFrameworkStore.setLoading(false); + }), + take(1) + ) + .subscribe((n) => { + this.updateStore(n); + }); + } + + delete(id: string) { + this.competencyFrameworkService + .deleteCompetencyFramework(id) + .pipe(take(1)) + .subscribe((r) => { + this.deleteFromStore(id); + }); + } + + setActive(id: string) { + this.competencyFrameworkStore.setActive(id); + } + + setPageEvent(pageEvent: PageEvent) { + this.competencyFrameworkStore.update({ pageEvent: pageEvent }); + } + + updateStore(competencyFramework: CompetencyFramework) { + this.competencyFrameworkStore.upsert(competencyFramework.id, competencyFramework); + } + + deleteFromStore(id: string) { + this.competencyFrameworkStore.remove(id); + } + +} diff --git a/src/app/data/competency-framework/competency-framework.query.ts b/src/app/data/competency-framework/competency-framework.query.ts new file mode 100644 index 00000000..67d778c9 --- /dev/null +++ b/src/app/data/competency-framework/competency-framework.query.ts @@ -0,0 +1,28 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { Order, QueryConfig, QueryEntity } from '@datorama/akita'; +import { + CompetencyFrameworkState, + CompetencyFrameworkStore, +} from './competency-framework.store'; +import { CompetencyFramework } from 'src/app/generated/blueprint.api'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@QueryConfig({ + sortBy: 'name', + sortByOrder: Order.ASC, +}) +@Injectable({ + providedIn: 'root', +}) +export class CompetencyFrameworkQuery extends QueryEntity { + constructor(protected store: CompetencyFrameworkStore) { + super(store); + } + + selectById(id: string): Observable { + return this.selectEntity(id); + } +} diff --git a/src/app/data/competency-framework/competency-framework.store.ts b/src/app/data/competency-framework/competency-framework.store.ts new file mode 100644 index 00000000..8232be1a --- /dev/null +++ b/src/app/data/competency-framework/competency-framework.store.ts @@ -0,0 +1,18 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; +import { CompetencyFramework } from 'src/app/generated/blueprint.api'; +import { Injectable } from '@angular/core'; + +export interface CompetencyFrameworkState extends EntityState {} + +@Injectable({ + providedIn: 'root', +}) +@StoreConfig({ name: 'competencyFrameworks' }) +export class CompetencyFrameworkStore extends EntityStore { + constructor() { + super(); + } +} diff --git a/src/app/data/msel-competency/msel-competency-data.service.ts b/src/app/data/msel-competency/msel-competency-data.service.ts new file mode 100644 index 00000000..387100a7 --- /dev/null +++ b/src/app/data/msel-competency/msel-competency-data.service.ts @@ -0,0 +1,83 @@ +// Copyright 2024 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { MselCompetencyStore } from './msel-competency.store'; +import { MselCompetencyQuery } from './msel-competency.query'; +import { Injectable } from '@angular/core'; +import { + MselCompetency, + MselCompetencyService, +} from 'src/app/generated/blueprint.api'; +import { take, tap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class MselCompetencyDataService { + readonly MselCompetencyList: Observable; + + constructor( + private mselCompetencyStore: MselCompetencyStore, + private mselCompetencyQuery: MselCompetencyQuery, + private mselCompetencyService: MselCompetencyService + ) { + this.MselCompetencyList = this.mselCompetencyQuery.selectAll(); + } + + loadByMsel(mselId: string) { + this.mselCompetencyStore.setLoading(true); + this.mselCompetencyService + .getMselCompetencies(mselId) + .pipe( + tap(() => { + this.mselCompetencyStore.setLoading(false); + }), + take(1) + ) + .subscribe( + (mselCompetencies) => { + this.mselCompetencyStore.set(mselCompetencies); + }, + (error) => { + this.mselCompetencyStore.set([]); + } + ); + } + + unload() { + this.mselCompetencyStore.set([]); + } + + add(mselCompetency: MselCompetency) { + this.mselCompetencyStore.setLoading(true); + this.mselCompetencyService + .createMselCompetency(mselCompetency) + .pipe( + tap(() => { + this.mselCompetencyStore.setLoading(false); + }), + take(1) + ) + .subscribe((s) => { + this.mselCompetencyStore.add(s); + }); + } + + delete(id: string) { + this.mselCompetencyService + .deleteMselCompetency(id) + .pipe(take(1)) + .subscribe((r) => { + this.mselCompetencyStore.remove(id); + }); + } + + updateStore(mselCompetency: MselCompetency) { + this.mselCompetencyStore.upsert(mselCompetency.id, mselCompetency); + } + + deleteFromStore(id: string) { + this.mselCompetencyStore.remove(id); + } +} diff --git a/src/app/data/msel-competency/msel-competency.query.ts b/src/app/data/msel-competency/msel-competency.query.ts new file mode 100644 index 00000000..9755bb07 --- /dev/null +++ b/src/app/data/msel-competency/msel-competency.query.ts @@ -0,0 +1,28 @@ +// Copyright 2024 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { Order, QueryConfig, QueryEntity } from '@datorama/akita'; +import { + MselCompetencyState, + MselCompetencyStore, +} from './msel-competency.store'; +import { MselCompetency } from 'src/app/generated/blueprint.api'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@QueryConfig({ + sortBy: 'competencyId', + sortByOrder: Order.ASC, +}) +@Injectable({ + providedIn: 'root', +}) +export class MselCompetencyQuery extends QueryEntity { + constructor(protected store: MselCompetencyStore) { + super(store); + } + + selectById(id: string): Observable { + return this.selectEntity(id); + } +} diff --git a/src/app/data/msel-competency/msel-competency.store.ts b/src/app/data/msel-competency/msel-competency.store.ts new file mode 100644 index 00000000..26c37cb7 --- /dev/null +++ b/src/app/data/msel-competency/msel-competency.store.ts @@ -0,0 +1,18 @@ +// Copyright 2024 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; +import { MselCompetency } from 'src/app/generated/blueprint.api'; +import { Injectable } from '@angular/core'; + +export interface MselCompetencyState extends EntityState {} + +@Injectable({ + providedIn: 'root', +}) +@StoreConfig({ name: 'mselCompetencies' }) +export class MselCompetencyStore extends EntityStore { + constructor() { + super(); + } +} diff --git a/src/app/data/msel/msel-data.service.ts b/src/app/data/msel/msel-data.service.ts index 602e7aac..4cb0eff1 100755 --- a/src/app/data/msel/msel-data.service.ts +++ b/src/app/data/msel/msel-data.service.ts @@ -66,6 +66,10 @@ export class MselPlus implements Msel { showIntegrationTargetOnScenarioEventList?: boolean; showIntegrationTargetOnExerciseView?: boolean; integrationTargetDisplayOrder?: number; + showTimeOnAssessorView?: boolean; + showMoveOnAssessorView?: boolean; + showGroupOnAssessorView?: boolean; + showIntegrationTargetOnAssessorView?: boolean; moves?: Array; dataFields?: Array; scenarioEvents?: Array; @@ -80,6 +84,12 @@ export class MselPlus implements Msel { galleryIntegrationType?: IntegrationType; citeIntegrationType?: IntegrationType; steamfitterIntegrationType?: IntegrationType; + educationalLevel?: string; + subject?: string; + keywords?: string; + educationalUse?: string; + courseMode?: string; + language?: string; hasRole(userId: string, scenarioEventId: string) { // initialize to no roles diff --git a/src/app/data/scenario-event/scenario-event-data.service.ts b/src/app/data/scenario-event/scenario-event-data.service.ts index 2e86648d..0e088680 100755 --- a/src/app/data/scenario-event/scenario-event-data.service.ts +++ b/src/app/data/scenario-event/scenario-event-data.service.ts @@ -105,6 +105,7 @@ export class ScenarioEventDataService { DataFieldType.Checkbox, DataFieldType.User, DataFieldType.Url, + DataFieldType.Competency, ]; loadByMsel(mselId: string) { diff --git a/src/app/data/team-competency/team-competency-data.service.ts b/src/app/data/team-competency/team-competency-data.service.ts new file mode 100644 index 00000000..f99dbf78 --- /dev/null +++ b/src/app/data/team-competency/team-competency-data.service.ts @@ -0,0 +1,95 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { TeamCompetencyStore } from './team-competency.store'; +import { TeamCompetencyQuery } from './team-competency.query'; +import { Injectable } from '@angular/core'; +import { + TeamCompetency, + TeamCompetencyService, +} from 'src/app/generated/blueprint.api'; +import { take, tap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class TeamCompetencyDataService { + readonly TeamCompetencyList: Observable; + + constructor( + private teamCompetencyStore: TeamCompetencyStore, + private teamCompetencyQuery: TeamCompetencyQuery, + private teamCompetencyService: TeamCompetencyService + ) { + this.TeamCompetencyList = this.teamCompetencyQuery.selectAll(); + } + + loadByMsel(mselId: string) { + this.teamCompetencyStore.setLoading(true); + this.teamCompetencyService + .getMselTeamCompetencies(mselId) + .pipe( + tap(() => { + this.teamCompetencyStore.setLoading(false); + }), + take(1) + ) + .subscribe( + (teamCompetencies) => { + this.teamCompetencyStore.set(teamCompetencies); + }, + (error) => { + this.teamCompetencyStore.set([]); + } + ); + } + + loadByTeam(teamId: string) { + this.teamCompetencyStore.setLoading(true); + this.teamCompetencyService + .getTeamCompetencies(teamId) + .pipe( + tap(() => { + this.teamCompetencyStore.setLoading(false); + }), + take(1) + ) + .subscribe( + (teamCompetencies) => { + this.teamCompetencyStore.set(teamCompetencies); + }, + (error) => { + this.teamCompetencyStore.set([]); + } + ); + } + + unload() { + this.teamCompetencyStore.set([]); + } + + add(teamCompetency: TeamCompetency) { + this.teamCompetencyStore.setLoading(true); + this.teamCompetencyService + .createTeamCompetency(teamCompetency) + .pipe( + tap(() => { + this.teamCompetencyStore.setLoading(false); + }), + take(1) + ) + .subscribe((s) => { + this.teamCompetencyStore.add(s); + }); + } + + delete(id: string) { + this.teamCompetencyService + .deleteTeamCompetency(id) + .pipe(take(1)) + .subscribe((r) => { + this.teamCompetencyStore.remove(id); + }); + } +} diff --git a/src/app/data/team-competency/team-competency.query.ts b/src/app/data/team-competency/team-competency.query.ts new file mode 100644 index 00000000..8a65155d --- /dev/null +++ b/src/app/data/team-competency/team-competency.query.ts @@ -0,0 +1,28 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { Order, QueryConfig, QueryEntity } from '@datorama/akita'; +import { + TeamCompetencyState, + TeamCompetencyStore, +} from './team-competency.store'; +import { TeamCompetency } from 'src/app/generated/blueprint.api'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@QueryConfig({ + sortBy: 'competencyId', + sortByOrder: Order.ASC, +}) +@Injectable({ + providedIn: 'root', +}) +export class TeamCompetencyQuery extends QueryEntity { + constructor(protected store: TeamCompetencyStore) { + super(store); + } + + selectById(id: string): Observable { + return this.selectEntity(id); + } +} diff --git a/src/app/data/team-competency/team-competency.store.ts b/src/app/data/team-competency/team-competency.store.ts new file mode 100644 index 00000000..e9257f44 --- /dev/null +++ b/src/app/data/team-competency/team-competency.store.ts @@ -0,0 +1,18 @@ +// Copyright 2026 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; +import { TeamCompetency } from 'src/app/generated/blueprint.api'; +import { Injectable } from '@angular/core'; + +export interface TeamCompetencyState extends EntityState {} + +@Injectable({ + providedIn: 'root', +}) +@StoreConfig({ name: 'teamCompetencies' }) +export class TeamCompetencyStore extends EntityStore { + constructor() { + super(); + } +} diff --git a/src/app/data/team/team-data.service.ts b/src/app/data/team/team-data.service.ts index 4411e403..bd0387e8 100755 --- a/src/app/data/team/team-data.service.ts +++ b/src/app/data/team/team-data.service.ts @@ -135,7 +135,7 @@ export class TeamDataService { load() { this.teamStore.setLoading(true); this.teamService - .getTeams() + .getMyTeams() .pipe( tap(() => { this.teamStore.setLoading(false); diff --git a/src/app/generated/blueprint.api/.openapi-generator/FILES b/src/app/generated/blueprint.api/.openapi-generator/FILES index 1bf3a776..b26272e5 100644 --- a/src/app/generated/blueprint.api/.openapi-generator/FILES +++ b/src/app/generated/blueprint.api/.openapi-generator/FILES @@ -11,6 +11,7 @@ api/catalogUnit.service.ts api/cite.service.ts api/citeAction.service.ts api/citeDuty.service.ts +api/competencyFramework.service.ts api/dataField.service.ts api/dataOption.service.ts api/dataValue.service.ts @@ -19,24 +20,30 @@ api/healthCheck.service.ts api/inject.service.ts api/injectType.service.ts api/invitation.service.ts +api/lmt.service.ts api/move.service.ts api/msel.service.ts +api/mselCompetency.service.ts api/mselPage.service.ts api/mselUnit.service.ts api/organization.service.ts api/player.service.ts api/playerApplication.service.ts api/playerApplicationTeam.service.ts +api/proficiencyLevel.service.ts +api/proficiencyScale.service.ts api/scenarioEvent.service.ts api/systemPermissions.service.ts api/systemRoles.service.ts api/team.service.ts +api/teamCompetency.service.ts api/teamUser.service.ts api/unit.service.ts api/unitUser.service.ts api/user.service.ts api/userMselRole.service.ts api/userTeamRole.service.ts +api/xApi.service.ts configuration.ts encoder.ts git_push.sh @@ -55,6 +62,10 @@ model/catalogUnit.ts model/citeAction.ts model/citeDuty.ts model/compareInfo.ts +model/competency.ts +model/competencyAssertion.ts +model/competencyFramework.ts +model/competencyFrameworkImportPreview.ts model/constructorInfo.ts model/createFromInjectsForm.ts model/cultureInfo.ts @@ -66,6 +77,8 @@ model/dataColumn.ts model/dataField.ts model/dataFieldType.ts model/dataOption.ts +model/dataOptionImportPreview.ts +model/dataOptionImportPreviewItem.ts model/dataSet.ts model/dataSetDateTime.ts model/dataTable.ts @@ -73,12 +86,14 @@ model/dataValue.ts model/dateTimeFormatInfo.ts model/dayOfWeek.ts model/digitShapes.ts +model/elementTypeCount.ts model/eventAttributes.ts model/eventInfo.ts model/eventType.ts model/exception.ts model/fieldAttributes.ts model/fieldInfo.ts +model/frameworkDeleteCheck.ts model/genericParameterAttributes.ts model/group.ts model/groupMembership.ts @@ -106,8 +121,10 @@ model/module.ts model/moduleHandle.ts model/move.ts model/msel.ts +model/mselCompetency.ts model/mselItemStatus.ts model/mselPage.ts +model/mselReference.ts model/mselRole.ts model/mselUnit.ts model/numberFormatInfo.ts @@ -118,6 +135,8 @@ model/permission.ts model/playerApplication.ts model/playerApplicationTeam.ts model/problemDetails.ts +model/proficiencyLevel.ts +model/proficiencyScale.ts model/propertyAttributes.ts model/propertyInfo.ts model/rightSideDisplay.ts @@ -142,6 +161,8 @@ model/structLayoutAttribute.ts model/systemPermission.ts model/systemRole.ts model/team.ts +model/teamCompetency.ts +model/teamPermission.ts model/teamRole.ts model/teamType.ts model/teamUser.ts diff --git a/src/app/generated/blueprint.api/api/api.ts b/src/app/generated/blueprint.api/api/api.ts index 37e50522..9a7f4aca 100644 --- a/src/app/generated/blueprint.api/api/api.ts +++ b/src/app/generated/blueprint.api/api/api.ts @@ -20,6 +20,8 @@ export * from './citeAction.service'; import { CiteActionService } from './citeAction.service'; export * from './citeDuty.service'; import { CiteDutyService } from './citeDuty.service'; +export * from './competencyFramework.service'; +import { CompetencyFrameworkService } from './competencyFramework.service'; export * from './dataField.service'; import { DataFieldService } from './dataField.service'; export * from './dataOption.service'; @@ -36,10 +38,14 @@ export * from './injectType.service'; import { InjectTypeService } from './injectType.service'; export * from './invitation.service'; import { InvitationService } from './invitation.service'; +export * from './lmt.service'; +import { LmtService } from './lmt.service'; export * from './move.service'; import { MoveService } from './move.service'; export * from './msel.service'; import { MselService } from './msel.service'; +export * from './mselCompetency.service'; +import { MselCompetencyService } from './mselCompetency.service'; export * from './mselPage.service'; import { MselPageService } from './mselPage.service'; export * from './mselUnit.service'; @@ -52,6 +58,10 @@ export * from './playerApplication.service'; import { PlayerApplicationService } from './playerApplication.service'; export * from './playerApplicationTeam.service'; import { PlayerApplicationTeamService } from './playerApplicationTeam.service'; +export * from './proficiencyLevel.service'; +import { ProficiencyLevelService } from './proficiencyLevel.service'; +export * from './proficiencyScale.service'; +import { ProficiencyScaleService } from './proficiencyScale.service'; export * from './scenarioEvent.service'; import { ScenarioEventService } from './scenarioEvent.service'; export * from './systemPermissions.service'; @@ -60,6 +70,8 @@ export * from './systemRoles.service'; import { SystemRolesService } from './systemRoles.service'; export * from './team.service'; import { TeamService } from './team.service'; +export * from './teamCompetency.service'; +import { TeamCompetencyService } from './teamCompetency.service'; export * from './teamUser.service'; import { TeamUserService } from './teamUser.service'; export * from './unit.service'; @@ -72,4 +84,6 @@ export * from './userMselRole.service'; import { UserMselRoleService } from './userMselRole.service'; export * from './userTeamRole.service'; import { UserTeamRoleService } from './userTeamRole.service'; -export const APIS = [CardService, CardTeamService, CatalogService, CatalogInjectService, CatalogUnitService, CiteService, CiteActionService, CiteDutyService, DataFieldService, DataOptionService, DataValueService, GroupService, HealthCheckService, InjectService, InjectTypeService, InvitationService, MoveService, MselService, MselPageService, MselUnitService, OrganizationService, PlayerService, PlayerApplicationService, PlayerApplicationTeamService, ScenarioEventService, SystemPermissionsService, SystemRolesService, TeamService, TeamUserService, UnitService, UnitUserService, UserService, UserMselRoleService, UserTeamRoleService]; +export * from './xApi.service'; +import { XApiService } from './xApi.service'; +export const APIS = [CardService, CardTeamService, CatalogService, CatalogInjectService, CatalogUnitService, CiteService, CiteActionService, CiteDutyService, CompetencyFrameworkService, DataFieldService, DataOptionService, DataValueService, GroupService, HealthCheckService, InjectService, InjectTypeService, InvitationService, LmtService, MoveService, MselService, MselCompetencyService, MselPageService, MselUnitService, OrganizationService, PlayerService, PlayerApplicationService, PlayerApplicationTeamService, ProficiencyLevelService, ProficiencyScaleService, ScenarioEventService, SystemPermissionsService, SystemRolesService, TeamService, TeamCompetencyService, TeamUserService, UnitService, UnitUserService, UserService, UserMselRoleService, UserTeamRoleService, XApiService]; diff --git a/src/app/generated/blueprint.api/api/cite.service.ts b/src/app/generated/blueprint.api/api/cite.service.ts index b5e2f12f..46b37a83 100644 --- a/src/app/generated/blueprint.api/api/cite.service.ts +++ b/src/app/generated/blueprint.api/api/cite.service.ts @@ -27,6 +27,8 @@ import { ProblemDetails } from '../model/problemDetails'; // @ts-ignore import { ScoringModel } from '../model/scoringModel'; // @ts-ignore +import { TeamRole } from '../model/teamRole'; +// @ts-ignore import { TeamType } from '../model/teamType'; // @ts-ignore @@ -102,15 +104,15 @@ export class CiteService extends BaseService { } /** - * Gets all TeamTypes - * Returns a list of all of the TeamTypes. + * Gets all TeamRoles from CITE + * Returns a list of all of the TeamRoles. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public getTeamTypes(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getTeamTypes(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getTeamTypes(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getTeamTypes(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + public getTeamRoles(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getTeamRoles(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getTeamRoles(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getTeamRoles(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { let localVarHeaders = this.defaultHeaders; @@ -142,9 +144,9 @@ export class CiteService extends BaseService { } } - let localVarPath = `/api/teamtypes`; + let localVarPath = `/api/teamroles`; const { basePath, withCredentials } = this.configuration; - return this.httpClient.request>('get', `${basePath}${localVarPath}`, + return this.httpClient.request>('get', `${basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, @@ -158,15 +160,15 @@ export class CiteService extends BaseService { } /** - * Gets all TeamRoles from CITE - * Returns a list of all of the TeamRoles. + * Gets all TeamTypes + * Returns a list of all of the TeamTypes. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public getTeamRoles(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getTeamRoles(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getTeamRoles(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getTeamRoles(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + public getTeamTypes(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getTeamTypes(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getTeamTypes(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getTeamTypes(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { let localVarHeaders = this.defaultHeaders; @@ -198,9 +200,9 @@ export class CiteService extends BaseService { } } - let localVarPath = `/api/teamroles`; + let localVarPath = `/api/teamtypes`; const { basePath, withCredentials } = this.configuration; - return this.httpClient.request>('get', `${basePath}${localVarPath}`, + return this.httpClient.request>('get', `${basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, @@ -214,9 +216,3 @@ export class CiteService extends BaseService { } } - -export interface CiteTeamRole { - id?: string; - name?: string; -} - diff --git a/src/app/generated/blueprint.api/api/competencyElement.service.ts b/src/app/generated/blueprint.api/api/competencyElement.service.ts new file mode 100644 index 00000000..524972b4 --- /dev/null +++ b/src/app/generated/blueprint.api/api/competencyElement.service.ts @@ -0,0 +1,361 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { CompetencyElement } from '../model/competencyElement'; +// @ts-ignore +import { ProblemDetails } from '../model/problemDetails'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; + + + +@Injectable({ + providedIn: 'root' +}) +export class CompetencyElementService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Creates a new CompetencyElement + * @param competencyElement + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createCompetencyElement(competencyElement?: CompetencyElement, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createCompetencyElement(competencyElement?: CompetencyElement, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createCompetencyElement(competencyElement?: CompetencyElement, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createCompetencyElement(competencyElement?: CompetencyElement, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyelements`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: competencyElement, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a CompetencyElement + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteCompetencyElement(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteCompetencyElement(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteCompetencyElement(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteCompetencyElement(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteCompetencyElement.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyelements/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets a specific CompetencyElement by id + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getCompetencyElement(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getCompetencyElement(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getCompetencyElement(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getCompetencyElement(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getCompetencyElement.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyelements/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets CompetencyElements by CompetencyFramework + * @param frameworkId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getCompetencyElementsByFramework(frameworkId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getCompetencyElementsByFramework(frameworkId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getCompetencyElementsByFramework(frameworkId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getCompetencyElementsByFramework(frameworkId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (frameworkId === null || frameworkId === undefined) { + throw new Error('Required parameter frameworkId was null or undefined when calling getCompetencyElementsByFramework.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/${this.configuration.encodeParam({name: "frameworkId", value: frameworkId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/competencyelements`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Updates a CompetencyElement + * @param id + * @param competencyElement + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public updateCompetencyElement(id: string, competencyElement?: CompetencyElement, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateCompetencyElement(id: string, competencyElement?: CompetencyElement, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateCompetencyElement(id: string, competencyElement?: CompetencyElement, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateCompetencyElement(id: string, competencyElement?: CompetencyElement, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling updateCompetencyElement.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyelements/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('put', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: competencyElement, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/generated/blueprint.api/api/competencyFramework.service.ts b/src/app/generated/blueprint.api/api/competencyFramework.service.ts new file mode 100644 index 00000000..b2104920 --- /dev/null +++ b/src/app/generated/blueprint.api/api/competencyFramework.service.ts @@ -0,0 +1,1143 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { Competency } from '../model/competency'; +// @ts-ignore +import { CompetencyFramework } from '../model/competencyFramework'; +// @ts-ignore +import { CompetencyFrameworkImportPreview } from '../model/competencyFrameworkImportPreview'; +// @ts-ignore +import { FrameworkDeleteCheck } from '../model/frameworkDeleteCheck'; +// @ts-ignore +import { ProblemDetails } from '../model/problemDetails'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; + + + +@Injectable({ + providedIn: 'root' +}) +export class CompetencyFrameworkService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Checks if a Competency Framework can be deleted + * Returns dependency information showing which MSELs, data fields, and teams are using competencies from this framework. + * @param id The id of the Competency Framework to check + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public checkCanDeleteCompetencyFramework(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public checkCanDeleteCompetencyFramework(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public checkCanDeleteCompetencyFramework(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public checkCanDeleteCompetencyFramework(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling checkCanDeleteCompetencyFramework.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/can-delete`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Creates a new Competency within a Framework + * @param frameworkId The id of the parent CompetencyFramework + * @param competency The data to create the Competency with + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createCompetency(frameworkId: string, competency?: Competency, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createCompetency(frameworkId: string, competency?: Competency, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createCompetency(frameworkId: string, competency?: Competency, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createCompetency(frameworkId: string, competency?: Competency, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (frameworkId === null || frameworkId === undefined) { + throw new Error('Required parameter frameworkId was null or undefined when calling createCompetency.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/${this.configuration.encodeParam({name: "frameworkId", value: frameworkId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/competencies`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: competency, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Creates a new Competency Framework + * @param competencyFramework The data to create the CompetencyFramework with + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createCompetencyFramework(competencyFramework?: CompetencyFramework, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createCompetencyFramework(competencyFramework?: CompetencyFramework, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createCompetencyFramework(competencyFramework?: CompetencyFramework, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createCompetencyFramework(competencyFramework?: CompetencyFramework, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: competencyFramework, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a Competency + * @param competencyId The id of the Competency to delete + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteCompetency(competencyId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteCompetency(competencyId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteCompetency(competencyId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteCompetency(competencyId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (competencyId === null || competencyId === undefined) { + throw new Error('Required parameter competencyId was null or undefined when calling deleteCompetency.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencies/${this.configuration.encodeParam({name: "competencyId", value: competencyId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a Competency Framework + * Deletes the framework and all associated competencies and relationships (cascade). Will fail with BadRequest if the framework is in use by any MSELs, data fields, or teams. + * @param id The id of the Competency Framework to delete + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteCompetencyFramework(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteCompetencyFramework(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteCompetencyFramework(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteCompetencyFramework(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteCompetencyFramework.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets a specific Competency Framework by id + * Returns the framework with all competencies and relationships. + * @param id The id of the Competency Framework + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getCompetencyFramework(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getCompetencyFramework(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getCompetencyFramework(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getCompetencyFramework(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getCompetencyFramework.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets all Competency Frameworks + * Returns a list of all competency frameworks (without competencies). + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getCompetencyFrameworks(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getCompetencyFrameworks(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getCompetencyFrameworks(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getCompetencyFrameworks(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Imports a Competency Framework from a Moodle-format CSV + * Accepts a CSV file in the Moodle lpimportcsv 14-column format. Creates the framework, all competencies with hierarchy, and cross-reference relationships. + * @param source Framework source (e.g. \"NICE\", \"DCWF\") + * @param version Framework version (e.g. \"5.1\") + * @param file The CSV file + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public importCompetencyFramework(source?: string, version?: string, file?: Blob, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public importCompetencyFramework(source?: string, version?: string, file?: Blob, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public importCompetencyFramework(source?: string, version?: string, file?: Blob, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public importCompetencyFramework(source?: string, version?: string, file?: Blob, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new HttpParams({encoder: this.encoder}); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + source, 'source'); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + version, 'version'); + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + // to determine the Content-Type header + const consumes: string[] = [ + 'multipart/form-data' + ]; + + const canConsumeForm = this.canConsumeForm(consumes); + + let localVarFormParams: { append(param: string, value: any): any; }; + let localVarUseForm = false; + let localVarConvertFormParamsToString = false; + // use FormData to transmit files using content-type "multipart/form-data" + // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data + localVarUseForm = canConsumeForm; + if (localVarUseForm) { + localVarFormParams = new FormData(); + } else { + localVarFormParams = new HttpParams({encoder: this.encoder}); + } + + if (file !== undefined) { + localVarFormParams = localVarFormParams.append('file', file) as any || localVarFormParams; + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/import`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: localVarConvertFormParamsToString ? localVarFormParams.toString() : localVarFormParams, + params: localVarQueryParameters, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Imports a Competency Framework from a NICE-format JSON file + * Accepts a JSON file in the NICE/NIST CPRT format (response.elements with documents, elements, and relationships). Creates the framework, all competencies with hierarchy, and work-role-to-TKSA relationships. + * @param file The JSON file + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public importCompetencyFrameworkJson(file?: Blob, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public importCompetencyFrameworkJson(file?: Blob, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public importCompetencyFrameworkJson(file?: Blob, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public importCompetencyFrameworkJson(file?: Blob, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + // to determine the Content-Type header + const consumes: string[] = [ + 'multipart/form-data' + ]; + + const canConsumeForm = this.canConsumeForm(consumes); + + let localVarFormParams: { append(param: string, value: any): any; }; + let localVarUseForm = false; + let localVarConvertFormParamsToString = false; + // use FormData to transmit files using content-type "multipart/form-data" + // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data + localVarUseForm = canConsumeForm; + if (localVarUseForm) { + localVarFormParams = new FormData(); + } else { + localVarFormParams = new HttpParams({encoder: this.encoder}); + } + + if (file !== undefined) { + localVarFormParams = localVarFormParams.append('file', file) as any || localVarFormParams; + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/import-json`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: localVarConvertFormParamsToString ? localVarFormParams.toString() : localVarFormParams, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Imports a Competency Framework from a DCWF-format XLSX file + * Accepts an XLSX file with columns: ID, Name, Description, ParentID, RelatedIDs. Creates the framework, all competencies with hierarchy, and cross-reference relationships. + * @param source Framework source (e.g. \"DCWF\") + * @param version Framework version (e.g. \"1.0\") + * @param file The XLSX file + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public importCompetencyFrameworkXlsx(source?: string, version?: string, file?: Blob, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public importCompetencyFrameworkXlsx(source?: string, version?: string, file?: Blob, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public importCompetencyFrameworkXlsx(source?: string, version?: string, file?: Blob, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public importCompetencyFrameworkXlsx(source?: string, version?: string, file?: Blob, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new HttpParams({encoder: this.encoder}); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + source, 'source'); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + version, 'version'); + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + // to determine the Content-Type header + const consumes: string[] = [ + 'multipart/form-data' + ]; + + const canConsumeForm = this.canConsumeForm(consumes); + + let localVarFormParams: { append(param: string, value: any): any; }; + let localVarUseForm = false; + let localVarConvertFormParamsToString = false; + // use FormData to transmit files using content-type "multipart/form-data" + // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data + localVarUseForm = canConsumeForm; + if (localVarUseForm) { + localVarFormParams = new FormData(); + } else { + localVarFormParams = new HttpParams({encoder: this.encoder}); + } + + if (file !== undefined) { + localVarFormParams = localVarFormParams.append('file', file) as any || localVarFormParams; + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/import-xlsx`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: localVarConvertFormParamsToString ? localVarFormParams.toString() : localVarFormParams, + params: localVarQueryParameters, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Preview a Competency Framework from a Moodle CSV file + * Returns preview information: element counts, relationships, source/version. + * @param source Framework source (e.g. \"NICE\", \"DCWF\") + * @param version Framework version (e.g. \"5.1\") + * @param file The CSV file + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public previewCompetencyFrameworkCsv(source?: string, version?: string, file?: Blob, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public previewCompetencyFrameworkCsv(source?: string, version?: string, file?: Blob, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public previewCompetencyFrameworkCsv(source?: string, version?: string, file?: Blob, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public previewCompetencyFrameworkCsv(source?: string, version?: string, file?: Blob, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new HttpParams({encoder: this.encoder}); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + source, 'source'); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + version, 'version'); + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + // to determine the Content-Type header + const consumes: string[] = [ + 'multipart/form-data' + ]; + + const canConsumeForm = this.canConsumeForm(consumes); + + let localVarFormParams: { append(param: string, value: any): any; }; + let localVarUseForm = false; + let localVarConvertFormParamsToString = false; + // use FormData to transmit files using content-type "multipart/form-data" + // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data + localVarUseForm = canConsumeForm; + if (localVarUseForm) { + localVarFormParams = new FormData(); + } else { + localVarFormParams = new HttpParams({encoder: this.encoder}); + } + + if (file !== undefined) { + localVarFormParams = localVarFormParams.append('file', file) as any || localVarFormParams; + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/preview-csv`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: localVarConvertFormParamsToString ? localVarFormParams.toString() : localVarFormParams, + params: localVarQueryParameters, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Preview a Competency Framework from a NICE JSON file + * Returns preview information: element counts, relationships, source/version. + * @param file The JSON file + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public previewCompetencyFrameworkJson(file?: Blob, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public previewCompetencyFrameworkJson(file?: Blob, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public previewCompetencyFrameworkJson(file?: Blob, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public previewCompetencyFrameworkJson(file?: Blob, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + // to determine the Content-Type header + const consumes: string[] = [ + 'multipart/form-data' + ]; + + const canConsumeForm = this.canConsumeForm(consumes); + + let localVarFormParams: { append(param: string, value: any): any; }; + let localVarUseForm = false; + let localVarConvertFormParamsToString = false; + // use FormData to transmit files using content-type "multipart/form-data" + // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data + localVarUseForm = canConsumeForm; + if (localVarUseForm) { + localVarFormParams = new FormData(); + } else { + localVarFormParams = new HttpParams({encoder: this.encoder}); + } + + if (file !== undefined) { + localVarFormParams = localVarFormParams.append('file', file) as any || localVarFormParams; + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/preview-json`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: localVarConvertFormParamsToString ? localVarFormParams.toString() : localVarFormParams, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Preview a Competency Framework from a DCWF XLSX file + * Returns preview information: element counts, relationships, source/version. + * @param source Framework source (e.g. \"DCWF\") + * @param version Framework version (e.g. \"1.0\") + * @param file The XLSX file + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public previewCompetencyFrameworkXlsx(source?: string, version?: string, file?: Blob, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public previewCompetencyFrameworkXlsx(source?: string, version?: string, file?: Blob, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public previewCompetencyFrameworkXlsx(source?: string, version?: string, file?: Blob, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public previewCompetencyFrameworkXlsx(source?: string, version?: string, file?: Blob, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new HttpParams({encoder: this.encoder}); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + source, 'source'); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + version, 'version'); + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + // to determine the Content-Type header + const consumes: string[] = [ + 'multipart/form-data' + ]; + + const canConsumeForm = this.canConsumeForm(consumes); + + let localVarFormParams: { append(param: string, value: any): any; }; + let localVarUseForm = false; + let localVarConvertFormParamsToString = false; + // use FormData to transmit files using content-type "multipart/form-data" + // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data + localVarUseForm = canConsumeForm; + if (localVarUseForm) { + localVarFormParams = new FormData(); + } else { + localVarFormParams = new HttpParams({encoder: this.encoder}); + } + + if (file !== undefined) { + localVarFormParams = localVarFormParams.append('file', file) as any || localVarFormParams; + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/preview-xlsx`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: localVarConvertFormParamsToString ? localVarFormParams.toString() : localVarFormParams, + params: localVarQueryParameters, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Updates a Competency + * @param competencyId The id of the Competency to update + * @param competency The updated data + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public updateCompetency(competencyId: string, competency?: Competency, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateCompetency(competencyId: string, competency?: Competency, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateCompetency(competencyId: string, competency?: Competency, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateCompetency(competencyId: string, competency?: Competency, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (competencyId === null || competencyId === undefined) { + throw new Error('Required parameter competencyId was null or undefined when calling updateCompetency.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencies/${this.configuration.encodeParam({name: "competencyId", value: competencyId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('put', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: competency, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Updates a Competency Framework + * @param id The id of the CompetencyFramework to update + * @param competencyFramework The updated data + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public updateCompetencyFramework(id: string, competencyFramework?: CompetencyFramework, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateCompetencyFramework(id: string, competencyFramework?: CompetencyFramework, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateCompetencyFramework(id: string, competencyFramework?: CompetencyFramework, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateCompetencyFramework(id: string, competencyFramework?: CompetencyFramework, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling updateCompetencyFramework.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/competencyframeworks/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('put', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: competencyFramework, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/generated/blueprint.api/api/dataOption.service.ts b/src/app/generated/blueprint.api/api/dataOption.service.ts index a2f90342..35e9d614 100644 --- a/src/app/generated/blueprint.api/api/dataOption.service.ts +++ b/src/app/generated/blueprint.api/api/dataOption.service.ts @@ -25,6 +25,8 @@ import { Observable } from 'rxjs'; // @ts-ignore import { DataOption } from '../model/dataOption'; // @ts-ignore +import { DataOptionImportPreview } from '../model/dataOptionImportPreview'; +// @ts-ignore import { ProblemDetails } from '../model/problemDetails'; // @ts-ignore @@ -350,6 +352,90 @@ export class DataOptionService extends BaseService { ); } + /** + * Preview imported DataOptions from a file + * Parses a JSON, CSV, or XLSX file and returns a preview of data options that would be imported. Shows which options already exist and which are new. Supports formats: JSON (arrays or NICE Framework), CSV, XLSX/XLS. <para /> Accessible only to a ContentDeveloper or an Administrator + * @param dataFieldId The id of the DataField to import options for + * @param file The file to parse (JSON, CSV, or XLSX) + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public previewDataOptionImport(dataFieldId: string, file?: Blob, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public previewDataOptionImport(dataFieldId: string, file?: Blob, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public previewDataOptionImport(dataFieldId: string, file?: Blob, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public previewDataOptionImport(dataFieldId: string, file?: Blob, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (dataFieldId === null || dataFieldId === undefined) { + throw new Error('Required parameter dataFieldId was null or undefined when calling previewDataOptionImport.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + // to determine the Content-Type header + const consumes: string[] = [ + 'multipart/form-data' + ]; + + const canConsumeForm = this.canConsumeForm(consumes); + + let localVarFormParams: { append(param: string, value: any): any; }; + let localVarUseForm = false; + let localVarConvertFormParamsToString = false; + // use FormData to transmit files using content-type "multipart/form-data" + // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data + localVarUseForm = canConsumeForm; + if (localVarUseForm) { + localVarFormParams = new FormData(); + } else { + localVarFormParams = new HttpParams({encoder: this.encoder}); + } + + if (file !== undefined) { + localVarFormParams = localVarFormParams.append('file', file) as any || localVarFormParams; + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/datafields/${this.configuration.encodeParam({name: "dataFieldId", value: dataFieldId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/options/preview`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: localVarConvertFormParamsToString ? localVarFormParams.toString() : localVarFormParams, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + /** * Updates a DataOption * Updates a DataOption with the attributes specified. The ID from the route MUST MATCH the ID contained in the dataOption parameter <para /> Accessible only to a ContentDeveloper or an Administrator diff --git a/src/app/generated/blueprint.api/api/lmt.service.ts b/src/app/generated/blueprint.api/api/lmt.service.ts new file mode 100644 index 00000000..251f003c --- /dev/null +++ b/src/app/generated/blueprint.api/api/lmt.service.ts @@ -0,0 +1,104 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { ProblemDetails } from '../model/problemDetails'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; + + + +@Injectable({ + providedIn: 'root' +}) +export class LmtService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Gets IEEE 2881 (LMT) metadata for an MSEL as JSON-LD + * Returns LMT (Learning Metadata) as JSON-LD conforming to IEEE 2881 schema. This endpoint is publicly accessible to enable catalog discovery by LMS systems, PCTE registries, and other TLA components. The JSON-LD includes: - Exercise name, description, objectives - Competencies assessed (lrmi:assesses from MselCompetency associations) - Educational metadata (difficulty, purpose, mode, keywords) - Prerequisites (if configured) Any LMS that supports LTI 1.3 Deep Linking or Content-Item can consume this metadata to auto-link competencies, tags, and prerequisites when importing the exercise. + * @param mselId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getLmtResource(mselId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getLmtResource(mselId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getLmtResource(mselId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getLmtResource(mselId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (mselId === null || mselId === undefined) { + throw new Error('Required parameter mselId was null or undefined when calling getLmtResource.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/lmt/resource/${this.configuration.encodeParam({name: "mselId", value: mselId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/generated/blueprint.api/api/msel.service.ts b/src/app/generated/blueprint.api/api/msel.service.ts index aa8514c0..c04734d0 100644 --- a/src/app/generated/blueprint.api/api/msel.service.ts +++ b/src/app/generated/blueprint.api/api/msel.service.ts @@ -175,6 +175,64 @@ export class MselService extends BaseService { ); } + /** + * Cancel pushing integrations and remove any partial integrations + * Cancels an in-progress integration push and removes any partially created integrations <para /> Accessible only to a ContentDeveloper or MSEL owner + * @param id The id of the MSEL + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public cancelIntegrations(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public cancelIntegrations(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public cancelIntegrations(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public cancelIntegrations(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling cancelIntegrations.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/msels/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/integrations/cancel`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + /** * Creates a new MSEL by copying an existing MSEL * Creates a new MSEL from the specified existing MSEL <para /> Accessible only to a ContentDeveloper or an Administrator @@ -1066,49 +1124,10 @@ export class MselService extends BaseService { ); } - /** - * Cancel pushing integrations and remove any partial integrations - * Cancels an in-progress integration push and removes any partially created integrations <para /> Accessible only to a ContentDeveloper or MSEL owner - * @param id The id of the MSEL - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public cancelIntegrations(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; - public cancelIntegrations(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; - public cancelIntegrations(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; - public cancelIntegrations(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { - if (id === null || id === undefined) { - throw new Error('Required parameter id was null or undefined when calling cancelIntegrations.'); - } - - let localVarHeaders = this.defaultHeaders; - - // authentication (oauth2) required - localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); - - const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); - - const localVarTransferCache: boolean = options?.transferCache ?? true; - - let localVarPath = `/api/msels/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/integrations/cancel`; - const { basePath, withCredentials } = this.configuration; - return this.httpClient.request('post', `${basePath}${localVarPath}`, - { - context: localVarHttpContext, - responseType: 'text' as 'json', - ...(withCredentials ? { withCredentials } : {}), - headers: localVarHeaders, - observe: observe, - transferCache: localVarTransferCache, - reportProgress: reportProgress - } - ); - } - /** * Push Integrations * Pushes all MSEL Integrations to the associated applications <para /> Accessible only to a ContentDeveloper or MSEL owner - * @param id + * @param id * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ diff --git a/src/app/generated/blueprint.api/api/mselCompetency.service.ts b/src/app/generated/blueprint.api/api/mselCompetency.service.ts new file mode 100644 index 00000000..12a23ab3 --- /dev/null +++ b/src/app/generated/blueprint.api/api/mselCompetency.service.ts @@ -0,0 +1,355 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { MselCompetency } from '../model/mselCompetency'; +// @ts-ignore +import { ProblemDetails } from '../model/problemDetails'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; + + + +@Injectable({ + providedIn: 'root' +}) +export class MselCompetencyService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Creates a new MselCompetency + * Creates a new MselCompetency with the attributes specified <para /> + * @param mselCompetency The data to create the MselCompetency with + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createMselCompetency(mselCompetency?: MselCompetency, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createMselCompetency(mselCompetency?: MselCompetency, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createMselCompetency(mselCompetency?: MselCompetency, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createMselCompetency(mselCompetency?: MselCompetency, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/mselcompetencies`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: mselCompetency, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a MselCompetency + * Deletes a MselCompetency with the specified id <para /> + * @param id The id of the MselCompetency to delete + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteMselCompetency(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteMselCompetency(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteMselCompetency(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteMselCompetency(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteMselCompetency.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/mselcompetencies/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a MselCompetency by msel ID and competency ID + * Deletes a MselCompetency with the specified msel ID and competency ID <para /> + * @param mselId ID of a msel. + * @param competencyId ID of a competency. + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteMselCompetencyByIds(mselId: string, competencyId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteMselCompetencyByIds(mselId: string, competencyId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteMselCompetencyByIds(mselId: string, competencyId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteMselCompetencyByIds(mselId: string, competencyId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (mselId === null || mselId === undefined) { + throw new Error('Required parameter mselId was null or undefined when calling deleteMselCompetencyByIds.'); + } + if (competencyId === null || competencyId === undefined) { + throw new Error('Required parameter competencyId was null or undefined when calling deleteMselCompetencyByIds.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/msels/${this.configuration.encodeParam({name: "mselId", value: mselId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/competencies/${this.configuration.encodeParam({name: "competencyId", value: competencyId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets all MselCompetencies for a MSEL + * Returns a list of all of the MselCompetencies for the msel. + * @param mselId The id of the Msel + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getMselCompetencies(mselId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getMselCompetencies(mselId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getMselCompetencies(mselId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getMselCompetencies(mselId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (mselId === null || mselId === undefined) { + throw new Error('Required parameter mselId was null or undefined when calling getMselCompetencies.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/msels/${this.configuration.encodeParam({name: "mselId", value: mselId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/mselcompetencies`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets a specific MselCompetency by id + * Returns the MselCompetency with the id specified <para /> + * @param id The id of the MselCompetency + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getMselCompetency(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getMselCompetency(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getMselCompetency(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getMselCompetency(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getMselCompetency.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/mselcompetencies/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/generated/blueprint.api/api/proficiencyLevel.service.ts b/src/app/generated/blueprint.api/api/proficiencyLevel.service.ts new file mode 100644 index 00000000..0a0909bd --- /dev/null +++ b/src/app/generated/blueprint.api/api/proficiencyLevel.service.ts @@ -0,0 +1,361 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { ProblemDetails } from '../model/problemDetails'; +// @ts-ignore +import { ProficiencyLevel } from '../model/proficiencyLevel'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; + + + +@Injectable({ + providedIn: 'root' +}) +export class ProficiencyLevelService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Creates a new ProficiencyLevel + * @param proficiencyLevel + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createProficiencyLevel(proficiencyLevel?: ProficiencyLevel, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createProficiencyLevel(proficiencyLevel?: ProficiencyLevel, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createProficiencyLevel(proficiencyLevel?: ProficiencyLevel, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createProficiencyLevel(proficiencyLevel?: ProficiencyLevel, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencylevels`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: proficiencyLevel, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a ProficiencyLevel + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteProficiencyLevel(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteProficiencyLevel(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteProficiencyLevel(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteProficiencyLevel(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteProficiencyLevel.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencylevels/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets a specific ProficiencyLevel by id + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getProficiencyLevel(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getProficiencyLevel(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProficiencyLevel(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProficiencyLevel(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getProficiencyLevel.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencylevels/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets ProficiencyLevels by ProficiencyScale + * @param scaleId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getProficiencyLevelsByScale(scaleId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProficiencyLevelsByScale(scaleId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getProficiencyLevelsByScale(scaleId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getProficiencyLevelsByScale(scaleId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (scaleId === null || scaleId === undefined) { + throw new Error('Required parameter scaleId was null or undefined when calling getProficiencyLevelsByScale.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencyscales/${this.configuration.encodeParam({name: "scaleId", value: scaleId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/proficiencylevels`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Updates a ProficiencyLevel + * @param id + * @param proficiencyLevel + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public updateProficiencyLevel(id: string, proficiencyLevel?: ProficiencyLevel, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateProficiencyLevel(id: string, proficiencyLevel?: ProficiencyLevel, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateProficiencyLevel(id: string, proficiencyLevel?: ProficiencyLevel, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateProficiencyLevel(id: string, proficiencyLevel?: ProficiencyLevel, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling updateProficiencyLevel.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencylevels/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('put', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: proficiencyLevel, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/generated/blueprint.api/api/proficiencyScale.service.ts b/src/app/generated/blueprint.api/api/proficiencyScale.service.ts new file mode 100644 index 00000000..9ed21a7d --- /dev/null +++ b/src/app/generated/blueprint.api/api/proficiencyScale.service.ts @@ -0,0 +1,357 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { ProblemDetails } from '../model/problemDetails'; +// @ts-ignore +import { ProficiencyScale } from '../model/proficiencyScale'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; + + + +@Injectable({ + providedIn: 'root' +}) +export class ProficiencyScaleService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Creates a new ProficiencyScale + * @param proficiencyScale + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createProficiencyScale(proficiencyScale?: ProficiencyScale, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createProficiencyScale(proficiencyScale?: ProficiencyScale, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createProficiencyScale(proficiencyScale?: ProficiencyScale, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createProficiencyScale(proficiencyScale?: ProficiencyScale, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencyscales`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: proficiencyScale, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a ProficiencyScale + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteProficiencyScale(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteProficiencyScale(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteProficiencyScale(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteProficiencyScale(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteProficiencyScale.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencyscales/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets a specific ProficiencyScale by id + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getProficiencyScale(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getProficiencyScale(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProficiencyScale(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProficiencyScale(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getProficiencyScale.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencyscales/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets all ProficiencyScales + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getProficiencyScales(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProficiencyScales(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getProficiencyScales(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getProficiencyScales(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencyscales`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Updates a ProficiencyScale + * @param id + * @param proficiencyScale + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public updateProficiencyScale(id: string, proficiencyScale?: ProficiencyScale, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateProficiencyScale(id: string, proficiencyScale?: ProficiencyScale, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateProficiencyScale(id: string, proficiencyScale?: ProficiencyScale, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateProficiencyScale(id: string, proficiencyScale?: ProficiencyScale, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling updateProficiencyScale.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/proficiencyscales/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('put', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: proficiencyScale, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/generated/blueprint.api/api/team.service.ts b/src/app/generated/blueprint.api/api/team.service.ts index d6c1271a..96d41344 100644 --- a/src/app/generated/blueprint.api/api/team.service.ts +++ b/src/app/generated/blueprint.api/api/team.service.ts @@ -350,62 +350,6 @@ export class TeamService extends BaseService { ); } - /** - * Gets all Team in the system - * Returns a list of all of the Teams in the system. <para /> Only accessible to a SuperUser - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public getTeams(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getTeams(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getTeams(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getTeams(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { - - let localVarHeaders = this.defaultHeaders; - - // authentication (oauth2) required - localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); - - const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ - 'text/plain', - 'application/json', - 'text/json' - ]); - if (localVarHttpHeaderAcceptSelected !== undefined) { - localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); - } - - const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); - - const localVarTransferCache: boolean = options?.transferCache ?? true; - - - let responseType_: 'text' | 'json' | 'blob' = 'json'; - if (localVarHttpHeaderAcceptSelected) { - if (localVarHttpHeaderAcceptSelected.startsWith('text')) { - responseType_ = 'text'; - } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { - responseType_ = 'json'; - } else { - responseType_ = 'blob'; - } - } - - let localVarPath = `/api/teams`; - const { basePath, withCredentials } = this.configuration; - return this.httpClient.request>('get', `${basePath}${localVarPath}`, - { - context: localVarHttpContext, - responseType: responseType_, - ...(withCredentials ? { withCredentials } : {}), - headers: localVarHeaders, - observe: observe, - transferCache: localVarTransferCache, - reportProgress: reportProgress - } - ); - } - /** * Gets Teams for the specified MSEL * Returns a list of the Teams for the specified MSEL. <para /> diff --git a/src/app/generated/blueprint.api/api/teamCompetency.service.ts b/src/app/generated/blueprint.api/api/teamCompetency.service.ts new file mode 100644 index 00000000..c92fd9eb --- /dev/null +++ b/src/app/generated/blueprint.api/api/teamCompetency.service.ts @@ -0,0 +1,409 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { ProblemDetails } from '../model/problemDetails'; +// @ts-ignore +import { TeamCompetency } from '../model/teamCompetency'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; + + + +@Injectable({ + providedIn: 'root' +}) +export class TeamCompetencyService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Creates a new TeamCompetency + * @param teamCompetency The data to create the TeamCompetency with + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createTeamCompetency(teamCompetency?: TeamCompetency, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createTeamCompetency(teamCompetency?: TeamCompetency, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createTeamCompetency(teamCompetency?: TeamCompetency, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createTeamCompetency(teamCompetency?: TeamCompetency, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/teamcompetencies`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: teamCompetency, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a TeamCompetency + * @param id The id of the TeamCompetency to delete + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteTeamCompetency(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteTeamCompetency(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteTeamCompetency(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteTeamCompetency(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteTeamCompetency.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/teamcompetencies/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes a TeamCompetency by team ID and competency ID + * @param teamId ID of a team. + * @param competencyId ID of a competency. + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteTeamCompetencyByIds(teamId: string, competencyId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteTeamCompetencyByIds(teamId: string, competencyId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteTeamCompetencyByIds(teamId: string, competencyId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteTeamCompetencyByIds(teamId: string, competencyId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (teamId === null || teamId === undefined) { + throw new Error('Required parameter teamId was null or undefined when calling deleteTeamCompetencyByIds.'); + } + if (competencyId === null || competencyId === undefined) { + throw new Error('Required parameter competencyId was null or undefined when calling deleteTeamCompetencyByIds.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/teams/${this.configuration.encodeParam({name: "teamId", value: teamId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/competencies/${this.configuration.encodeParam({name: "competencyId", value: competencyId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets all TeamCompetencies for a MSEL + * @param mselId The id of the Msel + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getMselTeamCompetencies(mselId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getMselTeamCompetencies(mselId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getMselTeamCompetencies(mselId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getMselTeamCompetencies(mselId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (mselId === null || mselId === undefined) { + throw new Error('Required parameter mselId was null or undefined when calling getMselTeamCompetencies.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/msels/${this.configuration.encodeParam({name: "mselId", value: mselId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/teamcompetencies`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets all TeamCompetencies for a Team + * @param teamId The id of the Team + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getTeamCompetencies(teamId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getTeamCompetencies(teamId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getTeamCompetencies(teamId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getTeamCompetencies(teamId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (teamId === null || teamId === undefined) { + throw new Error('Required parameter teamId was null or undefined when calling getTeamCompetencies.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/teams/${this.configuration.encodeParam({name: "teamId", value: teamId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/teamcompetencies`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets a specific TeamCompetency by id + * @param id The id of the TeamCompetency + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getTeamCompetency(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getTeamCompetency(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getTeamCompetency(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getTeamCompetency(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getTeamCompetency.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/teamcompetencies/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/generated/blueprint.api/api/teamUser.service.ts b/src/app/generated/blueprint.api/api/teamUser.service.ts index da634c17..af83688a 100644 --- a/src/app/generated/blueprint.api/api/teamUser.service.ts +++ b/src/app/generated/blueprint.api/api/teamUser.service.ts @@ -45,7 +45,7 @@ export class TeamUserService extends BaseService { /** * Creates a new TeamUser - * Creates a new TeamUser with the attributes specified <para /> Accessible only to a SuperUser + * Creates a new TeamUser with the attributes specified <para /> Accessible to a SuperUser or a MSEL Owner * @param teamUser The data to create the TeamUser with * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. @@ -114,7 +114,7 @@ export class TeamUserService extends BaseService { /** * Deletes a TeamUser - * Deletes a TeamUser with the specified id <para /> Accessible only to a SuperUser + * Deletes a TeamUser with the specified id <para /> Accessible to a SuperUser or a MSEL Owner * @param id The id of the TeamUser to delete * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. @@ -172,7 +172,7 @@ export class TeamUserService extends BaseService { /** * Deletes a TeamUser by user ID and team ID - * Deletes a TeamUser with the specified user ID and team ID <para /> Accessible only to a SuperUser + * Deletes a TeamUser with the specified user ID and team ID <para /> Accessible to a SuperUser or a MSEL Owner * @param teamId ID of a team. * @param userId ID of a user. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. diff --git a/src/app/generated/blueprint.api/api/xApi.service.ts b/src/app/generated/blueprint.api/api/xApi.service.ts new file mode 100644 index 00000000..fb5acdbb --- /dev/null +++ b/src/app/generated/blueprint.api/api/xApi.service.ts @@ -0,0 +1,298 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { CompetencyAssertion } from '../model/competencyAssertion'; +// @ts-ignore +import { ProblemDetails } from '../model/problemDetails'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; + + + +@Injectable({ + providedIn: 'root' +}) +export class XApiService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Creates a competency assertion xAPI statement + * Writes an xAPI statement with the \"asserted\" verb to the LRS, recording an assessor\'s competency rating for a participant/team on a specific scenario event. + * @param competencyAssertion + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createCompetencyAssertion(competencyAssertion?: CompetencyAssertion, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createCompetencyAssertion(competencyAssertion?: CompetencyAssertion, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createCompetencyAssertion(competencyAssertion?: CompetencyAssertion, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createCompetencyAssertion(competencyAssertion?: CompetencyAssertion, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json', + 'text/json', + 'application/*+json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/xapi/assertions`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: competencyAssertion, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Gets xAPI statements for an MSEL from the LRS + * Queries the LRS for xAPI statements related to the MSEL\'s integrations. When source is omitted, queries all configured integrations (Blueprint, CITE, Steamfitter, Player, Gallery). When source is specified, queries only that integration\'s activity ID. + * @param mselId + * @param since + * @param until + * @param limit + * @param source + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getXApiStatements(mselId?: string, since?: Date, until?: Date, limit?: number, source?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getXApiStatements(mselId?: string, since?: Date, until?: Date, limit?: number, source?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getXApiStatements(mselId?: string, since?: Date, until?: Date, limit?: number, source?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getXApiStatements(mselId?: string, since?: Date, until?: Date, limit?: number, source?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new HttpParams({encoder: this.encoder}); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + mselId, 'mselId'); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + since, 'since'); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + until, 'until'); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + limit, 'limit'); + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + source, 'source'); + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain', + 'application/json', + 'text/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/xapi/statements`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + params: localVarQueryParameters, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Writes an xAPI statement when a user views the join page + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public viewedJoinPage(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public viewedJoinPage(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public viewedJoinPage(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public viewedJoinPage(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/xapi/viewed/joinpage`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Logs an MSEL viewed xAPI statement + * Writes an xAPI statement with the \"viewed\" verb to the LRS when a user views an MSEL. This endpoint is called explicitly from the UI when viewing an MSEL, not when building. + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public viewedMsel(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public viewedMsel(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public viewedMsel(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public viewedMsel(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling viewedMsel.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (oauth2) required + localVarHeaders = this.configuration.addCredentialToHeaders('oauth2', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/xapi/viewed/msel/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/generated/blueprint.api/model/competency.ts b/src/app/generated/blueprint.api/model/competency.ts new file mode 100644 index 00000000..9e0b1b32 --- /dev/null +++ b/src/app/generated/blueprint.api/model/competency.ts @@ -0,0 +1,40 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface Competency { + dateCreated?: Date; + dateModified?: Date | null; + createdBy?: string; + modifiedBy?: string | null; + id?: string; + competencyFrameworkId?: string; + parentId?: string | null; + idNumber?: string | null; + shortName?: string | null; + description?: string | null; + descriptionFormat?: number; + path?: string | null; + sortOrder?: number; + ruleType?: string | null; + ruleOutcome?: number; + ruleConfig?: string | null; + scaleValues?: string | null; + scaleConfiguration?: string | null; + children?: Array | null; + relatedIdNumbers?: Array | null; +} + diff --git a/src/app/generated/blueprint.api/model/competencyAssertion.ts b/src/app/generated/blueprint.api/model/competencyAssertion.ts new file mode 100644 index 00000000..e01d09d5 --- /dev/null +++ b/src/app/generated/blueprint.api/model/competencyAssertion.ts @@ -0,0 +1,28 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface CompetencyAssertion { + mselId?: string; + competencyId?: string; + scenarioEventId?: string | null; + teamId?: string | null; + proficiencyLevelId?: string; + comment?: string | null; + moveNumber?: number | null; + groupNumber?: number | null; +} + diff --git a/src/app/generated/blueprint.api/model/competencyElement.ts b/src/app/generated/blueprint.api/model/competencyElement.ts new file mode 100644 index 00000000..2c951069 --- /dev/null +++ b/src/app/generated/blueprint.api/model/competencyElement.ts @@ -0,0 +1,32 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface CompetencyElement { + dateCreated?: Date; + dateModified?: Date | null; + createdBy?: string; + modifiedBy?: string | null; + id?: string; + competencyFrameworkId?: string; + elementIdentifier?: string | null; + elementType?: string | null; + name?: string | null; + description?: string | null; + parentId?: string | null; + children?: Array | null; +} + diff --git a/src/app/generated/blueprint.api/model/competencyFramework.ts b/src/app/generated/blueprint.api/model/competencyFramework.ts new file mode 100644 index 00000000..5d737cda --- /dev/null +++ b/src/app/generated/blueprint.api/model/competencyFramework.ts @@ -0,0 +1,37 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { Competency } from './competency'; + + +export interface CompetencyFramework { + dateCreated?: Date; + dateModified?: Date | null; + createdBy?: string; + modifiedBy?: string | null; + id?: string; + name?: string | null; + idNumber?: string | null; + description?: string | null; + descriptionFormat?: number; + source?: string | null; + version?: string | null; + scaleValues?: string | null; + scaleConfiguration?: string | null; + taxonomies?: string | null; + defaultProficiencyScaleId?: string | null; + competencies?: Array | null; +} + diff --git a/src/app/generated/blueprint.api/model/competencyFrameworkImportPreview.ts b/src/app/generated/blueprint.api/model/competencyFrameworkImportPreview.ts new file mode 100644 index 00000000..5d382053 --- /dev/null +++ b/src/app/generated/blueprint.api/model/competencyFrameworkImportPreview.ts @@ -0,0 +1,28 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { ElementTypeCount } from './elementTypeCount'; + + +export interface CompetencyFrameworkImportPreview { + source?: string | null; + version?: string | null; + frameworkName?: string | null; + elementTypeCounts?: Array | null; + totalElements?: number; + totalRelationships?: number; + error?: string | null; +} + diff --git a/src/app/generated/blueprint.api/model/dataField.ts b/src/app/generated/blueprint.api/model/dataField.ts index 9e326e42..5eb9ff28 100644 --- a/src/app/generated/blueprint.api/model/dataField.ts +++ b/src/app/generated/blueprint.api/model/dataField.ts @@ -42,6 +42,7 @@ export interface DataField { isTemplate?: boolean; isInformationField?: boolean; isFacilitationField?: boolean; + isAssessorVisible?: boolean; } export namespace DataField { } diff --git a/src/app/generated/blueprint.api/model/dataFieldType.ts b/src/app/generated/blueprint.api/model/dataFieldType.ts index 0567d783..ae799e98 100644 --- a/src/app/generated/blueprint.api/model/dataFieldType.ts +++ b/src/app/generated/blueprint.api/model/dataFieldType.ts @@ -31,7 +31,9 @@ export const DataFieldType = { User: 'User', Checkbox: 'Checkbox', Url: 'Url', - Move: 'Move' + Move: 'Move', + IntegrationTarget: 'IntegrationTarget', + Competency: 'Competency' } as const; export type DataFieldType = typeof DataFieldType[keyof typeof DataFieldType]; diff --git a/src/app/generated/blueprint.api/model/dataOption.ts b/src/app/generated/blueprint.api/model/dataOption.ts index 81a98bf2..f3f1ead9 100644 --- a/src/app/generated/blueprint.api/model/dataOption.ts +++ b/src/app/generated/blueprint.api/model/dataOption.ts @@ -24,6 +24,7 @@ export interface DataOption { dataFieldId?: string; optionName?: string | null; optionValue?: string | null; + optionDescription?: string | null; displayOrder?: number; } diff --git a/src/app/generated/blueprint.api/model/dataOptionImportPreview.ts b/src/app/generated/blueprint.api/model/dataOptionImportPreview.ts new file mode 100644 index 00000000..1629670d --- /dev/null +++ b/src/app/generated/blueprint.api/model/dataOptionImportPreview.ts @@ -0,0 +1,23 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { DataOptionImportPreviewItem } from './dataOptionImportPreviewItem'; + + +export interface DataOptionImportPreview { + items?: Array | null; + error?: string | null; +} + diff --git a/src/app/generated/blueprint.api/model/dataOptionImportPreviewItem.ts b/src/app/generated/blueprint.api/model/dataOptionImportPreviewItem.ts new file mode 100644 index 00000000..c028e6bb --- /dev/null +++ b/src/app/generated/blueprint.api/model/dataOptionImportPreviewItem.ts @@ -0,0 +1,24 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface DataOptionImportPreviewItem { + optionName?: string | null; + optionValue?: string | null; + optionDescription?: string | null; + exists?: boolean; +} + diff --git a/src/app/generated/blueprint.api/model/elementTypeCount.ts b/src/app/generated/blueprint.api/model/elementTypeCount.ts new file mode 100644 index 00000000..1934efd4 --- /dev/null +++ b/src/app/generated/blueprint.api/model/elementTypeCount.ts @@ -0,0 +1,22 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ElementTypeCount { + type?: string | null; + count?: number; +} + diff --git a/src/app/generated/blueprint.api/model/frameworkDeleteCheck.ts b/src/app/generated/blueprint.api/model/frameworkDeleteCheck.ts new file mode 100644 index 00000000..b0d30066 --- /dev/null +++ b/src/app/generated/blueprint.api/model/frameworkDeleteCheck.ts @@ -0,0 +1,23 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { MselReference } from './mselReference'; + + +export interface FrameworkDeleteCheck { + canDelete?: boolean; + affectedMsels?: Array | null; +} + diff --git a/src/app/generated/blueprint.api/model/models.ts b/src/app/generated/blueprint.api/model/models.ts index fd2c8d8d..606300aa 100644 --- a/src/app/generated/blueprint.api/model/models.ts +++ b/src/app/generated/blueprint.api/model/models.ts @@ -18,6 +18,10 @@ export * from './catalogUnit'; export * from './citeAction'; export * from './citeDuty'; export * from './compareInfo'; +export * from './competency'; +export * from './competencyAssertion'; +export * from './competencyFramework'; +export * from './competencyFrameworkImportPreview'; export * from './constructorInfo'; export * from './createFromInjectsForm'; export * from './cultureInfo'; @@ -29,6 +33,8 @@ export * from './dataColumn'; export * from './dataField'; export * from './dataFieldType'; export * from './dataOption'; +export * from './dataOptionImportPreview'; +export * from './dataOptionImportPreviewItem'; export * from './dataSet'; export * from './dataSetDateTime'; export * from './dataTable'; @@ -36,12 +42,14 @@ export * from './dataValue'; export * from './dateTimeFormatInfo'; export * from './dayOfWeek'; export * from './digitShapes'; +export * from './elementTypeCount'; export * from './eventAttributes'; export * from './eventInfo'; export * from './eventType'; export * from './exception'; export * from './fieldAttributes'; export * from './fieldInfo'; +export * from './frameworkDeleteCheck'; export * from './genericParameterAttributes'; export * from './group'; export * from './groupMembership'; @@ -68,8 +76,10 @@ export * from './module'; export * from './moduleHandle'; export * from './move'; export * from './msel'; +export * from './mselCompetency'; export * from './mselItemStatus'; export * from './mselPage'; +export * from './mselReference'; export * from './mselRole'; export * from './mselUnit'; export * from './numberFormatInfo'; @@ -80,6 +90,8 @@ export * from './permission'; export * from './playerApplication'; export * from './playerApplicationTeam'; export * from './problemDetails'; +export * from './proficiencyLevel'; +export * from './proficiencyScale'; export * from './propertyAttributes'; export * from './propertyInfo'; export * from './rightSideDisplay'; @@ -104,6 +116,8 @@ export * from './structLayoutAttribute'; export * from './systemPermission'; export * from './systemRole'; export * from './team'; +export * from './teamCompetency'; +export * from './teamPermission'; export * from './teamRole'; export * from './teamType'; export * from './teamUser'; diff --git a/src/app/generated/blueprint.api/model/msel.ts b/src/app/generated/blueprint.api/model/msel.ts index 8f3230fa..c7af240c 100644 --- a/src/app/generated/blueprint.api/model/msel.ts +++ b/src/app/generated/blueprint.api/model/msel.ts @@ -15,6 +15,7 @@ */ import { Organization } from './organization'; import { MselPage } from './mselPage'; +import { MselCompetency } from './mselCompetency'; import { Invitation } from './invitation'; import { UserMselRole } from './userMselRole'; import { ScenarioEvent } from './scenarioEvent'; @@ -69,6 +70,10 @@ export interface Msel { showIntegrationTargetOnScenarioEventList?: boolean; showIntegrationTargetOnExerciseView?: boolean; integrationTargetDisplayOrder?: number; + showTimeOnAssessorView?: boolean; + showMoveOnAssessorView?: boolean; + showGroupOnAssessorView?: boolean; + showIntegrationTargetOnAssessorView?: boolean; moves?: Array | null; dataFields?: Array | null; scenarioEvents?: Array | null; @@ -76,6 +81,12 @@ export interface Msel { units?: Array | null; userMselRoles?: Array | null; headerRowMetadata?: string | null; + educationalLevel?: string | null; + subject?: string | null; + keywords?: string | null; + educationalUse?: string | null; + courseMode?: string | null; + language?: string | null; organizations?: Array | null; cards?: Array | null; galleryArticleParameters?: Array | null; @@ -85,6 +96,7 @@ export interface Msel { playerApplications?: Array | null; pages?: Array | null; invitations?: Array | null; + mselCompetencies?: Array | null; } export namespace Msel { } diff --git a/src/app/generated/blueprint.api/model/mselCompetency.ts b/src/app/generated/blueprint.api/model/mselCompetency.ts new file mode 100644 index 00000000..65eef601 --- /dev/null +++ b/src/app/generated/blueprint.api/model/mselCompetency.ts @@ -0,0 +1,25 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { Competency } from './competency'; + + +export interface MselCompetency { + id?: string; + mselId?: string; + competencyId?: string; + competency?: Competency; +} + diff --git a/src/app/generated/blueprint.api/model/mselReference.ts b/src/app/generated/blueprint.api/model/mselReference.ts new file mode 100644 index 00000000..e01772ad --- /dev/null +++ b/src/app/generated/blueprint.api/model/mselReference.ts @@ -0,0 +1,22 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface MselReference { + id?: string; + name?: string | null; +} + diff --git a/src/app/generated/blueprint.api/model/proficiencyLevel.ts b/src/app/generated/blueprint.api/model/proficiencyLevel.ts new file mode 100644 index 00000000..2d504e0d --- /dev/null +++ b/src/app/generated/blueprint.api/model/proficiencyLevel.ts @@ -0,0 +1,30 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ProficiencyLevel { + dateCreated?: Date; + dateModified?: Date | null; + createdBy?: string; + modifiedBy?: string | null; + id?: string; + proficiencyScaleId?: string; + name?: string | null; + value?: number; + description?: string | null; + displayOrder?: number; +} + diff --git a/src/app/generated/blueprint.api/model/proficiencyScale.ts b/src/app/generated/blueprint.api/model/proficiencyScale.ts new file mode 100644 index 00000000..1388ad1b --- /dev/null +++ b/src/app/generated/blueprint.api/model/proficiencyScale.ts @@ -0,0 +1,29 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { ProficiencyLevel } from './proficiencyLevel'; + + +export interface ProficiencyScale { + dateCreated?: Date; + dateModified?: Date | null; + createdBy?: string; + modifiedBy?: string | null; + id?: string; + name?: string | null; + description?: string | null; + proficiencyLevels?: Array | null; +} + diff --git a/src/app/generated/blueprint.api/model/systemPermission.ts b/src/app/generated/blueprint.api/model/systemPermission.ts index 177cc0c1..62eb20c8 100644 --- a/src/app/generated/blueprint.api/model/systemPermission.ts +++ b/src/app/generated/blueprint.api/model/systemPermission.ts @@ -41,7 +41,9 @@ export const SystemPermission = { ViewRoles: 'ViewRoles', ManageRoles: 'ManageRoles', ViewGroups: 'ViewGroups', - ManageGroups: 'ManageGroups' + ManageGroups: 'ManageGroups', + ViewCompetencyFrameworks: 'ViewCompetencyFrameworks', + ManageCompetencyFrameworks: 'ManageCompetencyFrameworks' } as const; export type SystemPermission = typeof SystemPermission[keyof typeof SystemPermission]; diff --git a/src/app/generated/blueprint.api/model/team.ts b/src/app/generated/blueprint.api/model/team.ts index 9fefabbf..73356f5e 100644 --- a/src/app/generated/blueprint.api/model/team.ts +++ b/src/app/generated/blueprint.api/model/team.ts @@ -31,9 +31,6 @@ export interface Team { mselId?: string | null; citeTeamTypeId?: string | null; email?: string | null; - playerTeamId?: string | null; - galleryTeamId?: string | null; - citeTeamId?: string | null; canTeamLeaderInvite?: boolean; canTeamMemberInvite?: boolean; users?: Array | null; diff --git a/src/app/generated/blueprint.api/model/teamCompetency.ts b/src/app/generated/blueprint.api/model/teamCompetency.ts new file mode 100644 index 00000000..65ce38ad --- /dev/null +++ b/src/app/generated/blueprint.api/model/teamCompetency.ts @@ -0,0 +1,25 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { Competency } from './competency'; + + +export interface TeamCompetency { + id?: string; + teamId?: string; + competencyId?: string; + competency?: Competency; +} + diff --git a/src/app/generated/blueprint.api/model/teamPermission.ts b/src/app/generated/blueprint.api/model/teamPermission.ts new file mode 100644 index 00000000..dce0cc41 --- /dev/null +++ b/src/app/generated/blueprint.api/model/teamPermission.ts @@ -0,0 +1,28 @@ +/* + Copyright 2026 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the + project root for license information. +*/ + +/** + * Blueprint API + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export const TeamPermission = { + ViewTeam: 'ViewTeam', + EditTeamScore: 'EditTeamScore', + SubmitTeamScore: 'SubmitTeamScore', + ManageTeam: 'ManageTeam', + ViewPastOfficialScore: 'ViewPastOfficialScore', + ViewCurrentOfficialScore: 'ViewCurrentOfficialScore', + EditOfficialScore: 'EditOfficialScore' +} as const; +export type TeamPermission = typeof TeamPermission[keyof typeof TeamPermission]; + diff --git a/src/app/generated/blueprint.api/model/teamRole.ts b/src/app/generated/blueprint.api/model/teamRole.ts index a9278c83..8cc5c5b5 100644 --- a/src/app/generated/blueprint.api/model/teamRole.ts +++ b/src/app/generated/blueprint.api/model/teamRole.ts @@ -13,7 +13,12 @@ * https://openapi-generator.tech * Do not edit the class manually. */ +import { TeamPermission } from './teamPermission'; -export type TeamRole = string; +export interface TeamRole { + id?: string; + name?: string | null; + permissions?: Array | null; +} diff --git a/src/app/generated/blueprint.api/model/userTeamRole.ts b/src/app/generated/blueprint.api/model/userTeamRole.ts index cd0b54a1..36e70cd9 100644 --- a/src/app/generated/blueprint.api/model/userTeamRole.ts +++ b/src/app/generated/blueprint.api/model/userTeamRole.ts @@ -13,7 +13,9 @@ * https://openapi-generator.tech * Do not edit the class manually. */ -export interface UserTeamRole { + + +export interface UserTeamRole { dateCreated?: Date; dateModified?: Date | null; createdBy?: string; @@ -21,7 +23,6 @@ export interface UserTeamRole { id?: string; teamId?: string; userId?: string; - role?: string; + role?: string | null; } - diff --git a/src/app/services/dialog/dialog.service.ts b/src/app/services/dialog/dialog.service.ts index ede04100..5d3d15de 100755 --- a/src/app/services/dialog/dialog.service.ts +++ b/src/app/services/dialog/dialog.service.ts @@ -17,7 +17,7 @@ export class DialogService { public confirm(title: string, message: string, data?: any): Observable { - const dialogRef: MatDialogRef = this.dialog.open(ConfirmDialogComponent, { maxWidth: '90vw', width: 'auto', data: data || {} }); + const dialogRef: MatDialogRef = this.dialog.open(ConfirmDialogComponent, { maxWidth: '500px', width: 'auto', data: data || {} }); dialogRef.componentInstance.title = title; dialogRef.componentInstance.message = message; diff --git a/src/styles/styles.scss b/src/styles/styles.scss index a825a5fc..dd06473c 100755 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -207,3 +207,32 @@ mat-paginator { } /* end of FlexLayout replacements */ + +/* Competency select panel (rendered outside component as overlay) */ +.competency-select-panel { + .competency-search-box { + display: flex; + align-items: center; + gap: 4px; + padding: 8px 12px; + position: sticky; + top: 0; + background: var(--mat-sys-surface); + z-index: 1; + + input { + border: none; + outline: none; + flex: 1; + font-size: 14px; + background: transparent; + color: var(--mat-sys-on-surface); + } + } + + .competency-no-results { + padding: 12px 16px; + font-size: 13px; + color: var(--mat-sys-on-surface-variant); + } +}