diff --git a/Common-UI b/Common-UI
index 063e6e34..6bb2b204 160000
--- a/Common-UI
+++ b/Common-UI
@@ -1 +1 @@
-Subproject commit 063e6e347418f804e5df9ee0be9f3df82fb04fe1
+Subproject commit 6bb2b204b7381300cb9d0436c8f57c3fe86cb0e0
diff --git a/pom.xml b/pom.xml
index 6e9344ed..c6b72b85 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0
com.iemr.admin-ui
admin-ui
- 3.8.1
+ 3.8.2
Admin-UI
Piramal - admin: Module ui
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 4769b8cf..1832505a 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
@@ -89,7 +89,7 @@
Employee Master
Lock/Unlock |
|
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 a7806f21..e28a5077 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
@@ -127,4 +127,43 @@ export class WorkLocationMapping {
getTaluks(districtId: number) {
return this.http.get(environment._getTalukListURL + districtId);
}
+
+ // Nikshay location master (Stop TB) — read-only cascading lookups
+ getNikshayStates() {
+ return this.http.get(environment.nikshayStates_url);
+ }
+
+ getNikshayDistricts(stateID: any) {
+ return this.http.get(
+ `${environment.nikshayDistricts_url}?stateID=${stateID}`,
+ );
+ }
+
+ getNikshayTUs(districtID: any) {
+ return this.http.get(
+ `${environment.nikshayTUs_url}?districtID=${districtID}`,
+ );
+ }
+
+ getNikshayFacilities(tuIDs: any[]) {
+ return this.http.get(
+ `${environment.nikshayFacilities_url}?tuIDs=${tuIDs.join(',')}`,
+ );
+ }
+
+ getNikshayVillages(facilityIDs: any[]) {
+ return this.http.get(
+ `${environment.nikshayVillages_url}?facilityIDs=${facilityIDs.join(',')}`,
+ );
+ }
+
+ // Stop TB Edit: v_userservicerolemapping (backing mappedWorkLocationsList)
+ // never exposes DistrictID/NikshayTUID/NikshayFacilityID, so Edit can't
+ // read back what was saved from that list alone — this reads the raw
+ // table directly for just those fields, by USRMappingID.
+ getNikshayUserMappingData(usrMappingID: any) {
+ return this.http.get(
+ `${environment.nikshayUserMapping_url}?usrMappingID=${usrMappingID}`,
+ );
+ }
}
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 b519d3e4..b34a8f3f 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
@@ -6,6 +6,21 @@
}
}
+/* Village list cell in the mapping tables — long comma-joined village
+ lists (esp. Stop TB's) get their own fixed-width horizontal scroll
+ instead of stretching the row height or the whole table width, so the
+ rest of the columns (Edit/Deactivate etc.) stay on screen. */
+.village-cell-scroll {
+ max-width: 260px;
+ overflow-x: auto;
+ white-space: nowrap;
+ cursor: grab;
+}
+
+.village-cell-scroll:active {
+ cursor: grabbing;
+}
+
.role-chip {
display: inline-block;
padding: 2px 10px;
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 2868ae58..ee73e5b1 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
@@ -72,7 +72,9 @@
| Village |
- {{ element.villageName }}
+
+ {{ asVillageText(element.villageName) }}
+
|
@@ -325,8 +327,15 @@
[(ngModel)]="State"
name="state"
(selectionChange)="
- getAllDistricts(Serviceline.serviceID, User, State);
- resetBlockVillageFields()
+ resetBlockVillageFields();
+ getAllRoles(
+ Serviceline.serviceID,
+ State.providerServiceMapID,
+ User.userID
+ );
+ isStopTBServiceline
+ ? resolveNikshayStateAndLoadDistricts(State)
+ : getAllDistricts(Serviceline.serviceID, User, State)
"
(openedChange)="$event ? null : stateSearch = ''"
required
@@ -350,15 +359,7 @@
[disabled]="isNational"
[style.width]="'100%'"
[(ngModel)]="District"
- (selectionChange)="
- getAllWorkLocations(State, Serviceline, isNational);
- getBlockMaster(District);
- getAllRoles(
- Serviceline.serviceID,
- State.providerServiceMapID,
- User.userID
- )
- "
+ (selectionChange)="onDistrictChange()"
name="district"
(openedChange)="$event ? null : districtSearch = ''"
required
@@ -368,7 +369,7 @@
-
+
{{ item.districtName }}
@@ -376,7 +377,7 @@
-
+
Select Block
-
+
+
+
+ Select Block
+
+
+ search
+
+
+
+ {{ item.tUName }}
+
+
+
+
+
+
+
+ Select TU
+
+
+ search
+
+
+
+
+ {{ item.tUName }}
+
+
+
+
+
+
+
+ Select Health Facility
+
+
+ search
+
+
+
+
+ {{ item.facilityName }}
+
+
+
+
+
+
+
+ Select Village
+
+
+ search
+
+
+
+
+ {{ item.villageName }}
+
+
+
+
+
+
+
Select Work Location
-
+
Select Village
| Village |
- {{ element.villageName }}
+
+ {{ asVillageText(element.villageName) }}
+
|
@@ -811,15 +928,18 @@
Select District
{{ item.districtName }}
@@ -828,7 +948,7 @@
-
+
Select Block
-
+
Select Work Location
+
+
+
+ Select Block
+
+
+ search
+
+
+
+ {{ item.tUName }}
+
+
+
+
+
+
+ Select TU
+
+
+ search
+
+
+
+
+ {{ item.tUName }}
+
+
+
+
+
+
+ Select Health Facility
+
+
+ search
+
+
+
+
+ {{ item.facilityName }}
+
+
+
+
+
+
+ Select Village
+
+
+ search
+
+
+
+
+ {{ item.villageName }}
+
+
+
+
+
+
Select Role
@@ -917,7 +1148,7 @@
-
+
Select Village
[(ngModel)]="serviceEditvillage"
name="serviceEditvillage"
required
+ (openedChange)="$event ? null : serviceEditVillageSearch = ''"
>
+
+ search
+
+
>Select All
{{ item.villageName }} [{{ item.districtBranchID }}]
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 025f2610..9f935f4c 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
@@ -19,124 +19,177 @@
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { WorkLocationMappingComponent } from './work-location-mapping.component';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ActivatedRoute, Params } from '@angular/router';
-import { Observable } from 'rxjs/Observable';
-import 'rxjs/add/observable/of';
-import { dataService } from '../services/dataService/data.service';
-import { Router } from '@angular/router';
-import { Md2Module } from 'md2';
import { FormsModule } from '@angular/forms';
-import { fakeAsync } from '@angular/core/testing';
-import { By } from '@angular/platform-browser';
-import { tick } from '@angular/core/testing';
-import { ConfirmationDialogsService } from '../services/dialog/confirmation.service';
-import { WorkLocationMapping } from '../services/ProviderAdminServices/work-location-mapping.service';
+import { of } from 'rxjs';
+import { WorkLocationMappingComponent } from './work-location-mapping.component';
+import { WorkLocationMapping } from '../services/work-location-mapping.service';
+import { ConfirmationDialogsService } from 'src/app/core/services/dialog/confirmation.service';
+import { VillageMasterService } from 'src/app/core/services/adminServices/AdminVillage/village-master-service.service';
+import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service';
+import { FacilityMasterService } from 'src/app/core/services/inventory-services/facilitytypemaster.service';
+
let component: WorkLocationMappingComponent;
let fixture: ComponentFixture;
-const FakeConfirmationDialogsService = {};
-
-const providerForFakeConfirmationDialogsService = {
- provide: ConfirmationDialogsService,
- useValue: FakeConfirmationDialogsService,
-};
-const FakeDataService = {
- service_providerID: 'serviceProviderID',
- uname: 'admin',
+const FakeConfirmationDialogsService = {
+ alert: (_msg?: any, _type?: any) => undefined,
};
-const providerForFakeDataService = {
- provide: dataService,
- useValue: FakeDataService,
+const FakeWorkLocationMapping = {
+ getServices: (_userID: any) => of({ data: [] }),
+ getMappedWorkLocationList: (_serviceProviderID: any) => of({ data: [] }),
+ getUserName: (_serviceProviderID: any) => of({ data: [] }),
};
-class FakeWorkLocationMapping {
- getUserName(data) {
- return Observable.of([
- {
- userID: '1',
- },
- ]);
- }
- getMappedWorkLocationList() {
- return Observable.of([
- {
- userLangID: '1',
- },
- ]);
- }
- getAllServiceLinesByProvider(data) {
- return Observable.of([
- {
- languageID: '1',
- LanguageName: 'english',
- },
- ]);
- }
-}
-const providerForWorkLocationMapping = {
- provide: WorkLocationMapping,
- useClass: FakeWorkLocationMapping,
+const FakeVillageMasterService = {};
+const FakeFacilityMasterService = {};
+
+const sessionValues: Record = {
+ service_providerID: 'SP1',
+ uid: 'U1',
+ uname: 'admin',
+};
+const FakeSessionStorageService = {
+ getItem: (key: string) => sessionValues[key] ?? null,
};
-function InitializeAdminTestBed() {
- beforeEach(async(() => {
+function initTestBed() {
+ beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [WorkLocationMappingComponent],
schemas: [NO_ERRORS_SCHEMA],
- imports: [Md2Module, FormsModule],
+ imports: [FormsModule],
providers: [
- providerForFakeConfirmationDialogsService,
- providerForFakeDataService,
- providerForWorkLocationMapping,
+ {
+ provide: ConfirmationDialogsService,
+ useValue: FakeConfirmationDialogsService,
+ },
+ { provide: WorkLocationMapping, useValue: FakeWorkLocationMapping },
+ { provide: VillageMasterService, useValue: FakeVillageMasterService },
+ { provide: SessionStorageService, useValue: FakeSessionStorageService },
+ { provide: FacilityMasterService, useValue: FakeFacilityMasterService },
],
}).compileComponents();
}));
beforeEach(() => {
+ // Deliberately not calling fixture.detectChanges(): the real template
+ // uses mat-table/matSort/matPaginator directives whose modules aren't
+ // imported here, and these tests only exercise component logic
+ // (setWorkLocationObject etc.), not the rendered DOM. ngOnInit is
+ // still run explicitly so session-storage-derived fields are set.
fixture = TestBed.createComponent(WorkLocationMappingComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
+ component.ngOnInit();
});
}
-describe('Work-Location-mapping', () => {
- fdescribe('When the component is getting loaded, then ngOninit', () => {
- InitializeAdminTestBed();
+
+describe('WorkLocationMappingComponent', () => {
+ describe('When the component is loaded, then ngOnInit', () => {
+ initTestBed();
it('should be created', () => {
expect(component).toBeTruthy();
});
- it('should be defined', () => {
- expect(component).toBeDefined();
- });
- it('checking the value of uname should not be null and shoul have some value username', () => {
- expect(component.createdBy).not.toBe('1');
+
+ it('should read createdBy from session storage', () => {
expect(component.createdBy).toBe('admin');
});
- it('should set the ProviderServiceId after OnInit', () => {
- expect(component.serviceProviderID).not.toBe('');
- expect(component.serviceProviderID).toBe('serviceProviderID');
+
+ it('should read serviceProviderID from session storage', () => {
+ expect(component.serviceProviderID).toBe('SP1');
+ });
+ });
+
+ // These tests exercise setWorkLocationObject() directly — it is pure,
+ // synchronous request-building logic (no HTTP), so it can be verified
+ // without a running backend. They confirm the on-Create village-save
+ // behavior for each service line, including the known Stop TB gap:
+ // TU/Facility save correctly, but Village is hardcoded to null on the
+ // Create path (see setWorkLocationObject's isStopTBServiceline branch).
+ describe('setWorkLocationObject — village save behavior per serviceline (Create)', () => {
+ initTestBed();
+
+ const baseUser = { userID: 1, userName: 'worker1' };
+ const baseRole = { roleID: 5, roleName: 'Field Worker' };
+
+ it('MMU: saves villageID/villageName from the plain village picker', () => {
+ component.isStopTBServiceline = false;
+ component.isFacilityServiceline = false;
+
+ const objectToBeAdded: any = {
+ user: baseUser,
+ serviceline: { serviceID: 9, serviceName: 'MMU', isNational: false },
+ state: { stateID: 2, stateName: 'Karnataka', providerServiceMapID: 77 },
+ district: { districtID: 20, districtName: 'Bidar' },
+ Serviceblock: { blockID: 300, blockName: 'Bhalki' },
+ Servicevillage: [{ villageName: 'Alamkeri', districtBranchID: 555 }],
+ };
+
+ component.setWorkLocationObject(objectToBeAdded, baseRole, false, false);
+
+ const saved = component.bufferArray.data[0];
+ expect(saved.villageID).toEqual([555]);
+ expect(saved.villageName).toEqual(['Alamkeri']);
+ });
+
+ it('FLW/HWC: saves villageID/villageName resolved via the facility sub-component', () => {
+ component.isStopTBServiceline = false;
+ component.isFacilityServiceline = true;
+ component.currentFacilityMappingData = {
+ villageIDs: [7, 8],
+ villageNames: ['V7', 'V8'],
+ facilityID: 99,
+ };
+
+ const objectToBeAdded: any = {
+ user: baseUser,
+ serviceline: { serviceID: 3, serviceName: 'HWC', isNational: false },
+ state: { stateID: 2, stateName: 'Karnataka', providerServiceMapID: 77 },
+ district: { districtID: 20, districtName: 'Bidar' },
+ };
+
+ component.setWorkLocationObject(objectToBeAdded, baseRole, false, false);
+
+ const saved = component.bufferArray.data[0];
+ expect(saved.villageID).toEqual([7, 8]);
+ expect(saved.villageName).toEqual(['V7', 'V8']);
+ expect(saved.facilityID).toBe(99);
+ });
+
+ it('Stop TB: saves TU, Facility, and Village all together on Create', () => {
+ component.isStopTBServiceline = true;
+ component.selectedNikshayTUs = [
+ { nikshayTUID: 8903, tUName: 'Bhalki-TU' },
+ ];
+ component.selectedNikshayFacilities = [
+ { nikshayFacilityID: 648139, facilityName: 'Shadole hospital' },
+ ];
+ component.selectedNikshayVillages = [
+ { nikshayVillageID: 100, villageName: 'Alamkeri' },
+ ];
+
+ const objectToBeAdded: any = {
+ user: baseUser,
+ serviceline: {
+ serviceID: 11,
+ serviceName: 'Stop TB',
+ isNational: false,
+ },
+ state: { stateID: 2, stateName: 'Karnataka', providerServiceMapID: 77 },
+ district: { nikshayDistrictID: 312, districtName: 'Bidar' },
+ };
+
+ component.setWorkLocationObject(objectToBeAdded, baseRole, false, false);
+
+ const saved = component.bufferArray.data[0];
+ expect(saved.nikshayTUID).toBe('8903');
+ expect(saved.nikshayFacilityID).toBe('648139');
+ // Village selection is now saved on Create too, same as Edit.
+ expect(saved.villageID).toEqual([100]);
+ expect(saved.villageName).toEqual(['Alamkeri']);
});
- // it(' getUserName should be called after OnInit', () => {
- // spyOn(component, 'getUserName');
- // component.ngOnInit();
- // expect(component.getUserName).toHaveBeenCalled;
- // expect(component.userNamesList).not.toBe('');
- // });
- // it('getAllMappedWorkLocations method should be called after OnInit', () => {
- // spyOn(component, 'getAllMappedWorkLocations');
- // component.ngOnInit();
- // expect(component.getAllMappedWorkLocations).toHaveBeenCalled;
- // expect(component.mappedWorkLocationsList).not.toBe('');
- // });
- // it(' getAllServicelines should be called after OnInit', () => {
- // spyOn(component, 'getAllServicelines');
- // component.ngOnInit();
- // expect(component.getAllServicelines).toHaveBeenCalled;
- // expect(component.services_array).not.toBe('');
- // });
});
});
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 95a135f7..8113a2fd 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
@@ -214,6 +214,592 @@ export class WorkLocationMappingComponent
}
}
+ // --- Nikshay location master (Stop TB) — District / Block / TU / Facility / Village cascade ---
+ // State is AMRIT's own field (needed for providerServiceMapID/roles regardless
+ // of service line) — selecting it resolves the matching Nikshay state by name
+ // and loads Nikshay's own district list directly into "Select District" for
+ // Stop TB. District itself is 100% Nikshay-sourced from there on, no further
+ // AMRIT matching — AMRIT's own m_District proved unreliable for this (stale
+ // post-2022 district data, orphaned rows, inconsistent between environments).
+ isStopTBServiceline = false;
+ nikshayStateList: any[] = [];
+ nikshayDistrictList: any[] = [];
+ nikshayTUList: any[] = [];
+ nikshayFacilityList: any[] = [];
+ nikshayVillageList: any[] = [];
+ selectedNikshayBlock: any = null;
+ selectedNikshayTUs: any[] = [];
+ selectedNikshayFacilities: any[] = [];
+ selectedNikshayVillages: any[] = [];
+
+ nikshayBlockSearch = '';
+ nikshayTUSearch = '';
+ nikshayFacilitySearch = '';
+ nikshayVillageSearch = '';
+
+ get filteredNikshayBlockList(): any[] {
+ if (!this.nikshayBlockSearch) return this.nikshayTUList;
+ const s = this.nikshayBlockSearch.toLowerCase();
+ return this.nikshayTUList.filter((t: any) =>
+ (t.tUName || '').toLowerCase().includes(s),
+ );
+ }
+
+ get filteredNikshayTUList(): any[] {
+ if (!this.nikshayTUSearch) return this.nikshayTUList;
+ const s = this.nikshayTUSearch.toLowerCase();
+ const selectedIDs = new Set(
+ (this.selectedNikshayTUs || []).map((t: any) => t.nikshayTUID),
+ );
+ return this.nikshayTUList.filter(
+ (t: any) =>
+ selectedIDs.has(t.nikshayTUID) ||
+ (t.tUName || '').toLowerCase().includes(s),
+ );
+ }
+
+ 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),
+ );
+ }
+
+ 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),
+ );
+ }
+
+ get allNikshayTUsSelected(): boolean {
+ if (!this.nikshayTUList?.length) return false;
+ const selectedIDs = new Set(
+ (this.selectedNikshayTUs || []).map((t: any) => t.nikshayTUID),
+ );
+ return this.nikshayTUList.every((t: any) => selectedIDs.has(t.nikshayTUID));
+ }
+
+ toggleSelectAllNikshayTUs() {
+ this.selectedNikshayTUs = this.allNikshayTUsSelected
+ ? []
+ : [...this.nikshayTUList];
+ this.onNikshayTUChange();
+ }
+
+ get allNikshayFacilitiesSelected(): boolean {
+ if (!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),
+ );
+ }
+
+ toggleSelectAllNikshayFacilities() {
+ this.selectedNikshayFacilities = this.allNikshayFacilitiesSelected
+ ? []
+ : [...this.nikshayFacilityList];
+ this.onNikshayFacilityChange();
+ }
+
+ get allNikshayVillagesSelected(): boolean {
+ if (!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),
+ );
+ }
+
+ toggleSelectAllNikshayVillages() {
+ this.selectedNikshayVillages = this.allNikshayVillagesSelected
+ ? []
+ : [...this.nikshayVillageList];
+ }
+
+ // Called on District's selectionChange — Stop TB loads the Nikshay TU list
+ // directly from the selected (already-Nikshay) district; every other
+ // service line keeps its existing AMRIT-driven behavior unchanged.
+ onDistrictChange() {
+ if (this.isStopTBServiceline) {
+ this.loadNikshayTUs((this.District as any)?.nikshayDistrictID);
+ } else {
+ this.getAllWorkLocations(this.State, this.Serviceline, this.isNational);
+ this.getBlockMaster(this.District);
+ }
+ }
+
+ // Called when AMRIT State is selected, for Stop TB only: resolves the
+ // matching Nikshay state by name (low-risk — only ~10 stable state names,
+ // no reorganization ambiguity like districts have) and loads its district
+ // list directly into "Select District". From here on District is 100%
+ // Nikshay-sourced, no further AMRIT matching.
+ resolveNikshayStateAndLoadDistricts(state: any) {
+ this.nikshayDistrictList = [];
+ this.District = null as any;
+ this.nikshayTUList = [];
+ this.nikshayFacilityList = [];
+ this.nikshayVillageList = [];
+ this.selectedNikshayBlock = null;
+ this.selectedNikshayTUs = [];
+ this.selectedNikshayFacilities = [];
+ this.selectedNikshayVillages = [];
+ if (!state?.stateName) return;
+
+ // AMRIT's m_state spells this "Chattisgarh" (one 'h'); Nikshay's own data
+ // spells it "Chhattisgarh" (correct spelling, two 'h's). Every other of
+ // the ~10 covered states matches exactly — this is the one known,
+ // verified exception, not a general fuzzy-match problem.
+ const STATE_NAME_ALIASES: Record = {
+ chattisgarh: 'chhattisgarh',
+ };
+ const normalize = (v: string) => {
+ const n = (v || '').trim().toLowerCase();
+ return STATE_NAME_ALIASES[n] || n;
+ };
+ const stateName = normalize(state.stateName);
+
+ const resolveWithStates = (states: any[]) => {
+ const nikshayState = (states || []).find(
+ (s: any) => normalize(s.stateName) === stateName,
+ );
+ if (!nikshayState) {
+ this.alertService.alert(
+ `"${state.stateName}" is not yet in Nikshay's location master — districts unavailable until it's imported.`,
+ 'error',
+ );
+ return;
+ }
+ this.worklocationmapping
+ .getNikshayDistricts(nikshayState.nikshayStateID)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(
+ (response: any) => {
+ this.nikshayDistrictList = response.data || [];
+ },
+ () => {
+ this.alertService.alert(
+ 'Failed to load Nikshay districts',
+ 'error',
+ );
+ },
+ );
+ };
+
+ if (this.nikshayStateList?.length) {
+ resolveWithStates(this.nikshayStateList);
+ } else {
+ this.worklocationmapping
+ .getNikshayStates()
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(
+ (response: any) => {
+ this.nikshayStateList = response.data || [];
+ resolveWithStates(this.nikshayStateList);
+ },
+ () => {
+ this.alertService.alert('Failed to load Nikshay states', 'error');
+ },
+ );
+ }
+ }
+
+ // Called when Nikshay District is selected — populates both Block (single) and TU (multi)
+ loadNikshayTUs(districtID: any) {
+ this.nikshayTUList = [];
+ this.nikshayFacilityList = [];
+ this.nikshayVillageList = [];
+ this.selectedNikshayBlock = null;
+ this.selectedNikshayTUs = [];
+ this.selectedNikshayFacilities = [];
+ this.selectedNikshayVillages = [];
+ if (!districtID) return;
+ this.worklocationmapping
+ .getNikshayTUs(districtID)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(
+ (response: any) => {
+ this.nikshayTUList = response.data || [];
+ },
+ () => {
+ this.alertService.alert('Failed to load Nikshay TUs', 'error');
+ },
+ );
+ }
+
+ // Select Block is a single-select quick-add: picking one TU there adds it
+ // into the same selectedNikshayTUs multi-select Select TU manages,
+ // instead of being a separate, disconnected filter. Covers the common
+ // case (one TU) fast, while Select TU still handles picking more. Shared
+ // by both Create and Edit — operates purely on component state, not
+ // tied to either form.
+ onNikshayBlockChange() {
+ if (this.selectedNikshayBlock) {
+ const alreadySelected = (this.selectedNikshayTUs || []).some(
+ (t: any) => t.nikshayTUID === this.selectedNikshayBlock.nikshayTUID,
+ );
+ if (!alreadySelected) {
+ this.selectedNikshayTUs = [
+ ...(this.selectedNikshayTUs || []),
+ this.selectedNikshayBlock,
+ ];
+ }
+ }
+ this.onNikshayTUChange();
+ }
+
+ // Called whenever the TU multi-select changes
+ onNikshayTUChange() {
+ this.nikshayFacilityList = [];
+ this.nikshayVillageList = [];
+ this.selectedNikshayFacilities = [];
+ this.selectedNikshayVillages = [];
+ 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');
+ },
+ );
+ }
+
+ // Called whenever the Facility multi-select changes
+ onNikshayFacilityChange() {
+ this.nikshayVillageList = [];
+ this.selectedNikshayVillages = [];
+ 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');
+ },
+ );
+ }
+
+ resetNikshaySelection() {
+ this.nikshayStateList = [];
+ this.nikshayDistrictList = [];
+ this.nikshayTUList = [];
+ this.nikshayFacilityList = [];
+ this.nikshayVillageList = [];
+ this.selectedNikshayBlock = null;
+ this.selectedNikshayTUs = [];
+ this.selectedNikshayFacilities = [];
+ this.selectedNikshayVillages = [];
+ this.nikshayBlockSearch = '';
+ this.nikshayTUSearch = '';
+ this.nikshayFacilitySearch = '';
+ this.nikshayVillageSearch = '';
+ }
+
+ /**
+ * Edit mode: find this worker's Stop TB row(s), pull out the TU/Facility
+ * IDs from their comma-joined lists plus the Village ID array, then load
+ * each dropdown's option list and pre-select the matching entries.
+ */
+ loadNikshayEditSelections() {
+ this.resetNikshaySelection();
+
+ const splitIDs = (value: any): number[] =>
+ String(value || '')
+ .split(',')
+ .map((v: string) => Number(v.trim()))
+ .filter((n: number) => !isNaN(n));
+
+ // edit_Details IS this user-role's row already (set directly in
+ // editGroupedRow/editRow) — read villageidDb/stateName straight off it
+ // instead of re-filtering mappedWorkLocationsList by roleName, which
+ // can silently come up empty on a mismatch and quietly break every
+ // fallback that depended on it (district list, village pre-selection).
+ // row.villageID (the transient array field on the backend entity) is
+ // write-only — it's never populated when reading from the DB, only
+ // when the frontend sends it on save. villageidDb is the raw
+ // comma-joined string column, which IS populated on read.
+ const uniqueVillageIDs = [
+ ...new Set(splitIDs(this.edit_Details?.villageidDb)),
+ ];
+
+ // NikshayTUID/NikshayFacilityID/DistrictID are comma-joined/direct
+ // values on m_userservicerolemapping (see setWorkLocationObject), but
+ // v_userservicerolemapping — what mappedWorkLocationsList is built from
+ // — never exposes those columns. Fetch them straight from the raw table
+ // by USRMappingID instead.
+ const usrMappingID = this.uSRMappingID || this.edit_Details.uSRMappingID;
+ if (!usrMappingID) {
+ return;
+ }
+
+ this.worklocationmapping
+ .getNikshayUserMappingData(usrMappingID)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((response: any) => {
+ const data = response?.data || {};
+ const existingTUIDs = splitIDs(data.nikshayTUID);
+ const existingFacilityIDs = splitIDs(data.nikshayFacilityID);
+ // BlockID/BlockName on m_userservicerolemapping ARE the saved
+ // "Select Block" value — stamped by onNikshayBlockChange() at save
+ // time, independent of how many TUs ended up in NikshayTUID.
+ // getNikshayUserMappingData's raw-table response does NOT include
+ // these two columns (verified against live UAT response — only
+ // nikshayTUID/nikshayFacilityID/districtID come back), so pull it
+ // from edit_Details instead — the row is already sitting there
+ // with blockID/blockName intact, sourced from getUserRoleMapped
+ // (v_userservicerolemapping) when the row was opened for Edit.
+ const savedBlockID = parseInt(this.edit_Details?.blockID, 10);
+
+ // DistrictID is repurposed to hold the Nikshay district ID directly
+ // for Stop TB rows (AMRIT's own DistrictID is never used for Stop
+ // TB — see setWorkLocationObject). Rows saved after that fix can
+ // use it straight away, no name matching needed.
+ const savedNikshayDistrictID = parseInt(data.districtID, 10);
+ if (!isNaN(savedNikshayDistrictID) && savedNikshayDistrictID) {
+ this.district_duringEdit = savedNikshayDistrictID;
+ // nikshayDistrictList also needs populating — it's what the
+ // "Select District" dropdown's options come from in edit mode —
+ // otherwise the resolved ID has nothing to display against.
+ this.loadNikshayDistrictListForEdit(this.edit_Details?.stateName);
+ this.loadNikshayTUsForEditContinued(
+ savedNikshayDistrictID,
+ existingTUIDs,
+ existingFacilityIDs,
+ uniqueVillageIDs,
+ savedBlockID,
+ );
+ return;
+ }
+
+ // Old-style user (mapped through the pre-Nikshay AMRIT flow), or a
+ // fresh row with nothing selected yet: no saved Nikshay DistrictID
+ // to resolve from. District/TU/Facility/Village are left fully
+ // blank — deliberately NOT guessed from the row's old AMRIT
+ // WorkingDistrictName, since that name space doesn't reliably line
+ // up with Nikshay's. Only User/Serviceline/Role carry over from
+ // the old row; the admin picks fresh Nikshay values and Update
+ // re-saves this worker on the new schema. district_duringEdit is
+ // explicitly cleared here — editGroupedRow/editRow already set it
+ // to the row's old AMRIT district ID earlier for every service
+ // line, which would otherwise show as a bogus selection against
+ // the Nikshay district list. The District dropdown's OPTIONS still
+ // populate from the one thing reliably known (State); the dropdown
+ // is editable for Stop TB (see template) and picking a district
+ // calls onNikshayDistrictChangeEdit().
+ this.district_duringEdit = null;
+ const rowStateName = this.edit_Details?.stateName;
+ if (rowStateName) {
+ this.loadNikshayDistrictListForEdit(rowStateName);
+ }
+ });
+ }
+
+ onNikshayDistrictChangeEdit() {
+ this.selectedNikshayBlock = null;
+ this.selectedNikshayTUs = [];
+ this.selectedNikshayFacilities = [];
+ this.selectedNikshayVillages = [];
+ this.nikshayTUList = [];
+ this.nikshayFacilityList = [];
+ this.nikshayVillageList = [];
+ if (!this.district_duringEdit) {
+ return;
+ }
+ this.worklocationmapping
+ .getNikshayTUs(this.district_duringEdit)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((response: any) => {
+ this.nikshayTUList = response.data || [];
+ });
+ }
+
+ // Populates nikshayDistrictList (the "Select District" dropdown's option
+ // source in edit mode) for a known state name. If amritDistrictName is
+ // given, also tries a best-effort name match against it and auto-selects
+ // on a hit — meaningful only for genuinely old, pre-Nikshay rows that
+ // went through the old AMRIT work-location flow and therefore have a
+ // real WorkingDistrictName (rows saved through the newer Nikshay flow
+ // never set WorkingLocationID at all, so this never fires for them —
+ // they fall through to the plain empty-options case, same as before).
+ private loadNikshayDistrictListForEdit(stateName: string) {
+ const STATE_NAME_ALIASES: Record = {
+ chattisgarh: 'chhattisgarh',
+ };
+ const normalize = (v: string) => {
+ const n = (v || '').trim().toLowerCase();
+ return STATE_NAME_ALIASES[n] || n;
+ };
+ const normStateName = normalize(stateName);
+
+ const resolveAndLoad = (states: any[]) => {
+ const nikshayState = (states || []).find(
+ (s: any) => normalize(s.stateName) === normStateName,
+ );
+ if (!nikshayState) return;
+ this.worklocationmapping
+ .getNikshayDistricts(nikshayState.nikshayStateID)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((distResponse: any) => {
+ this.nikshayDistrictList = distResponse.data || [];
+ });
+ };
+
+ if (this.nikshayStateList?.length) {
+ resolveAndLoad(this.nikshayStateList);
+ } else {
+ this.worklocationmapping
+ .getNikshayStates()
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((response: any) => {
+ this.nikshayStateList = response.data || [];
+ resolveAndLoad(this.nikshayStateList);
+ });
+ }
+ }
+
+ private loadNikshayTUsForEdit(
+ stateName: string,
+ districtName: string,
+ existingTUIDs: any[],
+ existingFacilityIDs: any[],
+ uniqueVillageIDs: any[],
+ ) {
+ const STATE_NAME_ALIASES: Record = {
+ chattisgarh: 'chhattisgarh',
+ };
+ const normalize = (v: string) => {
+ const n = (v || '').trim().toLowerCase();
+ return STATE_NAME_ALIASES[n] || n;
+ };
+ const normStateName = normalize(stateName);
+ const normDistrictName = normalize(districtName);
+
+ const resolveDistrict = (states: any[]) => {
+ const nikshayState = (states || []).find(
+ (s: any) => normalize(s.stateName) === normStateName,
+ );
+ if (!nikshayState) return;
+ this.worklocationmapping
+ .getNikshayDistricts(nikshayState.nikshayStateID)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((distResponse: any) => {
+ const districts = distResponse.data || [];
+ const nikshayDistrict = districts.find(
+ (d: any) => normalize(d.districtName) === normDistrictName,
+ );
+ if (!nikshayDistrict) return;
+ // Pre-select District itself, same object shape Create uses.
+ this.District = nikshayDistrict;
+ this.district_duringEdit = nikshayDistrict.nikshayDistrictID;
+ this.loadNikshayTUsForEditContinued(
+ nikshayDistrict.nikshayDistrictID,
+ existingTUIDs,
+ existingFacilityIDs,
+ uniqueVillageIDs,
+ );
+ });
+ };
+
+ if (this.nikshayStateList?.length) {
+ resolveDistrict(this.nikshayStateList);
+ } else {
+ this.worklocationmapping
+ .getNikshayStates()
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((response: any) => {
+ this.nikshayStateList = response.data || [];
+ resolveDistrict(this.nikshayStateList);
+ });
+ }
+ }
+
+ private loadNikshayTUsForEditContinued(
+ nikshayDistrictID: any,
+ existingTUIDs: any[],
+ existingFacilityIDs: any[],
+ uniqueVillageIDs: any[],
+ savedBlockID?: number,
+ ) {
+ this.worklocationmapping
+ .getNikshayTUs(nikshayDistrictID)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((tuResponse: any) => {
+ this.nikshayTUList = tuResponse.data || [];
+ this.selectedNikshayTUs = this.nikshayTUList.filter((t: any) =>
+ existingTUIDs.includes(t.nikshayTUID),
+ );
+ // "Select Block" is a single-select quick-add convenience over this
+ // same TU list (see onNikshayBlockChange), but the saved BlockID IS
+ // its own real value on the row (stamped there at save time) —
+ // independent of how many TUs ended up in NikshayTUID afterwards.
+ // Look it up directly instead of guessing off selectedNikshayTUs.
+ this.selectedNikshayBlock =
+ savedBlockID && !isNaN(savedBlockID)
+ ? this.nikshayTUList.find(
+ (t: any) => t.nikshayTUID === savedBlockID,
+ ) || null
+ : null;
+ if (!this.selectedNikshayTUs.length) return; // old user: nothing to pre-select, but list is loaded and usable
+
+ const tuIDs = this.selectedNikshayTUs.map((t: any) => t.nikshayTUID);
+ this.worklocationmapping
+ .getNikshayFacilities(tuIDs)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((facResponse: any) => {
+ this.nikshayFacilityList = facResponse.data || [];
+ this.selectedNikshayFacilities = this.nikshayFacilityList.filter(
+ (f: any) => existingFacilityIDs.includes(f.nikshayFacilityID),
+ );
+ if (!this.selectedNikshayFacilities.length) return;
+
+ const facilityIDs = this.selectedNikshayFacilities.map(
+ (f: any) => f.nikshayFacilityID,
+ );
+ this.worklocationmapping
+ .getNikshayVillages(facilityIDs)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((villResponse: any) => {
+ this.nikshayVillageList = villResponse.data || [];
+ this.selectedNikshayVillages = this.nikshayVillageList.filter(
+ (v: any) => uniqueVillageIDs.includes(v.nikshayVillageID),
+ );
+ });
+ });
+ });
+ }
+
// flag values
formMode = false;
tableMode = true;
@@ -229,6 +815,7 @@ export class WorkLocationMappingComponent
// Edit-mode facility integration
isFacilityServicelineEdit = false;
+ isStopTBServicelineEdit = false;
editExistingVillageIDs: number[] = [];
editExistingVillageNames: string[] = [];
editExistingFacilityID: any = null;
@@ -533,14 +1120,14 @@ export class WorkLocationMappingComponent
const group = groupMap.get(key)!;
group.originalRows.push(row);
- if (Array.isArray(row.villageID)) {
+ if (Array.isArray(row.villageID) && !row.userServciceRoleDeleted) {
for (const vid of row.villageID) {
if (!group.villageID.includes(vid)) {
group.villageID.push(vid);
}
}
}
- if (Array.isArray(row.villageName)) {
+ if (Array.isArray(row.villageName) && !row.userServciceRoleDeleted) {
for (const vn of row.villageName) {
if (!group.villageName.includes(vn)) {
group.villageName.push(vn);
@@ -929,10 +1516,23 @@ export class WorkLocationMappingComponent
});
this.availableRoles = this.RolesList.slice();
+ // Also collect existing role NAMES to block duplicate-named roles (e.g. two "Asha" with different roleIDs)
+ const existingRoleNames = this.mappedWorkLocationsList
+ .filter(
+ (m: any) =>
+ m.userID === userID &&
+ m.providerServiceMapID === providerServiceMapID &&
+ !m.userServciceRoleDeleted,
+ )
+ .map((m: any) => (m.roleName || '').toLowerCase());
+
const temp: any = [];
this.availableRoles.forEach((roles: any) => {
- const index = this.existingRoles.indexOf(roles.roleID);
- if (index < 0) {
+ const idBlocked = this.existingRoles.indexOf(roles.roleID) >= 0;
+ const nameBlocked = existingRoleNames.includes(
+ (roles.roleName || '').toLowerCase(),
+ );
+ if (!idBlocked && !nameBlocked) {
temp.push(roles);
}
});
@@ -1610,6 +2210,110 @@ export class WorkLocationMappingComponent
InboundValue: any,
OnboundValue: any,
) {
+ // Stop TB — Nikshay TU/Facility/Village: a single row per user-role,
+ // carrying all selected TUs/Facilities/Villages as comma-joined lists
+ // (NikshayTUID/NikshayFacilityID are TEXT columns, same idea as
+ // Villageid already was). Previously this looped one row per TU x
+ // Facility combination, which multiplied rows for no reason (e.g. 10
+ // TUs x 100 facilities = 1,000 rows for one user/role).
+ if (this.isStopTBServiceline) {
+ const roleObjStopTB = [obj];
+ const providerServiceMapID =
+ objectToBeAdded.serviceline.isNational === false
+ ? objectToBeAdded.state.providerServiceMapID
+ : this.states_array[0].providerServiceMapID;
+
+ const tuIDArrTB: any[] = [];
+ const tuNameArrTB: any[] = [];
+ (this.selectedNikshayTUs || []).forEach((tu: any) => {
+ tuIDArrTB.push(tu.nikshayTUID);
+ tuNameArrTB.push(tu.tUName);
+ });
+
+ const facilityIDArrTB: any[] = [];
+ const facilityNameArrTB: any[] = [];
+ (this.selectedNikshayFacilities || []).forEach((facility: any) => {
+ facilityIDArrTB.push(facility.nikshayFacilityID);
+ facilityNameArrTB.push(facility.facilityName);
+ });
+
+ const villageIDArrTB: any[] = [];
+ const villageNameArrTB: any[] = [];
+ (this.selectedNikshayVillages || []).forEach((village: any) => {
+ villageIDArrTB.push(village.nikshayVillageID);
+ villageNameArrTB.push(village.villageName);
+ });
+
+ const stopTBWorkLocationObj: any = {
+ previleges: [],
+ userID: objectToBeAdded.user.userID,
+ userName: objectToBeAdded.user.userName,
+ serviceID: objectToBeAdded.serviceline.serviceID,
+ serviceName: objectToBeAdded.serviceline.serviceName,
+ roleID1: roleObjStopTB,
+ providerServiceMapID: providerServiceMapID,
+ createdBy: this.createdBy,
+ stateID: objectToBeAdded.state?.stateID || null,
+ stateName: objectToBeAdded.state?.stateName || 'All States',
+ // DistrictID is repurposed to hold the Nikshay district ID for Stop
+ // TB (AMRIT's own DistrictID is never used here — District is 100%
+ // Nikshay-sourced). The Create form's District control binds the
+ // NikshayDistrict object, whose ID field is nikshayDistrictID, not
+ // districtID — reading .districtID here always came back null.
+ districtID: objectToBeAdded.district?.nikshayDistrictID || null,
+ district: objectToBeAdded.district?.districtName || null,
+ // Block is a single-value legacy field (both ID and Name). Store
+ // whichever TU the admin actually picked via "Select Block"
+ // (selectedNikshayBlock) — not just whichever TU happened to land
+ // first in the multi-select array, which was order-dependent and
+ // didn't necessarily match what "Select Block" showed on screen.
+ // Falls back to the first selected TU only when Block was never
+ // touched (e.g. admin picked TUs solely via "Select TU"). The full
+ // multi-TU list lives in NikshayTUID/NikshayTUName, not here.
+ blockID:
+ this.selectedNikshayBlock?.nikshayTUID ??
+ (tuIDArrTB.length ? tuIDArrTB[0] : null),
+ blockName:
+ this.selectedNikshayBlock?.tUName ??
+ (tuNameArrTB.length ? tuNameArrTB[0] : null),
+ nikshayTUID: tuIDArrTB.length ? tuIDArrTB.join(',') : null,
+ nikshayTUName: tuNameArrTB.length ? tuNameArrTB.join(', ') : null,
+ nikshayFacilityID: facilityIDArrTB.length
+ ? facilityIDArrTB.join(',')
+ : null,
+ nikshayFacilityName: facilityNameArrTB.length
+ ? facilityNameArrTB.join(', ')
+ : null,
+ // Villageid takes the Nikshay Village picker's selection directly,
+ // same as the Edit path (see updateGroupedWorkLocation). This
+ // column holds AMRIT village IDs everywhere else in the system,
+ // including the mobile app's own beneficiary worklist match
+ // (BenFlowStatus.villageID) — writing Nikshay village IDs here
+ // means that match won't resolve for these villages until a
+ // Nikshay-village-to-AMRIT-village bridge table exists to
+ // translate between the two numbering systems. Previously this was
+ // left null specifically on Create to avoid that collision, but
+ // Edit was already writing it the same way — silently dropping the
+ // admin's village selection on every new Stop TB worker until they
+ // were separately edited was a worse outcome than the collision
+ // risk both paths already carry identically.
+ villageID: villageIDArrTB.length ? villageIDArrTB : null,
+ villageName: villageNameArrTB.length ? villageNameArrTB : null,
+ Inbound: 'N/A',
+ Outbound: 'N/A',
+ teleConsultation: [null],
+ };
+ this.bufferArray.data.push(stopTBWorkLocationObj);
+ if (this.paginatorSecond) {
+ this.bufferArray.paginator = this.paginatorSecond;
+ }
+ if (this.sortSecond) {
+ this.bufferArray.sort = this.sortSecond;
+ }
+ this.rebuildGroupedBuffer();
+ return;
+ }
+
const villageIDArr: any = [];
const villageNameArr: any = [];
if (this.isFacilityServiceline) {
@@ -1833,6 +2537,11 @@ export class WorkLocationMappingComponent
villageName: this.bufferArray.data[i].villageName,
facilityID: this.bufferArray.data[i].facilityID,
+
+ nikshayTUID: this.bufferArray.data[i].nikshayTUID || null,
+
+ nikshayFacilityID:
+ this.bufferArray.data[i].nikshayFacilityID || null,
},
],
@@ -2170,6 +2879,13 @@ export class WorkLocationMappingComponent
this.edit_Details.serviceName === 'FLW' ||
this.edit_Details.serviceName === 'HWC';
this.isBlockRequiredEdit = this.edit_Details.serviceName === 'Stop TB';
+ this.isStopTBServicelineEdit = this.edit_Details.serviceName === 'Stop TB';
+
+ if (this.isStopTBServicelineEdit) {
+ this.loadNikshayEditSelections();
+ } else {
+ this.resetNikshaySelection();
+ }
if (this.isFacilityServicelineEdit) {
this.editExistingVillageIDs = Array.isArray(this.edit_Details.villageID)
@@ -3646,6 +4362,23 @@ export class WorkLocationMappingComponent
this.collectUserExistingVillages();
}
+ // Edit-mode search for the plain (MMU/TM) Village dropdown — mirrors
+ // Create's villageSearch/filteredVillagesList, kept separate since Edit
+ // uses its own array (editVillageArr) and a villageName-string model
+ // (serviceEditvillage) rather than Create's village-object model.
+ serviceEditVillageSearch = '';
+
+ get filteredEditVillageList(): any[] {
+ if (!this.serviceEditVillageSearch) return this.editVillageArr;
+ const s = this.serviceEditVillageSearch.toLowerCase();
+ const selectedNames = new Set(this.serviceEditvillage || []);
+ return this.editVillageArr.filter(
+ (v: any) =>
+ selectedNames.has(v.villageName) ||
+ (v.villageName || '').toLowerCase().includes(s),
+ );
+ }
+
get allEditVillagesSelected(): boolean {
if (!this.editVillageArr?.length) return false;
const selected = new Set(this.serviceEditvillage || []);
@@ -3780,20 +4513,24 @@ export class WorkLocationMappingComponent
this.villageFlag = true;
this.isFacilityServiceline =
serviceline === 'FLW' || serviceline === 'HWC';
+ this.isStopTBServiceline = serviceline === 'Stop TB';
if (serviceline === 'TM' || serviceline === 'MMU') {
this.isBlockRequired = false;
this.isVillageRequired = false;
} else {
this.isBlockRequired = true;
- this.isVillageRequired = this.isFacilityServiceline ? false : true;
+ this.isVillageRequired =
+ this.isFacilityServiceline || this.isStopTBServiceline ? false : true;
}
} else {
this.blockFlag = false;
this.villageFlag = false;
this.isFacilityServiceline = false;
+ this.isStopTBServiceline = false;
this.isBlockRequired = false;
this.isVillageRequired = false;
}
+ this.resetNikshaySelection();
}
showEditBlockDrop(serviceName: any) {
@@ -4099,6 +4836,18 @@ export class WorkLocationMappingComponent
// ── Grouped row helpers ──
+ // Village lists (especially Stop TB's, sourced from an entire TU/facility)
+ // can run to hundreds of comma-joined names. The list table renders this
+ // in a fixed-width scrollable cell (see .village-cell-scroll) instead of
+ // stretching the row or the whole table, so joining with ", " here is
+ // just for readability in that cell and its tooltip.
+ asVillageText(villageName: any): string {
+ if (!villageName) return '';
+ return Array.isArray(villageName)
+ ? villageName.join(', ')
+ : String(villageName);
+ }
+
getUniqueRoles(roles: RoleEntry[]): RoleEntry[] {
if (!roles) return [];
const seen = new Set();
@@ -4272,6 +5021,19 @@ export class WorkLocationMappingComponent
// Facility serviceline detection
this.isFacilityServicelineEdit =
firstRow.serviceName === 'FLW' || firstRow.serviceName === 'HWC';
+
+ // Stop TB / Nikshay detection — this was previously only set in the
+ // unused editRow() function, never here (the actual grouped-row edit
+ // entry point), so the TU/Facility/Village fields never rendered on
+ // Edit and it silently fell through to the generic Work Location field.
+ this.isBlockRequiredEdit = firstRow.serviceName === 'Stop TB';
+ this.isStopTBServicelineEdit = firstRow.serviceName === 'Stop TB';
+ if (this.isStopTBServicelineEdit) {
+ this.loadNikshayEditSelections();
+ } else {
+ this.resetNikshaySelection();
+ }
+
if (this.isFacilityServicelineEdit) {
// Use merged group villages (union of all roles) instead of firstRow only
this.editExistingVillageIDs = Array.isArray(group.villageID)
@@ -4392,7 +5154,7 @@ export class WorkLocationMappingComponent
this.stateID_duringEdit = '';
this.district_duringEdit = null;
- if (this.isFacilityServicelineEdit) {
+ if (this.isFacilityServicelineEdit || this.isStopTBServicelineEdit) {
// HWC/FLW with null workLocationID: derive state from providerServiceMapID,
// then load districts + blocks so dropdowns are pre-populated
this.worklocationmapping
@@ -4462,11 +5224,17 @@ export class WorkLocationMappingComponent
(response: any) => {
if (response) {
this.districts_array = response.data;
- // Set district value AFTER options are loaded
- this.district_duringEdit = parseInt(
- this.edit_Details.workingDistrictID,
- 10,
- );
+ // Set district value AFTER options are loaded. Skipped for Stop
+ // TB — district_duringEdit already holds the Nikshay district
+ // ID resolved by loadNikshayEditSelections() above, and
+ // workingDistrictID (AMRIT) is always null for Stop TB rows,
+ // which would otherwise overwrite it with NaN.
+ if (!this.isStopTBServicelineEdit) {
+ this.district_duringEdit = parseInt(
+ this.edit_Details.workingDistrictID,
+ 10,
+ );
+ }
// Load blocks → set ServiceEditblock → load villages
if (
!isNaN(this.district_duringEdit) &&
@@ -4477,9 +5245,12 @@ export class WorkLocationMappingComponent
) {
this.getEditBlockPatchMaster(this.district_duringEdit);
}
- // HWC/FLW: no workingLocationID — load roles directly
+ // HWC/FLW/Stop TB: no workingLocationID — load roles directly
// Other services: chain via work locations
- if (this.isFacilityServicelineEdit) {
+ if (
+ this.isFacilityServicelineEdit ||
+ this.isStopTBServicelineEdit
+ ) {
this.getAllRoles_duringEdit2(
this.serviceID_duringEdit,
this.providerServiceMapID_duringEdit,
@@ -4569,6 +5340,74 @@ export class WorkLocationMappingComponent
const { editVillageIdArray, editVillageNameArray } =
this.getEditVillageArrays();
+ // Stop TB: block/village fields come from the Nikshay TU/Facility/
+ // Village pickers instead of the generic AMRIT block/village pickers
+ // every other serviceline uses here. This is the same in-place update
+ // path everyone else already uses (UpdateWorkLocationMapping ->
+ // updateUserRoleMapping, which already handles NikshayTUID/
+ // NikshayFacilityID) — no need for Stop TB's own deactivate-and-
+ // recreate mechanism, which left a trail of deactivated ghost rows on
+ // every edit.
+ const nikshayTUIDArr = this.isStopTBServicelineEdit
+ ? (this.selectedNikshayTUs || []).map((tu: any) => tu.nikshayTUID)
+ : [];
+ const nikshayTUNameArr = this.isStopTBServicelineEdit
+ ? (this.selectedNikshayTUs || []).map((tu: any) => tu.tUName)
+ : [];
+ const nikshayFacilityIDArr = this.isStopTBServicelineEdit
+ ? (this.selectedNikshayFacilities || []).map(
+ (f: any) => f.nikshayFacilityID,
+ )
+ : [];
+ // Villageid takes the Nikshay Village picker's selection directly on
+ // Edit (explicit instruction — Edit overwrites, Create does not).
+ // NOTE: Villageid holds AMRIT village IDs everywhere else in the
+ // system, including the mobile app's own beneficiary worklist match
+ // (BenFlowStatus.villageID) — writing Nikshay village IDs here means
+ // that match breaks until a Nikshay-village-to-AMRIT-village bridge
+ // table exists to translate between the two numbering systems. A
+ // worker saved through this path will not see beneficiaries in the
+ // mobile worklist for these villages until that bridge is built.
+ const nikshayVillageIDArr = this.isStopTBServicelineEdit
+ ? (this.selectedNikshayVillages || []).map((v: any) => v.nikshayVillageID)
+ : [];
+ const nikshayVillageNameArr = this.isStopTBServicelineEdit
+ ? (this.selectedNikshayVillages || []).map((v: any) => v.villageName)
+ : [];
+
+ // Same as Create (setWorkLocationObject): store whichever TU the admin
+ // actually picked via "Select Block" (selectedNikshayBlock), not just
+ // whichever TU landed first in the multi-select array — otherwise a
+ // saved row's blockID silently drifts to array order instead of the
+ // admin's actual Block pick, and re-editing shows the wrong block.
+ // Falls back to the first selected TU only when Block was never touched.
+ const blockIDToUse = this.isStopTBServicelineEdit
+ ? this.selectedNikshayBlock?.nikshayTUID ??
+ (nikshayTUIDArr.length ? nikshayTUIDArr[0] : null)
+ : this.ServiceEditblock;
+ const blockNameToUse = this.isStopTBServicelineEdit
+ ? this.selectedNikshayBlock?.tUName ??
+ (nikshayTUNameArr.length ? nikshayTUNameArr[0] : null)
+ : this.blockname;
+ const villageIDToUse = this.isStopTBServicelineEdit
+ ? nikshayVillageIDArr.length
+ ? nikshayVillageIDArr
+ : null
+ : editVillageIdArray;
+ const villageNameToUse = this.isStopTBServicelineEdit
+ ? nikshayVillageNameArr.length
+ ? nikshayVillageNameArr
+ : null
+ : editVillageNameArray.length > 0
+ ? editVillageNameArray
+ : null;
+ const nikshayTUIDToSend = nikshayTUIDArr.length
+ ? nikshayTUIDArr.join(',')
+ : null;
+ const nikshayFacilityIDToSend = nikshayFacilityIDArr.length
+ ? nikshayFacilityIDArr.join(',')
+ : null;
+
// UPDATE existing rows for kept roles
for (const rid of rolesToKeep) {
const existingEntry = group.roles.find(
@@ -4586,13 +5425,16 @@ export class WorkLocationMappingComponent
: this.workLocationID_duringEdit,
stateID: this.stateID_duringEdit,
districtID: this.district_duringEdit,
- blockID: this.ServiceEditblock,
- blockName: this.blockname,
- villageID: editVillageIdArray,
- villageName:
- editVillageNameArray.length > 0 ? editVillageNameArray : null,
+ 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'
@@ -4666,15 +5508,20 @@ export class WorkLocationMappingComponent
: this.workLocationID_duringEdit,
stateID: this.stateID_duringEdit,
districtID: this.district_duringEdit,
- blockID: this.ServiceEditblock,
- blockName: this.blockname,
- villageID: editVillageIdArray,
- villageName:
- editVillageNameArray.length > 0 ? editVillageNameArray : null,
+ blockID: blockIDToUse,
+ blockName: blockNameToUse,
+ villageID: villageIDToUse,
+ villageName: villageNameToUse,
facilityID:
this.isFacilityServicelineEdit && this.editFacilityMappingData
? this.editFacilityMappingData.facilityID
: null,
+ ...(this.isStopTBServicelineEdit
+ ? {
+ nikshayTUID: nikshayTUIDToSend,
+ nikshayFacilityID: nikshayFacilityIDToSend,
+ }
+ : {}),
},
],
userID: this.userID_duringEdit,
diff --git a/src/app/app-provider-admin/provider-admin/inventory/item-to-store-mapping/item-to-store-mapping.component.ts b/src/app/app-provider-admin/provider-admin/inventory/item-to-store-mapping/item-to-store-mapping.component.ts
index 4ac1df94..70b6496f 100644
--- a/src/app/app-provider-admin/provider-admin/inventory/item-to-store-mapping/item-to-store-mapping.component.ts
+++ b/src/app/app-provider-admin/provider-admin/inventory/item-to-store-mapping/item-to-store-mapping.component.ts
@@ -362,7 +362,9 @@ export class ItemToStoreMappingComponent implements OnInit {
}
if (obj.itemID1.length > 0) {
if (this.checkinBuffer(obj)) {
- this.bufferarray.data.push(obj);
+ this.bufferarray.data = [...this.bufferarray.data, obj];
+ } else {
+ this.bufferarray.data = [...this.bufferarray.data];
}
} else {
this.dialogService.alert('No Items to add');
@@ -372,16 +374,20 @@ export class ItemToStoreMappingComponent implements OnInit {
}
removeRow(index: any) {
- this.bufferarray.data.splice(index, 1);
+ const data = [...this.bufferarray.data];
+ data.splice(index, 1);
+ this.bufferarray.data = data;
}
removeItem(rowIndex: any, stateIndex: any) {
- this.bufferarray.data[rowIndex].itemID1.splice(stateIndex, 1);
- this.bufferarray.data[rowIndex].item.splice(stateIndex, 1);
+ const data = [...this.bufferarray.data];
+ data[rowIndex].itemID1.splice(stateIndex, 1);
+ data[rowIndex].item.splice(stateIndex, 1);
- if (this.bufferarray.data[rowIndex].itemID1.length === 0) {
- this.bufferarray.data.splice(rowIndex, 1);
+ if (data[rowIndex].itemID1.length === 0) {
+ data.splice(rowIndex, 1);
}
+ this.bufferarray.data = data;
}
checkinBuffer(obj: any) {
diff --git a/src/app/app-provider-admin/super-admin/super-admin/Activities/service-provider-master/service-provider-master.component.ts b/src/app/app-provider-admin/super-admin/super-admin/Activities/service-provider-master/service-provider-master.component.ts
index acb1298f..8e64c88e 100644
--- a/src/app/app-provider-admin/super-admin/super-admin/Activities/service-provider-master/service-provider-master.component.ts
+++ b/src/app/app-provider-admin/super-admin/super-admin/Activities/service-provider-master/service-provider-master.component.ts
@@ -28,7 +28,6 @@ import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
-
@Component({
selector: 'app-service-provider-master',
templateUrl: './service-provider-master.component.html',
@@ -105,16 +104,24 @@ export class ServiceProviderMasterComponent implements OnInit {
this.createdBy = this.commonDataService.uname;
this.today = new Date();
this.validFrom = this.today;
- this.validTill = this.today;
+ this.validTill = this.getDefaultValidTill();
this.getAllProviders();
}
+ /* Defaults Valid Till to 5 years out so a provider isn't accidentally
+ * created with an expiry of "today", which locks out login the next day. */
+ getDefaultValidTill(): Date {
+ const defaultValidTill = new Date(this.today);
+ defaultValidTill.setFullYear(defaultValidTill.getFullYear() + 5);
+ return defaultValidTill;
+ }
+
@ViewChild(MatSort) sort!: MatSort;
ngAfterViewInit() {
- this.filteredsearchResult.sort = this.sort;
+ this.filteredsearchResult.sort = this.sort;
}
-
+
showTable() {
if (this.editMode) {
this.tableMode = true;
@@ -127,7 +134,7 @@ export class ServiceProviderMasterComponent implements OnInit {
/* resetting date if moving BACK from edit mode */
this.today = new Date();
this.validFrom = this.today;
- this.validTill = this.today;
+ this.validTill = this.getDefaultValidTill();
} else {
this.tableMode = true;
this.formMode = false;
@@ -427,6 +434,6 @@ export class ServiceProviderMasterComponent implements OnInit {
resetDates() {
this.today = new Date();
this.validFrom = this.today;
- this.validTill = this.today;
+ this.validTill = this.getDefaultValidTill();
}
}
diff --git a/src/app/core/services/httpInterceptor/http-interceptor.service.ts b/src/app/core/services/httpInterceptor/http-interceptor.service.ts
index 0599f51a..fa42e4a0 100644
--- a/src/app/core/services/httpInterceptor/http-interceptor.service.ts
+++ b/src/app/core/services/httpInterceptor/http-interceptor.service.ts
@@ -82,29 +82,24 @@ export class HttpInterceptorService implements HttpInterceptor {
}),
catchError((error: HttpErrorResponse) => {
console.error(error);
- if(error.status === 401){
+ if (error.status === 401) {
this.sessionstorage.clear();
- this.confirmationService.alert('Session expired. Please login again.', 'error');
-
- } else if (error.status === 403) {
+ sessionStorage.clear();
this.confirmationService.alert(
- 'Access Denied',
+ 'Session expired. Please login again.',
'error',
);
+ this.router.navigate(['/login']);
+ } else if (error.status === 403) {
+ this.confirmationService.alert('Access Denied', 'error');
} else if (error.status === 500) {
- this.confirmationService.alert(
- 'Internal Server Error',
- 'error',
- );
+ this.confirmationService.alert('Internal Server Error', 'error');
} else {
this.confirmationService.alert(
error.message || 'Something went wrong',
'error',
);
}
- this.router.navigate(['/login']);
- sessionStorage.clear();
- this.sessionstorage.clear();
this.spinnerService.setLoading(false);
return throwError(error.error);
}),
@@ -131,9 +126,7 @@ export class HttpInterceptorService implements HttpInterceptor {
() => {
console.log('there', Date());
- if (
- sessionStorage.getItem('isAuthenticated')
- ) {
+ if (sessionStorage.getItem('isAuthenticated')) {
this.confirmationService
.alert(
'Your session is about to Expire. Do you need more time ? ',
@@ -151,7 +144,8 @@ export class HttpInterceptorService implements HttpInterceptor {
this.sessionstorage.clear();
localStorage.clear();
this.confirmationService.alert(
- this.currentLanguageSet.sessionExpired || 'Your session has expired. Please login again.',
+ this.currentLanguageSet.sessionExpired ||
+ 'Your session has expired. Please login again.',
'error',
);
this.router.navigate(['/login']);
@@ -161,7 +155,8 @@ export class HttpInterceptorService implements HttpInterceptor {
this.sessionstorage.clear();
localStorage.clear();
this.confirmationService.alert(
- this.currentLanguageSet.sessionExpired || 'Your session has expired. Please login again.',
+ this.currentLanguageSet.sessionExpired ||
+ 'Your session has expired. Please login again.',
'error',
);
this.router.navigate(['/login']);
diff --git a/src/environments/environment.ci.ts.template b/src/environments/environment.ci.ts.template
index 214aa37b..612d8c61 100644
--- a/src/environments/environment.ci.ts.template
+++ b/src/environments/environment.ci.ts.template
@@ -165,6 +165,12 @@ export const environment = {
get_Service_new_Url: `${adminBaseUrl}m/role/serviceNew`,
get_facilities_Url: `${adminBaseUrl}getFacility`,
save_facilities_Url: `${adminBaseUrl}addFacility`,
+ nikshayStates_url: `${adminBaseUrl}nikshay/location/states`,
+ nikshayDistricts_url: `${adminBaseUrl}nikshay/location/districts`,
+ nikshayTUs_url: `${adminBaseUrl}nikshay/location/tus`,
+ nikshayFacilities_url: `${adminBaseUrl}nikshay/location/facilities`,
+ nikshayVillages_url: `${adminBaseUrl}nikshay/location/villages`,
+ nikshayUserMapping_url: `${adminBaseUrl}nikshay/location/userMapping`,
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 20f7cca3..668d2f0d 100644
--- a/src/environments/environment.local.ts
+++ b/src/environments/environment.local.ts
@@ -165,6 +165,12 @@ export const environment = {
get_Service_new_Url: `${adminBaseUrl}m/role/serviceNew`,
get_facilities_Url: `${adminBaseUrl}getFacility`,
save_facilities_Url: `${adminBaseUrl}addFacility`,
+ nikshayStates_url: `${adminBaseUrl}nikshay/location/states`,
+ nikshayDistricts_url: `${adminBaseUrl}nikshay/location/districts`,
+ nikshayTUs_url: `${adminBaseUrl}nikshay/location/tus`,
+ nikshayFacilities_url: `${adminBaseUrl}nikshay/location/facilities`,
+ nikshayVillages_url: `${adminBaseUrl}nikshay/location/villages`,
+ nikshayUserMapping_url: `${adminBaseUrl}nikshay/location/userMapping`,
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 56133ece..273283a2 100644
--- a/src/environments/environment.prod.ts
+++ b/src/environments/environment.prod.ts
@@ -163,6 +163,12 @@ export const environment = {
get_Service_new_Url: `${adminBaseUrl}m/role/serviceNew`,
get_facilities_Url: `${adminBaseUrl}getFacility`,
save_facilities_Url: `${adminBaseUrl}addFacility`,
+ nikshayStates_url: `${adminBaseUrl}nikshay/location/states`,
+ nikshayDistricts_url: `${adminBaseUrl}nikshay/location/districts`,
+ nikshayTUs_url: `${adminBaseUrl}nikshay/location/tus`,
+ nikshayFacilities_url: `${adminBaseUrl}nikshay/location/facilities`,
+ nikshayVillages_url: `${adminBaseUrl}nikshay/location/villages`,
+ nikshayUserMapping_url: `${adminBaseUrl}nikshay/location/userMapping`,
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 9e2f6cc8..007064b5 100644
--- a/src/environments/environment.test.ts
+++ b/src/environments/environment.test.ts
@@ -159,6 +159,12 @@ export const environment = {
get_Service_new_Url: `${adminBaseUrl}m/role/serviceNew`,
get_facilities_Url: `${adminBaseUrl}getFacility`,
save_facilities_Url: `${adminBaseUrl}addFacility`,
+ nikshayStates_url: `${adminBaseUrl}nikshay/location/states`,
+ nikshayDistricts_url: `${adminBaseUrl}nikshay/location/districts`,
+ nikshayTUs_url: `${adminBaseUrl}nikshay/location/tus`,
+ nikshayFacilities_url: `${adminBaseUrl}nikshay/location/facilities`,
+ nikshayVillages_url: `${adminBaseUrl}nikshay/location/villages`,
+ nikshayUserMapping_url: `${adminBaseUrl}nikshay/location/userMapping`,
update_facilities_Url: `${adminBaseUrl}editFacility`,
delete_facilities_Url: `${adminBaseUrl}deleteFacility`,
get_FacilityLevels_Url: `${adminBaseUrl}getFacilityLevels`,