From 170b0c781baad81286768c2e38e0b7c60f9002ff Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 9 Sep 2026 12:57:18 +0530 Subject: [PATCH 01/16] fix: batch Nikshay villages request to avoid oversized GET query string Selecting all facilities across multiple Nikshay TUs (e.g. a full district) could push 1000+ facility IDs into a single comma-separated GET query string, exceeding the ~8KB request-line limit enforced by proxies/Tomcat and causing a 400 before the request ever reached the controller (observed as a 400/OK Http failure on /nikshay/location/villages). getNikshayVillages now splits facilityIDs into batches of 200, fetches them in parallel via forkJoin, and merges the responses into the same { data: [...] } shape callers already expect, so no changes were needed at either call site in work-location-mapping.component.ts. Co-Authored-By: Claude Sonnet 5 --- .../services/work-location-mapping.service.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts b/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts index e28a507..073198a 100644 --- a/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts +++ b/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts @@ -21,9 +21,18 @@ */ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { forkJoin, of } from 'rxjs'; +import { map } from 'rxjs/operators'; import { ConfigService } from 'src/app/core/services/config/config.service'; import { environment } from 'src/environments/environment'; +// A "select all" facility pick (e.g. every facility under several Nikshay +// TUs) can put 1000+ IDs on this GET's query string, which blows past the +// server/proxy request-line limit (~8KB) and comes back as a 400. The +// backend only exposes a comma-separated-param GET (no POST/body variant), +// so batch client-side instead of widening a single request. +const NIKSHAY_VILLAGES_BATCH_SIZE = 200; + /** * Author: krishna Gunti ( 378952 ) * Date: 05-03-2018 @@ -152,8 +161,29 @@ export class WorkLocationMapping { } getNikshayVillages(facilityIDs: any[]) { - return this.http.get( - `${environment.nikshayVillages_url}?facilityIDs=${facilityIDs.join(',')}`, + const ids = (facilityIDs || []).filter( + (id) => id !== null && id !== undefined, + ); + if (!ids.length) { + return of({ data: [] }); + } + const chunks: any[][] = []; + for (let i = 0; i < ids.length; i += NIKSHAY_VILLAGES_BATCH_SIZE) { + chunks.push(ids.slice(i, i + NIKSHAY_VILLAGES_BATCH_SIZE)); + } + return forkJoin( + chunks.map((chunk) => + this.http.get( + `${environment.nikshayVillages_url}?facilityIDs=${chunk.join(',')}`, + ), + ), + ).pipe( + map((responses: any[]) => ({ + data: responses.reduce( + (acc: any[], r: any) => acc.concat(r?.data || []), + [], + ), + })), ); } From 9a1829f3977866b7c079ff82b497748d1effaf05 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:03:32 +0530 Subject: [PATCH 02/16] Role Mapping Not Saving When Updating Multiple Roles (Registrar/Nurse/Counsellor) (#162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: normalize roleID types before diffing existing vs new roles existingRoleIDs (from group.roles) and newRoleIDs (from roleIDs_duringEdit) were compared with plain Array.includes() with no type coercion. When one source returns roleID as a numeric string and the other as a number, an already-existing role fails the match and gets bucketed into both rolesToAdd and rolesToRemove, firing simultaneous create+delete calls that race each other and can silently drop a role (e.g. Counsellor) on update. Co-Authored-By: Claude Sonnet 5 * fix: batch newly-added roles into a single SaveWorkLocationMapping call rolesToAdd previously fired one SaveWorkLocationMapping POST per role in parallel via forkJoin. The backend treats a create as the sole active role for the user+serviceline+block scope, so separate parallel creates raced each other and only the last commit stayed active - dropping roles added alongside others (reproduced with StopTB Counsellor + Nurse, where Nurse's row was born already deleted). Batch all rolesToAdd into one call instead, with every new role in the previleges[0].ID array - the same shape already used when two roles are added together via Create New Mapping, which works correctly. Location fields (incl. StopTB's nikshayTUID/nikshayFacilityID) are per-group, not per-role, so batching does not change what is sent for them. Co-Authored-By: Claude Sonnet 5 * test: cover role add/keep/remove diffing in updateGroupedWorkLocation - newly-added roles (e.g. Nurse + Counsellor) must go out in a single SaveWorkLocationMapping call, not one per role - an existing roleID stored as a numeric string must be treated as equal to the same roleID selected as a number, so it is kept in-place rather than spuriously added and removed at once Regression coverage for the StopTB Counsellor/Nurse role-dropping bug fixed in the previous two commits. Co-Authored-By: Claude Sonnet 5 * fix: reactivate soft-deleted role on re-add; group Stop TB rows like HWC/FLW Two related changes to updateGroupedWorkLocation()'s edit flow, both scoped to make the Select Role checkboxes behave as the single source of truth: checked = active, unchecked = soft-deleted, and re-checking a previously-removed role reactivates it rather than piling up duplicates. 1. Stop TB rows are now grouped by user+TU (Nikshay block), same as HWC/FLW already are, so multiple roles for one user+location show as one row with multiple role chips instead of a separate row per role. This also removes the risk of an edit on one role's row reaching into an unrelated sibling row's role, since everything for that user+location is already one group. 2. A role in rolesToAdd that has an old soft-deleted row (group.roles keeps deleted entries; only existingRoleIDs filters them out) is now reactivated in place via the same {uSRMappingID, deleted:false} call the existing Activate button already uses, with its location/role fields refreshed to the current edit form — instead of always creating a brand-new duplicate row. Only roles with no prior row at all go through the batched create path added in the previous fix. Co-Authored-By: Claude Sonnet 5 * chore: bump version to 3.8.4 Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Claude Sonnet 5 --- pom.xml | 2 +- .../work-location-mapping.component.spec.ts | 91 ++++++++++ .../work-location-mapping.component.ts | 162 ++++++++++++------ 3 files changed, 204 insertions(+), 51 deletions(-) diff --git a/pom.xml b/pom.xml index c6b72b8..dbb8e47 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.admin-ui admin-ui - 3.8.2 + 3.8.4 Admin-UI Piramal - admin: Module ui diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.spec.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.spec.ts index 9f935f4..ad75c8d 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.spec.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.spec.ts @@ -41,6 +41,10 @@ const FakeWorkLocationMapping = { getServices: (_userID: any) => of({ data: [] }), getMappedWorkLocationList: (_serviceProviderID: any) => of({ data: [] }), getUserName: (_serviceProviderID: any) => of({ data: [] }), + SaveWorkLocationMapping: (_data: any) => of({}), + UpdateWorkLocationMapping: (_data: any) => of({}), + DeleteWorkLocationMapping: (_data: any) => of({}), + DeleteWorkLocationMappingForTM: (_data: any) => of({}), }; const FakeVillageMasterService = {}; @@ -192,4 +196,91 @@ describe('WorkLocationMappingComponent', () => { expect(saved.villageName).toEqual(['Alamkeri']); }); }); + + // Regression coverage for the StopTB "Counsellor/Nurse silently + // dropped on Update" bug: (1) existingRoleIDs/newRoleIDs must compare + // equal regardless of which side is a numeric string, and (2) every + // newly-added role must go out in one SaveWorkLocationMapping call, + // not one racing call per role. + describe('updateGroupedWorkLocation — role add/keep/remove diffing', () => { + initTestBed(); + + const baseGroup = (roles: any[]): any => ({ + userID: 4426, + serviceID: 11, + serviceName: 'Stop TB', + roles, + }); + + beforeEach(() => { + component.RolesList = [ + { roleID: 168, roleName: 'Registration Officer' }, + { roleID: 169, roleName: 'Nurse' }, + { roleID: 170, roleName: 'Counsellor' }, + ]; + component.userID_duringEdit = 4426; + component.providerServiceMapID_duringEdit = 1734; + component.stateID_duringEdit = 2; + component.district_duringEdit = 1; + component.workLocationID_duringEdit = null; + component.isFacilityServicelineEdit = false; + component.isStopTBServicelineEdit = false; + component.editIsAshaSupervisor = false; + component.selectedNikshayTUs = []; + component.selectedNikshayFacilities = []; + component.selectedNikshayVillages = []; + component.selectedNikshayBlock = null; + component.serviceEditvillage = []; + component.editVillageArr = []; + }); + + it('batches two newly-added roles (Nurse + Counsellor) into a single SaveWorkLocationMapping call', () => { + const service = TestBed.inject(WorkLocationMapping); + const saveSpy = spyOn(service, 'SaveWorkLocationMapping').and.returnValue( + of({}), + ); + + component.editGroupedElement = baseGroup([]); + component.roleIDs_duringEdit = [169, 170]; + + component.updateGroupedWorkLocation({}); + + expect(saveSpy).toHaveBeenCalledTimes(1); + const [payload] = saveSpy.calls.mostRecent().args; + const sentRoleIDs = payload[0].previleges[0].ID.map((r: any) => r.roleID); + expect(sentRoleIDs).toEqual([169, 170]); + }); + + it('treats a numeric-string existing roleID as equal to a numeric selected roleID (no spurious add+remove)', () => { + const service = TestBed.inject(WorkLocationMapping); + const saveSpy = spyOn(service, 'SaveWorkLocationMapping').and.returnValue( + of({}), + ); + const updateSpy = spyOn( + service, + 'UpdateWorkLocationMapping', + ).and.returnValue(of({})); + const deleteSpy = spyOn( + service, + 'DeleteWorkLocationMapping', + ).and.returnValue(of({})); + + // Registration Officer's roleID arrives as a string from the + // work-location list endpoint; the checkbox sends it back as a number. + component.editGroupedElement = baseGroup([ + { + roleID: '168' as any, + uSRMappingID: 5907, + userServciceRoleDeleted: false, + }, + ]); + component.roleIDs_duringEdit = [168]; + + component.updateGroupedWorkLocation({}); + + expect(updateSpy).toHaveBeenCalledTimes(1); + expect(saveSpy).not.toHaveBeenCalled(); + expect(deleteSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts index 8113a2f..11445fb 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts @@ -1072,10 +1072,15 @@ export class WorkLocationMappingComponent const groupMap = new Map(); for (const row of flatRows) { - // Group rows for HWC/FLW service lines (including ASHA Supervisor) by user+block - // Other services get one row per entry + // Group rows for HWC/FLW (including ASHA Supervisor) and Stop TB by + // user+block (Stop TB's "block" is the Nikshay TU — see blockIDToUse + // in updateGroupedWorkLocation) so multiple roles for the same + // user+location show as one row with multiple role chips, instead of + // a separate row per role. Other services still get one row per entry. const isFacilityService = - row.serviceName === 'HWC' || row.serviceName === 'FLW'; + row.serviceName === 'HWC' || + row.serviceName === 'FLW' || + row.serviceName === 'Stop TB'; let key: string; if (isFacilityService) { key = [ @@ -5321,10 +5326,12 @@ export class WorkLocationMappingComponent ...new Set( group.roles .filter((r) => !r.userServciceRoleDeleted) - .map((r) => r.roleID), + .map((r) => Number(r.roleID)), ), ]; - const newRoleIDs: number[] = this.roleIDs_duringEdit; + const newRoleIDs: number[] = (this.roleIDs_duringEdit || []).map( + (rid: any) => Number(rid), + ); const rolesToAdd = newRoleIDs.filter( (rid) => !existingRoleIDs.includes(rid), @@ -5408,51 +5415,59 @@ export class WorkLocationMappingComponent ? nikshayFacilityIDArr.join(',') : null; + // Shared by "keep" (role stays checked) and "reactivate" (role was + // unchecked before, is checked again now) — both are an in-place + // update of an existing uSRMappingID, refreshed to the edit form's + // current location/role fields. + const buildRoleUpdateObj = (rid: number, uSRMappingID: any) => { + const roleName = this.getRoleNameById(rid); + const updateObj: any = { + uSRMappingID, + userID: this.userID_duringEdit, + roleID: rid, + providerServiceMapID: this.providerServiceMapID_duringEdit, + workingLocationID: this.isFacilityServicelineEdit + ? null + : this.workLocationID_duringEdit, + stateID: this.stateID_duringEdit, + districtID: this.district_duringEdit, + blockID: blockIDToUse, + blockName: blockNameToUse, + villageID: villageIDToUse, + villageName: villageNameToUse, + modifiedBy: this.createdBy, + }; + if (this.isStopTBServicelineEdit) { + updateObj.nikshayTUID = nikshayTUIDToSend; + updateObj.nikshayFacilityID = nikshayFacilityIDToSend; + } + if (group.serviceName === '1097') { + updateObj.inbound = + roleName?.toLowerCase() === 'supervisor' ? false : this.isInboundEdit; + updateObj.outbound = + roleName?.toLowerCase() === 'supervisor' + ? false + : this.isOutboundEdit; + } + if (group.serviceName === 'HWC') { + updateObj.teleConsultation = this.teleConsultationEdit; + } + if (this.isFacilityServicelineEdit && this.editFacilityMappingData) { + updateObj.facilityID = this.editFacilityMappingData.facilityID; + } + return updateObj; + }; + // UPDATE existing rows for kept roles for (const rid of rolesToKeep) { const existingEntry = group.roles.find( (r) => r.roleID === rid && !r.userServciceRoleDeleted, ); if (existingEntry) { - const roleName = this.getRoleNameById(rid); - const updateObj: any = { - uSRMappingID: existingEntry.uSRMappingID, - userID: this.userID_duringEdit, - roleID: rid, - providerServiceMapID: this.providerServiceMapID_duringEdit, - workingLocationID: this.isFacilityServicelineEdit - ? null - : this.workLocationID_duringEdit, - stateID: this.stateID_duringEdit, - districtID: this.district_duringEdit, - blockID: blockIDToUse, - blockName: blockNameToUse, - villageID: villageIDToUse, - villageName: villageNameToUse, - modifiedBy: this.createdBy, - }; - if (this.isStopTBServicelineEdit) { - updateObj.nikshayTUID = nikshayTUIDToSend; - updateObj.nikshayFacilityID = nikshayFacilityIDToSend; - } - if (group.serviceName === '1097') { - updateObj.inbound = - roleName?.toLowerCase() === 'supervisor' - ? false - : this.isInboundEdit; - updateObj.outbound = - roleName?.toLowerCase() === 'supervisor' - ? false - : this.isOutboundEdit; - } - if (group.serviceName === 'HWC') { - updateObj.teleConsultation = this.teleConsultationEdit; - } - if (this.isFacilityServicelineEdit && this.editFacilityMappingData) { - updateObj.facilityID = this.editFacilityMappingData.facilityID; - } allRequests.push( - this.worklocationmapping.UpdateWorkLocationMapping(updateObj), + this.worklocationmapping.UpdateWorkLocationMapping( + buildRoleUpdateObj(rid, existingEntry.uSRMappingID), + ), ); } } @@ -5475,14 +5490,61 @@ export class WorkLocationMappingComponent } } - // CREATE new rows for added roles - for (const rid of rolesToAdd) { - const roleName = this.getRoleNameById(rid); + // A checked role with no active row may still have an old soft-deleted + // row from a previous remove (group.roles keeps deleted entries too — + // only existingRoleIDs filters them out). Reactivate that row in place + // (same {uSRMappingID, deleted:false} call the Activate button already + // uses) instead of creating a duplicate — otherwise every remove-then- + // re-add cycle leaves another dead row behind for the same role. + const rolesToReactivate = rolesToAdd.filter((rid) => + group.roles.some((r) => r.roleID === rid && r.userServciceRoleDeleted), + ); + const rolesToCreateFresh = rolesToAdd.filter( + (rid) => !rolesToReactivate.includes(rid), + ); + + for (const rid of rolesToReactivate) { + const deadEntry = group.roles.find( + (r) => r.roleID === rid && r.userServciceRoleDeleted, + ); + if (deadEntry) { + const reactivateObj = { + uSRMappingID: deadEntry.uSRMappingID, + deleted: false, + }; + allRequests.push( + group.serviceID === 4 + ? this.worklocationmapping.DeleteWorkLocationMappingForTM( + reactivateObj, + ) + : this.worklocationmapping.DeleteWorkLocationMapping(reactivateObj), + ); + // Reactivating only flips the deleted flag — refresh its location/ + // role fields to what the edit form currently shows, same as a kept role. + allRequests.push( + this.worklocationmapping.UpdateWorkLocationMapping( + buildRoleUpdateObj(rid, deadEntry.uSRMappingID), + ), + ); + } + } + + // CREATE new rows for roles that never existed for this user+scope. + // Batched into a single SaveWorkLocationMapping call (one previleges + // entry, ID array holding every added role) instead of one call per + // role. Firing them separately let the backend treat each create as + // the sole active role for this user+serviceline+block, so parallel + // creates raced and only the last commit stayed active — dropping + // roles added alongside others (reproduced with StopTB Counsellor/ + // Nurse). Location fields (incl. StopTB's nikshayTUID/nikshayFacilityID) + // are per-group, not per-role, so batching them here is unchanged. + if (rolesToCreateFresh.length > 0) { const newObj: any = { previleges: [ { - ID: [ - { + ID: rolesToCreateFresh.map((rid) => { + const roleName = this.getRoleNameById(rid); + return { roleID: rid, teleConsultation: group.serviceName === 'HWC' && @@ -5500,8 +5562,8 @@ export class WorkLocationMappingComponent roleName?.toLowerCase() !== 'supervisor' ? this.isOutboundEdit : null, - }, - ], + }; + }), providerServiceMapID: this.providerServiceMapID_duringEdit, workingLocationID: this.isFacilityServicelineEdit ? null From 115d843cfc33a67f6498d6b4a062e325fcd26bec Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:04:23 +0530 Subject: [PATCH 03/16] fix: surface activation/deactivation failures instead of reporting success (#163) OutputResponse returns HTTP 200 with statusCode 5000 on failure, and the global interceptor only special-cases 5002 (session expiry), so an API-level failure reached the success callback. Clicking Activate on a user the backend could not update showed "Activated successfully", then refreshed the list with the user still deactivated -- the failure was only ever visible in the browser console. The success callback now checks statusCode before alerting, and the error callback raises the message instead of just logging it. Co-authored-by: Sneha Co-authored-by: Claude Opus 5 (1M context) --- .../employee-master-new.component.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.ts b/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.ts index 7fdde2a..98b30c3 100644 --- a/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.ts @@ -1616,8 +1616,17 @@ export class EmployeeMasterNewComponent implements OnInit { this.employeeMasterNewService .userActivationDeactivation(obj) .subscribe( - (res) => { - console.log('Activation or deactivation response', res.data); + (res: any) => { + if (res?.statusCode !== 200) { + this.dialogService.alert( + res?.errorMessage || + 'Failed to ' + + this.confirmMessage.toLowerCase() + + ' the user', + 'error', + ); + return; + } this.dialogService.alert( this.confirmMessage + 'd successfully', 'success', @@ -1625,7 +1634,16 @@ export class EmployeeMasterNewComponent implements OnInit { this.getAllUserDetails(); this.searchTerm = null; }, - (err) => console.log('error', err), + (err: any) => { + console.log('error', err); + this.dialogService.alert( + err?.errorMessage || + 'Failed to ' + + this.confirmMessage.toLowerCase() + + ' the user', + 'error', + ); + }, ); } }, From 7a5139a7c485b7e820e693790f3ed55744332539 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 28 Sep 2026 09:49:36 +0530 Subject: [PATCH 04/16] perf(work-location-mapping): keep Nikshay dropdowns responsive on large districts Stop TB's TU/Facility/Village multi-selects can hold thousands of options, which froze or crashed the tab on low-end PCs: - mat-select re-sorts its selection on every click, Select All and Edit prefill using options.indexOf() inside the sort. Record each item's position when the list is assigned and pass a sortComparator that uses it (5,327 facilities: 81 ms -> 2 ms per sort on a dev PC). - Load facilities/villages after a 400 ms pause and keep only the latest request (switchMap), instead of one request per click where a slow earlier response could overwrite a newer one. - The "all selected" getters run on every change detection; skip building the ID Set unless the selected count can match. Co-Authored-By: Claude Opus 5.5 --- .../work-location-mapping.component.html | 6 + .../work-location-mapping.component.ts | 155 ++++++++++++++---- 2 files changed, 132 insertions(+), 29 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html index ee73e5b..a3b1126 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html @@ -431,6 +431,7 @@

