diff --git a/pom.xml b/pom.xml index c6b72b8..33d2ab1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.admin-ui admin-ui - 3.8.2 + 3.11.0 Admin-UI Piramal - admin: Module ui diff --git a/src/app/app-provider-admin/provider-admin/configurations/configurations.module.ts b/src/app/app-provider-admin/provider-admin/configurations/configurations.module.ts index 1a051ba..c849ade 100644 --- a/src/app/app-provider-admin/provider-admin/configurations/configurations.module.ts +++ b/src/app/app-provider-admin/provider-admin/configurations/configurations.module.ts @@ -25,9 +25,11 @@ import { HospitalInstituteDirectorySubdirectoryMappingComponent } from './hospit import { HospitalInstituteMappingService } from '../activities/services/hospital-institute-mapping-service.service'; import { ResetUserPasswordService } from 'src/app/core/services/ProviderAdminServices/reset-user-password.service'; import { ChangeUsernameService } from 'src/app/core/services/ProviderAdminServices/change-username.service'; +import { UpdateVillageService } from 'src/app/core/services/ProviderAdminServices/update-village.service'; import { UtcDatePipe } from './utc-date.pipe'; import { ResetUserPasswordComponent } from './reset-user-password/reset-user-password.component'; import { ChangeUsernameComponent } from './change-username/change-username.component'; +import { UpdateVillageComponent } from './update-village/update-village.component'; import { SwymedUserMappingComponent } from './swymed-user-mapping/swymed-user-mapping.component'; import { SwymedUserConfigurationService } from './services/swymed-user-service'; import { ServicePointVillageMapComponent } from './service-point-village-mapping/service-point-village-mapping.component'; @@ -66,6 +68,7 @@ import { MatInputModule } from '@angular/material/input'; HospitalInstituteDirectorySubdirectoryMappingComponent, ResetUserPasswordComponent, ChangeUsernameComponent, + UpdateVillageComponent, SwymedUserMappingComponent, UtcDatePipe, ServicePointVillageMapComponent, @@ -102,6 +105,7 @@ import { MatInputModule } from '@angular/material/input'; HospitalInstituteMappingService, ResetUserPasswordService, ChangeUsernameService, + UpdateVillageService, SwymedUserConfigurationService, ServicePointVillageMapService, VanServicePointMappingService, @@ -125,6 +129,7 @@ import { MatInputModule } from '@angular/material/input'; HospitalInstituteDirectorySubdirectoryMappingComponent, ResetUserPasswordComponent, ChangeUsernameComponent, + UpdateVillageComponent, SwymedUserMappingComponent, UtcDatePipe, ServicePointVillageMapComponent, diff --git a/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.css b/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.css new file mode 100644 index 0000000..3799b71 --- /dev/null +++ b/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.css @@ -0,0 +1,62 @@ +/* Keep the Update button and result clear of the fixed 30px app footer */ +.update-village-page { + padding-bottom: 70px; +} + +.screen-note { + font-size: 12px; + color: rgba(0, 0, 0, 0.6); + margin: 4px 0 0 0; +} + +.validation-note { + font-size: 12px; + color: #d32f2f; + margin: 4px 0 0 0; +} + +.warning-note { + color: #ef6c00; +} + +.mapping-table-wrapper { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +/* Scroll horizontally on narrow screens instead of squashing the 9 columns */ +.mapping-table { + width: 100%; + min-width: 900px; +} + +.mapping-table th.mat-mdc-header-cell { + white-space: nowrap; +} + +.mapping-table th.mat-mdc-header-cell, +.mapping-table td.mat-mdc-cell { + padding: 8px 12px; +} + +.mapping-table td.mat-mdc-cell { + word-break: break-word; +} + +.selected-row { + background-color: rgba(76, 175, 80, 0.12); +} + +@media (max-width: 575.98px) { + h4 { + font-size: 18px; + } + + h5 { + font-size: 15px; + } + + .update-button { + width: 100%; + } +} diff --git a/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.html b/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.html new file mode 100644 index 0000000..b097e77 --- /dev/null +++ b/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.html @@ -0,0 +1,345 @@ +
+
+
+

Update Village

+

+ Select a user, pick the work location mapping and the village to + replace, then choose the new village by State, District and Block. This + cannot be undone. +

