Add customer search dialog and sponsor DD/reconciliation modules
Adds sponsor-dd and sponsor-dd-reconciliation feature modules, a shared customer search dialog, and wires them into routing/shell/ organization screens alongside cashier and deposit-request updates.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="detail-container">
|
||||
<div class="header-row">
|
||||
<button
|
||||
mat-button
|
||||
routerLink="/sponsor-dd-reconciliation"
|
||||
style="margin-bottom: 8px; color: #64748b; padding-left: 0"
|
||||
>
|
||||
← Back to Batches
|
||||
</button>
|
||||
<h2 *ngIf="batchDetails">Reconciliation Batch: {{ batchDetails.batchReference }}</h2>
|
||||
<p *ngIf="batchDetails">
|
||||
File: <strong>{{ batchDetails.fileName }}</strong> | Status: <strong>{{ batchDetails.status }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="batch-summary" *ngIf="batchDetails">
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">Total Scroll Items</span>
|
||||
<span class="summary-value">{{ batchDetails.totalRecords }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">Total Amount</span>
|
||||
<span class="summary-value">₹{{ batchDetails.totalAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="summary-card" style="border-left: 4px solid #16a34a">
|
||||
<span class="summary-label" style="color: #16a34a">Auto Matched</span>
|
||||
<span class="summary-value" style="color: #16a34a">{{ batchDetails.successCount }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="summary-card"
|
||||
[style.border-left]="batchDetails.mismatchCount > 0 ? '4px solid #dc2626' : '4px solid #e2e8f0'"
|
||||
>
|
||||
<span class="summary-label" [style.color]="batchDetails.mismatchCount > 0 ? '#dc2626' : '#64748b'"
|
||||
>Exceptions</span
|
||||
>
|
||||
<span class="summary-value" [style.color]="batchDetails.mismatchCount > 0 ? '#dc2626' : '#334155'">{{
|
||||
batchDetails.mismatchCount
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-chips">
|
||||
<span style="font-size: 13px; font-weight: 500; color: #64748b">Filter Status:</span>
|
||||
<mat-chip-listbox>
|
||||
<mat-chip-option [selected]="selectedFilter === 'ALL'" (click)="applyFilter('ALL')">All</mat-chip-option>
|
||||
<mat-chip-option [selected]="selectedFilter === 'EXACT_MATCH'" (click)="applyFilter('EXACT_MATCH')"
|
||||
>Exact Match</mat-chip-option
|
||||
>
|
||||
<mat-chip-option [selected]="selectedFilter === 'AMOUNT_MISMATCH'" (click)="applyFilter('AMOUNT_MISMATCH')"
|
||||
>Amount Mismatch</mat-chip-option
|
||||
>
|
||||
<mat-chip-option [selected]="selectedFilter === 'UNADVISED_CLEARING'" (click)="applyFilter('UNADVISED_CLEARING')"
|
||||
>Unadvised Clearing</mat-chip-option
|
||||
>
|
||||
<mat-chip-option [selected]="selectedFilter === 'DUPLICATE_CLEARING'" (click)="applyFilter('DUPLICATE_CLEARING')"
|
||||
>Duplicate Clearing</mat-chip-option
|
||||
>
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
|
||||
<mat-card>
|
||||
<table mat-table [dataSource]="filteredRecords">
|
||||
<!-- DD Number Column -->
|
||||
<ng-container matColumnDef="ddNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>DD Number</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<strong>{{ element.ddNumber }}</strong>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Clearing Amount Column -->
|
||||
<ng-container matColumnDef="clearingAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>Scroll Amount</th>
|
||||
<td mat-cell *matCellDef="let element">₹{{ element.clearingAmount | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Clearing Date Column -->
|
||||
<ng-container matColumnDef="clearingDate">
|
||||
<th mat-header-cell *matHeaderCellDef>Clearing Date</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.clearingDate }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Ref Number Column -->
|
||||
<ng-container matColumnDef="clearingRefNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>Clearing Ref No.</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.clearingRefNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Match Status Column -->
|
||||
<ng-container matColumnDef="matchStatus">
|
||||
<th mat-header-cell *matHeaderCellDef>Match Status</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<span class="status-badge" [ngClass]="element.matchStatus.toLowerCase()">
|
||||
{{ element.matchStatus.replace('_', ' ') }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Remarks Column -->
|
||||
<ng-container matColumnDef="remarks">
|
||||
<th mat-header-cell *matHeaderCellDef>Remarks / Resolution</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.remarks || '-' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
size="small"
|
||||
*ngIf="element.matchStatus !== 'EXACT_MATCH' && !element.remarks?.includes('Resolved')"
|
||||
(click)="openResolveDialog(element)"
|
||||
>
|
||||
Resolve Match
|
||||
</button>
|
||||
<span
|
||||
*ngIf="element.matchStatus === 'EXACT_MATCH' || element.remarks?.includes('Resolved')"
|
||||
style="color: #16a34a; font-size: 12px; font-weight: 500"
|
||||
>
|
||||
<fa-icon icon="check"></fa-icon> Reconciled
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
<div
|
||||
*ngIf="filteredRecords.length === 0"
|
||||
class="no-records-row"
|
||||
style="text-align: center; padding: 32px; color: #64748b"
|
||||
>
|
||||
No scroll records found for the selected status filter.
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog, MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { SponsorDDReconciliationService } from '../sponsor-dd-reconciliation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-reco-batch-detail',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatCardModule,
|
||||
MatTableModule,
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
MatDialogModule,
|
||||
MatSnackBarModule,
|
||||
FaIconComponent
|
||||
],
|
||||
templateUrl: './reco-batch-detail.component.html',
|
||||
styles: [
|
||||
`
|
||||
.detail-container {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.header-row h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.batch-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.summary-card {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
.filter-chips {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.mat-mdc-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
td {
|
||||
color: #334155;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-badge.exact_match {
|
||||
background-color: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
.status-badge.amount_mismatch {
|
||||
background-color: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.status-badge.unadvised_clearing {
|
||||
background-color: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
.status-badge.duplicate_clearing {
|
||||
background-color: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
.action-btn {
|
||||
color: #7c3aed;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
export class RecoBatchDetailComponent implements OnInit {
|
||||
private recoService = inject(SponsorDDReconciliationService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
batchId!: number;
|
||||
batchDetails: any = null;
|
||||
records: any[] = [];
|
||||
filteredRecords: any[] = [];
|
||||
selectedFilter: string = 'ALL';
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'ddNumber',
|
||||
'clearingAmount',
|
||||
'clearingDate',
|
||||
'clearingRefNumber',
|
||||
'matchStatus',
|
||||
'remarks',
|
||||
'actions'
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.batchId = +this.route.snapshot.params['batchId'];
|
||||
this.loadBatchDetails();
|
||||
this.loadRecords();
|
||||
}
|
||||
|
||||
loadBatchDetails(): void {
|
||||
this.recoService.getReconciliationBatch(this.batchId).subscribe({
|
||||
next: (data) => {
|
||||
this.batchDetails = data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadRecords(): void {
|
||||
this.recoService.getReconciliationRecords(this.batchId).subscribe({
|
||||
next: (data) => {
|
||||
this.records = data;
|
||||
this.applyFilter(this.selectedFilter);
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Error loading batch records: ' + (err.error?.message || err.message), 'Close', {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(filter: string): void {
|
||||
this.selectedFilter = filter;
|
||||
if (filter === 'ALL') {
|
||||
this.filteredRecords = this.records;
|
||||
} else {
|
||||
this.filteredRecords = this.records.filter((r) => r.matchStatus === filter);
|
||||
}
|
||||
}
|
||||
|
||||
openResolveDialog(record: any): void {
|
||||
const dialogRef = this.dialog.open(ResolveMismatchDialogComponent, {
|
||||
width: '500px',
|
||||
data: { record },
|
||||
disableClose: true
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.loadBatchDetails();
|
||||
this.loadRecords();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── RESOLVE MISMATCH DIALOG COMPONENT ──
|
||||
@Component({
|
||||
selector: 'mifosx-resolve-mismatch-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatButtonModule
|
||||
],
|
||||
template: `
|
||||
<h2 mat-dialog-title>Resolve Clearing Exception (DD No: {{ data.record.ddNumber }})</h2>
|
||||
<mat-dialog-content>
|
||||
<form [formGroup]="resolveForm" class="dialog-form">
|
||||
<p>
|
||||
Clearing Item status is <strong>{{ data.record.matchStatus.replace('_', ' ') }}</strong
|
||||
>. Sponsor Scroll Amount: <strong>₹{{ data.record.clearingAmount | number: '1.2-2' }}</strong
|
||||
>.
|
||||
</p>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Resolution Action</mat-label>
|
||||
<mat-select formControlName="actionType">
|
||||
<mat-option value="FORCE_MATCH">Force Match (Map to Customer Debit)</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="resolveForm.get('actionType')?.hasError('required')">Action is required</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="row">
|
||||
<mat-form-field appearance="outline" class="half-width">
|
||||
<mat-label>Savings Transaction ID</mat-label>
|
||||
<input matInput type="number" formControlName="resolvedSavingsTxnId" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="half-width">
|
||||
<mat-label>Current Transaction ID</mat-label>
|
||||
<input matInput type="number" formControlName="resolvedCurrentTxnId" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Supervisor Authorization Remarks</mat-label>
|
||||
<textarea matInput formControlName="remarks" rows="2"></textarea>
|
||||
<mat-error *ngIf="resolveForm.get('remarks')?.hasError('required')">Remarks are 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="primary" [disabled]="resolveForm.invalid || loading" (click)="onSubmit()">
|
||||
Resolve Mismatch
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.dialog-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.half-width {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
export class ResolveMismatchDialogComponent implements OnInit {
|
||||
private fb = inject(FormBuilder);
|
||||
private recoService = inject(SponsorDDReconciliationService);
|
||||
private dialogRef = inject(MatDialogRef<ResolveMismatchDialogComponent>);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
protected data = inject(MAT_DIALOG_DATA);
|
||||
|
||||
resolveForm!: FormGroup;
|
||||
loading = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.resolveForm = this.fb.group({
|
||||
actionType: [
|
||||
'FORCE_MATCH',
|
||||
Validators.required
|
||||
],
|
||||
resolvedSavingsTxnId: [null],
|
||||
resolvedCurrentTxnId: [null],
|
||||
remarks: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.resolveForm.invalid) return;
|
||||
|
||||
this.loading = true;
|
||||
const formVal = this.resolveForm.value;
|
||||
|
||||
const payload = {
|
||||
actionType: formVal.actionType,
|
||||
resolvedSavingsTxnId: formVal.resolvedSavingsTxnId ? formVal.resolvedSavingsTxnId : undefined,
|
||||
resolvedCurrentTxnId: formVal.resolvedCurrentTxnId ? formVal.resolvedCurrentTxnId : undefined
|
||||
};
|
||||
|
||||
this.recoService.resolveMismatch(this.data.record.id, payload).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Mismatch resolved successfully', 'Close', { duration: 3000 });
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (err) => {
|
||||
this.loading = false;
|
||||
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 5000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="reco-container">
|
||||
<div class="header-row">
|
||||
<div class="title-area">
|
||||
<h2>Demand Draft Reconciliation</h2>
|
||||
<p>
|
||||
Upload and process daily clearing scroll statements from the Sponsor Bank to auto-match and reconcile issued
|
||||
drafts.
|
||||
</p>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" [routerLink]="['/sponsor-dd-reconciliation/upload']">
|
||||
Upload Clearing Scroll
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-card>
|
||||
<table mat-table [dataSource]="dataSource">
|
||||
<!-- Batch Reference Column -->
|
||||
<ng-container matColumnDef="batchReference">
|
||||
<th mat-header-cell *matHeaderCellDef>Batch Reference</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<strong>{{ element.batchReference }}</strong>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- File Name Column -->
|
||||
<ng-container matColumnDef="fileName">
|
||||
<th mat-header-cell *matHeaderCellDef>Clearing File</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.fileName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<span class="status-badge" [ngClass]="element.status.toLowerCase()">
|
||||
{{ element.status.replace('_', ' ') }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Total Records Column -->
|
||||
<ng-container matColumnDef="totalRecords">
|
||||
<th mat-header-cell *matHeaderCellDef>Total Items</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.totalRecords }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Total Amount Column -->
|
||||
<ng-container matColumnDef="totalAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>Total Amount</th>
|
||||
<td mat-cell *matCellDef="let element">₹{{ element.totalAmount | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Success Count Column -->
|
||||
<ng-container matColumnDef="successCount">
|
||||
<th mat-header-cell *matHeaderCellDef>Matched</th>
|
||||
<td mat-cell *matCellDef="let element" style="color: #15803d; font-weight: 500">
|
||||
{{ element.successCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Mismatch Count Column -->
|
||||
<ng-container matColumnDef="mismatchCount">
|
||||
<th mat-header-cell *matHeaderCellDef>Exceptions</th>
|
||||
<td
|
||||
mat-cell
|
||||
*matCellDef="let element"
|
||||
[style.color]="element.mismatchCount > 0 ? '#b91c1c' : '#475569'"
|
||||
[style.font-weight]="element.mismatchCount > 0 ? '600' : '400'"
|
||||
>
|
||||
{{ element.mismatchCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Created Date Column -->
|
||||
<ng-container matColumnDef="createdDate">
|
||||
<th mat-header-cell *matHeaderCellDef>Uploaded On</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.createdDate | date: 'medium' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button mat-button class="action-btn" [routerLink]="['/sponsor-dd-reconciliation/batches', element.id]">
|
||||
View Details
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
<div
|
||||
*ngIf="dataSource.length === 0"
|
||||
class="no-records-row"
|
||||
style="text-align: center; padding: 32px; color: #64748b"
|
||||
>
|
||||
No clearing files uploaded yet. Click "Upload Clearing Scroll" to reconcile clearing sheets.
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { SponsorDDReconciliationService } from '../sponsor-dd-reconciliation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-reco-batch-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatCardModule,
|
||||
MatTableModule,
|
||||
MatButtonModule,
|
||||
MatSnackBarModule,
|
||||
FaIconComponent
|
||||
],
|
||||
templateUrl: './reco-batch-list.component.html',
|
||||
styles: [
|
||||
`
|
||||
.reco-container {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.title-area h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.title-area p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mat-mdc-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
td {
|
||||
color: #334155;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-badge.reconciled {
|
||||
background-color: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
.status-badge.partially_reconciled {
|
||||
background-color: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
.status-badge.processing {
|
||||
background-color: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.status-badge.failed {
|
||||
background-color: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.action-btn {
|
||||
color: #7c3aed;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
export class RecoBatchListComponent implements OnInit {
|
||||
private recoService = inject(SponsorDDReconciliationService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'batchReference',
|
||||
'fileName',
|
||||
'status',
|
||||
'totalRecords',
|
||||
'totalAmount',
|
||||
'successCount',
|
||||
'mismatchCount',
|
||||
'createdDate',
|
||||
'actions'
|
||||
];
|
||||
dataSource: any[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadBatches();
|
||||
}
|
||||
|
||||
loadBatches(): void {
|
||||
this.recoService.getReconciliationBatches().subscribe({
|
||||
next: (data) => {
|
||||
this.dataSource = data;
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Error loading reconciliation batches: ' + (err.error?.message || err.message), 'Close', {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="upload-container">
|
||||
<div class="header-row">
|
||||
<button
|
||||
mat-button
|
||||
routerLink="/sponsor-dd-reconciliation"
|
||||
style="margin-bottom: 8px; color: #64748b; padding-left: 0"
|
||||
>
|
||||
← Back to Batches
|
||||
</button>
|
||||
<h2>Upload Sponsor Clearing Scroll</h2>
|
||||
<p>Upload a CSV format clearing scroll file containing Sponsor Bank draft payments.</p>
|
||||
</div>
|
||||
|
||||
<mat-card>
|
||||
<div class="dropzone" (click)="triggerFileSelect(fileInput)">
|
||||
<input type="file" #fileInput (change)="onFileSelected($event)" accept=".csv" style="display: none" />
|
||||
<div class="dropzone-icon">
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
</div>
|
||||
<h3>Choose a clearing file or drag & drop it here</h3>
|
||||
<p style="color: #64748b; font-size: 13px">Supports only .csv format files.</p>
|
||||
</div>
|
||||
|
||||
<div class="file-details" *ngIf="selectedFile">
|
||||
<div>
|
||||
<div class="file-name">{{ selectedFile.name }}</div>
|
||||
<div class="file-size">{{ selectedFile.size / 1024 | number: '1.1-1' }} KB</div>
|
||||
</div>
|
||||
<button mat-icon-button color="warn" (click)="selectedFile = null" [disabled]="loading">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="progress-section" *ngIf="loading">
|
||||
<mat-progress-bar mode="indeterminate" color="primary"></mat-progress-bar>
|
||||
<div class="progress-text">{{ progressMessage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button mat-button routerLink="/sponsor-dd-reconciliation" [disabled]="loading">Cancel</button>
|
||||
<button mat-flat-button color="primary" [disabled]="!selectedFile || loading" (click)="onSubmit()">
|
||||
Process Reconciliation
|
||||
</button>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { SponsorDDReconciliationService } from '../sponsor-dd-reconciliation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-reco-batch-upload',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatSnackBarModule,
|
||||
MatProgressBarModule,
|
||||
FaIconComponent
|
||||
],
|
||||
templateUrl: './reco-batch-upload.component.html',
|
||||
styles: [
|
||||
`
|
||||
.upload-container {
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.header-row h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.header-row p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mat-mdc-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
padding: 32px;
|
||||
}
|
||||
.dropzone {
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
background-color: #f8fafc;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background-color 0.2s;
|
||||
}
|
||||
.dropzone:hover {
|
||||
border-color: #7c3aed;
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
.dropzone-icon {
|
||||
font-size: 48px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.file-details {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
}
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.progress-section {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
export class RecoBatchUploadComponent {
|
||||
private recoService = inject(SponsorDDReconciliationService);
|
||||
private router = inject(Router);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
selectedFile: File | null = null;
|
||||
loading = false;
|
||||
progressMessage = '';
|
||||
|
||||
onFileSelected(event: any): void {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.selectedFile = file;
|
||||
}
|
||||
}
|
||||
|
||||
triggerFileSelect(fileInput: HTMLInputElement): void {
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (!this.selectedFile) return;
|
||||
|
||||
this.loading = true;
|
||||
this.progressMessage = 'Reading file and preparing upload payload...';
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const fileContent = reader.result as string;
|
||||
this.progressMessage = 'Uploading clearing scroll for processing...';
|
||||
|
||||
this.recoService
|
||||
.uploadClearingScroll({
|
||||
fileName: this.selectedFile!.name,
|
||||
fileContent: fileContent
|
||||
})
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
this.snackBar.open(
|
||||
`Reconciliation batch processed successfully. ${res.successCount} matched, ${res.mismatchCount} exceptions.`,
|
||||
'Close',
|
||||
{ duration: 7000 }
|
||||
);
|
||||
this.router.navigate([
|
||||
'/sponsor-dd-reconciliation/batches',
|
||||
res.id
|
||||
]);
|
||||
},
|
||||
error: (err) => {
|
||||
this.loading = false;
|
||||
this.progressMessage = '';
|
||||
this.snackBar.open('Reconciliation failed: ' + (err.error?.message || err.message), 'Close', {
|
||||
duration: 6000
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsDataURL(this.selectedFile); // Reads as Base64 Data URL, which our backend decodes safely.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { Route } from '../core/route/route.service';
|
||||
import { RecoBatchListComponent } from './reco-batch-list/reco-batch-list.component';
|
||||
import { RecoBatchUploadComponent } from './reco-batch-upload/reco-batch-upload.component';
|
||||
import { RecoBatchDetailComponent } from './reco-batch-detail/reco-batch-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
{
|
||||
path: 'sponsor-dd-reconciliation',
|
||||
data: { title: 'DD Reconciliation', breadcrumb: 'DD Reconciliation' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'batches',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'batches',
|
||||
component: RecoBatchListComponent,
|
||||
data: { title: 'Reconciliation Batches', breadcrumb: 'Batches' }
|
||||
},
|
||||
{
|
||||
path: 'upload',
|
||||
component: RecoBatchUploadComponent,
|
||||
data: { title: 'Upload Clearing Scroll', breadcrumb: 'Upload' }
|
||||
},
|
||||
{
|
||||
path: 'batches/:batchId',
|
||||
component: RecoBatchDetailComponent,
|
||||
data: { title: 'Batch Details', breadcrumb: 'Batch Details' }
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class SponsorDDReconciliationRoutingModule {}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { SponsorDDReconciliationRoutingModule } from './sponsor-dd-reconciliation-routing.module';
|
||||
import { RecoBatchListComponent } from './reco-batch-list/reco-batch-list.component';
|
||||
import { RecoBatchUploadComponent } from './reco-batch-upload/reco-batch-upload.component';
|
||||
import { RecoBatchDetailComponent } from './reco-batch-detail/reco-batch-detail.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SponsorDDReconciliationRoutingModule,
|
||||
RecoBatchListComponent,
|
||||
RecoBatchUploadComponent,
|
||||
RecoBatchDetailComponent
|
||||
]
|
||||
})
|
||||
export class SponsorDDReconciliationModule {}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SponsorDDReconciliationService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getReconciliationBatches(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/sponsordd/reconcile/batches');
|
||||
}
|
||||
|
||||
getReconciliationBatch(batchId: number): Observable<any> {
|
||||
return this.http.get<any>(`/sponsordd/reconcile/batches/${batchId}`);
|
||||
}
|
||||
|
||||
getReconciliationRecords(batchId: number): Observable<any[]> {
|
||||
return this.http.get<any[]>(`/sponsordd/reconcile/batches/${batchId}/records`);
|
||||
}
|
||||
|
||||
uploadClearingScroll(data: { fileName: string; fileContent: string }): Observable<any> {
|
||||
return this.http.post<any>('/sponsordd/reconcile/upload', data);
|
||||
}
|
||||
|
||||
resolveMismatch(
|
||||
recordId: number,
|
||||
data: {
|
||||
actionType: string; // FORCE_MATCH
|
||||
resolvedSavingsTxnId?: number;
|
||||
resolvedCurrentTxnId?: number;
|
||||
}
|
||||
): Observable<any> {
|
||||
return this.http.post<any>(`/sponsordd/reconcile/resolve/${recordId}`, data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user