multiple [style.width]="'100%'" [(ngModel)]="selectedNikshayTUs" + [sortComparator]="sortNikshayOptions" name="nikshayTUs" (selectionChange)="onNikshayTUChange()" (openedChange)="$event ? null : nikshayTUSearch = ''" @@ -461,6 +462,7 @@

multiple [style.width]="'100%'" [(ngModel)]="selectedNikshayFacilities" + [sortComparator]="sortNikshayOptions" name="nikshayFacilities" (selectionChange)="onNikshayFacilityChange()" (openedChange)="$event ? null : nikshayFacilitySearch = ''" @@ -491,6 +493,7 @@

multiple [style.width]="'100%'" [(ngModel)]="selectedNikshayVillages" + [sortComparator]="sortNikshayOptions" name="nikshayVillages" (openedChange)="$event ? null : nikshayVillageSearch = ''" required @@ -1017,6 +1020,7 @@

multiple [style.width]="'100%'" [(ngModel)]="selectedNikshayTUs" + [sortComparator]="sortNikshayOptions" name="nikshayTUsEdit" (selectionChange)="onNikshayTUChange()" (openedChange)="$event ? null : nikshayTUSearch = ''" @@ -1046,6 +1050,7 @@

multiple [style.width]="'100%'" [(ngModel)]="selectedNikshayFacilities" + [sortComparator]="sortNikshayOptions" name="nikshayFacilitiesEdit" (selectionChange)="onNikshayFacilityChange()" (openedChange)="$event ? null : nikshayFacilitySearch = ''" @@ -1075,6 +1080,7 @@

