/** * 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 { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatInputModule } from '@angular/material/input'; import { forkJoin, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { CashierService } from '../cashier.service'; import { AuthenticationService } from '../../core/authentication/authentication.service'; interface DenomRow { denominationId: number; denominationValue: number; kind: string; morningCount: number; count: number; amount: number; } interface CashierPos { cashierId: number; cashierName?: string; asOfDate: string; totalAmount: number; morningTotal: number; denominations: DenomRow[]; } /** Denomination-wise net cash position per cashier as of a given date. */ @Component({ selector: 'mifosx-cashier-position', templateUrl: './cashier-position.component.html', styleUrls: [ '../cashier-shared.scss', './cashier-position.component.scss' ], standalone: true, imports: [ CommonModule, FormsModule, MatIconModule, MatFormFieldModule, MatSelectModule, MatInputModule ] }) export class CashierPositionComponent implements OnInit { private service = inject(CashierService); private authService = inject(AuthenticationService); tellers: { id: number; name: string }[] = []; cashiers: { id: number; label: string }[] = []; selectedTellerId: number | null = null; selectedCashierId: number | null = null; asOfDate = ''; loading = false; results: CashierPos[] = []; errorMsg = ''; // True when logged-in user is a regular cashier — hides teller/cashier filters isCashierMode = false; ngOnInit(): void { const d = new Date(); this.asOfDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; this.isCashierMode = this.authService.isCashierUser(); if (this.isCashierMode) { // Auto-load current cashier's own position immediately this.loadMe(); } else { this.service .getTellers() .pipe(catchError(() => of([] as any[]))) .subscribe((list) => { this.tellers = (list || []).map((t: any) => ({ id: t.id, name: t.name })); if (this.tellers.length === 1) { this.onTellerSelect(this.tellers[0].id); } }); } } /** Load position for the currently authenticated cashier via /me endpoint. */ loadMe(): void { this.loading = true; this.results = []; this.errorMsg = ''; forkJoin({ position: this.service.getCashierPositionForMe(this.asOfDate).pipe(catchError(() => of(null))), morning: this.service.getCashierMorningBalance(0, this.asOfDate).pipe(catchError(() => of(null))) }).subscribe(({ position, morning }) => { this.loading = false; if (!position || !position.cashierId) { this.errorMsg = 'No cashier record linked to your account. Contact your administrator.'; return; } // Reload morning balance using the resolved cashier ID this.service .getCashierMorningBalance(position.cashierId, this.asOfDate) .pipe(catchError(() => of(null))) .subscribe((morningData: any) => { const morningMap = new Map(); ((morningData?.denominations || []) as any[]).forEach((d: any) => morningMap.set(d.denominationId, d.morningCount ?? 0) ); const morningTotal = Number(morningData?.totalMorning ?? 0); this.results = [ { ...position, morningTotal, denominations: ((position.denominations || []) as any[]).map((d: any) => ({ ...d, morningCount: morningMap.get(d.denominationId) ?? 0 })) } ]; }); }); } onTellerSelect(tellerId: number): void { this.selectedTellerId = tellerId; this.selectedCashierId = null; this.cashiers = []; if (!tellerId) return; this.service .getCashiersForTeller(tellerId) .pipe(catchError(() => of({ cashiers: [] }))) .subscribe((r: any) => { this.cashiers = (r?.cashiers || []).map((c: any) => ({ id: c.id, label: c.staffName })); if (this.cashiers.length === 1) { this.selectedCashierId = this.cashiers[0].id; } }); } load(): void { this.loading = true; this.results = []; this.errorMsg = ''; if (this.selectedCashierId) { forkJoin({ position: this.service .cashierPositionReport(this.selectedCashierId, this.asOfDate) .pipe(catchError(() => of(null))), morning: this.service .getCashierMorningBalance(this.selectedCashierId, this.asOfDate) .pipe(catchError(() => of(null))) }).subscribe(({ position, morning }) => { this.loading = false; if (!position) { this.errorMsg = 'Failed to load position data.'; return; } const morningMap = new Map(); ((morning?.denominations || []) as any[]).forEach((d: any) => morningMap.set(d.denominationId, d.morningCount ?? 0) ); const morningTotal = Number(morning?.totalMorning ?? 0); const arr: any[] = Array.isArray(position) ? position : [position]; this.results = arr.map((p: any) => ({ ...p, morningTotal, denominations: ((p.denominations || []) as any[]).map((d: any) => ({ ...d, morningCount: morningMap.get(d.denominationId) ?? 0 })) })); }); } else { this.service .cashierPositionReport(undefined, this.asOfDate) .pipe(catchError(() => of(null))) .subscribe((data: any) => { this.loading = false; if (!data) { this.errorMsg = 'Failed to load position data.'; return; } const arr: any[] = Array.isArray(data) ? data : [data]; this.results = arr.map((p: any) => ({ ...p, morningTotal: 0, denominations: ((p.denominations || []) as any[]).map((d: any) => ({ ...d, morningCount: 0 })) })); }); } } labelForCashier(id: number): string { const c = this.cashiers.find((x) => x.id === id); return c ? c.label : `Cashier #${id}`; } notes(pos: CashierPos): DenomRow[] { return pos.denominations.filter((d) => d.kind !== 'coin'); } coins(pos: CashierPos): DenomRow[] { return pos.denominations.filter((d) => d.kind === 'coin'); } }