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 + + + + {{ item.tUName }} + + + +
+ +
+ + Select TU + + + + + {{ item.tUName }} + + + +
+ +
+ + Select Health Facility + + + + + {{ item.facilityName }} + + + +
+ +
+ + Select Village + + + + + {{ 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 + + + + {{ item.tUName }} + + + +
+
+ + Select TU + + + + + {{ item.tUName }} + + + +
+
+ + Select Health Facility + + + + + {{ item.facilityName }} + + + +
+
+ + Select Village + + + + + {{ item.villageName }} + + + +
+
+
Select Role @@ -917,7 +1148,7 @@

-
+
Select Village [(ngModel)]="serviceEditvillage" name="serviceEditvillage" required + (openedChange)="$event ? null : serviceEditVillageSearch = ''" > +
>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`,