multiple [style.width]="'100%'" [(ngModel)]="selectedNikshayVillages" + [sortComparator]="sortNikshayOptions" name="nikshayVillagesEdit" (openedChange)="$event ? null : nikshayVillageSearch = ''" required diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts index 11445fb..cf7378c 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts @@ -36,8 +36,9 @@ import { ConfirmationDialogsService } from 'src/app/core/services/dialog/confirm import { WorkLocationMapping } from '../services/work-location-mapping.service'; import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { FacilityMasterService } from 'src/app/core/services/inventory-services/facilitytypemaster.service'; -import { forkJoin, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { MatOption } from '@angular/material/core'; +import { forkJoin, of, Subject } from 'rxjs'; +import { catchError, debounceTime, switchMap, takeUntil } from 'rxjs/operators'; interface RoleEntry { roleID: number; @@ -224,9 +225,60 @@ export class WorkLocationMappingComponent isStopTBServiceline = false; nikshayStateList: any[] = []; nikshayDistrictList: any[] = []; - nikshayTUList: any[] = []; - nikshayFacilityList: any[] = []; - nikshayVillageList: any[] = []; + // TU/Facility/Village lists can hold thousands of entries (e.g. Pune MC: + // 5,327 facilities, Ganganagar: 3,058 villages). Each setter records every + // item's position once so nikshayOptionOrder can sort mat-select's selection + // cheaply — see sortNikshayOptions. + private _nikshayTUList: any[] = []; + private _nikshayFacilityList: any[] = []; + private _nikshayVillageList: any[] = []; + private nikshayOptionOrder = new WeakMap(); + + get nikshayTUList(): any[] { + return this._nikshayTUList; + } + set nikshayTUList(list: any[]) { + this._nikshayTUList = this.indexNikshayOptions(list); + } + + get nikshayFacilityList(): any[] { + return this._nikshayFacilityList; + } + set nikshayFacilityList(list: any[]) { + this._nikshayFacilityList = this.indexNikshayOptions(list); + } + + get nikshayVillageList(): any[] { + return this._nikshayVillageList; + } + set nikshayVillageList(list: any[]) { + this._nikshayVillageList = this.indexNikshayOptions(list); + } + + private indexNikshayOptions(list: any[]): any[] { + const items = list || []; + items.forEach((item: any, i: number) => { + if (item && typeof item === 'object') { + this.nikshayOptionOrder.set(item, i); + } + }); + return items; + } + + // mat-select (multiple) re-sorts its selection on every click, Select All + // and Edit prefill. Its default comparator calls options.indexOf() inside + // the sort — O(n^2 log n) — which freezes low-end PCs once a list reaches a + // few thousand entries. Sorting by the position recorded above keeps the + // same order at O(n log n). + sortNikshayOptions = (a: MatOption, b: MatOption): number => + (this.nikshayOptionOrder.get(a.value) ?? 0) - + (this.nikshayOptionOrder.get(b.value) ?? 0); + + // Ticking facilities/TUs one by one used to fire one request per click, + // and a slow earlier response could overwrite a newer one. Wait for the + // user to pause, then keep only the latest request. + private nikshayTUSelection$ = new Subject(); + private nikshayFacilitySelection$ = new Subject(); selectedNikshayBlock: any = null; selectedNikshayTUs: any[] = []; selectedNikshayFacilities: any[] = []; @@ -288,6 +340,10 @@ export class WorkLocationMappingComponent get allNikshayTUsSelected(): boolean { if (!this.nikshayTUList?.length) return false; + // Runs on every change detection — skip the Set build unless the counts + // could actually match. + if ((this.selectedNikshayTUs?.length || 0) < this.nikshayTUList.length) + return false; const selectedIDs = new Set( (this.selectedNikshayTUs || []).map((t: any) => t.nikshayTUID), ); @@ -303,6 +359,13 @@ export class WorkLocationMappingComponent get allNikshayFacilitiesSelected(): boolean { if (!this.nikshayFacilityList?.length) return false; + // Runs on every change detection — skip the Set build unless the counts + // could actually match. + if ( + (this.selectedNikshayFacilities?.length || 0) < + this.nikshayFacilityList.length + ) + return false; const selectedIDs = new Set( (this.selectedNikshayFacilities || []).map( (f: any) => f.nikshayFacilityID, @@ -322,6 +385,13 @@ export class WorkLocationMappingComponent get allNikshayVillagesSelected(): boolean { if (!this.nikshayVillageList?.length) return false; + // Runs on every change detection — skip the Set build unless the counts + // could actually match. + if ( + (this.selectedNikshayVillages?.length || 0) < + this.nikshayVillageList.length + ) + return false; const selectedIDs = new Set( (this.selectedNikshayVillages || []).map((v: any) => v.nikshayVillageID), ); @@ -476,18 +546,9 @@ export class WorkLocationMappingComponent const tuIDs = (this.selectedNikshayTUs || []).map( (t: any) => t.nikshayTUID, ); - if (!tuIDs.length) return; - this.worklocationmapping - .getNikshayFacilities(tuIDs) - .pipe(takeUntil(this.destroy$)) - .subscribe( - (response: any) => { - this.nikshayFacilityList = response.data || []; - }, - () => { - this.alertService.alert('Failed to load Nikshay facilities', 'error'); - }, - ); + // Always emit (even when empty) so a pending request is cancelled + this.nikshayTUSelection$.next(tuIDs); + this.nikshayFacilitySelection$.next([]); } // Called whenever the Facility multi-select changes @@ -497,18 +558,53 @@ export class WorkLocationMappingComponent const facilityIDs = (this.selectedNikshayFacilities || []).map( (f: any) => f.nikshayFacilityID, ); - if (!facilityIDs.length) return; - this.worklocationmapping - .getNikshayVillages(facilityIDs) - .pipe(takeUntil(this.destroy$)) - .subscribe( - (response: any) => { - this.nikshayVillageList = response.data || []; - }, - () => { - this.alertService.alert('Failed to load Nikshay villages', 'error'); - }, - ); + this.nikshayFacilitySelection$.next(facilityIDs); + } + + private initNikshayCascade() { + this.nikshayTUSelection$ + .pipe( + debounceTime(400), + switchMap((tuIDs: number[]) => + !tuIDs.length + ? of(null) + : this.worklocationmapping.getNikshayFacilities(tuIDs).pipe( + catchError(() => { + this.alertService.alert( + 'Failed to load Nikshay facilities', + 'error', + ); + return of(null); + }), + ), + ), + takeUntil(this.destroy$), + ) + .subscribe((response: any) => { + this.nikshayFacilityList = response?.data || []; + }); + + this.nikshayFacilitySelection$ + .pipe( + debounceTime(400), + switchMap((facilityIDs: number[]) => + !facilityIDs.length + ? of(null) + : this.worklocationmapping.getNikshayVillages(facilityIDs).pipe( + catchError(() => { + this.alertService.alert( + 'Failed to load Nikshay villages', + 'error', + ); + return of(null); + }), + ), + ), + takeUntil(this.destroy$), + ) + .subscribe((response: any) => { + this.nikshayVillageList = response?.data || []; + }); } resetNikshaySelection() { @@ -939,6 +1035,7 @@ export class WorkLocationMappingComponent this.getProviderServices(this.userID); this.getAllMappedWorkLocations(); this.getUserName(this.serviceProviderID); + this.initNikshayCascade(); } ngOnDestroy() { From 2ab4b580f07e677da3159a57e222d9b60ef04e30 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 28 Sep 2026 09:59:18 +0530 Subject: [PATCH 05/16] fix(work-location-mapping): let reset and Edit cancel pending Nikshay loads The debounced facility/village loads emitted an empty list on reset or deselect, which was applied 400 ms later and could wipe lists that Edit prefill had just filled; a request already in flight could also land after a reset. Emit null instead: it cancels any pending request at once (no debounce) and never assigns anything. Co-Authored-By: Claude Opus 5.5 --- .../work-location-mapping.component.ts | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts index cf7378c..1fc966d 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts @@ -37,8 +37,8 @@ import { WorkLocationMapping } from '../services/work-location-mapping.service'; import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { FacilityMasterService } from 'src/app/core/services/inventory-services/facilitytypemaster.service'; import { MatOption } from '@angular/material/core'; -import { forkJoin, of, Subject } from 'rxjs'; -import { catchError, debounceTime, switchMap, takeUntil } from 'rxjs/operators'; +import { EMPTY, forkJoin, of, Subject, timer } from 'rxjs'; +import { catchError, debounce, switchMap, takeUntil } from 'rxjs/operators'; interface RoleEntry { roleID: number; @@ -276,9 +276,11 @@ export class WorkLocationMappingComponent // Ticking facilities/TUs one by one used to fire one request per click, // and a slow earlier response could overwrite a newer one. Wait for the - // user to pause, then keep only the latest request. - private nikshayTUSelection$ = new Subject(); - private nikshayFacilitySelection$ = new Subject(); + // user to pause, then keep only the latest request. null cancels any + // pending request without touching the lists, so a late response can't + // overwrite a reset or an Edit prefill. + private nikshayTUSelection$ = new Subject(); + private nikshayFacilitySelection$ = new Subject(); selectedNikshayBlock: any = null; selectedNikshayTUs: any[] = []; selectedNikshayFacilities: any[] = []; @@ -546,9 +548,8 @@ export class WorkLocationMappingComponent const tuIDs = (this.selectedNikshayTUs || []).map( (t: any) => t.nikshayTUID, ); - // Always emit (even when empty) so a pending request is cancelled - this.nikshayTUSelection$.next(tuIDs); - this.nikshayFacilitySelection$.next([]); + this.nikshayTUSelection$.next(tuIDs.length ? tuIDs : null); + this.nikshayFacilitySelection$.next(null); } // Called whenever the Facility multi-select changes @@ -558,16 +559,19 @@ export class WorkLocationMappingComponent const facilityIDs = (this.selectedNikshayFacilities || []).map( (f: any) => f.nikshayFacilityID, ); - this.nikshayFacilitySelection$.next(facilityIDs); + this.nikshayFacilitySelection$.next( + facilityIDs.length ? facilityIDs : null, + ); } private initNikshayCascade() { this.nikshayTUSelection$ .pipe( - debounceTime(400), - switchMap((tuIDs: number[]) => - !tuIDs.length - ? of(null) + // cancels (null) go through at once; selections wait for a pause + debounce((ids: number[] | null) => (ids ? timer(400) : of(0))), + switchMap((tuIDs: number[] | null) => + !tuIDs + ? EMPTY : this.worklocationmapping.getNikshayFacilities(tuIDs).pipe( catchError(() => { this.alertService.alert( @@ -581,15 +585,16 @@ export class WorkLocationMappingComponent takeUntil(this.destroy$), ) .subscribe((response: any) => { - this.nikshayFacilityList = response?.data || []; + if (response) this.nikshayFacilityList = response.data || []; }); this.nikshayFacilitySelection$ .pipe( - debounceTime(400), - switchMap((facilityIDs: number[]) => - !facilityIDs.length - ? of(null) + // cancels (null) go through at once; selections wait for a pause + debounce((ids: number[] | null) => (ids ? timer(400) : of(0))), + switchMap((facilityIDs: number[] | null) => + !facilityIDs + ? EMPTY : this.worklocationmapping.getNikshayVillages(facilityIDs).pipe( catchError(() => { this.alertService.alert( @@ -603,11 +608,13 @@ export class WorkLocationMappingComponent takeUntil(this.destroy$), ) .subscribe((response: any) => { - this.nikshayVillageList = response?.data || []; + if (response) this.nikshayVillageList = response.data || []; }); } resetNikshaySelection() { + this.nikshayTUSelection$.next(null); + this.nikshayFacilitySelection$.next(null); this.nikshayStateList = []; this.nikshayDistrictList = []; this.nikshayTUList = []; From 842c365741d952116c6c554a08c8ba51d7ac534c Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 9 Oct 2026 20:54:08 +0530 Subject: [PATCH 06/16] perf(work-location-mapping): use Set lookups when prefilling Nikshay Edit Edit prefill matched saved TU/facility/village IDs with includes() inside filter(), which is quadratic. Bengaluru City has 6,167 facilities, so a mapping saved with Select All did ~38M comparisons on Edit. Match against a Set instead; same strict equality, same result. Co-Authored-By: Claude Opus 5.5 --- .../work-location-mapping.component.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts index 1fc966d..4a53cac 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts @@ -860,8 +860,11 @@ export class WorkLocationMappingComponent .pipe(takeUntil(this.destroy$)) .subscribe((tuResponse: any) => { this.nikshayTUList = tuResponse.data || []; + // Set lookups: a district can have thousands of saved facilities and + // villages, and includes() inside filter() is quadratic. + const tuIDSet = new Set(existingTUIDs); this.selectedNikshayTUs = this.nikshayTUList.filter((t: any) => - existingTUIDs.includes(t.nikshayTUID), + tuIDSet.has(t.nikshayTUID), ); // "Select Block" is a single-select quick-add convenience over this // same TU list (see onNikshayBlockChange), but the saved BlockID IS @@ -882,8 +885,9 @@ export class WorkLocationMappingComponent .pipe(takeUntil(this.destroy$)) .subscribe((facResponse: any) => { this.nikshayFacilityList = facResponse.data || []; + const facilityIDSet = new Set(existingFacilityIDs); this.selectedNikshayFacilities = this.nikshayFacilityList.filter( - (f: any) => existingFacilityIDs.includes(f.nikshayFacilityID), + (f: any) => facilityIDSet.has(f.nikshayFacilityID), ); if (!this.selectedNikshayFacilities.length) return; @@ -895,8 +899,9 @@ export class WorkLocationMappingComponent .pipe(takeUntil(this.destroy$)) .subscribe((villResponse: any) => { this.nikshayVillageList = villResponse.data || []; + const villageIDSet = new Set(uniqueVillageIDs); this.selectedNikshayVillages = this.nikshayVillageList.filter( - (v: any) => uniqueVillageIDs.includes(v.nikshayVillageID), + (v: any) => villageIDSet.has(v.nikshayVillageID), ); }); }); From f76086a23b7474045cb25c101d26fad8d8eddcad Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 9 Oct 2026 21:20:39 +0530 Subject: [PATCH 07/16] perf(work-location-mapping): select large Nikshay lists in one pass When the bound value is replaced (Select All, Deselect All, Edit prefill) or the option list changes (reload, search filter), mat-select re-selects every value one by one: a linear search per value, then a full re-sort after each option. On Bengaluru City (6,167 facilities) that froze the page ~0.9 s on a dev PC and ~5.5 s at 6x CPU slowdown. Add an attribute directive on the six Nikshay selects that matches values to options through a Map and sorts once. Same selection, same order; single clicks still go through mat-select. Falls back to mat-select's own code if its internals change. Co-Authored-By: Claude Opus 5.5 --- .../activities/activities.module.ts | 2 + .../fast-bulk-select.directive.ts | 87 +++++++++++++++++++ .../work-location-mapping.component.html | 6 ++ 3 files changed, 95 insertions(+) create mode 100644 src/app/app-provider-admin/provider-admin/activities/work-location-mapping/fast-bulk-select.directive.ts diff --git a/src/app/app-provider-admin/provider-admin/activities/activities.module.ts b/src/app/app-provider-admin/provider-admin/activities/activities.module.ts index da4ab8d..c407c4e 100644 --- a/src/app/app-provider-admin/provider-admin/activities/activities.module.ts +++ b/src/app/app-provider-admin/provider-admin/activities/activities.module.ts @@ -25,6 +25,7 @@ import { RoleMasterComponent } from './role-master/provider-admin-role-master.co import { ServicelineCdssMappingComponent } from './serviceline-cdss-mapping/servicelineCdssMapping.component'; import { SpecialistMappingComponent } from './specialist-mapping/specialist-mapping.component'; import { WorkLocationMappingComponent } from './work-location-mapping/work-location-mapping.component'; +import { FastBulkSelectDirective } from './work-location-mapping/fast-bulk-select.directive'; import { UserFacilityMappingComponent } from './work-location-mapping/user-facility-mapping/user-facility-mapping.component'; import { VillageMasterService } from 'src/app/core/services/adminServices/AdminVillage/village-master-service.service'; import { CommonServices } from 'src/app/core/services/inventory-services/commonServices'; @@ -123,6 +124,7 @@ import { CoreModule } from 'src/app/core/core.module'; InstituteDirectoryMasterComponent, EditInstituteDirectoryComponent, WorkLocationMappingComponent, + FastBulkSelectDirective, FeedbackComplaintNatureMasterComponent, EditFeedbackNatureModalComponent, NatureOfComplaintCategoryMappingComponent, diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/fast-bulk-select.directive.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/fast-bulk-select.directive.ts new file mode 100644 index 0000000..69f4ab1 --- /dev/null +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/fast-bulk-select.directive.ts @@ -0,0 +1,87 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { Directive, Self } from '@angular/core'; +import { MatSelect } from '@angular/material/select'; + +/** + * Bulk selection for very large multi-selects (Stop TB Nikshay TU/Facility/ + * Village lists hold up to 6,167 options). + * + * Whenever the bound value is replaced (Select All, Deselect All, Edit + * prefill) or the option list changes (list reload, search filter), + * mat-select re-selects every value one by one: a linear search per value, + * then a full re-sort after each selected option. That is quadratic and froze + * low-end PCs for 5-45 s on large districts. This replaces only that step: + * values are matched to options through a Map and the selection is sorted + * once at the end. The resulting selection and its order are the same, and + * single clicks still go through mat-select unchanged. + * + * Assumes mat-select's default identity compareWith (none of the Nikshay + * selects set one). If Material's internals change, nothing is patched and + * mat-select's own code runs. + */ +@Directive({ + selector: 'mat-select[appFastBulkSelect]', +}) +export class FastBulkSelectDirective { + constructor(@Self() select: MatSelect) { + const s: any = select; + const original = s._setSelectionByValue; + if (typeof original !== 'function' || typeof s._sortValues !== 'function') { + return; + } + s._setSelectionByValue = function (value: any) { + if ( + !this.multiple || + !Array.isArray(value) || + !this.options || + !this._selectionModel + ) { + return original.call(this, value); + } + const sortValues = this._sortValues; + // Every option selected below triggers a full re-sort; sort once instead. + this._sortValues = () => undefined; + try { + this.options.forEach((option: any) => option.setInactiveStyles()); + this._selectionModel.clear(); + const optionByValue = new Map(); + this.options.forEach((option: any) => { + const v = option.value; + if (v !== null && v !== undefined && !optionByValue.has(v)) { + optionByValue.set(v, option); + } + }); + const matched = new Set(); + value.forEach((v: any) => { + const option = optionByValue.get(v); + if (option) matched.add(option); + }); + if (matched.size) this._selectionModel.select(...matched); + } finally { + this._sortValues = sortValues; + } + this._sortValues(); + this._changeDetectorRef.markForCheck(); + }; + } +} diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html index a3b1126..f85fae6 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html @@ -432,6 +432,7 @@

[style.width]="'100%'" [(ngModel)]="selectedNikshayTUs" [sortComparator]="sortNikshayOptions" + appFastBulkSelect name="nikshayTUs" (selectionChange)="onNikshayTUChange()" (openedChange)="$event ? null : nikshayTUSearch = ''" @@ -463,6 +464,7 @@

[style.width]="'100%'" [(ngModel)]="selectedNikshayFacilities" [sortComparator]="sortNikshayOptions" + appFastBulkSelect name="nikshayFacilities" (selectionChange)="onNikshayFacilityChange()" (openedChange)="$event ? null : nikshayFacilitySearch = ''" @@ -494,6 +496,7 @@

[style.width]="'100%'" [(ngModel)]="selectedNikshayVillages" [sortComparator]="sortNikshayOptions" + appFastBulkSelect name="nikshayVillages" (openedChange)="$event ? null : nikshayVillageSearch = ''" required @@ -1021,6 +1024,7 @@

[style.width]="'100%'" [(ngModel)]="selectedNikshayTUs" [sortComparator]="sortNikshayOptions" + appFastBulkSelect name="nikshayTUsEdit" (selectionChange)="onNikshayTUChange()" (openedChange)="$event ? null : nikshayTUSearch = ''" @@ -1051,6 +1055,7 @@

[style.width]="'100%'" [(ngModel)]="selectedNikshayFacilities" [sortComparator]="sortNikshayOptions" + appFastBulkSelect name="nikshayFacilitiesEdit" (selectionChange)="onNikshayFacilityChange()" (openedChange)="$event ? null : nikshayFacilitySearch = ''" @@ -1081,6 +1086,7 @@

