feat(cfcfrms): I4C Live screens for the I4C-MHA integration
New CFCFRMS > I4C Live section backed by /v1/i4c/admin and /v1/i4c/cases (the demo mock interceptor does not match these paths): - Dashboard, complaints from I4C, holds register, approvals, responses and API log, bank code master, intimation settings. - Hold popup: overview, hold transactions, customer intimation (automatic and manual), maker request (full / partial release, freeze, unfreeze), GRM / MRM and documents, request history. - GRM grievances, MRM restorations and MRM balance report pages; shared date range filter (today, yesterday, week, month, custom), server-side paging and filters on every list; document upload / download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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, EventEmitter, Input, OnChanges, Output, inject } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { DOCUMENT_TYPES, I4cDocument, I4cDocumentOwner, I4cService, i4cErrorMessage, saveBlob } from '../i4c.service';
|
||||
|
||||
const MAX_BYTES = 5 * 1024 * 1024;
|
||||
const ACCEPTED = [
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/png'
|
||||
];
|
||||
|
||||
/**
|
||||
* Documents behind a hold, grievance or restoration: list, download, and upload (PDF / JPEG / PNG up to 5 MB). Shows
|
||||
* which of the {@link required} types are still missing.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-i4c-documents',
|
||||
templateUrl: './i4c-documents.component.html',
|
||||
styleUrls: ['./i4c-documents.component.scss'],
|
||||
imports: [
|
||||
DatePipe,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class I4cDocumentsComponent implements OnChanges {
|
||||
private i4c = inject(I4cService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
@Input({ required: true }) ownerType!: I4cDocumentOwner;
|
||||
@Input({ required: true }) ownerId!: number;
|
||||
/** Document types this record needs; missing ones are flagged. */
|
||||
@Input() required: string[] = [];
|
||||
/** Types offered first in the picker. */
|
||||
@Input() suggested: string[] = [];
|
||||
@Input() canUpload = true;
|
||||
/** Documents already loaded with the record, to avoid a second call. */
|
||||
@Input() documents: I4cDocument[] | null = null;
|
||||
@Output() changed = new EventEmitter<void>();
|
||||
|
||||
rows: I4cDocument[] = [];
|
||||
docType = '';
|
||||
remarks = '';
|
||||
file: File | null = null;
|
||||
uploading = false;
|
||||
|
||||
ngOnChanges(): void {
|
||||
if (this.documents) {
|
||||
this.rows = this.documents;
|
||||
} else {
|
||||
this.reload();
|
||||
}
|
||||
if (!this.docType) {
|
||||
this.docType = this.suggested[0] || this.missing[0] || 'OTHER';
|
||||
}
|
||||
}
|
||||
|
||||
get types(): { value: string; label: string }[] {
|
||||
const first = DOCUMENT_TYPES.filter((t) => this.suggested.includes(t.value) || this.required.includes(t.value));
|
||||
return [
|
||||
...first,
|
||||
...DOCUMENT_TYPES.filter((t) => !first.includes(t))
|
||||
];
|
||||
}
|
||||
|
||||
get missing(): string[] {
|
||||
return this.required.filter((t) => !this.rows.some((d) => d.doc_type === t));
|
||||
}
|
||||
|
||||
label(type: string): string {
|
||||
return DOCUMENT_TYPES.find((t) => t.value === type)?.label || type;
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
if (!this.ownerId) {
|
||||
return;
|
||||
}
|
||||
this.i4c.documents(this.ownerType, this.ownerId).subscribe({
|
||||
next: (rows) => (this.rows = rows),
|
||||
error: (e) => this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 6000 })
|
||||
});
|
||||
}
|
||||
|
||||
pick(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const f = input.files?.[0] || null;
|
||||
if (f && !ACCEPTED.includes(f.type)) {
|
||||
this.snackBar.open('Only PDF, JPEG and PNG files can be attached.', 'Close', { duration: 5000 });
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
if (f && f.size > MAX_BYTES) {
|
||||
this.snackBar.open('A document can be at most 5 MB.', 'Close', { duration: 5000 });
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
this.file = f;
|
||||
}
|
||||
|
||||
upload(input: HTMLInputElement): void {
|
||||
if (!this.file || !this.docType) {
|
||||
this.snackBar.open('Choose the document type and the file.', 'Close', { duration: 4000 });
|
||||
return;
|
||||
}
|
||||
const file = this.file;
|
||||
this.uploading = true;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const dataUrl = String(reader.result || '');
|
||||
const content = dataUrl.substring(dataUrl.indexOf(',') + 1);
|
||||
this.i4c
|
||||
.uploadDocument({
|
||||
ownerType: this.ownerType,
|
||||
ownerId: this.ownerId,
|
||||
docType: this.docType,
|
||||
fileName: file.name,
|
||||
contentType: file.type,
|
||||
content,
|
||||
remarks: this.remarks.trim() || undefined
|
||||
})
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.uploading = false;
|
||||
this.file = null;
|
||||
this.remarks = '';
|
||||
input.value = '';
|
||||
this.snackBar.open(this.label(this.docType) + ' attached.', 'OK', { duration: 3000 });
|
||||
this.documents = null;
|
||||
this.reload();
|
||||
this.changed.emit();
|
||||
},
|
||||
error: (e) => {
|
||||
this.uploading = false;
|
||||
this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 6000 });
|
||||
}
|
||||
});
|
||||
};
|
||||
reader.onerror = () => {
|
||||
this.uploading = false;
|
||||
this.snackBar.open('The file could not be read.', 'Close', { duration: 5000 });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
download(d: I4cDocument): void {
|
||||
this.i4c.downloadDocument(d.id).subscribe({
|
||||
next: (blob) => saveBlob(blob, d.file_name),
|
||||
error: (e) => this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 6000 })
|
||||
});
|
||||
}
|
||||
|
||||
size(bytes: number): string {
|
||||
return bytes < 1024 * 1024 ? Math.ceil(bytes / 1024) + ' KB' : (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user