diff --git a/pom.xml b/pom.xml
index c6b72b8..dbb8e47 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0
com.iemr.admin-ui
admin-ui
- 3.8.2
+ 3.8.4
Admin-UI
Piramal - admin: Module ui
diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.spec.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.spec.ts
index 9f935f4..ad75c8d 100644
--- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.spec.ts
+++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.spec.ts
@@ -41,6 +41,10 @@ const FakeWorkLocationMapping = {
getServices: (_userID: any) => of({ data: [] }),
getMappedWorkLocationList: (_serviceProviderID: any) => of({ data: [] }),
getUserName: (_serviceProviderID: any) => of({ data: [] }),
+ SaveWorkLocationMapping: (_data: any) => of({}),
+ UpdateWorkLocationMapping: (_data: any) => of({}),
+ DeleteWorkLocationMapping: (_data: any) => of({}),
+ DeleteWorkLocationMappingForTM: (_data: any) => of({}),
};
const FakeVillageMasterService = {};
@@ -192,4 +196,91 @@ describe('WorkLocationMappingComponent', () => {
expect(saved.villageName).toEqual(['Alamkeri']);
});
});
+
+ // Regression coverage for the StopTB "Counsellor/Nurse silently
+ // dropped on Update" bug: (1) existingRoleIDs/newRoleIDs must compare
+ // equal regardless of which side is a numeric string, and (2) every
+ // newly-added role must go out in one SaveWorkLocationMapping call,
+ // not one racing call per role.
+ describe('updateGroupedWorkLocation — role add/keep/remove diffing', () => {
+ initTestBed();
+
+ const baseGroup = (roles: any[]): any => ({
+ userID: 4426,
+ serviceID: 11,
+ serviceName: 'Stop TB',
+ roles,
+ });
+
+ beforeEach(() => {
+ component.RolesList = [
+ { roleID: 168, roleName: 'Registration Officer' },
+ { roleID: 169, roleName: 'Nurse' },
+ { roleID: 170, roleName: 'Counsellor' },
+ ];
+ component.userID_duringEdit = 4426;
+ component.providerServiceMapID_duringEdit = 1734;
+ component.stateID_duringEdit = 2;
+ component.district_duringEdit = 1;
+ component.workLocationID_duringEdit = null;
+ component.isFacilityServicelineEdit = false;
+ component.isStopTBServicelineEdit = false;
+ component.editIsAshaSupervisor = false;
+ component.selectedNikshayTUs = [];
+ component.selectedNikshayFacilities = [];
+ component.selectedNikshayVillages = [];
+ component.selectedNikshayBlock = null;
+ component.serviceEditvillage = [];
+ component.editVillageArr = [];
+ });
+
+ it('batches two newly-added roles (Nurse + Counsellor) into a single SaveWorkLocationMapping call', () => {
+ const service = TestBed.inject(WorkLocationMapping);
+ const saveSpy = spyOn(service, 'SaveWorkLocationMapping').and.returnValue(
+ of({}),
+ );
+
+ component.editGroupedElement = baseGroup([]);
+ component.roleIDs_duringEdit = [169, 170];
+
+ component.updateGroupedWorkLocation({});
+
+ expect(saveSpy).toHaveBeenCalledTimes(1);
+ const [payload] = saveSpy.calls.mostRecent().args;
+ const sentRoleIDs = payload[0].previleges[0].ID.map((r: any) => r.roleID);
+ expect(sentRoleIDs).toEqual([169, 170]);
+ });
+
+ it('treats a numeric-string existing roleID as equal to a numeric selected roleID (no spurious add+remove)', () => {
+ const service = TestBed.inject(WorkLocationMapping);
+ const saveSpy = spyOn(service, 'SaveWorkLocationMapping').and.returnValue(
+ of({}),
+ );
+ const updateSpy = spyOn(
+ service,
+ 'UpdateWorkLocationMapping',
+ ).and.returnValue(of({}));
+ const deleteSpy = spyOn(
+ service,
+ 'DeleteWorkLocationMapping',
+ ).and.returnValue(of({}));
+
+ // Registration Officer's roleID arrives as a string from the
+ // work-location list endpoint; the checkbox sends it back as a number.
+ component.editGroupedElement = baseGroup([
+ {
+ roleID: '168' as any,
+ uSRMappingID: 5907,
+ userServciceRoleDeleted: false,
+ },
+ ]);
+ component.roleIDs_duringEdit = [168];
+
+ component.updateGroupedWorkLocation({});
+
+ expect(updateSpy).toHaveBeenCalledTimes(1);
+ expect(saveSpy).not.toHaveBeenCalled();
+ expect(deleteSpy).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts
index 8113a2f..11445fb 100644
--- a/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts
+++ b/src/app/app-provider-admin/provider-admin/activities/work-location-mapping/work-location-mapping.component.ts
@@ -1072,10 +1072,15 @@ export class WorkLocationMappingComponent
const groupMap = new Map();
for (const row of flatRows) {
- // Group rows for HWC/FLW service lines (including ASHA Supervisor) by user+block
- // Other services get one row per entry
+ // Group rows for HWC/FLW (including ASHA Supervisor) and Stop TB by
+ // user+block (Stop TB's "block" is the Nikshay TU — see blockIDToUse
+ // in updateGroupedWorkLocation) so multiple roles for the same
+ // user+location show as one row with multiple role chips, instead of
+ // a separate row per role. Other services still get one row per entry.
const isFacilityService =
- row.serviceName === 'HWC' || row.serviceName === 'FLW';
+ row.serviceName === 'HWC' ||
+ row.serviceName === 'FLW' ||
+ row.serviceName === 'Stop TB';
let key: string;
if (isFacilityService) {
key = [
@@ -5321,10 +5326,12 @@ export class WorkLocationMappingComponent
...new Set(
group.roles
.filter((r) => !r.userServciceRoleDeleted)
- .map((r) => r.roleID),
+ .map((r) => Number(r.roleID)),
),
];
- const newRoleIDs: number[] = this.roleIDs_duringEdit;
+ const newRoleIDs: number[] = (this.roleIDs_duringEdit || []).map(
+ (rid: any) => Number(rid),
+ );
const rolesToAdd = newRoleIDs.filter(
(rid) => !existingRoleIDs.includes(rid),
@@ -5408,51 +5415,59 @@ export class WorkLocationMappingComponent
? nikshayFacilityIDArr.join(',')
: null;
+ // Shared by "keep" (role stays checked) and "reactivate" (role was
+ // unchecked before, is checked again now) — both are an in-place
+ // update of an existing uSRMappingID, refreshed to the edit form's
+ // current location/role fields.
+ const buildRoleUpdateObj = (rid: number, uSRMappingID: any) => {
+ const roleName = this.getRoleNameById(rid);
+ const updateObj: any = {
+ uSRMappingID,
+ userID: this.userID_duringEdit,
+ roleID: rid,
+ providerServiceMapID: this.providerServiceMapID_duringEdit,
+ workingLocationID: this.isFacilityServicelineEdit
+ ? null
+ : this.workLocationID_duringEdit,
+ stateID: this.stateID_duringEdit,
+ districtID: this.district_duringEdit,
+ blockID: blockIDToUse,
+ blockName: blockNameToUse,
+ villageID: villageIDToUse,
+ villageName: villageNameToUse,
+ modifiedBy: this.createdBy,
+ };
+ if (this.isStopTBServicelineEdit) {
+ updateObj.nikshayTUID = nikshayTUIDToSend;
+ updateObj.nikshayFacilityID = nikshayFacilityIDToSend;
+ }
+ if (group.serviceName === '1097') {
+ updateObj.inbound =
+ roleName?.toLowerCase() === 'supervisor' ? false : this.isInboundEdit;
+ updateObj.outbound =
+ roleName?.toLowerCase() === 'supervisor'
+ ? false
+ : this.isOutboundEdit;
+ }
+ if (group.serviceName === 'HWC') {
+ updateObj.teleConsultation = this.teleConsultationEdit;
+ }
+ if (this.isFacilityServicelineEdit && this.editFacilityMappingData) {
+ updateObj.facilityID = this.editFacilityMappingData.facilityID;
+ }
+ return updateObj;
+ };
+
// UPDATE existing rows for kept roles
for (const rid of rolesToKeep) {
const existingEntry = group.roles.find(
(r) => r.roleID === rid && !r.userServciceRoleDeleted,
);
if (existingEntry) {
- const roleName = this.getRoleNameById(rid);
- const updateObj: any = {
- uSRMappingID: existingEntry.uSRMappingID,
- userID: this.userID_duringEdit,
- roleID: rid,
- providerServiceMapID: this.providerServiceMapID_duringEdit,
- workingLocationID: this.isFacilityServicelineEdit
- ? null
- : this.workLocationID_duringEdit,
- stateID: this.stateID_duringEdit,
- districtID: this.district_duringEdit,
- blockID: blockIDToUse,
- blockName: blockNameToUse,
- villageID: villageIDToUse,
- villageName: villageNameToUse,
- modifiedBy: this.createdBy,
- };
- if (this.isStopTBServicelineEdit) {
- updateObj.nikshayTUID = nikshayTUIDToSend;
- updateObj.nikshayFacilityID = nikshayFacilityIDToSend;
- }
- if (group.serviceName === '1097') {
- updateObj.inbound =
- roleName?.toLowerCase() === 'supervisor'
- ? false
- : this.isInboundEdit;
- updateObj.outbound =
- roleName?.toLowerCase() === 'supervisor'
- ? false
- : this.isOutboundEdit;
- }
- if (group.serviceName === 'HWC') {
- updateObj.teleConsultation = this.teleConsultationEdit;
- }
- if (this.isFacilityServicelineEdit && this.editFacilityMappingData) {
- updateObj.facilityID = this.editFacilityMappingData.facilityID;
- }
allRequests.push(
- this.worklocationmapping.UpdateWorkLocationMapping(updateObj),
+ this.worklocationmapping.UpdateWorkLocationMapping(
+ buildRoleUpdateObj(rid, existingEntry.uSRMappingID),
+ ),
);
}
}
@@ -5475,14 +5490,61 @@ export class WorkLocationMappingComponent
}
}
- // CREATE new rows for added roles
- for (const rid of rolesToAdd) {
- const roleName = this.getRoleNameById(rid);
+ // A checked role with no active row may still have an old soft-deleted
+ // row from a previous remove (group.roles keeps deleted entries too —
+ // only existingRoleIDs filters them out). Reactivate that row in place
+ // (same {uSRMappingID, deleted:false} call the Activate button already
+ // uses) instead of creating a duplicate — otherwise every remove-then-
+ // re-add cycle leaves another dead row behind for the same role.
+ const rolesToReactivate = rolesToAdd.filter((rid) =>
+ group.roles.some((r) => r.roleID === rid && r.userServciceRoleDeleted),
+ );
+ const rolesToCreateFresh = rolesToAdd.filter(
+ (rid) => !rolesToReactivate.includes(rid),
+ );
+
+ for (const rid of rolesToReactivate) {
+ const deadEntry = group.roles.find(
+ (r) => r.roleID === rid && r.userServciceRoleDeleted,
+ );
+ if (deadEntry) {
+ const reactivateObj = {
+ uSRMappingID: deadEntry.uSRMappingID,
+ deleted: false,
+ };
+ allRequests.push(
+ group.serviceID === 4
+ ? this.worklocationmapping.DeleteWorkLocationMappingForTM(
+ reactivateObj,
+ )
+ : this.worklocationmapping.DeleteWorkLocationMapping(reactivateObj),
+ );
+ // Reactivating only flips the deleted flag — refresh its location/
+ // role fields to what the edit form currently shows, same as a kept role.
+ allRequests.push(
+ this.worklocationmapping.UpdateWorkLocationMapping(
+ buildRoleUpdateObj(rid, deadEntry.uSRMappingID),
+ ),
+ );
+ }
+ }
+
+ // CREATE new rows for roles that never existed for this user+scope.
+ // Batched into a single SaveWorkLocationMapping call (one previleges
+ // entry, ID array holding every added role) instead of one call per
+ // role. Firing them separately let the backend treat each create as
+ // the sole active role for this user+serviceline+block, so parallel
+ // creates raced and only the last commit stayed active — dropping
+ // roles added alongside others (reproduced with StopTB Counsellor/
+ // Nurse). Location fields (incl. StopTB's nikshayTUID/nikshayFacilityID)
+ // are per-group, not per-role, so batching them here is unchanged.
+ if (rolesToCreateFresh.length > 0) {
const newObj: any = {
previleges: [
{
- ID: [
- {
+ ID: rolesToCreateFresh.map((rid) => {
+ const roleName = this.getRoleNameById(rid);
+ return {
roleID: rid,
teleConsultation:
group.serviceName === 'HWC' &&
@@ -5500,8 +5562,8 @@ export class WorkLocationMappingComponent
roleName?.toLowerCase() !== 'supervisor'
? this.isOutboundEdit
: null,
- },
- ],
+ };
+ }),
providerServiceMapID: this.providerServiceMapID_duringEdit,
workingLocationID: this.isFacilityServicelineEdit
? null