[style.width]="'100%'" [(ngModel)]="selectedNikshayVillages" [sortComparator]="sortNikshayOptions" + appFastBulkSelect name="nikshayVillagesEdit" (openedChange)="$event ? null : nikshayVillageSearch = ''" required From 46e60757b603c45099ba38add80125c3a60f3bc4 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 9 Oct 2026 22:42:21 +0530 Subject: [PATCH 08/16] perf(work-location-mapping): draw only visible Nikshay facility/village rows UAT districts reach 1 lakh+ villages (each village repeated under every facility). mat-select draws every option, so 2 lakh villages hung the tab for 10+ minutes even on a fast PC. - Facility and Village options sit in a cdk-virtual-scroll-viewport inside the same mat-select panel: same field, label, search box, Select All link and checkbox rows; only the rows on screen exist. - appFastBulkSelect keeps the full bound value when options are virtual (a click adds/removes just that option, the field is not shown empty when the selected rows are scrolled away) and measures the list when the panel opens. A mat-select-trigger shows the selected names. - Search filter and "all selected" checks are cached until the list, selection or search text changes. 6x CPU slowdown: 5 lakh villages open 0.00 s, Select All 1.1 s, click 0.3 s; Bengaluru facilities open 1.4 s -> 0.09 s. Create/Edit/Save/Reset results identical to release-3.8.4 on Bengaluru and Mayurbhanj. Co-Authored-By: Claude Opus 5.5 --- .../activities/activities.module.ts | 2 + .../fast-bulk-select.directive.ts | 185 +++++++++++++++--- .../work-location-mapping.component.css | 6 +- .../work-location-mapping.component.html | 52 +++-- .../work-location-mapping.component.ts | 125 +++++++++--- 5 files changed, 299 insertions(+), 71 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/activities.module.ts b/src/app/app-provider-admin/provider-admin/activities/activities.module.ts index c407c4e..1cc5471 100644 --- a/src/app/app-provider-admin/provider-admin/activities/activities.module.ts +++ b/src/app/app-provider-admin/provider-admin/activities/activities.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { ScrollingModule } from '@angular/cdk/scrolling'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ProviderAdminRoleService } from './services/state-serviceline-role.service'; import { MaterialModule } from 'src/app/core/material.module'; @@ -163,6 +164,7 @@ import { CoreModule } from 'src/app/core/core.module'; ], imports: [ CommonModule, + ScrollingModule, MaterialModule, FormsModule, ReactiveFormsModule, diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/fast-bulk-select.directive.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/fast-bulk-select.directive.ts index 69f4ab1..7572ae9 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/fast-bulk-select.directive.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/fast-bulk-select.directive.ts @@ -19,36 +19,79 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { Directive, Self } from '@angular/core'; +import { + AfterContentInit, + ContentChild, + Directive, + OnDestroy, + Self, +} from '@angular/core'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { MatSelect } from '@angular/material/select'; +import { Subscription } from 'rxjs'; /** * Bulk selection for very large multi-selects (Stop TB Nikshay TU/Facility/ - * Village lists hold up to 6,167 options). + * Village lists: thousands of options, and on UAT 1 lakh+ villages in one + * district). * - * Whenever the bound value is replaced (Select All, Deselect All, Edit - * prefill) or the option list changes (list reload, search filter), - * mat-select re-selects every value one by one: a linear search per value, - * then a full re-sort after each selected option. That is quadratic and froze - * low-end PCs for 5-45 s on large districts. This replaces only that step: - * values are matched to options through a Map and the selection is sorted - * once at the end. The resulting selection and its order are the same, and - * single clicks still go through mat-select unchanged. + * 1. Whenever the bound value is replaced (Select All, Deselect All, Edit + * prefill) or the option list changes (list reload, search filter), + * mat-select re-selects every value one by one: a linear search per value, + * then a full re-sort after each selected option. That is quadratic and + * froze low-end PCs for 5-45 s on large districts. Here values are matched + * to options through a Map/Set and the selection is sorted once. + * + * 2. When the options sit in a , only the rows + * on screen exist as mat-options, but mat-select assumes every option is + * rendered: on a click it would emit only the rendered selected options + * (dropping everything scrolled away) and it would show the field as empty + * when none of the selected rows are on screen. In that case this keeps + * the full bound value: a click adds/removes just the clicked option's + * value, `empty` reflects the full value, and the viewport is measured when + * the panel opens. Use a for the label text. * * Assumes mat-select's default identity compareWith (none of the Nikshay - * selects set one). If Material's internals change, nothing is patched and - * mat-select's own code runs. + * selects set one), and that sortComparator reads only `.value`. If + * Material's internals change, nothing is patched and mat-select's own code + * runs. */ @Directive({ selector: 'mat-select[appFastBulkSelect]', }) -export class FastBulkSelectDirective { - constructor(@Self() select: MatSelect) { +export class FastBulkSelectDirective implements AfterContentInit, OnDestroy { + @ContentChild(CdkVirtualScrollViewport, { descendants: true }) + viewport?: CdkVirtualScrollViewport; + + private openSub?: Subscription; + + constructor(@Self() private select: MatSelect) { const s: any = select; - const original = s._setSelectionByValue; - if (typeof original !== 'function' || typeof s._sortValues !== 'function') { + const originalSetSelection = s._setSelectionByValue; + const originalPropagate = s._propagateChanges; + const originalOnSelect = s._onSelect; + if ( + typeof originalSetSelection !== 'function' || + typeof originalPropagate !== 'function' || + typeof originalOnSelect !== 'function' || + typeof s._sortValues !== 'function' + ) { return; } + const isVirtual = () => !!this.viewport; + + // Set of the current bound value, rebuilt only when the array changes + // (virtual lists re-sync the selection on every scroll). + let valueRef: any[] | null = null; + let valueSet = new Set(); + const setOf = (value: any[]) => { + if (value !== valueRef) { + valueRef = value; + valueSet = new Set(value); + } + return valueSet; + }; + s._setSelectionByValue = function (value: any) { if ( !this.multiple || @@ -56,7 +99,7 @@ export class FastBulkSelectDirective { !this.options || !this._selectionModel ) { - return original.call(this, value); + return originalSetSelection.call(this, value); } const sortValues = this._sortValues; // Every option selected below triggers a full re-sort; sort once instead. @@ -64,18 +107,26 @@ export class FastBulkSelectDirective { try { this.options.forEach((option: any) => option.setInactiveStyles()); this._selectionModel.clear(); - const optionByValue = new Map(); - this.options.forEach((option: any) => { - const v = option.value; - if (v !== null && v !== undefined && !optionByValue.has(v)) { - optionByValue.set(v, option); - } - }); const matched = new Set(); - value.forEach((v: any) => { - const option = optionByValue.get(v); - if (option) matched.add(option); - }); + if (isVirtual()) { + // Few rendered options, possibly 1 lakh+ values: walk the options. + const selected = setOf(value); + this.options.forEach((option: any) => { + if (selected.has(option.value)) matched.add(option); + }); + } else { + const optionByValue = new Map(); + this.options.forEach((option: any) => { + const v = option.value; + if (v !== null && v !== undefined && !optionByValue.has(v)) { + optionByValue.set(v, option); + } + }); + value.forEach((v: any) => { + const option = optionByValue.get(v); + if (option) matched.add(option); + }); + } if (matched.size) this._selectionModel.select(...matched); } finally { this._sortValues = sortValues; @@ -83,5 +134,83 @@ export class FastBulkSelectDirective { this._sortValues(); this._changeDetectorRef.markForCheck(); }; + + // Remember which option a click toggled, so the full value can be + // updated for just that option. + let toggled: any = null; + s._onSelect = function (option: any, isUserInput: boolean) { + toggled = option; + try { + return originalOnSelect.call(this, option, isUserInput); + } finally { + toggled = null; + } + }; + + s._propagateChanges = function (fallbackValue: any) { + if (!isVirtual() || !this.multiple || !toggled) { + return originalPropagate.call(this, fallbackValue); + } + const value = toggled.value; + const next: any[] = Array.isArray(this._value) ? [...this._value] : []; + const at = next.indexOf(value); + if (toggled.selected && at === -1) { + // Insert at the position mat-select's own sort would give it. + const cmp = this.sortComparator; + let i = next.length; + if (cmp) { + let lo = 0; + let hi = next.length; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (cmp({ value: next[mid] }, { value }, []) <= 0) lo = mid + 1; + else hi = mid; + } + i = lo; + } + next.splice(i, 0, value); + } else if (!toggled.selected && at !== -1) { + next.splice(at, 1); + } + this._value = next; + this.valueChange.emit(next); + this._onChange(next); + this.selectionChange.emit(this._getChangeEvent(next)); + this._changeDetectorRef.markForCheck(); + }; + + // The selected rows may all be scrolled away; the field is only empty + // when the bound value is. + const emptyDescriptor = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(s), + 'empty', + ); + if (emptyDescriptor?.get) { + const originalEmpty = emptyDescriptor.get; + Object.defineProperty(s, 'empty', { + configurable: true, + get() { + if (isVirtual() && s.multiple) { + return !(Array.isArray(s._value) && s._value.length); + } + return originalEmpty.call(s); + }, + }); + } + } + + ngAfterContentInit() { + // The viewport is created while the panel is closed (zero size); measure + // it once the panel is open, starting from the top like a normal list. + this.openSub = this.select.openedChange.subscribe((open: boolean) => { + if (open && this.viewport) { + this.viewport.checkViewportSize(); + this.viewport.scrollToIndex(0); + } + }); + } + + ngOnDestroy() { + this.openSub?.unsubscribe(); } } diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css index b34a8f3..e5e8422 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css @@ -130,4 +130,8 @@ ::ng-deep .select-search-box input:focus { border-color: #3f51b5; -} \ No newline at end of file +} +/* Nikshay Facility/Village options: virtual scroll list inside the select panel */ +::ng-deep .nikshay-option-viewport { + width: 100%; +} diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html index f85fae6..bb3204b 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html @@ -481,9 +481,16 @@

allNikshayFacilitiesSelected ? 'Deselect All' : 'Select All' }} - - {{ item.facilityName }} - + {{ nikshayTriggerText(selectedNikshayFacilities, 'facilityName') }} + + + {{ item.facilityName }} + + @@ -512,9 +519,16 @@

allNikshayVillagesSelected ? 'Deselect All' : 'Select All' }} - - {{ item.villageName }} - + {{ nikshayTriggerText(selectedNikshayVillages, 'villageName') }} + + + {{ item.villageName }} + + @@ -1072,9 +1086,16 @@

allNikshayFacilitiesSelected ? 'Deselect All' : 'Select All' }} - - {{ item.facilityName }} - + {{ nikshayTriggerText(selectedNikshayFacilities, 'facilityName') }} + + + {{ item.facilityName }} + + @@ -1102,9 +1123,16 @@

allNikshayVillagesSelected ? 'Deselect All' : 'Select All' }} - - {{ item.villageName }} - + {{ nikshayTriggerText(selectedNikshayVillages, 'villageName') }} + + + {{ item.villageName }} + + diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts index 4a53cac..70f48ae 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts @@ -312,34 +312,85 @@ export class WorkLocationMappingComponent ); } + // The getters below run on every change detection. With 1 lakh+ villages + // the filter/Set work is cached until the list, selection or search text + // changes (all three are always replaced, never mutated in place). + private nikshayMemo = new Map(); + private memoNikshay(key: string, deps: any[], compute: () => T): T { + const hit = this.nikshayMemo.get(key); + if (hit && hit.deps.every((d, i) => d === deps[i])) return hit.value; + const value = compute(); + this.nikshayMemo.set(key, { deps, value }); + return value; + } + get filteredNikshayFacilityList(): any[] { if (!this.nikshayFacilitySearch) return this.nikshayFacilityList; - const s = this.nikshayFacilitySearch.toLowerCase(); - const selectedIDs = new Set( - (this.selectedNikshayFacilities || []).map( - (f: any) => f.nikshayFacilityID, - ), - ); - return this.nikshayFacilityList.filter( - (f: any) => - selectedIDs.has(f.nikshayFacilityID) || - (f.facilityName || '').toLowerCase().includes(s), + return this.memoNikshay( + 'filteredFacilities', + [ + this.nikshayFacilityList, + this.selectedNikshayFacilities, + this.nikshayFacilitySearch, + ], + () => { + const s = this.nikshayFacilitySearch.toLowerCase(); + const selectedIDs = new Set( + (this.selectedNikshayFacilities || []).map( + (f: any) => f.nikshayFacilityID, + ), + ); + return this.nikshayFacilityList.filter( + (f: any) => + selectedIDs.has(f.nikshayFacilityID) || + (f.facilityName || '').toLowerCase().includes(s), + ); + }, ); } get filteredNikshayVillageList(): any[] { if (!this.nikshayVillageSearch) return this.nikshayVillageList; - const s = this.nikshayVillageSearch.toLowerCase(); - const selectedIDs = new Set( - (this.selectedNikshayVillages || []).map((v: any) => v.nikshayVillageID), - ); - return this.nikshayVillageList.filter( - (v: any) => - selectedIDs.has(v.nikshayVillageID) || - (v.villageName || '').toLowerCase().includes(s), + return this.memoNikshay( + 'filteredVillages', + [ + this.nikshayVillageList, + this.selectedNikshayVillages, + this.nikshayVillageSearch, + ], + () => { + const s = this.nikshayVillageSearch.toLowerCase(); + const selectedIDs = new Set( + (this.selectedNikshayVillages || []).map( + (v: any) => v.nikshayVillageID, + ), + ); + return this.nikshayVillageList.filter( + (v: any) => + selectedIDs.has(v.nikshayVillageID) || + (v.villageName || '').toLowerCase().includes(s), + ); + }, ); } + // Text shown in a closed Facility/Village field: the selected names joined + // like mat-select's own label. The field shows one line, so only the first + // names are joined. + nikshayTriggerText(selected: any[], nameKey: string): string { + return (selected || []) + .slice(0, 50) + .map((item: any) => item?.[nameKey]) + .join(', '); + } + + // Facility/Village options are drawn in a virtual scroll list (only the + // rows on screen exist), sized like the panel's normal list area. + readonly nikshayOptionHeight = 48; + nikshayViewportHeight(count: number): number { + return Math.min(count * this.nikshayOptionHeight, 180); + } + get allNikshayTUsSelected(): boolean { if (!this.nikshayTUList?.length) return false; // Runs on every change detection — skip the Set build unless the counts @@ -368,13 +419,19 @@ export class WorkLocationMappingComponent this.nikshayFacilityList.length ) return false; - const selectedIDs = new Set( - (this.selectedNikshayFacilities || []).map( - (f: any) => f.nikshayFacilityID, - ), - ); - return this.nikshayFacilityList.every((f: any) => - selectedIDs.has(f.nikshayFacilityID), + return this.memoNikshay( + 'allFacilitiesSelected', + [this.nikshayFacilityList, this.selectedNikshayFacilities], + () => { + const selectedIDs = new Set( + (this.selectedNikshayFacilities || []).map( + (f: any) => f.nikshayFacilityID, + ), + ); + return this.nikshayFacilityList.every((f: any) => + selectedIDs.has(f.nikshayFacilityID), + ); + }, ); } @@ -394,11 +451,19 @@ export class WorkLocationMappingComponent this.nikshayVillageList.length ) return false; - const selectedIDs = new Set( - (this.selectedNikshayVillages || []).map((v: any) => v.nikshayVillageID), - ); - return this.nikshayVillageList.every((v: any) => - selectedIDs.has(v.nikshayVillageID), + return this.memoNikshay( + 'allVillagesSelected', + [this.nikshayVillageList, this.selectedNikshayVillages], + () => { + const selectedIDs = new Set( + (this.selectedNikshayVillages || []).map( + (v: any) => v.nikshayVillageID, + ), + ); + return this.nikshayVillageList.every((v: any) => + selectedIDs.has(v.nikshayVillageID), + ); + }, ); } From 85ddc80af8d755277b7861d045486618825bc301 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 10 Oct 2026 09:47:03 +0530 Subject: [PATCH 09/16] fix(work-location-mapping): keep Nikshay option rows one line high The virtual scroll list needs every row the same height, but long facility names wrapped onto 3 lines and the list scrolled sideways. Keep each row at 48 px on one line with "...", show the full name on hover, and hide the horizontal scroll. Co-Authored-By: Claude Opus 5.5 --- .../work-location-mapping.component.css | 20 ++++++++++++++++++- .../work-location-mapping.component.html | 8 ++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css index e5e8422..dba49ad 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css @@ -131,7 +131,25 @@ ::ng-deep .select-search-box input:focus { border-color: #3f51b5; } -/* Nikshay Facility/Village options: virtual scroll list inside the select panel */ +/* Nikshay Facility/Village options: virtual scroll list inside the select + panel. Every row must be the same height for the list to scroll correctly, + so long names stay on one line ("...") with the full name on hover. */ ::ng-deep .nikshay-option-viewport { width: 100%; + overflow-x: hidden; +} + +::ng-deep .nikshay-option-viewport .cdk-virtual-scroll-content-wrapper { + width: 100%; +} + +::ng-deep .nikshay-option-viewport .mat-mdc-option { + height: 48px; + min-height: 48px; +} + +::ng-deep .nikshay-option-viewport .mat-mdc-option .mdc-list-item__primary-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html index bb3204b..935b9b2 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html @@ -487,7 +487,7 @@

[itemSize]="nikshayOptionHeight" [style.height.px]="nikshayViewportHeight(filteredNikshayFacilityList.length)" > - + {{ item.facilityName }} @@ -525,7 +525,7 @@

[itemSize]="nikshayOptionHeight" [style.height.px]="nikshayViewportHeight(filteredNikshayVillageList.length)" > - + {{ item.villageName }} @@ -1092,7 +1092,7 @@

[itemSize]="nikshayOptionHeight" [style.height.px]="nikshayViewportHeight(filteredNikshayFacilityList.length)" > - + {{ item.facilityName }} @@ -1129,7 +1129,7 @@

