@sunbird-cb/cbp-ai 0.1.119 → 0.1.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3822,6 +3822,16 @@ class GapAnalysisRecommendedCourseComponent {
3822
3822
  'competencySubtheme'
3823
3823
  ]);
3824
3824
  }
3825
+ getProficiencyLevel(competency) {
3826
+ return this.getValue(competency, [
3827
+ 'proficiency_level',
3828
+ 'proficiencyLevel',
3829
+ 'requiredProficiency',
3830
+ 'required_proficiency',
3831
+ 'currentProficiency',
3832
+ 'current_proficiency'
3833
+ ]);
3834
+ }
3825
3835
  /**
3826
3836
  * IMPORTANT:
3827
3837
  * course_level is the authoritative STO level of a course.
@@ -3915,53 +3925,76 @@ class GapAnalysisRecommendedCourseComponent {
3915
3925
  const requiredArea = this.normalize(this.getCompetencyArea(requiredCompetency));
3916
3926
  const requiredTheme = this.normalize(this.getCompetencyTheme(requiredCompetency));
3917
3927
  const requiredSubtheme = this.normalize(this.getCompetencySubtheme(requiredCompetency));
3928
+ const requiredProficiencyLevel = this.normalize(this.getProficiencyLevel(requiredCompetency));
3918
3929
  if (!requiredTheme || !requiredSubtheme) {
3919
3930
  return;
3920
3931
  }
3932
+ const courseProficiencyLevel = this.normalize(this.getProficiencyLevel(courseCompetency));
3921
3933
  /*
3922
- * Area:
3923
- * If both course and role contain Area, they must match.
3924
- * If Area is absent on either side, Theme + Subtheme remain
3925
- * the matching keys.
3926
- */
3934
+ * Match:
3935
+ * Area + Theme + Subtheme + Proficiency Level
3936
+ */
3927
3937
  const areaMatches = !courseArea ||
3928
3938
  !requiredArea ||
3929
3939
  courseArea === requiredArea;
3930
3940
  const themeMatches = courseTheme === requiredTheme;
3931
3941
  const subthemeMatches = courseSubtheme === requiredSubtheme;
3932
- if (!areaMatches || !themeMatches || !subthemeMatches) {
3933
- return;
3934
- }
3942
+ const proficiencyLevelMatches = courseProficiencyLevel === requiredProficiencyLevel;
3943
+ /*
3944
+ * All 4 parameters must match.
3945
+ *
3946
+ * If all 4 match:
3947
+ * NOT part of gap
3948
+ *
3949
+ * If any one does not match:
3950
+ * PART of gap
3951
+ */
3952
+ const allFourMatch = areaMatches &&
3953
+ themeMatches &&
3954
+ subthemeMatches &&
3955
+ proficiencyLevelMatches;
3935
3956
  const requiredLevel = this.getRequiredLevel(requiredCompetency);
3936
3957
  const currentLevel = this.getCurrentLevel(requiredCompetency);
3937
3958
  const requiredRank = this.getLevelRank(requiredLevel);
3938
3959
  const courseRank = this.getLevelRank(courseLevel);
3939
3960
  let status;
3940
3961
  let reason;
