Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.iemr.admin-ui</groupId>
<artifactId>admin-ui</artifactId>
<version>3.8.2</version>
<version>3.8.4</version>
<name>Admin-UI</name>
<description>Piramal - admin: Module ui</description>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1072,10 +1072,15 @@ export class WorkLocationMappingComponent
const groupMap = new Map<string, GroupedWorkLocation>();

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 = [
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
),
);
}
}
Expand All @@ -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' &&
Expand All @@ -5500,8 +5562,8 @@ export class WorkLocationMappingComponent
roleName?.toLowerCase() !== 'supervisor'
? this.isOutboundEdit
: null,
},
],
};
}),
providerServiceMapID: this.providerServiceMapID_duringEdit,
workingLocationID: this.isFacilityServicelineEdit
? null
Expand Down
Loading