feat: KYC document update, FIU & AML recurring alerts integration, Sponsor DD revoke workflow, and TDS reports
This commit is contained in:
@@ -240,45 +240,83 @@ export class DdIssueApprovalsComponent implements OnInit, AfterViewInit {
|
||||
this.approveBatch(request);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`Approve DD issuance for DD No. ${request.ddNumber} (₹${request.faceValue})? This will debit the account (or clear the cheque) and issue the draft.`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
this.dialog
|
||||
.open(ApproveIssueRequestDialogComponent, {
|
||||
width: '440px',
|
||||
maxWidth: '95vw',
|
||||
data: {
|
||||
isBatch: false,
|
||||
ddNumber: request.ddNumber,
|
||||
beneficiaryName: request.beneficiaryName,
|
||||
clientName: request.clientName,
|
||||
totalFaceValue: Number(request.faceValue) || 0,
|
||||
// Named for how this particular draft is funded, rather than offering the approver every
|
||||
// possibility and leaving them to work out which one applies.
|
||||
effect: this.effectTextFor(request)
|
||||
}
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.sponsorDDService.approveIssueRequest(request.id).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('DD issuance approved and posted successfully', 'Close', { duration: 3000 });
|
||||
this.loadRequests();
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 6000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** What approving this request actually does, in the words of its own payment method. */
|
||||
private effectTextFor(request: any, count = 1): string {
|
||||
const drafts = count > 1 ? `${count} drafts` : 'the draft';
|
||||
const once = count > 1 ? ' once for the combined total' : '';
|
||||
switch ((request.paymentMethod || '').toUpperCase()) {
|
||||
case 'ACCOUNT_DEBIT':
|
||||
return `The customer's account is debited${once} and ${drafts} ${count > 1 ? 'are' : 'is'} issued. This cannot be undone — a draft issued in error has to be cancelled separately.`;
|
||||
case 'CHEQUE':
|
||||
return `The cheque is marked cleared${once} and ${drafts} ${count > 1 ? 'are' : 'is'} issued. This cannot be undone — a draft issued in error has to be cancelled separately.`;
|
||||
default:
|
||||
return `The cash collected is posted${once} and ${drafts} ${count > 1 ? 'are' : 'is'} issued. This cannot be undone — a draft issued in error has to be cancelled separately.`;
|
||||
}
|
||||
this.sponsorDDService.approveIssueRequest(request.id).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('DD issuance approved and posted successfully', 'Close', { duration: 3000 });
|
||||
this.loadRequests();
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 6000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private approveBatch(request: any): void {
|
||||
const siblings = this.batchSiblings(request);
|
||||
const lines = siblings.map((r) => ` • ${r.beneficiaryName} — ₹${r.faceValue}`).join('\n');
|
||||
const total = siblings.reduce((sum, r) => sum + (Number(r.faceValue) || 0), 0);
|
||||
if (
|
||||
!confirm(
|
||||
`Approve this DD slip - ${siblings.length} drafts, combined face value ₹${total}?\n${lines}\n\n` +
|
||||
'This debits the account (or clears the cheque) ONCE for the combined total and issues every draft in the slip.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.sponsorDDService.approveIssueRequestBatch(request.batchId).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('DD slip approved and posted successfully', 'Close', { duration: 3000 });
|
||||
this.loadRequests();
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 6000 });
|
||||
}
|
||||
});
|
||||
|
||||
this.dialog
|
||||
.open(ApproveIssueRequestDialogComponent, {
|
||||
width: '460px',
|
||||
maxWidth: '95vw',
|
||||
data: {
|
||||
isBatch: true,
|
||||
clientName: request.clientName,
|
||||
totalFaceValue: total,
|
||||
lines: siblings.map((r) => ({ beneficiaryName: r.beneficiaryName, faceValue: Number(r.faceValue) || 0 })),
|
||||
effect: this.effectTextFor(request, siblings.length)
|
||||
}
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.sponsorDDService.approveIssueRequestBatch(request.batchId).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('DD slip approved and posted successfully', 'Close', { duration: 3000 });
|
||||
this.loadRequests();
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 6000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
openRejectDialog(request: any): void {
|
||||
@@ -302,6 +340,123 @@ export class DdIssueApprovalsComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What the approver is about to commit, laid out rather than run together in a sentence.
|
||||
*
|
||||
* This was a browser `confirm()`, which the browser renders with its own "localhost:4200 says"
|
||||
* chrome and no formatting — so the one figure that matters, the amount leaving the customer's
|
||||
* account, sat mid-paragraph beside a parenthetical about cheques that does not apply to most
|
||||
* drafts. An approval is the point of no return on someone else's money; it should read like one.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-approve-issue-request-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatDialogModule,
|
||||
MatButtonModule
|
||||
],
|
||||
template: `
|
||||
<h2 mat-dialog-title class="approve-title">
|
||||
{{ data.isBatch ? 'Approve DD Slip' : 'Approve DD Issuance' }}
|
||||
</h2>
|
||||
<mat-dialog-content>
|
||||
<div class="approve-summary">
|
||||
<div class="approve-row">
|
||||
<span class="approve-label">{{ data.isBatch ? 'Drafts' : 'DD Number' }}</span>
|
||||
<strong class="approve-value">{{ data.isBatch ? data.lines.length + ' drafts' : data.ddNumber }}</strong>
|
||||
</div>
|
||||
<div class="approve-row" *ngIf="!data.isBatch">
|
||||
<span class="approve-label">In favour of</span>
|
||||
<strong class="approve-value">{{ data.beneficiaryName || '—' }}</strong>
|
||||
</div>
|
||||
<div class="approve-row" *ngIf="data.clientName">
|
||||
<span class="approve-label">Purchaser</span>
|
||||
<strong class="approve-value">{{ data.clientName }}</strong>
|
||||
</div>
|
||||
<div class="approve-row approve-row--total">
|
||||
<span class="approve-label">{{ data.isBatch ? 'Combined face value' : 'Face value' }}</span>
|
||||
<strong class="approve-amount">₹{{ data.totalFaceValue | number: '1.2-2' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- A slip is one debit for every draft on it, which is the part an approver is most likely
|
||||
to get wrong if it is not said plainly. -->
|
||||
<ul class="approve-lines" *ngIf="data.isBatch">
|
||||
<li *ngFor="let l of data.lines">{{ l.beneficiaryName }} — ₹{{ l.faceValue | number: '1.2-2' }}</li>
|
||||
</ul>
|
||||
|
||||
<p class="approve-effect">
|
||||
{{ data.effect }}
|
||||
</p>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-stroked-button type="button" (click)="dialogRef.close(false)">Cancel</button>
|
||||
<button mat-flat-button color="primary" type="button" (click)="dialogRef.close(true)">
|
||||
{{ data.isBatch ? 'Approve Slip' : 'Approve & Issue' }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.approve-title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.approve-summary {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.approve-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
font-size: 13px;
|
||||
}
|
||||
.approve-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.approve-label {
|
||||
color: #64748b;
|
||||
}
|
||||
.approve-value {
|
||||
text-align: right;
|
||||
}
|
||||
.approve-amount {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.approve-lines {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 20px;
|
||||
font-size: 12.5px;
|
||||
color: #334155;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.approve-effect {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
export class ApproveIssueRequestDialogComponent {
|
||||
dialogRef = inject(MatDialogRef<ApproveIssueRequestDialogComponent>);
|
||||
data = inject(MAT_DIALOG_DATA);
|
||||
}
|
||||
|
||||
// ── REJECT DIALOG COMPONENT ──
|
||||
@Component({
|
||||
selector: 'mifosx-reject-issue-request-dialog',
|
||||
|
||||
Reference in New Issue
Block a user