feat: KYC document update, FIU & AML recurring alerts integration, Sponsor DD revoke workflow, and TDS reports
This commit is contained in:
@@ -209,6 +209,31 @@
|
||||
<span class="pending-badge" *ngIf="element.status === 'ISSUED' && element.pendingReprintRequestId">
|
||||
Reprint Pending Approval (Req #{{ element.pendingReprintRequestId }})
|
||||
</span>
|
||||
|
||||
<!-- The leaflet is ruined and cannot be handed over. Offered beside Request Reprint
|
||||
because the two look alike from the counter: reprint when the paper survived, revoke
|
||||
when it did not. -->
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
size="small"
|
||||
*ngIf="element.status === 'ISSUED' && !element.pendingRevokeRequestId"
|
||||
(click)="openRevokeDialog(element)"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
<span class="pending-badge" *ngIf="element.status === 'ISSUED' && element.pendingRevokeRequestId">
|
||||
Revoke Pending Approval (Req #{{ element.pendingRevokeRequestId }})
|
||||
</span>
|
||||
|
||||
<!-- Both ends of a replacement, so the pad reconciles: a revoked leaflet says where its
|
||||
draft went, and the blank that took it says what it stands in for. -->
|
||||
<span class="pending-badge" *ngIf="element.revokedReplacedByDdNumber">
|
||||
Revoked → replaced by DD {{ element.revokedReplacedByDdNumber }}
|
||||
</span>
|
||||
<span class="pending-badge" *ngIf="element.revokedReplacementOfDdNumber">
|
||||
Replaces spoiled DD {{ element.revokedReplacementOfDdNumber }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -142,6 +142,12 @@ import { DividendClaimRequestService } from '../../clerk/dividend-claim-request.
|
||||
background-color: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
/* Spoiled stock. Grey rather than red: nothing went wrong with the customer's draft, only
|
||||
with the paper it was printed on, and the draft itself is alive on another leaflet. */
|
||||
.status-badge.revoked {
|
||||
background-color: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
.action-btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -212,7 +218,10 @@ export class DdListComponent implements OnInit, AfterViewInit {
|
||||
'ISSUED',
|
||||
'CANCELLED',
|
||||
'STOPPED',
|
||||
'CLEARED'
|
||||
'CLEARED',
|
||||
// Spoiled at the printer. Filterable on its own because it is the count that explains a pad
|
||||
// whose issued drafts do not add up to the numbers consumed.
|
||||
'REVOKED'
|
||||
];
|
||||
selectedStatus = 'ALL';
|
||||
searchText = '';
|
||||
@@ -444,6 +453,23 @@ export class DdListComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** The leaflet was ruined at the printer and its draft has to move onto a fresh blank. */
|
||||
openRevokeDialog(leaflet: any): void {
|
||||
this.dialog
|
||||
.open(RevokeDdDialogComponent, {
|
||||
width: '480px',
|
||||
maxWidth: '95vw',
|
||||
data: { leaflet },
|
||||
disableClose: true
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((result) => {
|
||||
if (result) {
|
||||
this.loadLeaflets();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── ISSUE DRAFT DIALOG COMPONENT ──
|
||||
@@ -1655,6 +1681,114 @@ export class RequestReprintDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacing a leaflet ruined at the printer.
|
||||
*
|
||||
* Not a reprint: a reprint puts the same draft back through the printer on the same paper, which is
|
||||
* right when the print jammed but the leaflet survived. Here the leaflet itself is spoiled — its
|
||||
* printed number can never be handed to anyone — so the draft moves onto the next blank and both
|
||||
* leaflets stay on the register, or the pad comes up short against the sponsor bank's range.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-revoke-dd-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule
|
||||
],
|
||||
template: `
|
||||
<h2 mat-dialog-title>Revoke Spoiled DD (DD No: {{ data.leaflet.ddNumber }})</h2>
|
||||
<mat-dialog-content>
|
||||
<p class="hint-text">
|
||||
Use this when the leaflet itself is ruined — misprinted, torn or jammed — and cannot be given to the customer.
|
||||
On approval this leaflet is marked <strong>REVOKED</strong> and the draft is re-issued on the next available
|
||||
blank.
|
||||
</p>
|
||||
<p class="hint-text">
|
||||
No money moves. {{ data.leaflet.clientName || data.leaflet.walkinPurchaserName || 'The purchaser' }} has already
|
||||
paid for this draft and is still owed the same one.
|
||||
</p>
|
||||
<form [formGroup]="revokeForm" class="dialog-form">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>What went wrong with this leaflet?</mat-label>
|
||||
<textarea matInput formControlName="reason" rows="3"></textarea>
|
||||
<mat-error *ngIf="revokeForm.get('reason')?.hasError('required')">Reason is required</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
<button mat-flat-button color="warn" [disabled]="revokeForm.invalid || loading" (click)="onSubmit()">
|
||||
Submit for Approval
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.dialog-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.hint-text {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin: 0 0 10px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
export class RevokeDdDialogComponent implements OnInit {
|
||||
private fb = inject(FormBuilder);
|
||||
private sponsorDDService = inject(SponsorDDService);
|
||||
private dialogRef = inject(MatDialogRef<RevokeDdDialogComponent>);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
protected data = inject(MAT_DIALOG_DATA);
|
||||
|
||||
revokeForm!: FormGroup;
|
||||
loading = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.revokeForm = this.fb.group({
|
||||
reason: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.revokeForm.invalid) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.sponsorDDService.requestRevoke(this.data.leaflet.id, this.revokeForm.value.reason).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Revoke request submitted for approval', 'Close', { duration: 3000 });
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (err) => {
|
||||
this.loading = false;
|
||||
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 6000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── PRINT DRAFT DIALOG COMPONENT ──
|
||||
@Component({
|
||||
selector: 'mifosx-print-draft-dialog',
|
||||
|
||||
Reference in New Issue
Block a user