feat: implement cashier dashboard, dividend dashboards, share accounts views, and checker tasks
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 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 { FormsModule } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { catchError, of } from 'rxjs';
|
||||
import { CashierService } from '../cashier.service';
|
||||
|
||||
interface VoucherException {
|
||||
voucherNo: string;
|
||||
systemAmount: number | null;
|
||||
slipAmount: number | null;
|
||||
remarks: string | null;
|
||||
raisedByName: string | null;
|
||||
raisedAt: string | null;
|
||||
accountNo: string | null;
|
||||
party: string | null;
|
||||
raisedAgainst: string | null;
|
||||
attachmentCount: number;
|
||||
resolutionStatus: string | null;
|
||||
resolutionNotes: string | null;
|
||||
resolvedByName: string | null;
|
||||
resolvedAt: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Voucher exceptions raised at the counter, and the conclusion recorded on each.
|
||||
*
|
||||
* The cashier who spots a difference cannot close it — this screen is where a clerk, head cashier
|
||||
* or manager sits down with them, records what was actually wrong, and attaches the paper that
|
||||
* supports the decision.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-voucher-exceptions',
|
||||
templateUrl: './voucher-exceptions.component.html',
|
||||
styleUrls: [
|
||||
'../cashier-shared.scss',
|
||||
'../day-book/day-book.component.scss',
|
||||
'./voucher-exceptions.component.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatIconModule
|
||||
]
|
||||
})
|
||||
export class VoucherExceptionsComponent implements OnInit {
|
||||
private service = inject(CashierService);
|
||||
|
||||
filter: 'OPEN' | 'RESOLVED' | 'ALL' = 'OPEN';
|
||||
exceptions: VoucherException[] = [];
|
||||
/** Set by the backend from the caller's own roles — the cashier who raised a flag cannot close it. */
|
||||
canResolve = false;
|
||||
loading = false;
|
||||
loaded = false;
|
||||
errorMsg = '';
|
||||
|
||||
// Resolution panel
|
||||
resolveRow: VoucherException | null = null;
|
||||
resolutionStatus = '';
|
||||
resolutionNotes = '';
|
||||
resolveSaving = false;
|
||||
|
||||
// Attachments for the voucher being resolved
|
||||
attachments: any[] = [];
|
||||
attachLoading = false;
|
||||
attachUploading = false;
|
||||
|
||||
readonly resolutionOptions = [
|
||||
{ value: 'RESOLVED_SYSTEM_ERROR', label: 'System entry was wrong' },
|
||||
{ value: 'RESOLVED_SLIP_ERROR', label: 'Slip was written wrong' },
|
||||
{ value: 'RESOLVED_NO_ACTION', label: 'No action needed' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.errorMsg = '';
|
||||
this.service
|
||||
.voucherExceptions(this.filter)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.errorMsg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.message ?? 'Could not load voucher exceptions.';
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
this.exceptions = res?.exceptions ?? [];
|
||||
this.canResolve = !!res?.canResolve;
|
||||
});
|
||||
}
|
||||
|
||||
difference(e: VoucherException): number {
|
||||
if (e.slipAmount == null) return 0;
|
||||
return Number(e.slipAmount) - Number(e.systemAmount ?? 0);
|
||||
}
|
||||
|
||||
/** True when the flag was about something other than the amount. */
|
||||
amountsAgree(e: VoucherException): boolean {
|
||||
return e.slipAmount == null || Number(e.slipAmount) === Number(e.systemAmount ?? 0);
|
||||
}
|
||||
|
||||
openResolve(e: VoucherException): void {
|
||||
this.resolveRow = e;
|
||||
this.resolutionStatus = '';
|
||||
this.resolutionNotes = '';
|
||||
this.loadAttachments(e);
|
||||
}
|
||||
|
||||
closeResolve(): void {
|
||||
this.resolveRow = null;
|
||||
this.attachments = [];
|
||||
}
|
||||
|
||||
private loadAttachments(e: VoucherException): void {
|
||||
this.attachments = [];
|
||||
this.attachLoading = true;
|
||||
this.service
|
||||
.listVoucherAttachments(e.voucherNo)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
this.attachLoading = false;
|
||||
this.attachments = res?.attachments ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
onFileSelected(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
const row = this.resolveRow;
|
||||
if (!file || !row) return;
|
||||
|
||||
// 10 MB, matching the backend — refusing here saves uploading the whole file to be rejected.
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
this.errorMsg = 'That file is larger than 10 MB. Please scan it at a lower resolution.';
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.attachUploading = true;
|
||||
this.errorMsg = '';
|
||||
this.service
|
||||
.uploadVoucherAttachment(
|
||||
row.voucherNo,
|
||||
file.name,
|
||||
file.type || 'application/octet-stream',
|
||||
String(reader.result),
|
||||
'Supporting document for exception resolution'
|
||||
)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.errorMsg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.message ?? 'Upload failed. Please try again.';
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.attachUploading = false;
|
||||
input.value = '';
|
||||
if (res) {
|
||||
this.loadAttachments(row);
|
||||
this.load();
|
||||
}
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
downloadAttachment(a: any): void {
|
||||
this.service
|
||||
.downloadVoucherAttachment(a.id)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((blob: Blob | null) => {
|
||||
if (!blob) {
|
||||
this.errorMsg = 'Could not download that file.';
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = a.fileName ?? 'attachment';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
/** Mirrors the backend's own rule, so the button explains itself before it is pressed. */
|
||||
get canSubmitResolution(): boolean {
|
||||
return (
|
||||
!!this.resolutionStatus && !!this.resolutionNotes.trim() && this.attachments.length > 0 && !this.resolveSaving
|
||||
);
|
||||
}
|
||||
|
||||
saveResolution(): void {
|
||||
const row = this.resolveRow;
|
||||
if (!row) return;
|
||||
|
||||
this.resolveSaving = true;
|
||||
this.errorMsg = '';
|
||||
this.service
|
||||
.resolveVoucherException(row.voucherNo, this.resolutionStatus, this.resolutionNotes.trim())
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.errorMsg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.message ?? 'Could not close the exception.';
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.resolveSaving = false;
|
||||
if (res) {
|
||||
this.closeResolve();
|
||||
this.load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolutionLabel(status: string | null): string {
|
||||
if (!status) return '';
|
||||
return this.resolutionOptions.find((o) => o.value === status)?.label ?? status;
|
||||
}
|
||||
|
||||
/** Date and time — an exception queue spans days, so the day matters here. */
|
||||
stamp(iso: string | null): string {
|
||||
if (!iso) return '—';
|
||||
const [
|
||||
d,
|
||||
t
|
||||
] = iso.split('T');
|
||||
return t ? `${d} ${t.slice(0, 5)}` : d;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user