+
+ +
+ + Select User + + + {{ item.userName }} + + + +
+
+ + +
+
+
User Details
+
+
+ + User ID + + +
+
+ + Username + + +
+
+ + Name + + +
+
+ + Employee ID + + +
+
+ +
+
+
Current Village Mappings
+

Loading mappings...

+

+ This user has no active work location mapping with villages. +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Select + + User Mapping ID + {{ element.uSRMappingID }} + Role (ID) + {{ element.roleName }} ({{ element.roleID }}) + Service Line + {{ element.serviceName }} + State{{ element.stateName }}District + {{ element.workingDistrictName }} + Block (ID) + {{ element.blockName }} ({{ element.blockID }}) + Facility + {{ element.facilityName || "-" }} + Mapped Villages (ID) +
+ {{ village.villageName }} ({{ village.villageID }}) +
+
+
+
+
+ + +
+
+
Village to Replace
+
+
+ + Old Village + + + {{ village.villageName }} ({{ village.villageID }}) + + + +
+
+ +
+
+
New Village
+
+
+ + State + + + {{ item.stateName }} + + + +
+
+ + District + + + {{ item.districtName }} + + + +
+
+ + Block + + + {{ item.blockName }} + + + +
+
+ + Facility (optional) + + All villages in block + + {{ item.facilityName }} + + + +
+
+ + Village + + + {{ item.villageName }} ({{ item.districtBranchID }}) + + - not in facility + + + + No villages are mapped to this facility + + + Not mapped to facility {{ selectedMapping.facilityName }} + + + Different block from the current mapping + + +
+
+
+ +
+
+

{{ validationMessage }}

+
+
+ +
+
+ +
+
+ +
+
+
Update complete
+

+ Mapping {{ updateResult.uSRMappingID }}: + {{ updateResult.oldVillageName }} → + {{ updateResult.newVillageName }} +

+ +

+ Beneficiary addresses updated: + {{ updateResult.permanentAddressesUpdated }} +

+

+ Beneficiary addresses moved from {{ updateResult.oldVillageName }}: + {{ updateResult.permanentAddressesUpdated }} permanent, + {{ updateResult.currentAddressesUpdated }} current +