[itemSize]="nikshayOptionHeight" [style.height.px]="nikshayViewportHeight(filteredNikshayVillageList.length)" > - + {{ item.villageName }} From 75f60c55f0cd8f3d006034bdeb872498c7b29568 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 15 Oct 2026 00:16:01 +0530 Subject: [PATCH 10/16] feat(work-location-mapping): add Stop TB team selection (AMM-2496) In AMRIT a team is a van. Stop TB gets an optional "Select Team" field on Create and Edit, listing the MMU vans of the selected state, with the team's current members shown under it. A team has many users and a user is in one team, so picking another team moves the user. - Create: the chosen team is kept on the added row and assigned after the mapping saves (once per user). - Edit: pre-selects the user's team; Update reassigns only when the team changed, and "No team" takes the user out of it. - A team that cannot be assigned is reported; the mapping stays saved. Needs Admin-API /stoptb/team/* (same branch name in Admin-API). Co-Authored-By: Claude Opus 5.5 --- .../services/work-location-mapping.service.ts | 34 ++++ .../work-location-mapping.component.css | 8 + .../work-location-mapping.component.html | 40 ++++ .../work-location-mapping.component.ts | 185 +++++++++++++++++- src/environments/environment.ci.ts.template | 5 + src/environments/environment.local.ts | 5 + src/environments/environment.prod.ts | 5 + src/environments/environment.test.ts | 5 + 8 files changed, 279 insertions(+), 8 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts b/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts index 073198a..24666e8 100644 --- a/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts +++ b/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts @@ -196,4 +196,38 @@ export class WorkLocationMapping { `${environment.nikshayUserMapping_url}?usrMappingID=${usrMappingID}`, ); } + + // Stop TB teams are the MMU vans of the user's state (AMM-2496). + getStopTBTeams(stateID: any, serviceProviderID: any) { + return this.http.get( + `${environment.stopTBTeams_url}?stateID=${stateID}&serviceProviderID=${serviceProviderID}`, + ); + } + + getStopTBUserTeam(userID: any, providerServiceMapID: any) { + return this.http.get( + `${environment.stopTBUserTeam_url}?userID=${userID}&providerServiceMapID=${providerServiceMapID}`, + ); + } + + getStopTBTeamMembers(vanID: any, providerServiceMapID: any) { + return this.http.get( + `${environment.stopTBTeamMembers_url}?vanID=${vanID}&providerServiceMapID=${providerServiceMapID}`, + ); + } + + // vanID null takes the user out of their team. + assignStopTBTeam( + userID: any, + providerServiceMapID: any, + vanID: any, + modifiedBy: any, + ) { + return this.http.post(environment.stopTBTeamAssign_url, { + userID, + providerServiceMapID, + vanID, + modifiedBy, + }); + } } diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css index dba49ad..b05839a 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.css @@ -153,3 +153,11 @@ overflow: hidden; text-overflow: ellipsis; } + +/* Stop TB team members under the Team field (AMM-2496) */ +.stoptb-team-members { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html index 935b9b2..f9ac85a 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html @@ -532,6 +532,26 @@

+ +
+ + Select Team + + No team + + {{ team.vanName }} ({{ team.parkingPlaceName }}) + + + + Members: {{ stopTBTeamMembers }} + + +
@@ -1136,6 +1156,26 @@

+ +
+ + Select Team + + No team + + {{ team.vanName }} ({{ team.parkingPlaceName }}) + + + + Members: {{ stopTBTeamMembers }} + + +
diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts index 70f48ae..8c4969d 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts @@ -37,8 +37,14 @@ import { WorkLocationMapping } from '../services/work-location-mapping.service'; import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { FacilityMasterService } from 'src/app/core/services/inventory-services/facilitytypemaster.service'; import { MatOption } from '@angular/material/core'; -import { EMPTY, forkJoin, of, Subject, timer } from 'rxjs'; -import { catchError, debounce, switchMap, takeUntil } from 'rxjs/operators'; +import { EMPTY, forkJoin, Observable, of, Subject, timer } from 'rxjs'; +import { + catchError, + debounce, + map, + switchMap, + takeUntil, +} from 'rxjs/operators'; interface RoleEntry { roleID: number; @@ -500,6 +506,9 @@ export class WorkLocationMappingComponent this.selectedNikshayTUs = []; this.selectedNikshayFacilities = []; this.selectedNikshayVillages = []; + this.selectedStopTBTeamID = null; + this.stopTBTeamMembers = ''; + this.loadStopTBTeams(state?.stateID, state?.providerServiceMapID); if (!state?.stateName) return; // AMRIT's m_state spells this "Chattisgarh" (one 'h'); Nikshay's own data @@ -677,7 +686,114 @@ export class WorkLocationMappingComponent }); } + // ── Stop TB team (AMM-2496) ── + // A team is an MMU van of the user's state. A team has many users; a user + // is in one team per Stop TB service line, so picking a new team moves them. + stopTBTeamList: any[] = []; + selectedStopTBTeamID: number | null = null; + stopTBTeamMembers = ''; + private stopTBTeamPSM: any = null; + // Team the user had when Edit opened; Update only reassigns on a change. + private stopTBTeamOriginalID: number | null = null; + + get stopTBTeamName(): string | null { + const team = this.stopTBTeamList.find( + (t: any) => t.vanID === this.selectedStopTBTeamID, + ); + return team ? team.vanName : null; + } + + resetStopTBTeam() { + this.stopTBTeamList = []; + this.selectedStopTBTeamID = null; + this.stopTBTeamMembers = ''; + this.stopTBTeamPSM = null; + this.stopTBTeamOriginalID = null; + } + + loadStopTBTeams(stateID: any, providerServiceMapID: any) { + this.stopTBTeamList = []; + this.stopTBTeamPSM = providerServiceMapID; + if (!stateID) return; + this.worklocationmapping + .getStopTBTeams(stateID, this.serviceProviderID) + .pipe(takeUntil(this.destroy$)) + .subscribe( + (response: any) => { + this.stopTBTeamList = response?.data || []; + }, + () => { + this.alertService.alert('Failed to load teams', 'error'); + }, + ); + } + + onStopTBTeamChange() { + this.stopTBTeamMembers = ''; + if (!this.selectedStopTBTeamID || !this.stopTBTeamPSM) return; + this.worklocationmapping + .getStopTBTeamMembers(this.selectedStopTBTeamID, this.stopTBTeamPSM) + .pipe(takeUntil(this.destroy$)) + .subscribe((response: any) => { + this.stopTBTeamMembers = (response?.data || []) + .map((m: any) => { + const name = [m.firstName, m.lastName].filter(Boolean).join(' '); + return name ? `${name} [${m.userName}]` : m.userName; + }) + .join(', '); + }); + } + + // Edit: load the state's teams and pre-select the user's current team. + loadStopTBTeamForEdit() { + this.loadStopTBTeams( + this.stateID_duringEdit, + this.providerServiceMapID_duringEdit, + ); + if (!this.userID_duringEdit || !this.providerServiceMapID_duringEdit) + return; + this.worklocationmapping + .getStopTBUserTeam( + this.userID_duringEdit, + this.providerServiceMapID_duringEdit, + ) + .pipe(takeUntil(this.destroy$)) + .subscribe((response: any) => { + const vanID = response?.data?.vanID ?? null; + this.selectedStopTBTeamID = vanID; + this.stopTBTeamOriginalID = vanID; + this.onStopTBTeamChange(); + }); + } + + // Assigns teams one user at a time; emits the first error message, or null. + private assignStopTBTeams( + assignments: { userID: any; providerServiceMapID: any; vanID: any }[], + ): Observable { + if (!assignments.length) return of(null); + return forkJoin( + assignments.map((a) => + this.worklocationmapping + .assignStopTBTeam( + a.userID, + a.providerServiceMapID, + a.vanID, + this.createdBy, + ) + .pipe( + map((r: any) => + r && r.statusCode !== 200 + ? r.errorMessage || 'Team could not be assigned' + : null, + ), + catchError(() => of('Team could not be assigned')), + ), + ), + ).pipe(map((errors) => errors.find((e) => !!e) || null)); + } + resetNikshaySelection() { + this.resetStopTBTeam(); this.nikshayTUSelection$.next(null); this.nikshayFacilitySelection$.next(null); this.nikshayStateList = []; @@ -702,6 +818,7 @@ export class WorkLocationMappingComponent */ loadNikshayEditSelections() { this.resetNikshaySelection(); + this.loadStopTBTeamForEdit(); const splitIDs = (value: any): number[] => String(value || '') @@ -2481,6 +2598,9 @@ export class WorkLocationMappingComponent Inbound: 'N/A', Outbound: 'N/A', teleConsultation: [null], + // Not part of the mapping request; assigned after Save (AMM-2496). + stopTBTeamVanID: this.selectedStopTBTeamID, + stopTBTeamName: this.stopTBTeamName, }; this.bufferArray.data.push(stopTBWorkLocationObj); if (this.paginatorSecond) { @@ -2808,12 +2928,35 @@ export class WorkLocationMappingComponent } saveWorkLocationOnly(requestArray: any) { + // One team per user: collect each saved Stop TB user's chosen team. + const teamAssignments = new Map(); + (this.bufferArray.data || []).forEach((row: any) => { + if (row?.stopTBTeamVanID) { + teamAssignments.set(`${row.userID}-${row.providerServiceMapID}`, { + userID: row.userID, + providerServiceMapID: row.providerServiceMapID, + vanID: row.stopTBTeamVanID, + }); + } + }); this.worklocationmapping .SaveWorkLocationMapping(requestArray) - .pipe(takeUntil(this.destroy$)) + .pipe( + switchMap(() => + this.assignStopTBTeams(Array.from(teamAssignments.values())), + ), + takeUntil(this.destroy$), + ) .subscribe( - () => { - this.alertService.alert('Mapping saved successfully', 'success'); + (teamError: string | null) => { + if (teamError) { + this.alertService.alert( + 'Mapping saved, but the team could not be assigned: ' + teamError, + 'error', + ); + } else { + this.alertService.alert('Mapping saved successfully', 'success'); + } this.cleanupAfterSave(); }, (err: any) => { @@ -5774,11 +5917,37 @@ export class WorkLocationMappingComponent return; } + const teamChanged = + this.isStopTBServicelineEdit && + this.selectedStopTBTeamID !== this.stopTBTeamOriginalID; forkJoin(allRequests) - .pipe(takeUntil(this.destroy$)) + .pipe( + switchMap(() => + this.assignStopTBTeams( + teamChanged + ? [ + { + userID: this.userID_duringEdit, + providerServiceMapID: this.providerServiceMapID_duringEdit, + vanID: this.selectedStopTBTeamID, + }, + ] + : [], + ), + ), + takeUntil(this.destroy$), + ) .subscribe( - () => { - this.alertService.alert('Mapping updated successfully', 'success'); + (teamError: string | null) => { + if (teamError) { + this.alertService.alert( + 'Mapping updated, but the team could not be changed: ' + + teamError, + 'error', + ); + } else { + this.alertService.alert('Mapping updated successfully', 'success'); + } this.showTable(); this.getAllMappedWorkLocations(); this.bufferArray.data = []; diff --git a/src/environments/environment.ci.ts.template b/src/environments/environment.ci.ts.template index 612d8c6..848ce31 100644 --- a/src/environments/environment.ci.ts.template +++ b/src/environments/environment.ci.ts.template @@ -171,6 +171,11 @@ export const environment = { nikshayFacilities_url: `${adminBaseUrl}nikshay/location/facilities`, nikshayVillages_url: `${adminBaseUrl}nikshay/location/villages`, nikshayUserMapping_url: `${adminBaseUrl}nikshay/location/userMapping`, + // Stop TB teams (MMU vans), AMM-2496 + stopTBTeams_url: `${adminBaseUrl}stoptb/team/vans`, + stopTBUserTeam_url: `${adminBaseUrl}stoptb/team/user`, + stopTBTeamMembers_url: `${adminBaseUrl}stoptb/team/members`, + stopTBTeamAssign_url: `${adminBaseUrl}stoptb/team/assign`, update_facilities_Url: `${adminBaseUrl}editFacility`, delete_facilities_Url: `${adminBaseUrl}deleteFacility`, get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`, diff --git a/src/environments/environment.local.ts b/src/environments/environment.local.ts index 668d2f0..1fed6e0 100644 --- a/src/environments/environment.local.ts +++ b/src/environments/environment.local.ts @@ -171,6 +171,11 @@ export const environment = { nikshayFacilities_url: `${adminBaseUrl}nikshay/location/facilities`, nikshayVillages_url: `${adminBaseUrl}nikshay/location/villages`, nikshayUserMapping_url: `${adminBaseUrl}nikshay/location/userMapping`, + // Stop TB teams (MMU vans), AMM-2496 + stopTBTeams_url: `${adminBaseUrl}stoptb/team/vans`, + stopTBUserTeam_url: `${adminBaseUrl}stoptb/team/user`, + stopTBTeamMembers_url: `${adminBaseUrl}stoptb/team/members`, + stopTBTeamAssign_url: `${adminBaseUrl}stoptb/team/assign`, update_facilities_Url: `${adminBaseUrl}editFacility`, delete_facilities_Url: `${adminBaseUrl}deleteFacility`, get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`, diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 273283a..079fb53 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -169,6 +169,11 @@ export const environment = { nikshayFacilities_url: `${adminBaseUrl}nikshay/location/facilities`, nikshayVillages_url: `${adminBaseUrl}nikshay/location/villages`, nikshayUserMapping_url: `${adminBaseUrl}nikshay/location/userMapping`, + // Stop TB teams (MMU vans), AMM-2496 + stopTBTeams_url: `${adminBaseUrl}stoptb/team/vans`, + stopTBUserTeam_url: `${adminBaseUrl}stoptb/team/user`, + stopTBTeamMembers_url: `${adminBaseUrl}stoptb/team/members`, + stopTBTeamAssign_url: `${adminBaseUrl}stoptb/team/assign`, update_facilities_Url: `${adminBaseUrl}editFacility`, delete_facilities_Url: `${adminBaseUrl}deleteFacility`, get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`, diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 007064b..434f26b 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -165,6 +165,11 @@ export const environment = { nikshayFacilities_url: `${adminBaseUrl}nikshay/location/facilities`, nikshayVillages_url: `${adminBaseUrl}nikshay/location/villages`, nikshayUserMapping_url: `${adminBaseUrl}nikshay/location/userMapping`, + // Stop TB teams (MMU vans), AMM-2496 + stopTBTeams_url: `${adminBaseUrl}stoptb/team/vans`, + stopTBUserTeam_url: `${adminBaseUrl}stoptb/team/user`, + stopTBTeamMembers_url: `${adminBaseUrl}stoptb/team/members`, + stopTBTeamAssign_url: `${adminBaseUrl}stoptb/team/assign`, update_facilities_Url: `${adminBaseUrl}editFacility`, delete_facilities_Url: `${adminBaseUrl}deleteFacility`, get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`, From cdd530443079195425500d889ede383569b65e0e Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 15 Oct 2026 00:22:47 +0530 Subject: [PATCH 11/16] feat(work-location-mapping): show the van ID in the Stop TB team list The van ID is what each camp laptop is configured with (STOPTB_VAN_ID), so admins can match a team to its laptop without relying on names. Co-Authored-By: Claude Opus 5.5 --- .../work-location-mapping.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html index f9ac85a..9201557 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.html @@ -544,7 +544,7 @@

> No team - {{ team.vanName }} ({{ team.parkingPlaceName }}) + {{ team.vanName }} – {{ team.vanID }} ({{ team.parkingPlaceName }}) @@ -1168,7 +1168,7 @@

> No team - {{ team.vanName }} ({{ team.parkingPlaceName }}) + {{ team.vanName }} – {{ team.vanID }} ({{ team.parkingPlaceName }}) From e202a35c61f80e97d221d0a86d42b93fd2c99f1a Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 15 Oct 2026 00:36:20 +0530 Subject: [PATCH 12/16] feat(work-location-mapping): send the Stop TB team with save and update (AMM-2496) The team is now stored as VanID on the mapping rows (Admin-API), so the chosen team goes in the same Save/Update request as the mapping instead of a separate assign call. Every role row of the user carries the same team; "No team" sends null. Save and Update now check statusCode in the response: the API returns failures such as a team from another state as HTTP 200 with a 5000 status, which used to show "saved successfully". Co-Authored-By: Claude Opus 5.5 --- .../services/work-location-mapping.service.ts | 15 --- .../work-location-mapping.component.ts | 118 ++++++------------ src/environments/environment.ci.ts.template | 1 - src/environments/environment.local.ts | 1 - src/environments/environment.prod.ts | 1 - src/environments/environment.test.ts | 1 - 6 files changed, 35 insertions(+), 102 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts b/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts index 24666e8..a967ffa 100644 --- a/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts +++ b/src/app/app-provider-admin/provider-admin/activities/services/work-location-mapping.service.ts @@ -215,19 +215,4 @@ export class WorkLocationMapping { `${environment.stopTBTeamMembers_url}?vanID=${vanID}&providerServiceMapID=${providerServiceMapID}`, ); } - - // vanID null takes the user out of their team. - assignStopTBTeam( - userID: any, - providerServiceMapID: any, - vanID: any, - modifiedBy: any, - ) { - return this.http.post(environment.stopTBTeamAssign_url, { - userID, - providerServiceMapID, - vanID, - modifiedBy, - }); - } } diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts index 8c4969d..afb3605 100644 --- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts @@ -37,14 +37,8 @@ import { WorkLocationMapping } from '../services/work-location-mapping.service'; import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { FacilityMasterService } from 'src/app/core/services/inventory-services/facilitytypemaster.service'; import { MatOption } from '@angular/material/core'; -import { EMPTY, forkJoin, Observable, of, Subject, timer } from 'rxjs'; -import { - catchError, - debounce, - map, - switchMap, - takeUntil, -} from 'rxjs/operators'; +import { EMPTY, forkJoin, of, Subject, timer } from 'rxjs'; +import { catchError, debounce, switchMap, takeUntil } from 'rxjs/operators'; interface RoleEntry { roleID: number; @@ -693,8 +687,6 @@ export class WorkLocationMappingComponent selectedStopTBTeamID: number | null = null; stopTBTeamMembers = ''; private stopTBTeamPSM: any = null; - // Team the user had when Edit opened; Update only reassigns on a change. - private stopTBTeamOriginalID: number | null = null; get stopTBTeamName(): string | null { const team = this.stopTBTeamList.find( @@ -708,7 +700,6 @@ export class WorkLocationMappingComponent this.selectedStopTBTeamID = null; this.stopTBTeamMembers = ''; this.stopTBTeamPSM = null; - this.stopTBTeamOriginalID = null; } loadStopTBTeams(stateID: any, providerServiceMapID: any) { @@ -761,35 +752,23 @@ export class WorkLocationMappingComponent .subscribe((response: any) => { const vanID = response?.data?.vanID ?? null; this.selectedStopTBTeamID = vanID; - this.stopTBTeamOriginalID = vanID; this.onStopTBTeamChange(); }); } - // Assigns teams one user at a time; emits the first error message, or null. - private assignStopTBTeams( - assignments: { userID: any; providerServiceMapID: any; vanID: any }[], - ): Observable { - if (!assignments.length) return of(null); - return forkJoin( - assignments.map((a) => - this.worklocationmapping - .assignStopTBTeam( - a.userID, - a.providerServiceMapID, - a.vanID, - this.createdBy, - ) - .pipe( - map((r: any) => - r && r.statusCode !== 200 - ? r.errorMessage || 'Team could not be assigned' - : null, - ), - catchError(() => of('Team could not be assigned')), - ), - ), - ).pipe(map((errors) => errors.find((e) => !!e) || null)); + // Admin-API reports some failures (e.g. a team not in the user's state) + // as HTTP 200 with statusCode != 200 in the body. Returns that error + // message, or null when every response succeeded. + private apiFailure(responses: any): string | null { + const failed = ([] as any[]) + .concat(responses) + .find( + (r: any) => + r && typeof r.statusCode === 'number' && r.statusCode !== 200, + ); + return failed + ? failed.errorMessage || failed.status || 'Request failed' + : null; } resetNikshaySelection() { @@ -2841,6 +2820,9 @@ export class WorkLocationMappingComponent nikshayFacilityID: this.bufferArray.data[i].nikshayFacilityID || null, + + // Stop TB team (van), AMM-2496 + vanID: this.bufferArray.data[i].stopTBTeamVanID ?? null, }, ], @@ -2928,35 +2910,20 @@ export class WorkLocationMappingComponent } saveWorkLocationOnly(requestArray: any) { - // One team per user: collect each saved Stop TB user's chosen team. - const teamAssignments = new Map(); - (this.bufferArray.data || []).forEach((row: any) => { - if (row?.stopTBTeamVanID) { - teamAssignments.set(`${row.userID}-${row.providerServiceMapID}`, { - userID: row.userID, - providerServiceMapID: row.providerServiceMapID, - vanID: row.stopTBTeamVanID, - }); - } - }); this.worklocationmapping .SaveWorkLocationMapping(requestArray) - .pipe( - switchMap(() => - this.assignStopTBTeams(Array.from(teamAssignments.values())), - ), - takeUntil(this.destroy$), - ) + .pipe(takeUntil(this.destroy$)) .subscribe( - (teamError: string | null) => { - if (teamError) { + (response: any) => { + const failure = this.apiFailure(response); + if (failure) { this.alertService.alert( - 'Mapping saved, but the team could not be assigned: ' + teamError, + 'Failed to save work location mapping: ' + failure, 'error', ); - } else { - this.alertService.alert('Mapping saved successfully', 'success'); + return; } + this.alertService.alert('Mapping saved successfully', 'success'); this.cleanupAfterSave(); }, (err: any) => { @@ -5757,6 +5724,7 @@ export class WorkLocationMappingComponent if (this.isStopTBServicelineEdit) { updateObj.nikshayTUID = nikshayTUIDToSend; updateObj.nikshayFacilityID = nikshayFacilityIDToSend; + updateObj.vanID = this.selectedStopTBTeamID ?? null; } if (group.serviceName === '1097') { updateObj.inbound = @@ -5899,6 +5867,7 @@ export class WorkLocationMappingComponent ? { nikshayTUID: nikshayTUIDToSend, nikshayFacilityID: nikshayFacilityIDToSend, + vanID: this.selectedStopTBTeamID ?? null, } : {}), }, @@ -5917,37 +5886,20 @@ export class WorkLocationMappingComponent return; } - const teamChanged = - this.isStopTBServicelineEdit && - this.selectedStopTBTeamID !== this.stopTBTeamOriginalID; forkJoin(allRequests) - .pipe( - switchMap(() => - this.assignStopTBTeams( - teamChanged - ? [ - { - userID: this.userID_duringEdit, - providerServiceMapID: this.providerServiceMapID_duringEdit, - vanID: this.selectedStopTBTeamID, - }, - ] - : [], - ), - ), - takeUntil(this.destroy$), - ) + .pipe(takeUntil(this.destroy$)) .subscribe( - (teamError: string | null) => { - if (teamError) { + (responses: any) => { + const failure = this.apiFailure(responses); + if (failure) { this.alertService.alert( - 'Mapping updated, but the team could not be changed: ' + - teamError, + 'Failed to update mapping: ' + failure, 'error', ); - } else { - this.alertService.alert('Mapping updated successfully', 'success'); + this.getAllMappedWorkLocations(); + return; } + this.alertService.alert('Mapping updated successfully', 'success'); this.showTable(); this.getAllMappedWorkLocations(); this.bufferArray.data = []; diff --git a/src/environments/environment.ci.ts.template b/src/environments/environment.ci.ts.template index 848ce31..5acb9fb 100644 --- a/src/environments/environment.ci.ts.template +++ b/src/environments/environment.ci.ts.template @@ -175,7 +175,6 @@ export const environment = { stopTBTeams_url: `${adminBaseUrl}stoptb/team/vans`, stopTBUserTeam_url: `${adminBaseUrl}stoptb/team/user`, stopTBTeamMembers_url: `${adminBaseUrl}stoptb/team/members`, - stopTBTeamAssign_url: `${adminBaseUrl}stoptb/team/assign`, update_facilities_Url: `${adminBaseUrl}editFacility`, delete_facilities_Url: `${adminBaseUrl}deleteFacility`, get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`, diff --git a/src/environments/environment.local.ts b/src/environments/environment.local.ts index 1fed6e0..e72fe9f 100644 --- a/src/environments/environment.local.ts +++ b/src/environments/environment.local.ts @@ -175,7 +175,6 @@ export const environment = { stopTBTeams_url: `${adminBaseUrl}stoptb/team/vans`, stopTBUserTeam_url: `${adminBaseUrl}stoptb/team/user`, stopTBTeamMembers_url: `${adminBaseUrl}stoptb/team/members`, - stopTBTeamAssign_url: `${adminBaseUrl}stoptb/team/assign`, update_facilities_Url: `${adminBaseUrl}editFacility`, delete_facilities_Url: `${adminBaseUrl}deleteFacility`, get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`, diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 079fb53..c698fcd 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -173,7 +173,6 @@ export const environment = { stopTBTeams_url: `${adminBaseUrl}stoptb/team/vans`, stopTBUserTeam_url: `${adminBaseUrl}stoptb/team/user`, stopTBTeamMembers_url: `${adminBaseUrl}stoptb/team/members`, - stopTBTeamAssign_url: `${adminBaseUrl}stoptb/team/assign`, update_facilities_Url: `${adminBaseUrl}editFacility`, delete_facilities_Url: `${adminBaseUrl}deleteFacility`, get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`, diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 434f26b..12f49f2 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -169,7 +169,6 @@ export const environment = { stopTBTeams_url: `${adminBaseUrl}stoptb/team/vans`, stopTBUserTeam_url: `${adminBaseUrl}stoptb/team/user`, stopTBTeamMembers_url: `${adminBaseUrl}stoptb/team/members`, - stopTBTeamAssign_url: `${adminBaseUrl}stoptb/team/assign`, update_facilities_Url: `${adminBaseUrl}editFacility`, delete_facilities_Url: `${adminBaseUrl}deleteFacility`, get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`, From dd39cb39d85bc9af535cb3e4aa1efcdced01e4e1 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 15 Oct 2026 00:38:43 +0530 Subject: [PATCH 13/16] chore: keep the bulk user upload template from release-3.8.4 The release-3.11.0 merge brought a new bulkuser_excel_sheet.xlsx; this branch keeps the 3.8.4 template. Co-Authored-By: Claude Opus 5.5 --- src/assets/bulkuser_excel_sheet.xlsx | Bin 14425 -> 12929 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/assets/bulkuser_excel_sheet.xlsx b/src/assets/bulkuser_excel_sheet.xlsx index d253dbd693aa494df45183d0326d21343da506b0..f52d752c341a9455f9664a570cdcca568e6cc8ef 100644 GIT binary patch literal 12929 zcmaJ{Wk4KTvc}!r-Q6L$YY0wocN;W7kl^m_?(Xic!GgPo5ZwJia(D0EynXNdn4ak_ zIp;gm)m>GeybLH94A9#M{IDhXcKp|Xe*IzuFqF3k*g7!Ezlx!~o`Cu#HeWtk#|sPu zqy+*5g!G?c`nI+VF4k7*G2POj%xGdaJ}*c$09FiSYE~t*c$+-!x?{sVS==!);u9yc zCk##ou$Qj*FS#40Ny*;aPNYq(j5qcU=aSrussXb|@;s?$vHCsOYP&GjAF*{AY#{d2 zh@~_ox9N=BA%`gM&J?&2zlspS4;lFPhgAkYszE7R7Lg(3TIVXY$V-J7szHBD(CCg= zN`;{dR)|T1UO-s@3GrUXJg>0Rn)xCFTZqHoruowy?CT8dNw{jv7VU**xsS+4l-4yG zCM%QhhNuuvigX{VPfUs2dKq(U6++Rla94&aJwdf@!IO2SZ3#j$GG_@CbMWOH1k^j3 z07k+SIo*lM;vi4wxe?F0$}4kz3s40-h*X~lQk+0PSXl^L)vv}`onD#&2#vkXD@l+I zqEH@vd_U2`PH-X7I*-u>)=6F<{)x0;bgR0|E7C@*|`6(0vLF7bAcLpO&GCO4T7B~xpMEYI{K8{B$JW8|?jcA!wR+$kgTb*}mcsB|&5 z3=~AKvI&nz#u~sskCoWhE&1UHc&6m;0 z4;*PxI0>>?uE5AIDHM{F!4eexF|5Y!vW0zFfw;$JVx#ox!(6E z1+6n)Um=0mk3jRHCs)uR64L!x2(HUs)#}#bDPqb+!vt&C!gdxAQf`RtgjQzWjFqgx>n36SFWFxz%x|03ueNNz#f9XtUh)JP89HAb-PGzujY2g1Ax4U#an(a9 z460%wVGzTPMYofFl%)X(({&-INVMmepWEY`1vGqTJ7mY1_;#P+=8&lB44c{a8h^}= zu(800yZH`tvNS)~t=g!FF~pRl^?@(X%q%^Z=SMyR>x8m7)8GW?+8yFufRuQWFz-MCG_b|keSF=+N%J?v4=)c%Cz0cENi1u zFgpqLJ;h*_B3A7$6t$ySM{_A^?`J$+Ng}rzrcVTK$ZNpg?IKX#f?J`_!Z!dX^VF8m z1?KYE$lzPwCRA?lrMWM#?faFqZe@So!#T0( z6uI`f(vM!>lMOPI{p9M8-k!i*vfWf7=)6Ir?VL2_0Op;w$}nt4?|AJ=nj7?f*8~gB z>gp_+n(I62i_A7~-S9E*;*2MwgLhXts(81J^JREI@6p#dS#F?;C5IDIC!KZpWs3XmCYMQF}I_BUBKAJ|K9t+`E4>^9nUk9`SyrGyyIRL&8S~ znKs?b3hSaF6{QTcaB?ha(Nh-RnD+Ce!D7#DV$0S9f&`yYRHEg^)vhZ^7x8vn)?c(Q z9KL3+=7|hsmVxJL?o#eeE-7-i2aFPgw!=0+c*&5EAm|KvZ|XOq*q|OjF8Qc{$gK~I zXFowK=cj6>S!rgK^cU9wrNhE$)hF2))|Ft?T8Dm`HDI9ELkU&jRtc@O4zrova>vd6 zSl<+d!8FrP^;FWYJ1rddqsK^|9k~!=vYC!%TDg5iKWF%JeXr=ul}1WjyhKWsKwj_y zCQOTD?6nDGSiw;_Td`kW@Ph+nNWt+c0;=rV13pDc8{#sBTI`KoU@*l=EE~0iAG_Gvg=YED9Ci@1~k~cwRiy0hU`2v zCo#O@iyCh{!ST`_=QBL%U|thbg!3x;&}k6q8PYR8v;GshUgJX~uAA=m`;iSoWV~B< z#?1mD_so1serR1|KF#V&lCZhQ5DI8lU{+!>-n4$^P+G*@>@Pm)&=w63={lGYD4k^U z{6gCfKO}T<3gLft2~3u{FxQnDJ%6-k{W*3W!(&cVXgBRu#)Z(#`ZMi_oD+RS78hU5 za8TblJpHi!r-{#M88Edco_*s&^Qw#u!_YWxRZOJFF~K>^^L2SJtI$aqu*?hBB|+6i zRpLrBk+UbSz1)K9$S^jlg2wfH0<6$g&Qyt+!x4yJ}RdP)SuEIfPnh({?8E1@)m;Q)t7B@ zC6PRI&0m1e`1fp5slJ+R(IV1@f4V+@Z_6Xz9RQAEdVSc~QhiX8GdyOI*L~dk&M{MI zmdp0#?nzk;)jgb#~?3+KoVdW91BN*%b#NTfa)+pWe{`MqtyL)}I~+ny>C+)|^=>c8&V zCu^0M#H3f&3PQNDO4Nwh^rjv1;4mM?sa z50&xd*JQaUtZ|PPtkGSlKtuH(%6m|S3FNFs6RWOr=8qc}kVfP8$(o_vn&7+&FNvwF zug46$tT#(EK6CB>M8fhk01P`56Vrz>BkAMJxq|b|R57xEUNj4}R4c91r=6YJ9IXHl zlp4tzk1CU`C_FIll@vBya^F*C2+X_~-F;!qIv08Z&EB8^8!Vl4XOUoehJ$ueIx#`g z3ASxR0sX=fU9NJURn@?0>qlmhe z>gS6G9?CbboZd6(8&lpqM7nbu;!(tp-uhf3z=gDoDor&G1jmQx2Hh1P@srBMD;I68 zSQC1=p9{gHS<`oyf8LQ-3Rn~o6uM_Og%OM=9_&Ts|LXn)Xjo*L_Z_CJD6)EKPs#WBGXIF&75D;^ARd} z*Aj%KmJ`g#1OPoWwgwEvN?E})irO=ufI6yoqTJ|j2J)abKlD`I^PK|e)=6DCa+SJi zfOPL++7{-*1HACET5Rm>)%i9q2+Lq2m{G(DkWpx8(h2mY_(G=rkGCjFdv_^bV@L47$;+A`ERD`UEE5G|7k;Pp`+clb0=}k9OKt-< z>-l-|69shT=zDTb8Y7dhewJVTWHw@@F=xJNh?tBWy=pA?A>Ealx|I4USVN`GDJm8b zvnI^JT$s`ovI9GP7%8U*PM&Bz9&zDJVQzd~nN2MFXlGJ@Wx$l@_@in|&qf$_&A>+5 zZXG$1j2R>mtX+kJVgQ+6r3VG9&${+Jxc~jlSD4pDjH^Rwg47^=Qj9#lcfb%6zU2mE zp+|gHWgnA4I1D}r`}Wy6q>te%oLrD_8Ghl%KuzC+AGZ<6wSp*-S=_KCaOh@=1OC{D zMJ;%{fMw6w>Ih$&(Ff~~DO^&B>xPC2s=^BRXetSu_`WP1Q+?cnhiT9yu{k65mK;>u zM_dGCK?)Q2q4GBN5aw;wuE#_oFp>ULoT$DWYl6JaQ6&C@25}zNlLPHPn|<-9c`@>= zKKOFjNKW9BNo__`c{ucrY<4*EC7Md<1IT37S0e1yEZulzAR>(pQgIa#NUbx-eI;rx z$LXm9qd6vGx=GdV_X^^AB~wj@H>&Bztq)=cE+EuVuvQ_YMj!S%;(9-?Jrl{mL>ikP z%iP6*n-w6KI+Ux%$Ms`;8TI{B1G)AI%>@NWW zwEv!fywyQoGZaTNV{79->L4>QFi~5txd$2c?;P>pV!wocr5#7=G5}moqz=sQU(|lK zM;jd?V@HE(Mm7bfgGI-XNLVB;8TV;3NRC9#^-{N`GG!1! zNAi~V;iY@6`>8gR%$kr3kzKW;k#AhD83}{@43Crf;=I`=x5*qY3=bBE7;kB%Zf3<* zPVJWz%CNh}oP$re2fa89B@o{s5gm1!v;FZZQg^XkL5Rm6IUd&vzSaAg`X`L9ZA$sDQ>7G&&gaC&6Bnph3q#v}2~ ze|hi|6rtTDGUn;r&)|M1tczr_h~$&LU~pc8uw&L^xGsG5MC8pWy6~0Qi{xN8ugPl5 zV+QGRup3isC@Q<=p92dyu9-Q?IZ%aapMT(|8vImSH54vuB8c{4?WUM^d*9|EnnsKr zU9BQ1lM5OnsI$l56JdYeD3c=J*t>|6^>}EPeyr>s z|LXymemP6lae^5o8J|-31$Ko_EB;uec>QWbc~fU|lFo_U)(Sf$EloT04dJ&vf|UFl zW~;!fwM&H*0qNS8&cr=gx4R$UuEpI%6F<_3F2DaaO3K{W+DL|Osq*SJf=GfvxGa7;%hkaf+(A+$r zduO2UxnDN>?TG~w(R;_fZHa1dvxCUNv9`WVBL0|tS#sSO6D;mXOcE=0k*ooqOe0hX zoy?;(AnTpv_jqmS&<5Xrr(MtJ5_Sy1@-Y~^a9%EXr$=%8_gzp7vO!z&yVP=5B(2=Z zACnJcyX$DOUgc!_)*o2X;1&>0)d_yJ0-J#)`43T01Keb|^Nd!em*UT|A5-5%k zV?&88N5K_kvJ{Ste(;El#y=0UuW9Y+T2*7jDtW_%AlJs6eg+98T{cxx3=|;R1(~5i zFdo&#IjlI8aMd`S1hdF40P_Q9WDt4WK-m#ZY0RN1;XEe7lg3bw7u_LmZWczVH_b-=B8w) z0aK&V{sUDj7+70aX6|7|E-}`(gjn@&XxmQMswvuGOYMf$U93gvg;H20>sn3P!J3Pt zX*1m$`P>2v<*M{>vKaf)_~uU&uoo||R*ztPxsbZ{rph5&L~Ld^y8q0D*l?KY6ki_+ zey`Qqw}ScWBf-H;-`?0r$^fY$-Aw0;38-%7=j*$ zn=8zWmsi8P+?$tXTha?3Sct9{Z7%orW(L4Z@*(*j)KRty5WqgHCyJMnD8j+w?Ln9VKa3#S7Fp)-rHcEw+<;{tbl#h6od~?ZtV->UEP!C+P0+__=W%`W zH}lT+AaPpsZ$aH6r3oO?K~Yjq!mZkhRl1A?ehlNHMnQ5*jDt6h_X1w4GNTSxJ(u4L zkUV83sFoGCH&iWyyWGu-%>L0Mn@25^yK1p ze}R|QP@9b4qVBP!4rQH6vndz|&9g?odV-fTitx`iP@C(yBZ0Hg(j2J-8%PO0Vnbsu z;`5IDj46EpV~ocr4B_0S=RpJzfwZ^s9vM~HDax-l0MHedxi_~VjQE34|MLkupma@@roXDbY&v&fI%XQSRr7PKcaSfpMf1WMb{(IfTsjNm2| zy_;M@OOedZLs&yXjlyU4Mu%s;h6!$Gp(`#9k_cBE3j9Kd10TZmV}|lshCFu=ZTV}Z z*jl|={Lj6B>jfTTXwOtbeX6?=Qm>1A8%;R6%z z2m0A)z6_*Bue}uOj|y9LjUMMsaaJ9}RI%1f^8;%Ro3L5+MoK*s8$ZNZjxyP*(dj?y zgJC*;UeD!eYx2Xb?#h{~2|8Ujt*ZOb!v}kgQWT`7mPf|o^Ryx-!yuf0NEdm4a;y!% z3u@2`P&5%7Hu4a~Q>eTN&zmCkGi|KHv@gY$7pDqjQ|XQ4m3}77`-yJ0_^Av?nb3f! z94?LkUqmqgT^3c%A@^dqp36*!h4mcK@SUq1yvKDAU$ty;`$IVCH}<14!=WT(%K5SM z^1UoM5f6uy${7A~y1=e53>9OVl#=&FGtRcC88%#JxPomyJolm`Koa1KM7hK+6V6HR z;?q(Xx4wbk6!*o=b;`f2r0{?+Osyt-?s))LEGUJ7*Ak+>$;*Naa1oU%n(UEgSZ1jS zK$$_^P%EyelB?Zibv)p!c6T?#)i$o3Jm2FJRVSfzmW(Zi3V{y^+bTD^mH2dc$lsup z7gy!+PA+wof;$<1jB@HOy>NM)ucPT@|9ZoaHgjeY*Oh#15-O!>YfF}UST}fO(iDI9 z413eJttec0@&T}{8$Uc1zv(t&Y5&F$oDaA%v}aE>u51m>9ZrcVgtRE;bS;O^KO@p*qpG3C^c zsk>VuGHY`1;K4aIJql64&!s91=p+A(Tt-cE|-1GkB& z{-EhlKMP5BHm2-_ZU=)PI`thPq2xOr0xl1~LOGE&Nz}NRHHC%ZeRpgPo)OgQ*23U5 z9LT}rAs4m%1VK*CqueRI?{W4`8VZ9vheor{{IELwl|zDkB!nz{e+d=K!;amflZ{$d(|8ll{)Qrv_rI`t z`i>t#?OJ7>>k``CwWUO9PJ7o#s>=ZZeZ&xfTI*H#ITO6E9qO zTlgb7V@kh+z-O%Dmo^7(5fv#`&XiUmJXcbu?ff69hXT)=G~*oJH+NGPEZ`|Y)6w@y z#-lQ_GNzF_-wQC$rb9Op#nSw2AOeSd6Btk@(hI@RAX17CJ?I^uB#r5{0&2O#{cO&i z>VJ-H*&diYe?xq<5?NM^xBYbYOdNvhjN?^u*vFs|P^l>!_)Hf(;}WecsHD-RcFPKo z5y`D2U*pVl8C0#T%u)I7r(QC)r4kqZO~+MQzq2hfQh8Jv-Qf=B4&do@SK@vKb65^;S)UN?;rf|Ug_f;ntDJj_@zMjQs}eHa<-(*A zXCO;=N@SRxGADli#`ZQ}#v_Ivjn9?IS*wK2A?U#>B?rpeiAYQz5^eE1$^=L#S~X{96^Z4ljG-6*gD ze-{y+XE7_g$WDG7yor6}rqS~3Iisfqz-kJA?)Nr1iB%lMrV4vEBUy_58203a>tv&3gvWG= zxmJQZp2#w(m$~GX42gv8_YnKBPCN~A_hzAu0K&FJIdt*GqjsIJeIL!l@$Fb-V?UNiDJS% z#O(nH2<{bkV~tst{B@p}%Wix$7UKOn2aX9@XEYP(UASNJhoy`T&R(O>x%%SgxX{03 zC)mKe@*Bi^f7u~>;pu%3Fx$lVj6><5mZ)-GFJALF^g&w}e2v7{ZLaT&3t6Um8<}&J zT|&3;-qq}iGvU?{{6?uNQHY$6-<6hH3x4q;DP22;Qr#W=*#g;@JRq~3X)SSCu-U*8 zB~+jg%+K?pOr(g*P!L0HN|Mm+&7CkZ+oSqy4%lbg!=iDkeI(8_b6wMrsuMq1Gw0bW zt3)t_`73of{h$;W{h;Jo{h;jNaPJ%)wQ-SiGQy$cXT<$nO2RK(z%?zL=6sv-fL?jI!ZO;4SK5xv~U3x|9tnPxTNIw?73jFdG2AcbAS*H<}O0K52^~v3tOK&wTqQ z6vM8Bp@!b=S!>sqAYnvMesa6Lah4WNNysDpP>@zXE9{X+?8Rj!P`|-Xq-B`s+Vm~h zyJAH!;B7%-qv#;MslekDb$J))y)I#+h+AKLq}d6r#X?7ptHMhzH^PIvH5 z@`o!>9+tq!xde=`%p-*RyMOWt^LLkK=@~sQu5Jza>15&C=bvuJ_;u9Cw=fLLHX~sX z{~LOnyT%Wk$^4}X5^S8IHmH)u{?JxsKO0ree%7!W`L8VU2OvkT8ved~G?_`^MzppB z7L$>*;E?0g{xDHl%DEZBPIzepe6J^5Ej|*1As#D?yff@zbw=kuHPuuW^+&)P6loPm z4wL&58!#OhEREY#kuROI1@XK%$Db=;wvXq< zkFG8dqtG^Hma&^9VuZ`<$6QT&J)~*Q^&CN-apUV(v9{&ERobr2Tz1pTdvnQMT!^x2 zgyGTX6Fi1LF8Z;h=`!wEp4mIHD}$%VidDNZw-gGg%_la3oBD&0HTdsY!|S^~-}Gs( zxmv0{n5I%TsP09xu`$={;w-}$6-57 zk#86&QTH~8b8|9U#y(h+!C%vvd!BjY;Ru-HP_Nx~f>mvy>ph;?ps7lz9!9LGD0cmI z)K)8ear_Q5-@i+lAo9(o7uyQRVOgi%c{de(AN^Ar%4I~cfL6e(yxAC}6okSKtEK-+ zkkpaSt)OFJBn~#IECx&mxsIBMY}pR^GPtxPNKQxi6Er%MA{mztKd-Wwe14qnr7kQS z5{&-$SSrmVEoL}hED|h_nrxT{>Ok1+nAp`I_f0kHM;uE9pRu+r2Y-jgGf){pD%O|GRn6L1!XuyVje>k7fXwev zMH2P?KQJl*nZR7el{A}?GpwUY!d2&iHQ1f_X3BS3?#J+o?pp9+^I zwIPHeaCSimp{Isb@%fM=V{2UEa5C=opu985MPt>Fq5C?T)LPi)b>Q6Zg9#*%U za-SI&p(qyO=*2@2ES7*qT?29~Og7YkMtz<`y9+*3`%}3Rlr>;f;#3a=v+3fRrE}B`J+OWzWkNUiTb{D=Oid1Y( zO>vD0R){A7v4zBt?)OC&2o7WErhqqj&V)G5R}at>n=V}+^n%};5W7UHLs39u{UtdN z!vdi>6iou4qXxX%g>E28(0DUQywvRPHSjN~0!m}JU(PP`+PX39^M+ChcHujF1BA@Q zb?PNKTM}~>gC15in@WjVX}{nVKR&D<)N&$KK9yKm=;9_;+Gy?aeo51&m5@7o{fha9v1N55jLf@C7#<`t4+Aw zCbYvv?DQo26 zmGu5)k^(hG{ivf|NP+HK3uI*S-CpXtyHP9dDv=IzK}4I6lY);RVCYka!l}E9+NzIa zS^F%gtFmnmSnH2-g9ks~J>Iw_2C`^vcg0DTKpyH?^Qi-6dBIE`cCo^D0*0i%9PKvp zHY4Za1DOQ!vb_CkG(m2y5O!StZFc}b$>G-xzT{ky30B%2<>+)Lt0?p^U-#UFPNXX_ z2#9wR_D=kxC(hD=W#uOH@M2p2NlGCryIP%gbjgES9|%Lpv&QcPXJ+<$x}&Ew>qaHH z{m63;U&vSn`*sps`Pp?P?a%2;o&Rqv|LPI_H#i~oEpsnlVU&1fy5YPYUkif2#oicg z|4K=Jq1zraYSGJtBzEKf?3?64zw99|y<;U!v^@6&q#sV$m*8hIJ@-X-Q+P|zma_o2 zJ6-4|1KYgziQw~Dus63Ux{(wNi1qG!Q(SPu-Q#0yu<8c!o3A-O$j}Y_N8@|&ZsTVt z_34zz+H}-r$xoqi%C8j&=`;yko_bi{Jt`5>_6md+q2$7YSU3knDu!2hsi530pmOj$ z-ajud&pLhFEG>-y)#yMW4j$x3Ad<$%F#_-84h|!#926+SS^wd+moBn$oMxb-53j3HN0RI1D=mGszkcT-OQ`0f zphiw(N^EK=$U(5W23Kf_m?U4{pBB%2FY#3&K@ZI2t8adC4>&0ad~cW=@xB3s`{d!O z!+{kT?u?pc)zm3AOm&gLc}>nbE+sIH(eR9k{`g4pX~(ppsQpNyD!%+QJ#p@c34FrR zHvXdCBUN_B9br|uTcMecYSeqtAV5zPd_`Is*nV_u_RqQz7(ei`y&r7r-uUCnw7-kX+WIWJvf?@jc<&k}b6mpdX|V+|dik5&T~AY9OLY z#%@ckx%PC|OA=ZiZJj{Fzhm3y$$+=rUGqRr4y6Hmwlclg+pJ-KO5(`Rz|KAbe>4e= z`jlzW;@l3%JFY~wIWV-{6@0$k7|-Rte~li%AZWkp*uRP2e{Rt09svOZ5xstXq5H4f z_1nOIZ>+urpx@(-L%u=%cR2c|(?0{!o6c{iU^M^I`E>*Rf6m_S(BI?D+1vOJZu>v| z{mE{B6aMXw@;}x7;=2C{(VvX%zqO;^c-;RY)c=dW{iph$wBx_kVP6Bt-|GJ+A^(4i zek0fZ9&d{_0sD(;`?lQwz9s)eiH`O^!quM>{WGq;`T32K(toP`uPFDYoj)5Uf7=np z{O|3PKP~)OX!+a1d&1uq{#|zY|G@g^+M!r{a^hx{lD1SzYf3XfATWmzixRHAQT|kS8oIiZ+ia&oRhfY literal 14425 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 Date: Thu, 15 Oct 2026 00:47:35 +0530 Subject: [PATCH 14/16] fix(bulk-upload): employee id in template, submit button, refresh user list - Template: EmployeeId column after UserName/Password (column N); the dropdowns after it moved with their columns. Admin-API 3.12.0 makes Employee Id mandatory and unique. - A chosen file is only checked (EmployeeId column present, not empty) and shows "N user(s) ready"; the upload starts on the new Submit button. - The user list reloads after users are created and when going back. Back only asks to confirm when a checked file is still waiting. Co-Authored-By: Claude Opus 5.5 --- ...employee-master-bulk-upload.component.html | 17 +++ .../employee-master-bulk-upload.component.ts | 106 ++++++++++++------ .../employee-master-new.component.html | 5 +- .../employee-master-new.component.ts | 2 + src/assets/bulkuser_excel_sheet.xlsx | Bin 12929 -> 11890 bytes 5 files changed, 95 insertions(+), 35 deletions(-) diff --git a/src/app/app-provider-admin/provider-admin/activities/employee-master-bulk-upload/employee-master-bulk-upload.component.html b/src/app/app-provider-admin/provider-admin/activities/employee-master-bulk-upload/employee-master-bulk-upload.component.html index da5bb93..1cd7bfe 100644 --- a/src/app/app-provider-admin/provider-admin/activities/employee-master-bulk-upload/employee-master-bulk-upload.component.html +++ b/src/app/app-provider-admin/provider-admin/activities/employee-master-bulk-upload/employee-master-bulk-upload.component.html @@ -56,6 +56,12 @@

Bulk User Upload

File size should not exceed {{ maxFileSize }}MB +
+ {{ pendingFileName }}: {{ pendingRowCount }} user(s) ready. + Click Submit to upload. +

Bulk User Upload

Back +
+ +
diff --git a/src/app/app-provider-admin/provider-admin/activities/employee-master-bulk-upload/employee-master-bulk-upload.component.ts b/src/app/app-provider-admin/provider-admin/activities/employee-master-bulk-upload/employee-master-bulk-upload.component.ts index 7beb672..2f785d8 100644 --- a/src/app/app-provider-admin/provider-admin/activities/employee-master-bulk-upload/employee-master-bulk-upload.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/employee-master-bulk-upload/employee-master-bulk-upload.component.ts @@ -22,6 +22,8 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; }) export class EmployeeMasterBulkUploadComponent { @Output() closeUpload = new EventEmitter(); + // Emitted after users are created, so the user list can reload. + @Output() usersUploaded = new EventEmitter(); objs = new MatTableDataSource(); paginator!: MatPaginator; @@ -56,6 +58,10 @@ export class EmployeeMasterBulkUploadComponent { fileuploadedCount = 0; filetotalCount = 0; errorloglist: any; + // File checked and ready, waiting for Submit. + pendingXml: string | null = null; + pendingFileName = ''; + pendingRowCount = 0; tableMode = true; editMode = false; @@ -128,57 +134,85 @@ export class EmployeeMasterBulkUploadComponent { const wb: XLSX.WorkBook = XLSX.read(binarystr, { type: 'binary' }); const wsname: string = wb.SheetNames[0]; const ws: XLSX.WorkSheet = wb.Sheets[wsname]; + const header: any[] = + (XLSX.utils.sheet_to_json(ws, { header: 1 })[0] as any[]) || []; + if (!header.includes('EmployeeId')) { + this.alertService.alert( + 'Employee Id column is missing. Download the latest Bulk User Template and fill Employee Id for every user.', + 'error', + ); + this.clearSelection(); + return; + } const data = XLSX.utils.sheet_to_json(ws); + if (!data.length) { + this.alertService.alert('The file has no users to upload', 'error'); + this.clearSelection(); + return; + } + // Checked and ready; the upload starts on Submit. + this.pendingXml = this.convertJsonToXml(data); + this.pendingFileName = file.name; + this.pendingRowCount = data.length; + this.successcount = false; + this.errorloglist = null; + }; + } - const xmlData = this.convertJsonToXml(data); - - this.showProgressBar = true; - this.blockSubcenterMappingService.memberBulkUploadXML(xmlData).subscribe( + submitUpload() { + if (!this.pendingXml || this.showProgressBar) return; + this.showProgressBar = true; + this.blockSubcenterMappingService + .memberBulkUploadXML(this.pendingXml) + .subscribe( (response: any) => { - if (response) { - this.fileuploadedCount = response.registeredUser; - this.filetotalCount = response.totalUser; - - if ( - Number(this.fileuploadedCount) !== Number(this.filetotalCount) - ) { - this.downloadFile(); - } - this.showProgressBar = false; - this.alertService.alert('File Uploaded', 'success'); - - this.resetFileInput(); - this.file = undefined; - this.successcount = true; - - this.errorloglist = response.error; - } else { - this.showProgressBar = false; - this.alertService.alert(response.errorMessage, 'error'); + this.showProgressBar = false; + if (!response || response.totalUser === undefined) { + this.alertService.alert( + response?.errorMessage || 'Upload failed', + 'error', + ); + return; + } + this.fileuploadedCount = response.registeredUser; + this.filetotalCount = response.totalUser; + if (Number(this.fileuploadedCount) !== Number(this.filetotalCount)) { + this.downloadFile(); + } + this.alertService.alert('File Uploaded', 'success'); + this.successcount = true; + this.errorloglist = response.error; + this.clearSelection(); + if (Number(this.fileuploadedCount) > 0) { + this.usersUploaded.emit(); } }, (error: HttpErrorResponse) => { + this.showProgressBar = false; if ( !navigator.onLine || (error.status === 0 && error.error instanceof ProgressEvent) ) { - this.showProgressBar = false; this.alertService.alert( 'Internet connection not available!', 'error', ); + } else { + this.alertService.alert( + error?.error?.errorMessage || 'Upload failed', + 'error', + ); } }, ); - (err: any) => { - this.showProgressBar = false; - this.alertService.alert(err.errorMessage, 'error'); - this.resetFileInput(); - this.file = undefined; - this.fileContent = null; - this.disableUpload = true; - }; - }; + } + + clearSelection() { + this.pendingXml = null; + this.pendingFileName = ''; + this.pendingRowCount = 0; + this.file = undefined; + this.resetFileInput(); } convertJsonToXml(json: any): string { @@ -231,6 +265,10 @@ export class EmployeeMasterBulkUploadComponent { } back() { + if (!this.pendingXml) { + this.closeUpload.emit(); + return; + } this.alertService .confirm( 'confirm', diff --git a/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.html b/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.html index 1832505..89399ac 100644 --- a/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.html +++ b/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.html @@ -1219,6 +1219,9 @@

Permanent Address

- +
diff --git a/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.ts b/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.ts index 98b30c3..4706c14 100644 --- a/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.ts +++ b/src/app/app-provider-admin/provider-admin/activities/employee-master-new/employee-master-new.component.ts @@ -1788,6 +1788,8 @@ export class EmployeeMasterNewComponent implements OnInit { this.employeeMasterUpload = false; this.tableMode = true; this.formMode = false; + // Show users created by the upload. + this.getAllUserDetails(); } private refreshFilteredData(rows: any[]) { diff --git a/src/assets/bulkuser_excel_sheet.xlsx b/src/assets/bulkuser_excel_sheet.xlsx index f52d752c341a9455f9664a570cdcca568e6cc8ef..b6a2c43af887cf657b58fe0177a271583ae4804f 100644 GIT binary patch literal 11890 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 literal 12929 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 From 08b0bd4df542eea178686b31ed6f78240897de80 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 15 Oct 2026 09:31:40 +0530 Subject: [PATCH 15/16] feat(AMM-2491): add min stock to item master create and edit Admin sets the stock threshold per item; Inventory's Low Stock report lists items at or below it. Empty keeps the current value; 0 clears it. Co-Authored-By: Claude Opus 5.5 --- .../item-master/edit-item-master.html | 6 +++++ .../item-master/item-master.component.html | 24 +++++++++++++++++ .../item-master/item-master.component.ts | 27 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/app/app-provider-admin/provider-admin/inventory/item-master/edit-item-master.html b/src/app/app-provider-admin/provider-admin/inventory/item-master/edit-item-master.html index cb758b4..74880f9 100644 --- a/src/app/app-provider-admin/provider-admin/inventory/item-master/edit-item-master.html +++ b/src/app/app-provider-admin/provider-admin/inventory/item-master/edit-item-master.html @@ -161,6 +161,12 @@ +
+ + Min Stock (low stock alert) + + +
diff --git a/src/app/app-provider-admin/provider-admin/inventory/item-master/item-master.component.html b/src/app/app-provider-admin/provider-admin/inventory/item-master/item-master.component.html index d7ba0c2..22c48d0 100644 --- a/src/app/app-provider-admin/provider-admin/inventory/item-master/item-master.component.html +++ b/src/app/app-provider-admin/provider-admin/inventory/item-master/item-master.component.html @@ -403,6 +403,18 @@

+
+ + Min Stock (low stock alert) + + +
@@ -841,6 +853,18 @@

/>

+
+ + Min Stock (low stock alert) + + +
diff --git a/src/app/app-provider-admin/provider-admin/inventory/item-master/item-master.component.ts b/src/app/app-provider-admin/provider-admin/inventory/item-master/item-master.component.ts index acf6c94..0bd28d4 100644 --- a/src/app/app-provider-admin/provider-admin/inventory/item-master/item-master.component.ts +++ b/src/app/app-provider-admin/provider-admin/inventory/item-master/item-master.component.ts @@ -102,6 +102,7 @@ export class ItemMasterComponent implements OnInit { edit_Uom: any; edit_DrugType: any; edit_Composition: any; + edit_MinStock: any; edit_Route: any; edit_Description: any; itemID: any; @@ -117,6 +118,7 @@ export class ItemMasterComponent implements OnInit { code: any; name: any; composition: any; + minStock: any; description: any; route: any; strength: any; @@ -490,6 +492,7 @@ export class ItemMasterComponent implements OnInit { this.itemCreationForm.controls['manufacturer'].reset(); this.itemCreationForm.controls['drugType'].reset(); this.itemCreationForm.controls['composition'].reset(); + this.itemCreationForm.controls['minStock']?.reset(); this.itemCreationForm.controls['route'].reset(); this.itemCreationForm.controls['description'].reset(); this.drugType = false; @@ -541,6 +544,7 @@ export class ItemMasterComponent implements OnInit { uomID: formValue.uom.uOMID, isScheduledDrug: formValue.drugType, composition: formValue.composition, + minStock: this.toStock(formValue.minStock), routeID: formValue.route.routeID, createdBy: this.createdBy, providerServiceMapID: this.providerServiceMapID, @@ -586,6 +590,7 @@ export class ItemMasterComponent implements OnInit { uomID: formValue.uom.uOMID, isScheduledDrug: formValue.drugType, composition: formValue.composition, + minStock: this.toStock(formValue.minStock), routeID: formValue.route.routeID, createdBy: this.createdBy, providerServiceMapID: this.providerServiceMapID, @@ -701,12 +706,25 @@ export class ItemMasterComponent implements OnInit { this.edit_Uom = itemlist.uomID; this.edit_DrugType = itemlist.isScheduledDrug; this.edit_Composition = itemlist.composition; + this.edit_MinStock = itemlist.minStock; this.edit_Route = itemlist.routeID; this.edit_Description = itemlist.itemDesc; if (itemlist.isEDL === true) this.editDrug = 'EDL'; else this.editDrug = 'Non-EDL'; this.showEditForm(); } + // Min Stock: whole number >= 0, or null when left empty + toStock(value: any) { + const n = Number(value); + return value === null || + value === undefined || + value === '' || + isNaN(n) || + n < 0 + ? null + : Math.floor(n); + } + showEditForm() { this.tableMode = false; this.showFormFlag = false; @@ -743,6 +761,7 @@ export class ItemMasterComponent implements OnInit { providerServiceMapID: this.providerServiceMapID, modifiedBy: this.createdBy, itemID: this.itemID, + minStock: this.toStock(editItemCreationForm.minStock), sctCode: this.editSnomedCode, sctTerm: this.editSnomedName, }; @@ -897,6 +916,7 @@ export class EditItemMasterModalComponent implements OnInit { uom: any; drugType: any; composition: any; + minStock: any; route: any; description: any; @@ -1033,6 +1053,7 @@ export class EditItemMasterModalComponent implements OnInit { this.uom = this.data.uomID; this.drugType = this.data.isScheduledDrug; this.composition = this.data.composition; + this.minStock = this.data.minStock; this.route = this.data.routeID; this.description = this.data.itemDesc; } @@ -1052,6 +1073,12 @@ export class EditItemMasterModalComponent implements OnInit { uomID: this.uom, isScheduledDrug: this.drugType, composition: this.composition, + minStock: + this.minStock === null || + this.minStock === undefined || + this.minStock === '' + ? null + : Math.max(0, Math.floor(Number(this.minStock))), routeID: this.route, status: 'active', providerServiceMapID: this.data.providerServiceMapID, From d989b07df0b3da4d8a8d161004507151c8a5e923 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 20 Oct 2026 10:27:05 +0530 Subject: [PATCH 16/16] chore: set version to 3.12.0 Co-Authored-By: Claude Opus 5.5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dbb8e47..4c4657f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.admin-ui admin-ui - 3.8.4 + 3.12.0 Admin-UI Piramal - admin: Module ui