- New "Pending Client Edits" screen under Checker Inbox & Tasks: server-driven pagination,
date range + Today/Yesterday/All presets, status filter (Pending/Approved/Rejected), search —
mirrors ChequebookRequestsComponent's architecture.
- Compare dialog diffs the maker's proposed values against a point-in-time "before" snapshot
(client-edit-diff.util.ts) captured when the edit page was opened, so only genuinely changed
fields show — works correctly for Pending, Approved, and Rejected edits alike, since it no
longer depends on comparing against the live (possibly already-updated) client record.
- edit-client.component.ts: General tab save now submits for approval instead of applying
immediately, shows a pending-approval banner, and blocks a second submission while one's
outstanding.
- error-handler.interceptor.ts: added the new /clients/{id}/edit/pending check to the existing
"expected 403/404, don't alert" allowlist (same pattern already used for client images/KYC).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
/**
|
|
* Copyright since 2025 Mifos Initiative
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
/**
|
|
* Shared between the maker side (edit-client, which captures a point-in-time snapshot of the
|
|
* client's current values right before staging an edit) and the checker side (pending-client-edits,
|
|
* which diffs the staged payload against that snapshot). Keeping both in one place means the two
|
|
* sides can never drift out of sync on which fields are captured/compared/labelled.
|
|
*
|
|
* Extracts a payload key's CURRENT value from a client record (as returned by GET /clients/{id}) —
|
|
* CodeValue fields come back as nested {id, name} objects, so most extractors pull the nested .id
|
|
* to compare against the payload's raw id.
|
|
*/
|
|
export const PENDING_EDIT_FIELD_EXTRACTORS: Record<string, (c: any) => any> = {
|
|
officeId: (c) => c.officeId,
|
|
legalFormId: (c) => c.legalForm?.id,
|
|
nationalityId: (c) => c.nationality,
|
|
isStaff: (c) => c.isStaff,
|
|
active: (c) => c.active,
|
|
accountNo: (c) => c.accountNo,
|
|
salutationId: (c) => c.salutation?.id,
|
|
firstname: (c) => c.firstname,
|
|
middlename: (c) => c.middlename,
|
|
lastname: (c) => c.lastname,
|
|
fullname: (c) => c.fullname,
|
|
genderId: (c) => c.gender?.id,
|
|
maritalStatusId: (c) => c.maritalStatus?.id,
|
|
educationLevelId: (c) => c.educationLevel?.id,
|
|
degreeQualificationId: (c) => c.degreeQualification?.id,
|
|
religionId: (c) => c.religion?.id,
|
|
socialCategoryId: (c) => c.socialCategory?.id,
|
|
mobileNo: (c) => c.mobileNo,
|
|
alternateMobileNo: (c) => c.alternateMobileNo,
|
|
emailAddress: (c) => c.emailAddress,
|
|
dateOfBirth: (c) => c.dateOfBirth,
|
|
occupationCategoryId: (c) => c.occupationCategory?.id,
|
|
occupationId: (c) => c.occupation?.id,
|
|
monthlyIncome: (c) => c.monthlyIncome,
|
|
annualIncome: (c) => c.annualIncome,
|
|
annualTurnover: (c) => c.annualTurnover,
|
|
monthlyTurnover: (c) => c.monthlyTurnover,
|
|
isPwd: (c) => c.isPwd,
|
|
pwdCategoryId: (c) => c.pwdCategory?.id,
|
|
isSeniorCitizen: (c) => c.isSeniorCitizen,
|
|
agricultureIncome: (c) => c.agricultureIncome,
|
|
nonAgricultureIncome: (c) => c.nonAgricultureIncome,
|
|
savingsProductId: (c) => c.savingsProductId,
|
|
staffId: (c) => c.staffId,
|
|
guardianClientId: (c) => c.guardianClientId,
|
|
guardianRelation: (c) => c.guardianRelation
|
|
};
|
|
|
|
/** Payload keys that are computed, internal, or otherwise never meaningfully "changed" from a
|
|
* checker's point of view — always sent by the form (age breakdown, OTP tracking ids, the
|
|
* business-date default on submittedOnDate) but with no comparable "current" value. */
|
|
export const NON_COMPARABLE_KEYS = new Set([
|
|
'dateFormat',
|
|
'locale',
|
|
'ageYears',
|
|
'ageMonths',
|
|
'ageDays',
|
|
'isMinor',
|
|
'addSavings',
|
|
'submittedOnDate',
|
|
'activationDate',
|
|
// Flattened onto top-level guardianClientId/guardianRelation right after this in the payload —
|
|
// that flattened pair already has real comparators above, so the nested group is redundant.
|
|
'guardianDetails',
|
|
// Mobile/email DIP verification + name-match + sanctions-screening results — carried along on
|
|
// every save to re-affirm the client's EXISTING verification state, not something the maker
|
|
// edited on this pass. dipTimestamp especially would almost never string-match exactly even
|
|
// when functionally unchanged, so these were never real "proposed changes" to review.
|
|
'dipVerified',
|
|
'dipLookupLogId',
|
|
'dipTimestamp',
|
|
'alternateDipVerified',
|
|
'alternateDipLookupLogId',
|
|
'alternateDipTimestamp',
|
|
'nameVerified',
|
|
'screeningLogId',
|
|
'mobileNewOtpId',
|
|
'mobileOldOtpId',
|
|
'emailNewOtpId',
|
|
'emailOldOtpId'
|
|
]);
|
|
|
|
export const PENDING_EDIT_FIELD_LABELS: Record<string, string> = {
|
|
officeId: 'Office',
|
|
legalFormId: 'Legal Form',
|
|
nationalityId: 'Nationality',
|
|
isStaff: 'Is Staff',
|
|
active: 'Active',
|
|
accountNo: 'Account Number',
|
|
salutationId: 'Salutation',
|
|
firstname: 'First Name',
|
|
middlename: 'Middle Name',
|
|
lastname: 'Last Name',
|
|
fullname: 'Full Name',
|
|
genderId: 'Gender',
|
|
maritalStatusId: 'Marital Status',
|
|
educationLevelId: 'Education Level',
|
|
degreeQualificationId: 'Degree / Qualification',
|
|
religionId: 'Religion',
|
|
socialCategoryId: 'Social Category',
|
|
mobileNo: 'Mobile Number',
|
|
alternateMobileNo: 'Alternate Mobile Number',
|
|
emailAddress: 'Email Address',
|
|
dateOfBirth: 'Date of Birth',
|
|
occupationCategoryId: 'Occupation Category',
|
|
occupationId: 'Occupation',
|
|
monthlyIncome: 'Monthly Income',
|
|
annualIncome: 'Annual Income',
|
|
annualTurnover: 'Yearly Turnover',
|
|
monthlyTurnover: 'Monthly Turnover',
|
|
isPwd: 'Person with Disability',
|
|
pwdCategoryId: 'Disability Type',
|
|
isSeniorCitizen: 'Senior Citizen',
|
|
agricultureIncome: 'Agriculture Income',
|
|
nonAgricultureIncome: 'Non-Agriculture Income',
|
|
submittedOnDate: 'Submitted On',
|
|
savingsProductId: 'Linked Savings Product',
|
|
staffId: 'Staff'
|
|
};
|
|
|
|
/** Key under which the maker's point-in-time "before" snapshot rides along in the submit payload —
|
|
* stripped out by the backend before replaying the rest through updateClient(), and stored
|
|
* separately as original_json for the checker's diff. Never a real Client field name, so it can't
|
|
* collide with anything ClientDataValidator actually expects. */
|
|
export const ORIGINAL_VALUES_PAYLOAD_KEY = '_originalValues';
|
|
|
|
/** Builds the maker-side "before" snapshot from the client record loaded when the edit page
|
|
* opened — flat key names matching the payload's own, so the checker side can diff two plain
|
|
* objects directly without needing to know Client's nested response shape at all. */
|
|
export function buildOriginalValuesSnapshot(clientData: any): Record<string, any> {
|
|
const snapshot: Record<string, any> = {};
|
|
if (!clientData) return snapshot;
|
|
for (const key of Object.keys(PENDING_EDIT_FIELD_EXTRACTORS)) {
|
|
const value = PENDING_EDIT_FIELD_EXTRACTORS[key](clientData);
|
|
if (value !== undefined) {
|
|
snapshot[key] = value;
|
|
}
|
|
}
|
|
return snapshot;
|
|
}
|