Files
cbsnexora/src/app/cashier/cashier-position/cashier-position.component.ts
T
2026-06-24 16:32:37 +05:30

216 lines
6.9 KiB
TypeScript

/**
* 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<number, number>();
((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<number, number>();
((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');
}
}