/** * 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 { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { MatIconModule } from '@angular/material/icon'; import { catchError, of } from 'rxjs'; import { CashierService } from '../cashier.service'; /** One side of a voucher: a debit or a credit against a single account. */ interface VoucherLine { lineNo: number; entryType: 'DR' | 'CR'; glAccountId: number | null; savingsAccountId: number | null; loanAccountId: number | null; partyName: string | null; accountNo: string | null; amount: number; description: string | null; } interface Voucher { voucherNo: string; voucherType: string | null; referenceType: string | null; referenceId: number | null; amount: number | null; branchOfficeId: number | null; branchName: string | null; createdBy: string | null; createdAt: string | null; verificationStatus: string | null; lines: VoucherLine[]; totalDebit: number; totalCredit: number; balanced: boolean; } /** * One voucher, as a slip: which accounts were debited, which credited, and for how much. * * Looked up by voucher number rather than listed, because that is how a voucher is reached — from a * paper slip in someone's hand, or from a number quoted on a statement. */ @Component({ selector: 'mifosx-voucher-view', templateUrl: './voucher-view.component.html', styleUrls: [ '../cashier-shared.scss', './voucher-view.component.scss' ], standalone: true, imports: [ CommonModule, FormsModule, MatIconModule ] }) export class VoucherViewComponent { private service = inject(CashierService); private route = inject(ActivatedRoute); private router = inject(Router); voucherNo = ''; voucher: Voucher | null = null; loading = false; /** Shown in place of the slip — a bad number and a server error read differently to the user. */ error: string | null = null; constructor() { // A voucher number in the URL means someone arrived from a link or a statement rather than by // typing, so the slip is fetched straight away instead of showing them an empty search box. const fromUrl = this.route.snapshot.queryParamMap.get('voucherNo'); if (fromUrl) { this.voucherNo = fromUrl; this.search(); } } search(): void { const trimmed = (this.voucherNo || '').trim(); if (!trimmed) { this.error = 'Enter a voucher number.'; this.voucher = null; return; } this.loading = true; this.error = null; // Kept in the URL so the slip can be linked to and survives a refresh. this.router.navigate([], { relativeTo: this.route, queryParams: { voucherNo: trimmed }, replaceUrl: true }); this.service .voucherDetail(trimmed) .pipe( catchError((err: any) => { this.error = err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.developerMessage ?? `Voucher ${trimmed} could not be loaded.`; return of(null); }) ) .subscribe((res: Voucher | null) => { this.loading = false; this.voucher = res; }); } /** Debits first, then credits — the order a slip is read in. */ get debitLines(): VoucherLine[] { return (this.voucher?.lines ?? []).filter((l) => l.entryType === 'DR'); } get creditLines(): VoucherLine[] { return (this.voucher?.lines ?? []).filter((l) => l.entryType === 'CR'); } /** * A voucher raised before its sides were recorded. The voucher is real and its number valid — only * the posting behind it was never captured — so this reads as an older record, not a broken one. */ get hasNoLines(): boolean { return !!this.voucher && this.voucher.lines.length === 0; } print(): void { window.print(); } }