feat: KYC document update, FIU & AML recurring alerts integration, Sponsor DD revoke workflow, and TDS reports
This commit is contained in:
@@ -57,6 +57,27 @@ interface TxnRow {
|
||||
* account — governs the share redemption (see submitShareRedemptionTransfer()). */
|
||||
resolutionNumber?: string;
|
||||
meetingDate?: string;
|
||||
/**
|
||||
* Beneficiary and leaflet captured in the DD dialog, held on the row until Save.
|
||||
*
|
||||
* The dialog used to submit the request itself, which left the worksheet's own Save button doing
|
||||
* nothing for a DD row — the clerk filled the dialog, the request was already gone, and pressing
|
||||
* Save afterwards had no effect. Filling in details and committing them are two different
|
||||
* actions, so the dialog now only records what was entered and Save is what sends it.
|
||||
*/
|
||||
ddDetails?: {
|
||||
leafletId: number;
|
||||
ddNumber: string;
|
||||
beneficiaryName: string;
|
||||
payableAtBranch: string;
|
||||
payableAtCity: string;
|
||||
/** Present only for a walk-in cash draft. */
|
||||
purchaser?: { name: string; address: string; aadhaar: string };
|
||||
/** Note-and-coin breakdown, cash drafts only. */
|
||||
denominations?: { [key: string]: number };
|
||||
fundingAccountId?: number;
|
||||
fundingClientId?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Denomination {
|
||||
@@ -1671,6 +1692,33 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
'GL',
|
||||
'LA'
|
||||
];
|
||||
|
||||
/**
|
||||
* Account types a clerk may fund a demand draft from.
|
||||
*
|
||||
* A clerk raises a draft against a member's own balance, so the only sensible sources are the
|
||||
* accounts a member actually holds money in. GL was offered here too, and picking it quietly
|
||||
* switched the row onto the cash path — the one that asks for a walk-in purchaser's name and a
|
||||
* note-and-coin count, neither of which a clerk debiting an account has. The option is removed
|
||||
* rather than validated afterwards, so the wrong path cannot be reached at all.
|
||||
*/
|
||||
private static readonly DD_CLERK_ACTYPS = [
|
||||
'SD',
|
||||
'CA'
|
||||
];
|
||||
|
||||
/**
|
||||
* The account types this particular row may use.
|
||||
*
|
||||
* Every other row keeps the full list; only a clerk's DD row is narrowed, because that is the
|
||||
* only row where the account type silently decides which funding flow runs.
|
||||
*/
|
||||
acTypOptionsFor(row: TxnRow): string[] {
|
||||
if (this.isClerk && row.typ === 'DD') {
|
||||
return TransactionEntryComponent.DD_CLERK_ACTYPS;
|
||||
}
|
||||
return this.acTypOptions;
|
||||
}
|
||||
dbCrOptions = [
|
||||
'Dr',
|
||||
'Cr'
|
||||
@@ -2230,12 +2278,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
// panel never appears and the row would post as an ordinary entry with the charges silently
|
||||
// dropped.
|
||||
//
|
||||
// A clerk funds a draft through an account or an instrument instead, and their TXN TP. list no
|
||||
// longer offers cash, so defaulting to it would open the field on a value it cannot show.
|
||||
// isDdCashGlRow() still holds for them through AC.TP = GL.
|
||||
// A clerk funds a draft by debiting a member's own account, so their row opens on savings and
|
||||
// is debited, and GL is not on their AC.TP list at all (see acTypOptionsFor). Opening them on
|
||||
// GL put the row on the cash path, which asks for a walk-in purchaser and a note-and-coin
|
||||
// count that a clerk debiting an account does not have.
|
||||
case 'DD':
|
||||
row.dbCr = 'Cr';
|
||||
row.acTyp = 'GL';
|
||||
row.dbCr = this.isClerk ? 'Dr' : 'Cr';
|
||||
row.acTyp = this.isClerk ? 'SD' : 'GL';
|
||||
row.paymentTypeId = this.isClerk ? this.findTransferPaymentTypeId() : this.findCashPaymentTypeId();
|
||||
row.chequeNo = '';
|
||||
row.cheqDate = '';
|
||||
@@ -2416,13 +2465,34 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
// Recorded on the row, not sent. Save is what commits the worksheet, and a DD row that
|
||||
// submitted itself here left that button with nothing to do — so the clerk had no way to
|
||||
// review the entered details, and no way to abandon them either.
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (!result) return;
|
||||
if (accountDebit) {
|
||||
this.submitAccountDebitDd(row, leaflet, result, fundingAccountId!, fundingClientId!);
|
||||
} else {
|
||||
this.submitWalkinCashDd(row, leaflet, result);
|
||||
}
|
||||
row.ddDetails = {
|
||||
leafletId: leaflet.id,
|
||||
ddNumber: leaflet.ddNumber,
|
||||
beneficiaryName: result.beneficiaryName,
|
||||
payableAtBranch: result.payableAtBranch,
|
||||
payableAtCity: result.payableAtCity,
|
||||
purchaser: accountDebit
|
||||
? undefined
|
||||
: {
|
||||
name: result.walkinPurchaserName,
|
||||
address: result.walkinPurchaserAddress,
|
||||
aadhaar: result.walkinPurchaserAadhaar
|
||||
},
|
||||
fundingAccountId: accountDebit ? fundingAccountId! : undefined,
|
||||
fundingClientId: accountDebit ? fundingClientId! : undefined
|
||||
};
|
||||
this.showAlert('success', 'DD Details Captured', [
|
||||
`DD No. ${leaflet.ddNumber} · in favour of ${result.beneficiaryName}.`,
|
||||
accountDebit
|
||||
? `₹${this.ddTotalPayableFor(row)} will be debited from ${row.acNo} once a checker approves.`
|
||||
: `₹${this.ddTotalPayableFor(row)} to be collected at the counter.`,
|
||||
'Press Save to send this draft for approval.'
|
||||
]);
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -2480,6 +2550,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
row.amount = null;
|
||||
row.ddCharges = null;
|
||||
row.description = '';
|
||||
// Cleared with the row: leaving the captured details behind would let a second Save raise
|
||||
// the same draft again against a leaflet that has already been used.
|
||||
row.ddDetails = undefined;
|
||||
},
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
@@ -2488,6 +2561,53 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the drafts on the worksheet for approval.
|
||||
*
|
||||
* Each row carries the beneficiary details the dialog captured; a row that was never filled in is
|
||||
* refused by name rather than skipped, because a clerk who pressed Save expecting one draft and
|
||||
* got none has no way of telling which row was missing.
|
||||
*/
|
||||
private saveDdRows(ddRows: TxnRow[]): void {
|
||||
const unfilled = ddRows.filter((r) => !r.ddDetails);
|
||||
if (unfilled.length > 0) {
|
||||
this.showAlert('error', 'DD Details Not Entered', [
|
||||
unfilled.length === 1
|
||||
? 'One DD row has no beneficiary details yet.'
|
||||
: `${unfilled.length} DD rows have no beneficiary details yet.`,
|
||||
'Use "Fill DD Details & Send for Approval" on each DD row first, then press Save.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of ddRows) {
|
||||
const details = row.ddDetails!;
|
||||
const leaflet = { id: details.leafletId, ddNumber: details.ddNumber };
|
||||
if (this.isDdAccountDebitRow(row)) {
|
||||
this.submitAccountDebitDd(
|
||||
row,
|
||||
leaflet,
|
||||
{
|
||||
beneficiaryName: details.beneficiaryName,
|
||||
payableAtBranch: details.payableAtBranch,
|
||||
payableAtCity: details.payableAtCity
|
||||
},
|
||||
details.fundingAccountId!,
|
||||
details.fundingClientId!
|
||||
);
|
||||
} else {
|
||||
this.submitWalkinCashDd(row, leaflet, {
|
||||
beneficiaryName: details.beneficiaryName,
|
||||
payableAtBranch: details.payableAtBranch,
|
||||
payableAtCity: details.payableAtCity,
|
||||
walkinPurchaserName: details.purchaser?.name,
|
||||
walkinPurchaserAddress: details.purchaser?.address,
|
||||
walkinPurchaserAadhaar: details.purchaser?.aadhaar
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private submitWalkinCashDd(row: TxnRow, leaflet: any, purchaserData: any): void {
|
||||
// Read through countFor, which knows the map is keyed by kind:value. Indexing by d.id looked
|
||||
// right but matched nothing, so every cash DD went in with no breakdown at all and its money
|
||||
@@ -2557,6 +2677,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
row.amount = null;
|
||||
row.ddCharges = null;
|
||||
row.description = '';
|
||||
// See the account-debit path: a second Save must not re-raise a draft already issued.
|
||||
row.ddDetails = undefined;
|
||||
this.clearDenominations();
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -4361,6 +4483,15 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onSave(): void {
|
||||
// Demand drafts commit here like everything else on the worksheet. Checked first because a DD
|
||||
// row shares its shape with the ordinary entries below — without this it would fall through and
|
||||
// post as a plain transaction, with the draft never raised and the charges silently dropped.
|
||||
const ddRows = this.rows.filter((r) => this.isDdRow(r));
|
||||
if (ddRows.length > 0) {
|
||||
this.saveDdRows(ddRows);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clerk bank transfer — N DR savings/CA + N CR savings/CA, immediate execution. 'Fund Transfer'
|
||||
// is the same flow under a second TYPE label — onTypChange() already defaults it identically to
|
||||
// 'Enter Bank Transfer', so submission has to recognise both or a share redemption or bank
|
||||
|
||||
Reference in New Issue
Block a user