+
+
+
+
diff --git a/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.spec.ts b/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.spec.ts new file mode 100644 index 0000000..d7bd055 --- /dev/null +++ b/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.spec.ts @@ -0,0 +1,427 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { of, Subject, throwError } from 'rxjs'; +import { UpdateVillageComponent } from './update-village.component'; + +describe('UpdateVillageComponent', () => { + let component: UpdateVillageComponent; + let service: any; + let alertService: any; + let sessionstorage: any; + + const user = { userID: 7, userName: 'asha01', firstName: 'Asha' }; + const mapping = { + uSRMappingID: 101, + userID: 7, + roleID: 3, + roleName: 'ASHA', + providerServiceMapID: 1717, + stateID: 5, + workingDistrictID: '50', + blockID: 500, + blockName: 'Block A', + villageID: ['9001', ' 9002'], + villageName: ['Village A', 'Village B '], + }; + + beforeEach(() => { + service = jasmine.createSpyObj('UpdateVillageService', [ + 'getUserList', + 'getUserDetail', + 'getUserRoleMapped', + 'getStates', + 'getDistricts', + 'getBlocks', + 'getVillages', + 'getFacilitiesByBlock', + 'getFacilityVillages', + 'updateVillage', + ]); + alertService = jasmine.createSpyObj('ConfirmationDialogsService', [ + 'alert', + 'confirm', + ]); + sessionstorage = jasmine.createSpyObj('SessionStorageService', ['getItem']); + sessionstorage.getItem.and.callFake((key: string) => + key === 'service_providerID' ? 12 : 'admin', + ); + + service.getUserList.and.returnValue(of({ data: [user] })); + service.getStates.and.returnValue( + of({ data: [{ stateID: 5, stateName: 'State' }] }), + ); + service.getUserDetail.and.returnValue(of({ data: { employeeID: 'E1' } })); + service.getUserRoleMapped.and.returnValue( + of({ data: [mapping, { ...mapping, uSRMappingID: 999, userID: 8 }] }), + ); + service.getDistricts.and.returnValue( + of({ data: [{ districtID: 50, districtName: 'District' }] }), + ); + service.getBlocks.and.returnValue( + of({ + data: [ + { blockID: 500, blockName: 'Block A' }, + { blockID: 600, blockName: 'Block B' }, + ], + }), + ); + service.getFacilitiesByBlock.and.returnValue(of({ data: [] })); + service.getVillages.and.returnValue( + of({ + data: [ + { districtBranchID: 9001, villageName: 'Village A' }, + { districtBranchID: 9003, villageName: 'Village C' }, + ], + }), + ); + + component = new UpdateVillageComponent( + service, + alertService, + sessionstorage, + ); + component.ngOnInit(); + }); + + it('loads users and states on init', () => { + expect(service.getUserList).toHaveBeenCalledWith(12); + expect(service.getStates).toHaveBeenCalledWith(1); + expect(component.userNamesList).toEqual([user]); + expect(component.statesList.length).toBe(1); + }); + + it('loads only the selected user mappings and pre-fills the hierarchy', () => { + component.user = user; + component.onUserChange(); + + expect(service.getUserDetail).toHaveBeenCalledWith('asha01'); + expect(component.userDetail.employeeID).toBe('E1'); + expect(component.userMappings.length).toBe(1); + expect(component.userMappings[0].mappedVillages).toEqual([ + { villageID: '9001', villageName: 'Village A' }, + { villageID: '9002', villageName: 'Village B' }, + ]); + // single mapping is auto-selected and its state/district/block pre-filled + expect(component.selectedMapping.uSRMappingID).toBe(101); + expect(component.state.stateID).toBe(5); + expect(component.district.districtID).toBe(50); + expect(component.block.blockID).toBe(500); + expect(component.villagesList.length).toBe(2); + }); + + it('validates the selection step by step', () => { + expect(component.validationMessage).toBe('Select a user'); + component.user = user; + expect(component.validationMessage).toContain('mapping'); + component.onUserChange(); + expect(component.validationMessage).toBe('Select the village to replace'); + component.oldVillage = component.selectedMapping.mappedVillages[0]; + expect(component.validationMessage).toBe('Select the new village'); + component.newVillage = component.villagesList[0]; + expect(component.validationMessage).toContain('already mapped'); + component.newVillage = component.villagesList[1]; + expect(component.validationMessage).toBeNull(); + expect(component.canSubmit).toBeTrue(); + }); + + it('flags a block different from the current mapping', () => { + component.user = user; + component.onUserChange(); + expect(component.blockChanged).toBeFalse(); + component.block = component.blocksList[1]; + expect(component.blockChanged).toBeTrue(); + }); + + it('builds the request and updates after confirmation', () => { + component.user = user; + component.onUserChange(); + component.oldVillage = component.selectedMapping.mappedVillages[1]; + component.newVillage = component.villagesList[1]; + alertService.confirm.and.returnValue(of(true)); + service.updateVillage.and.returnValue( + of({ + data: { permanentAddressesUpdated: 12, currentAddressesUpdated: 11 }, + }), + ); + + component.confirmAndUpdate(); + + expect(service.updateVillage).toHaveBeenCalledWith({ + userID: 7, + userName: 'asha01', + uSRMappingID: 101, + roleID: 3, + providerServiceMapID: 1717, + oldBlockID: 500, + oldVillageID: 9002, + oldVillageName: 'Village B', + newStateID: 5, + newDistrictID: 50, + newBlockID: 500, + newBlockName: 'Block A', + newVillageID: 9003, + newVillageName: 'Village C', + modifiedBy: 'admin', + }); + expect(alertService.alert).toHaveBeenCalledWith( + 'Village updated successfully', + 'success', + ); + expect(component.updateResult.newVillageName).toBe('Village C'); + expect(component.updateResult.permanentAddressesUpdated).toBe(12); + expect(component.submitting).toBeFalse(); + }); + + it('does not update when the confirmation is declined', () => { + component.user = user; + component.onUserChange(); + component.oldVillage = component.selectedMapping.mappedVillages[0]; + component.newVillage = component.villagesList[1]; + alertService.confirm.and.returnValue(of(false)); + + component.confirmAndUpdate(); + + expect(service.updateVillage).not.toHaveBeenCalled(); + }); + + it('shows the error and re-enables submit when the update fails', () => { + component.user = user; + component.onUserChange(); + component.oldVillage = component.selectedMapping.mappedVillages[0]; + component.newVillage = component.villagesList[1]; + alertService.confirm.and.returnValue(of(true)); + service.updateVillage.and.returnValue( + throwError(() => ({ errorMessage: 'failed' })), + ); + + component.confirmAndUpdate(); + + expect(alertService.alert).toHaveBeenCalledWith('failed', 'error'); + expect(component.submitting).toBeFalse(); + expect(component.updateResult).toBeNull(); + }); + + it('resets lower levels when a higher level changes', () => { + component.user = user; + component.onUserChange(); + component.newVillage = component.villagesList[1]; + + component.onBlockChange(); + expect(component.newVillage).toBeNull(); + + component.onDistrictChange(); + expect(service.getBlocks).toHaveBeenCalledWith(50); + + component.state = null; + component.onStateChange(); + expect(component.district).toBeNull(); + expect(component.districtsList).toEqual([]); + }); + + it('skips deactivated, Stop TB and village-less mappings', () => { + service.getUserRoleMapped.and.returnValue( + of({ + data: [ + { ...mapping, uSRMappingID: 1, userServciceRoleDeleted: true }, + { ...mapping, uSRMappingID: 2, serviceName: 'Stop TB' }, + { ...mapping, uSRMappingID: 3, villageID: [], villageName: [] }, + { ...mapping, uSRMappingID: 4 }, + ], + }), + ); + component.user = user; + component.onUserChange(); + + expect(component.mappingsWithVillages.map((m) => m.uSRMappingID)).toEqual([ + 4, + ]); + // the one selectable mapping is auto-selected, not the village-less one + expect(component.selectedMapping.uSRMappingID).toBe(4); + }); + + it('ignores a mappings response for a previously selected user', () => { + const pending = new Subject(); + service.getUserRoleMapped.and.returnValue(pending); + component.user = user; + component.onUserChange(); + component.user = { userID: 8, userName: 'asha02' }; + + pending.next({ data: [mapping] }); + + expect(component.userMappings).toEqual([]); + expect(component.selectedMapping).toBeNull(); + }); + + it('pre-fills the hierarchy once states arrive after the mapping', () => { + const states = new Subject(); + service.getStates.and.returnValue(states); + component.statesList = []; + component.user = user; + component.onUserChange(); + expect(component.state).toBeNull(); + + component.getStates(); + states.next({ data: [{ stateID: 5, stateName: 'State' }] }); + + expect(component.state.stateID).toBe(5); + expect(component.block.blockID).toBe(500); + }); + + it('ignores district results for a state that is no longer selected', () => { + const districts = new Subject(); + service.getDistricts.and.returnValue(districts); + component.user = user; + component.onUserChange(); + component.state = { stateID: 6, stateName: 'Other' }; + + districts.next({ data: [{ districtID: 50, districtName: 'District' }] }); + + expect(component.districtsList).toEqual([]); + expect(component.district).toBeNull(); + }); + + it('warns when the new village is outside the mapping facility', () => { + service.getUserRoleMapped.and.returnValue( + of({ data: [{ ...mapping, facilityID: 44, facilityName: 'SC One' }] }), + ); + service.getFacilityVillages.and.returnValue( + of({ data: [{ districtBranchID: 9001 }, { districtBranchID: 9002 }] }), + ); + component.user = user; + component.onUserChange(); + + expect(service.getFacilityVillages).toHaveBeenCalledWith(44); + component.oldVillage = component.selectedMapping.mappedVillages[0]; + component.newVillage = component.villagesList[1]; + expect(component.isOutsideFacility(component.newVillage)).toBeTrue(); + expect(component.isOutsideFacility(component.villagesList[0])).toBeFalse(); + expect(component.confirmMessage(component.buildRequest())).toContain( + 'not mapped to facility SC One', + ); + }); + + it('falls back to a generic message when an error has no text', () => { + service.getUserList.and.returnValue(throwError(() => ({ status: 404 }))); + component.getUserList(); + expect(alertService.alert).toHaveBeenCalledWith( + 'Something went wrong', + 'error', + ); + }); + + it('tells the admin which beneficiary addresses will move', () => { + component.user = user; + component.onUserChange(); + component.oldVillage = component.selectedMapping.mappedVillages[0]; + component.newVillage = component.villagesList[1]; + expect(component.confirmMessage(component.buildRequest())).toContain( + 'registered by asha01 in Village A will be moved to Village C', + ); + + component.selectedMapping = { + ...component.selectedMapping, + mappedVillages: [component.selectedMapping.mappedVillages[0]], + }; + expect(component.confirmMessage(component.buildRequest())).toContain( + 'All beneficiary addresses registered by asha01 will be moved', + ); + }); + + describe('when the block has facilities', () => { + const facilities = [ + { facilityID: 44, facilityName: 'SC One' }, + { facilityID: 45, facilityName: 'SC Two' }, + ]; + + beforeEach(() => { + service.getFacilitiesByBlock.and.returnValue(of({ data: facilities })); + service.getFacilityVillages.and.callFake((facilityID: number) => + of({ + data: + facilityID === 44 + ? [{ districtBranchID: 9001, villageName: 'Village A' }] + : [{ districtBranchID: 9004, villageName: 'Village D' }], + }), + ); + }); + + it('lists block villages until a facility is chosen', () => { + component.user = user; + component.onUserChange(); + component.oldVillage = component.selectedMapping.mappedVillages[0]; + + expect(service.getFacilitiesByBlock).toHaveBeenCalledWith(500); + expect(component.hasFacilities).toBeTrue(); + expect(component.facility).toBeNull(); + expect(component.villageSelectDisabled).toBeFalse(); + expect(component.villagesList.length).toBe(2); + + component.newVillage = component.villagesList[1]; + expect(component.validationMessage).toBeNull(); + + component.facility = facilities[1]; + component.onFacilityChange(); + + expect(service.getFacilityVillages).toHaveBeenCalledWith(45); + expect(component.newVillage).toBeNull(); + expect(component.villagesList).toEqual([ + { districtBranchID: 9004, villageName: 'Village D' }, + ]); + + component.facility = null; + component.onFacilityChange(); + + expect(component.villagesList.length).toBe(2); + }); + + it("pre-selects the mapping's own facility", () => { + service.getUserRoleMapped.and.returnValue( + of({ data: [{ ...mapping, facilityID: 44 }] }), + ); + component.user = user; + component.onUserChange(); + + expect(component.facility.facilityID).toBe(44); + expect(component.villagesList[0].districtBranchID).toBe(9001); + }); + + it('lists block villages when facilities fail to load', () => { + service.getFacilitiesByBlock.and.returnValue( + throwError(() => ({ errorMessage: 'down' })), + ); + component.user = user; + component.onUserChange(); + + expect(service.getVillages).toHaveBeenCalledWith(500); + expect(component.villagesList.length).toBe(2); + }); + }); + + it('lists block villages directly when the block has no facilities', () => { + component.user = user; + component.onUserChange(); + + expect(component.hasFacilities).toBeFalse(); + expect(service.getVillages).toHaveBeenCalledWith(500); + expect(component.villageSelectDisabled).toBeFalse(); + }); +}); diff --git a/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.ts b/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.ts new file mode 100644 index 0000000..d18582f --- /dev/null +++ b/src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.ts @@ -0,0 +1,587 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { Component, OnInit } from '@angular/core'; +import { UpdateVillageService } from 'src/app/core/services/ProviderAdminServices/update-village.service'; +import { ConfirmationDialogsService } from 'src/app/core/services/dialog/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +export interface MappedVillage { + villageID: string; + villageName: string; +} + +@Component({ + selector: 'app-update-village', + templateUrl: './update-village.component.html', + styleUrls: ['./update-village.component.css'], +}) +export class UpdateVillageComponent implements OnInit { + readonly countryID = 1; // hardcoded as country is INDIA + + serviceProviderID: any; + userNamesList: any = []; + user: any; + userDetail: any = null; + + userMappings: any[] = []; + mappingsLoaded = false; + selectedMapping: any = null; + oldVillage: MappedVillage | null = null; + + statesList: any[] = []; + districtsList: any[] = []; + blocksList: any[] = []; + facilitiesList: any[] = []; + facilityVillagesLoaded = false; + villagesList: any[] = []; + + state: any = null; + district: any = null; + block: any = null; + facility: any = null; + newVillage: any = null; + + // districtBranchIDs mapped to the selected mapping's facility; null when the + // mapping has no facility or they have not loaded. + facilityVillageIDs: Set | null = null; + private pendingPrefill: any = null; + + updateResult: any = null; + submitting = false; + + displayedColumns = [ + 'select', + 'uSRMappingID', + 'role', + 'serviceName', + 'stateName', + 'districtName', + 'blockName', + 'facilityName', + 'villages', + ]; + + constructor( + private updateVillageService: UpdateVillageService, + private alertService: ConfirmationDialogsService, + readonly sessionstorage: SessionStorageService, + ) {} + + ngOnInit() { + this.serviceProviderID = this.sessionstorage.getItem('service_providerID'); + this.getUserList(); + this.getStates(); + } + + getUserList() { + this.updateVillageService.getUserList(this.serviceProviderID).subscribe( + (response: any) => { + this.userNamesList = response.data; + }, + (err: any) => this.showError(err), + ); + } + + showError(err: any) { + this.alertService.alert( + err?.errorMessage || err?.error?.errorMessage || 'Something went wrong', + 'error', + ); + } + + getStates() { + this.updateVillageService.getStates(this.countryID).subscribe( + (response: any) => { + this.statesList = response.data || []; + if (this.pendingPrefill) { + const mapping = this.pendingPrefill; + this.pendingPrefill = null; + this.prefillLocation(mapping); + } + }, + (err: any) => this.showError(err), + ); + } + + onUserChange() { + this.updateResult = null; + this.userDetail = null; + this.userMappings = []; + this.mappingsLoaded = false; + this.clearMappingSelection(); + if (!this.user) { + return; + } + const userID = this.user.userID; + this.updateVillageService.getUserDetail(this.user.userName).subscribe( + (response: any) => { + if (this.user?.userID === userID) { + this.userDetail = response.data || null; + } + }, + (err: any) => this.showError(err), + ); + this.getUserMappings(this.user.userID); + } + + getUserMappings(userID: any) { + this.updateVillageService + .getUserRoleMapped(this.serviceProviderID) + .subscribe( + (response: any) => { + if (this.user?.userID !== userID) { + return; + } + const rows = Array.isArray(response.data) ? response.data : []; + this.userMappings = rows + .filter((row: any) => this.isEditableMapping(row, userID)) + .map((row: any) => ({ + ...row, + mappedVillages: this.toMappedVillages(row), + })); + this.mappingsLoaded = true; + if (this.mappingsWithVillages.length === 1) { + this.onMappingChange(this.mappingsWithVillages[0]); + } + }, + (err: any) => { + if (this.user?.userID !== userID) { + return; + } + this.mappingsLoaded = true; + this.showError(err); + }, + ); + } + + // Databases before the V100 view change still return deactivated rows, and + // Stop TB mappings hold Nikshay village IDs rather than AMRIT villages. + isEditableMapping(row: any, userID: any): boolean { + return ( + row.userID === userID && + row.userServciceRoleDeleted !== true && + row.userDeleted !== true && + row.serviceName !== 'Stop TB' + ); + } + + // getUserRoleMapped returns villageID/villageName as parallel arrays split + // from the comma-separated Villageid/VillageName columns. + toMappedVillages(row: any): MappedVillage[] { + const ids: any[] = row.villageID || []; + const names: any[] = row.villageName || []; + return ids + .map((id: any, index: number) => ({ + villageID: String(id).trim(), + villageName: (names[index] || '').toString().trim(), + })) + .filter((village: MappedVillage) => village.villageID !== ''); + } + + get displayName(): string { + const source = this.userDetail || this.user || {}; + return [source.firstName, source.middleName, source.lastName] + .filter((part: any) => !!part) + .join(' '); + } + + get mappingsWithVillages(): any[] { + return this.userMappings.filter( + (mapping: any) => mapping.mappedVillages.length > 0, + ); + } + + clearMappingSelection() { + this.selectedMapping = null; + this.oldVillage = null; + this.facilityVillageIDs = null; + this.pendingPrefill = null; + this.clearLocation(); + } + + clearLocation() { + this.state = null; + this.district = null; + this.block = null; + this.newVillage = null; + this.districtsList = []; + this.blocksList = []; + this.clearFacilities(); + } + + clearFacilities() { + this.facility = null; + this.facilitiesList = []; + this.facilityVillagesLoaded = false; + this.villagesList = []; + } + + selectMapping(mapping: any) { + this.updateResult = null; + this.onMappingChange(mapping); + } + + onMappingChange(mapping: any) { + this.selectedMapping = mapping; + this.oldVillage = + mapping.mappedVillages.length === 1 ? mapping.mappedVillages[0] : null; + this.clearLocation(); + this.loadFacilityVillages(mapping); + this.prefillLocation(mapping); + } + + loadFacilityVillages(mapping: any) { + this.facilityVillageIDs = null; + if (mapping.facilityID == null) { + return; + } + this.updateVillageService.getFacilityVillages(mapping.facilityID).subscribe( + (response: any) => { + if (this.selectedMapping !== mapping) { + return; + } + const villages = Array.isArray(response.data) ? response.data : []; + this.facilityVillageIDs = new Set( + villages.map((village: any) => String(village.districtBranchID)), + ); + }, + (err: any) => this.showError(err), + ); + } + + isOutsideFacility(village: any): boolean { + return ( + !!village && + !!this.facilityVillageIDs && + !this.facilityVillageIDs.has(String(village.districtBranchID)) + ); + } + + // Start the hierarchy at the mapping's current state/district/block, since + // a correction is usually to a village in the same block. + prefillLocation(mapping: any) { + if (this.statesList.length === 0) { + this.pendingPrefill = mapping; + return; + } + const state = this.statesList.find( + (item: any) => item.stateID === mapping.stateID, + ); + if (!state) { + return; + } + this.state = state; + this.loadDistricts( + state.stateID, + mapping.workingDistrictID, + mapping.blockID, + mapping.facilityID, + ); + } + + onStateChange() { + this.updateResult = null; + this.district = null; + this.block = null; + this.newVillage = null; + this.districtsList = []; + this.blocksList = []; + this.clearFacilities(); + if (this.state) { + this.loadDistricts(this.state.stateID); + } + } + + onDistrictChange() { + this.updateResult = null; + this.block = null; + this.newVillage = null; + this.blocksList = []; + this.clearFacilities(); + if (this.district) { + this.loadBlocks(this.district.districtID); + } + } + + onBlockChange() { + this.updateResult = null; + this.newVillage = null; + this.clearFacilities(); + if (this.block) { + this.loadFacilities(this.block.blockID); + } + } + + onFacilityChange() { + this.updateResult = null; + this.newVillage = null; + this.villagesList = []; + if (this.facility) { + this.loadFacilityVillageList(this.facility.facilityID); + } else if (this.block) { + this.loadVillages(this.block.blockID); + } + } + + onVillageChange() { + this.updateResult = null; + } + + loadDistricts( + stateID: any, + preselectDistrictID?: any, + preselectBlockID?: any, + preselectFacilityID?: any, + ) { + this.updateVillageService.getDistricts(stateID).subscribe( + (response: any) => { + if (this.state?.stateID !== stateID) { + return; + } + this.districtsList = response.data || []; + if (preselectDistrictID == null) { + return; + } + const district = this.districtsList.find( + (item: any) => + String(item.districtID) === String(preselectDistrictID), + ); + if (district) { + this.district = district; + this.loadBlocks( + district.districtID, + preselectBlockID, + preselectFacilityID, + ); + } + }, + (err: any) => this.showError(err), + ); + } + + loadBlocks( + districtID: any, + preselectBlockID?: any, + preselectFacilityID?: any, + ) { + this.updateVillageService.getBlocks(districtID).subscribe( + (response: any) => { + if (this.district?.districtID !== districtID) { + return; + } + this.blocksList = response.data || []; + if (preselectBlockID == null) { + return; + } + const block = this.blocksList.find( + (item: any) => String(item.blockID) === String(preselectBlockID), + ); + if (block) { + this.block = block; + this.loadFacilities(block.blockID, preselectFacilityID); + } + }, + (err: any) => this.showError(err), + ); + } + + // Facility is optional: without one the Village list is the whole block, + // with one it narrows to that facility's villages. + loadFacilities(blockID: any, preselectFacilityID?: any) { + this.updateVillageService.getFacilitiesByBlock(blockID).subscribe( + (response: any) => { + if (this.block?.blockID !== blockID) { + return; + } + this.facilitiesList = Array.isArray(response.data) ? response.data : []; + const facility = this.facilitiesList.find( + (item: any) => + preselectFacilityID != null && + String(item.facilityID) === String(preselectFacilityID), + ); + if (facility) { + this.facility = facility; + this.loadFacilityVillageList(facility.facilityID); + } else { + this.loadVillages(blockID); + } + }, + (err: any) => { + if (this.block?.blockID !== blockID) { + return; + } + this.showError(err); + this.loadVillages(blockID); + }, + ); + } + + loadFacilityVillageList(facilityID: any) { + this.facilityVillagesLoaded = false; + this.updateVillageService.getFacilityVillages(facilityID).subscribe( + (response: any) => { + if (this.facility?.facilityID !== facilityID) { + return; + } + this.villagesList = Array.isArray(response.data) ? response.data : []; + this.facilityVillagesLoaded = true; + }, + (err: any) => { + if (this.facility?.facilityID === facilityID) { + this.facilityVillagesLoaded = true; + } + this.showError(err); + }, + ); + } + + get hasFacilities(): boolean { + return this.facilitiesList.length > 0; + } + + get villageSelectDisabled(): boolean { + return !this.block; + } + + loadVillages(blockID: any) { + this.updateVillageService.getVillages(blockID).subscribe( + (response: any) => { + if (this.block?.blockID !== blockID || this.facility) { + return; + } + this.villagesList = response.data || []; + }, + (err: any) => this.showError(err), + ); + } + + isAlreadyMapped(village: any): boolean { + if (!this.selectedMapping || !village) { + return false; + } + return this.selectedMapping.mappedVillages.some( + (mapped: MappedVillage) => + mapped.villageID === String(village.districtBranchID), + ); + } + + get blockChanged(): boolean { + return ( + !!this.selectedMapping && + !!this.block && + this.selectedMapping.blockID != null && + String(this.selectedMapping.blockID) !== String(this.block.blockID) + ); + } + + get validationMessage(): string | null { + if (!this.user) { + return 'Select a user'; + } + if (!this.selectedMapping) { + return 'Select the work location mapping to update'; + } + if (!this.oldVillage) { + return 'Select the village to replace'; + } + if (!this.newVillage) { + return 'Select the new village'; + } + if (this.isAlreadyMapped(this.newVillage)) { + return `${this.newVillage.villageName} is already mapped to this user`; + } + return null; + } + + get canSubmit(): boolean { + return this.validationMessage === null && !this.submitting; + } + + buildRequest() { + return { + userID: this.user.userID, + userName: this.user.userName, + uSRMappingID: this.selectedMapping.uSRMappingID, + roleID: this.selectedMapping.roleID, + providerServiceMapID: this.selectedMapping.providerServiceMapID, + oldBlockID: this.selectedMapping.blockID, + oldVillageID: Number(this.oldVillage?.villageID), + oldVillageName: this.oldVillage?.villageName, + newStateID: this.state.stateID, + newDistrictID: this.district.districtID, + newBlockID: this.block.blockID, + newBlockName: this.block.blockName, + newVillageID: this.newVillage.districtBranchID, + newVillageName: this.newVillage.villageName, + modifiedBy: this.sessionstorage.getItem('uname'), + }; + } + + confirmAndUpdate() { + if (!this.canSubmit) { + return; + } + const request = this.buildRequest(); + this.alertService + .confirm('Confirm', this.confirmMessage(request)) + .subscribe((accept: any) => { + if (accept) { + this.update(request); + } + }); + } + + confirmMessage(request: any): string { + const blockNote = this.blockChanged + ? ` The new village is in a different block (${request.newBlockName}).` + : ''; + const facilityNote = this.isOutsideFacility(this.newVillage) + ? ` It is not mapped to facility ${this.selectedMapping.facilityName || this.selectedMapping.facilityID}, so Work Location Mapping will drop it on the next edit unless the facility's villages are updated.` + : ''; + const addressNote = + this.selectedMapping.mappedVillages.length === 1 + ? ` All beneficiary addresses registered by ${request.userName} will be moved to ${request.newVillageName}.` + : ` Beneficiary addresses registered by ${request.userName} in ${request.oldVillageName} will be moved to ${request.newVillageName}.`; + return ( + `Change village for ${request.userName} (mapping ${request.uSRMappingID}) ` + + `from ${request.oldVillageName} to ${request.newVillageName}?` + + `${addressNote}${blockNote}${facilityNote} This cannot be undone.` + ); + } + + update(request: any) { + this.submitting = true; + this.updateVillageService.updateVillage(request).subscribe( + (response: any) => { + this.submitting = false; + this.updateResult = { ...request, ...(response?.data || {}) }; + this.alertService.alert('Village updated successfully', 'success'); + this.clearMappingSelection(); + this.getUserMappings(this.user.userID); + }, + (err: any) => { + this.submitting = false; + this.showError(err); + }, + ); + } +} diff --git a/src/app/app-provider-admin/provider-admin/provider-admin.component.html b/src/app/app-provider-admin/provider-admin/provider-admin.component.html index bdd779c..04c5354 100644 --- a/src/app/app-provider-admin/provider-admin/provider-admin.component.html +++ b/src/app/app-provider-admin/provider-admin/provider-admin.component.html @@ -126,6 +126,7 @@ +