From 4950defc9b6178aaaa2cd1f655f689b4de919c97 Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:12:31 +0530 Subject: [PATCH 1/2] Release 3.11.0 from 3.10.0 (#168) * feat(admin): add Update Village screen New Configuration > Update Village page. Select a user to see their User ID, employee details and active work location mappings (user mapping ID, role, block, mapped villages), choose the village to replace, then pick the new one by State > District > Block > Village. The save call targets villageMapping/updateVillage, which is not yet implemented in Admin-API. Co-Authored-By: Claude Opus 5.5 * fix(admin): harden the Update Village screen Hide deactivated and Stop TB mappings, only auto-select a mapping that has villages, ignore responses for a user or location that is no longer selected, pre-fill the location once states load, warn when the new village is outside the mapping's facility, fall back to a generic error message, and show the beneficiary address counts returned by villageMapping/updateVillage. Co-Authored-By: Claude Opus 5.5 * feat(admin): add optional facility filter to Update Village - load facilities for the selected block; picking one narrows the village list to that facility's villages, "All villages in block" keeps the old list - preselect the mapping's facility when opening an existing mapping - ignore stale block/facility responses and fall back to block villages when the facility lookup fails - show a hint when a facility has no mapped villages - confirmation and result text now say how beneficiary addresses move (all addresses for single-village users, only the old village otherwise) Co-Authored-By: Claude Opus 5.5 * fix(admin): make Update Village screen responsive - Replace Bootstrap 3 col-xs-* classes with Bootstrap 5 col-12 - Scroll the mappings table horizontally on narrow screens - Let village hints wrap instead of overflowing the field - Full-width Update button and smaller headings on phones - Pad the page bottom so the fixed footer no longer hides the Update button Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Sneha Co-authored-by: Claude Opus 5.5 --- .../configurations/configurations.module.ts | 5 + .../update-village.component.css | 62 ++ .../update-village.component.html | 345 ++++++++++ .../update-village.component.spec.ts | 427 +++++++++++++ .../update-village.component.ts | 587 ++++++++++++++++++ .../provider-admin.component.html | 2 + .../update-village.service.ts | 108 ++++ 7 files changed, 1536 insertions(+) create mode 100644 src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.css create mode 100644 src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.html create mode 100644 src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.spec.ts create mode 100644 src/app/app-provider-admin/provider-admin/configurations/update-village/update-village.component.ts create mode 100644 src/app/core/services/ProviderAdminServices/update-village.service.ts 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 @@ +