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 }}
+
+
+
+
+
+
+
+
+
+
+
+
Current Village Mappings
+
Loading mappings...
+
+ This user has no active work location mapping with villages.
+
+
+
+
0"
+ >
+
+
+ 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 @@
Reset User Password
Change Username
+ Update Village
User Swymed Mapping
Wrapup Time Configuration
@@ -286,6 +287,7 @@
>
+
diff --git a/src/app/core/services/ProviderAdminServices/update-village.service.ts b/src/app/core/services/ProviderAdminServices/update-village.service.ts
new file mode 100644
index 0000000..0804869
--- /dev/null
+++ b/src/app/core/services/ProviderAdminServices/update-village.service.ts
@@ -0,0 +1,108 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { ConfigService } from '../config/config.service';
+
+@Injectable()
+export class UpdateVillageService {
+ adminBaseUrl: any;
+ commonBaseUrl: any;
+ getUserListUrl: any;
+ getUserDetailUrl: any;
+ getUserRoleMappedUrl: any;
+ getStatesUrl: any;
+ getDistrictsUrl: any;
+ getBlocksUrl: any;
+ getVillagesUrl: any;
+ getFacilityVillagesUrl: any;
+ getFacilitiesByBlockUrl: any;
+ updateVillageUrl: any;
+
+ constructor(
+ private http: HttpClient,
+ private basepaths: ConfigService,
+ ) {
+ this.adminBaseUrl = this.basepaths.getAdminBaseUrl();
+ this.commonBaseUrl = this.basepaths.getCommonBaseURL();
+
+ this.getUserListUrl = this.adminBaseUrl + 'm/SearchEmployee4';
+ this.getUserDetailUrl =
+ this.adminBaseUrl + 'm/FindEmployeeDetailsByUserName';
+ this.getUserRoleMappedUrl = this.adminBaseUrl + 'getUserRoleMapped';
+ this.getStatesUrl = this.commonBaseUrl + 'location/states/';
+ this.getDistrictsUrl = this.commonBaseUrl + 'location/districts/';
+ this.getBlocksUrl = this.commonBaseUrl + 'location/taluks/';
+ this.getVillagesUrl = this.commonBaseUrl + 'location/village/';
+ this.getFacilitiesByBlockUrl = this.adminBaseUrl + 'getFacilitiesByBlock';
+ this.getFacilityVillagesUrl =
+ this.adminBaseUrl + 'getVillageMappingsByFacility';
+ this.updateVillageUrl = this.adminBaseUrl + 'villageMapping/updateVillage';
+ }
+
+ getUserList(serviceProviderID: any): Observable {
+ return this.http.post(this.getUserListUrl, {
+ serviceProviderID: serviceProviderID,
+ });
+ }
+
+ getUserDetail(userName: any): Observable {
+ return this.http.post(this.getUserDetailUrl, { userName: userName });
+ }
+
+ getUserRoleMapped(serviceProviderID: any): Observable {
+ return this.http.post(this.getUserRoleMappedUrl, {
+ serviceProviderID: serviceProviderID,
+ });
+ }
+
+ getStates(countryID: any): Observable {
+ return this.http.get(this.getStatesUrl + countryID);
+ }
+
+ getDistricts(stateID: any): Observable {
+ return this.http.get(this.getDistrictsUrl + stateID);
+ }
+
+ getBlocks(districtID: any): Observable {
+ return this.http.get(this.getBlocksUrl + districtID);
+ }
+
+ getVillages(blockID: any): Observable {
+ return this.http.get(this.getVillagesUrl + blockID);
+ }
+
+ getFacilitiesByBlock(blockID: any): Observable {
+ return this.http.post(this.getFacilitiesByBlockUrl, { blockID: blockID });
+ }
+
+ getFacilityVillages(facilityID: any): Observable {
+ return this.http.post(this.getFacilityVillagesUrl, {
+ facilityID: facilityID,
+ });
+ }
+
+ updateVillage(updateObject: any): Observable {
+ return this.http.post(this.updateVillageUrl, updateObject);
+ }
+}