/** * 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(); 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; } clearFile(input: HTMLInputElement): void { this.file = null; input.value = ''; } 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'; } }