3941
- if (!courseLevel || !requiredLevel || !requiredRank || !courseRank) {
3942
- status = 'NO_MATCH';
3943
- reason =
3944
- 'Area, Theme and Subtheme match, but the required level or course_level is missing or invalid.';
3945
- }
3946
- else if (courseRank >= requiredRank) {
3962
+ if (allFourMatch) {
3963
+ /*
3964
+ * All four competency attributes match.
3965
+ * Therefore this competency is NOT part of the gap.
3966
+ */
3947
3967
  status = 'FULL';
3948
3968
  reason =
3949
- `Course covers the ${courseLevel} level, which meets or exceeds the required ${requiredLevel} level for this competency.`;
3950
- }
3951
- else if (courseRank === requiredRank - 1) {
3952
- status = 'PARTIAL';
3953
- reason =
3954
- `Course covers the ${courseLevel} level, which is one level below the required ${requiredLevel} level.`;
3969
+ 'Competency Area, Theme, Subtheme and Proficiency Level all match. This competency is not part of the gap.';
3955
3970
  }
3956
3971
  else {
3972
+ /*
3973
+ * At least one of the four attributes does not match.
3974
+ * Therefore this competency is part of the gap.
3975
+ */
3976
+ const mismatches = [];
3977
+ if (!areaMatches) {
3978
+ mismatches.push('Competency Area');
3979
+ }
3980
+ if (!themeMatches) {
3981
+ mismatches.push('Theme');
3982
+ }
3983
+ if (!subthemeMatches) {
3984
+ mismatches.push('Subtheme');
3985
+ }
3986
+ if (!proficiencyLevelMatches) {
3987
+ mismatches.push('Proficiency Level');
3988
+ }
3957
3989
  status = 'NO_MATCH';
3958
3990
  reason =
3959
- `Course covers the ${courseLevel} level, which is below the required ${requiredLevel} level.`;
3991
+ `${mismatches.join(', ')} ${mismatches.length === 1 ? 'does' : 'do'} not match. This competency is part of the gap.`;
3960
3992
  }
3961
3993
  results.push({
3962
3994
  area: this.getCompetencyArea(requiredCompetency),
3963
3995
  theme: this.getCompetencyTheme(requiredCompetency),
3964
3996
  subtheme: this.getCompetencySubtheme(requiredCompetency),
3997
+ proficiencyLevel: this.getProficiencyLevel(requiredCompetency),
3965
3998
  currentLevel,
3966
3999
  requiredLevel,
3967
4000
  courseLevel,
@@ -3979,6 +4012,7 @@ class GapAnalysisRecommendedCourseComponent {
3979
4012
  this.normalize(result.area),
3980
4013
  this.normalize(result.theme),
3981
4014
  this.normalize(result.subtheme),
4015
+ this.normalize(result.proficiencyLevel),
3982
4016
  this.normalize(result.requiredLevel),
3983
4017
  this.normalize(result.courseLevel)
3984
4018
  ].join('|');
@@ -4123,6 +4157,7 @@ class GenerateCourseRecommendationComponent {
4123
4157
  // "recommendations already exist" pause, which previously caused the progress
4124
4158
  // display to jump to 100% and then reset back to 0%).
4125
4159
  this.progressTimerId = null;
4160
+ this.immediateFailureHandled = false;
4126
4161
  this.searchText = '';
4127
4162
  this.filterdCourses = [];
4128
4163
  this.originalData = [];
@@ -5877,6 +5912,7 @@ class GenerateCourseRecommendationComponent {
5877
5912
  this.progressPercentage = 30;
5878
5913
  this.startProgressiveLoading();
5879
5914
  this.firstApiResponse = null;
5915
+ this.immediateFailureHandled = false;
5880
5916
  concat(of(null), // immediate call
5881
5917
  interval(5000) // poll every 5 sec
5882
5918
  )
@@ -5887,18 +5923,42 @@ class GenerateCourseRecommendationComponent {
5887
5923
  this.existingRecommendationFound = true;
5888
5924
  }
5889
5925
  console.log('First recommendation response:', this.firstApiResponse);
5926
+ if (response?.status === 'FAILED') {
5927
+ // takeWhile still forwards this same value to the subscriber below; tell it to skip
5928
+ this.immediateFailureHandled = true;
5929
+ this.stopProgressiveLoading();
5930
+ this.sharedService.deleteCourseRecommendations(role_mapping_id).pipe(catchError((error) => {
5931
+ // A 404 just means there were no course recommendations to delete yet;
5932
+ // either way, don't let this block the automatic retry below.
5933
+ if (error?.status !== 404) {
5934
+ console.error(`Failed to delete course recommendations for role mapping ${role_mapping_id}`, error);
5935
+ }
5936
+ return of(null);
5937
+ })).subscribe(() => {
5938
+ this.loading = false;
5939
+ this.firstApiResponse = null;
5940
+ // regenerate
5941
+ this.getRecommendedCourseWithProgress(role_mapping_id)
5942
+ .then(resolve)
5943
+ .catch(reject);
5944
+ });
5945
+ }
5890
5946
  }
5891
5947
  }), takeWhile((response) => {
5892
5948
  // stop polling if existing recommendations found
5893
5949
  if (this.firstApiResponse?.is_existing) {
5894
5950
  return false;
5895
5951
  }
5896
- return response?.status !== 'COMPLETED';
5952
+ return response?.status !== 'COMPLETED' && response?.status !== 'FAILED';
5897
5953
  }, true), finalize(() => {
5898
5954
  this.loading = false;
5899
5955
  }))
5900
5956
  .subscribe({
5901
5957
  next: (response) => {
5958
+ if (this.immediateFailureHandled) {
5959
+ this.immediateFailureHandled = false; // already handled in tap() above; avoid double-handling this same emission
5960
+ return;
5961
+ }
5902
5962
  // ===========================
5903
5963
  // EXISTING RECOMMENDATIONS
5904
5964
  // ===========================
@@ -5966,6 +6026,21 @@ class GenerateCourseRecommendationComponent {
5966
6026
  });
5967
6027
  resolve(response);
5968
6028
  }
6029
+ // ===========================
6030
+ // FAILED (after being in progress)
6031
+ // ===========================
6032
+ else if (response?.status === 'FAILED') {
6033
+ this.stopProgressiveLoading();
6034
+ this.loading = false;
6035
+ this.currentProcessingStage = '';
6036
+ this.progressPercentage = 0;
6037
+ this.firstApiResponse = null;
6038
+ this.snackBar.open('Generation failed please try again later', 'X', {
6039
+ duration: 3000,
6040
+ panelClass: ['snackbar-error']
6041
+ });
6042
+ reject(response);
6043
+ }
5969
6044
  },
5970
6045
  error: (error) => {
5971
6046
  this.stopProgressiveLoading();
@@ -11680,6 +11755,7 @@ class RoleMappingGenerationComponent {
11680
11755
  this.workAllocationOrderDocumentMissing = false;
11681
11756
  this.showWorkAllocationOrderDocumentMissing = false;
11682
11757
  this.destroy$ = new Subject();
11758
+ this.immediateFailureHandled = false;
11683
11759
  this.onDepartmentScroll = (event) => {
11684
11760
  const panel = event.target;
11685
11761
  if (panel.scrollTop + panel.clientHeight >= panel.scrollHeight - 60) {
@@ -12206,6 +12282,7 @@ class RoleMappingGenerationComponent {
12206
12282
  }
12207
12283
  generateFinalRoleMapping() {
12208
12284
  this.apiLoading = true;
12285
+ this.immediateFailureHandled = false;
12209
12286
  if (!this.roleMappingForm.valid) {
12210
12287
  this.roleMappingForm.markAllAsTouched();
12211
12288
  return;
@@ -12270,6 +12347,7 @@ class RoleMappingGenerationComponent {
12270
12347
  if (data && data?.status === 'FAILED') {
12271
12348
  this.apiLoading = false;
12272
12349
  this.loading = false;
12350
+ this.immediateFailureHandled = true; // takeWhile still forwards this same value to subscribe(); tell it to skip
12273
12351
  this.destroy$.next(); // 🛑 stop polling
12274
12352
  this.regenerateRoleMappingAfterFailedAndDelete();
12275
12353
  return;
@@ -12282,6 +12360,10 @@ class RoleMappingGenerationComponent {
12282
12360
  data?.status !== 'FAILED', true // emit the final COMPLETED/FAILED response
12283
12361
  ))
12284
12362
  .subscribe(data => {
12363
+ if (this.immediateFailureHandled) {
12364
+ this.immediateFailureHandled = false; // already handled in tap() above; avoid double-handling this same emission
12365
+ return;
12366
+ }
12285
12367
  console.log('role mapping data--', data);
12286
12368
  if (this.firstApiResponse?.is_existing) {
12287
12369
  this.showWorkAllocationOrderDocumentMissing = false;
@@ -12369,7 +12451,10 @@ class RoleMappingGenerationComponent {
12369
12451
  this.loading = false;
12370
12452
  this.apiLoading = false;
12371
12453
  this.destroy$.next(); // 🛑 stop polling
12372
- this.showRoleMappingFailedPopup();
12454
+ this.snackBar.open('Generation failed please try again later', 'X', {
12455
+ duration: 3000,
12456
+ panelClass: ['snackbar-error']
12457
+ });
12373
12458
  return;
12374
12459
  }
12375
12460
  });
@@ -12626,45 +12711,41 @@ class RoleMappingGenerationComponent {
12626
12711
  goToUploadDocument() {
12627
12712
  this.router.navigate(['/ai/upload-documents']);
12628
12713
  }
12629
- showRoleMappingFailedPopup() {
12630
- console.log('role mapping failed, try to regenerate role mapping?');
12631
- // alert('role mapping failed, try to regenerate role mapping?')
12632
- const dialogRef = this.dialog.open(DeleteRoleMappingPopupComponent, {
12633
- width: '400px',
12634
- data: { documents: this.documents, from: 'roleMappingFailed', },
12635
- panelClass: 'view-cbp-plan-popup',
12636
- minHeight: '300px',
12637
- maxHeight: '80vh',
12638
- disableClose: true // Optional: prevent closing with outside click
12639
- });
12640
- dialogRef.afterClosed().subscribe(result => {
12641
- if (result === 'saved') {
12642
- console.log('Changes saved!');
12643
- this.regenerateRoleMappingAfterFailedAndDelete();
12644
- this.loading = true;
12645
- }
12646
- else {
12647
- this.loading = false;
12648
- }
12649
- });
12650
- }
12651
12714
  regenerateRoleMappingAfterFailedAndDelete() {
12652
- this.sharedService.deleteRoleMappingByStateAndDepartment(this.roleMappingForm.value.ministry, this.roleMappingForm.value.departments).subscribe({
12653
- next: (res) => {
12654
- // Success handling
12655
- console.log('Success:', res);
12656
- this.loading = false;
12657
- this.firstApiResponse = [];
12658
- this.generateFinalRoleMapping();
12715
+ const stateCenterId = this.roleMappingForm.value.ministry;
12716
+ const departmentId = this.roleMappingForm.value.departments;
12717
+ this.sharedService.getRoleMappingByStateCenterAndDepartment(stateCenterId, departmentId).subscribe({
12718
+ next: (existingRoleMappings) => {
12719
+ const roleMappingIds = (Array.isArray(existingRoleMappings) ? existingRoleMappings : [])
12720
+ .map((roleMapping) => roleMapping?.id)
12721
+ .filter(Boolean);
12722
+ if (!roleMappingIds.length) {
12723
+ this.loading = false;
12724
+ this.firstApiResponse = null;
12725
+ this.generateFinalRoleMapping();
12726
+ return;
12727
+ }
12728
+ forkJoin(roleMappingIds.map((id) => this.sharedService.deleteCourseRecommendations(id).pipe(catchError((error) => {
12729
+ // A 404 just means this designation had no course recommendations yet.
12730
+ // Either way, one designation's delete failing shouldn't block the
12731
+ // regenerate retry for the rest.
12732
+ if (error?.status !== 404) {
12733
+ console.error(`Failed to delete course recommendations for role mapping ${id}`, error);
12734
+ }
12735
+ return of(null);
12736
+ })))).subscribe(() => {
12737
+ this.loading = false;
12738
+ this.firstApiResponse = null;
12739
+ this.generateFinalRoleMapping();
12740
+ });
12659
12741
  },
12660
12742
  error: (error) => {
12661
12743
  this.snackBar.open(error?.error?.detail, 'X', {
12662
12744
  duration: 3000,
12663
12745
  panelClass: ['snackbar-error']
12664
12746
  });
12665
- this.firstApiResponse = [];
12666
12747
  this.loading = false;
12667
- // this.generateFinalRoleMapping()
12748
+ this.firstApiResponse = null;
12668
12749
  }
12669
12750
  });
12670
12751
  }