feat(cashier): add full-screen cashier teller workstation dashboard
- Add Transaction Entry screen with full account details panel (personal info, joint holders, nominees, KYC, balances) - Role-based layout: CASHIER role hides nav/sidebar/ticker, redirects to /cashier/transaction-entry on login - Denomination entry with opening/closing position preview (notes first then coins, stepper +/- round button inputs) - Auto-fetch savings/current/FD/RD accounts with associations, client KYC, nominees, addresses, family members (S/D/W/H/O) - Submit deposit/withdrawal via POST /transactions with denominationBreakdown - Cashier position auto-loads for CASHIER role (no filter dropdowns) - Pagination added to cash-history tabs and currency-exchange-detail - Cashier menu simplified for cashier role permissions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c8e9ab8f84
commit
9094b9a0eb
@@ -35,27 +35,55 @@
|
||||
}
|
||||
|
||||
<mat-tab-group animationDuration="150ms">
|
||||
<mat-tab label="Cash Transfers ({{ transfers.length }})">
|
||||
<mat-tab label="Cash Transfers ({{ transfersTotal }})">
|
||||
<div class="pg-tab-body">
|
||||
<ng-container *ngTemplateOutlet="txnTable; context: { rows: transfers }"></ng-container>
|
||||
<mat-paginator
|
||||
[length]="transfersTotal"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
showFirstLastButtons
|
||||
(page)="onPageChange('transfers', $event)"
|
||||
></mat-paginator>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="Settlements ({{ settlements.length }})">
|
||||
<mat-tab label="Settlements ({{ settlementsTotal }})">
|
||||
<div class="pg-tab-body">
|
||||
<ng-container *ngTemplateOutlet="txnTable; context: { rows: settlements }"></ng-container>
|
||||
<mat-paginator
|
||||
[length]="settlementsTotal"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
showFirstLastButtons
|
||||
(page)="onPageChange('settlements', $event)"
|
||||
></mat-paginator>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="Inter-Cashier ({{ interCashier.length }})">
|
||||
<mat-tab label="Inter-Cashier ({{ interTotal }})">
|
||||
<div class="pg-tab-body">
|
||||
<ng-container *ngTemplateOutlet="txnTable; context: { rows: interCashier }"></ng-container>
|
||||
<mat-paginator
|
||||
[length]="interTotal"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
showFirstLastButtons
|
||||
(page)="onPageChange('inter', $event)"
|
||||
></mat-paginator>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="Cash Exchanges ({{ exchanges.length }})">
|
||||
<mat-tab label="Cash Exchanges ({{ exchangesTotal }})">
|
||||
<div class="pg-tab-body">
|
||||
<ng-container *ngTemplateOutlet="txnTable; context: { rows: exchanges }"></ng-container>
|
||||
<mat-paginator
|
||||
[length]="exchangesTotal"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
showFirstLastButtons
|
||||
(page)="onPageChange('exchanges', $event)"
|
||||
></mat-paginator>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { CashierService } from '../cashier.service';
|
||||
@@ -65,7 +66,8 @@ interface DetailView {
|
||||
FormsModule,
|
||||
MatIconModule,
|
||||
MatSnackBarModule,
|
||||
MatTabsModule
|
||||
MatTabsModule,
|
||||
MatPaginatorModule
|
||||
]
|
||||
})
|
||||
export class CashHistoryComponent implements OnInit {
|
||||
@@ -78,6 +80,23 @@ export class CashHistoryComponent implements OnInit {
|
||||
exchanges: TxnRow[] = [];
|
||||
loading = false;
|
||||
|
||||
// Pagination — per tab
|
||||
pageSize = 20;
|
||||
pageSizeOptions = [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
100
|
||||
];
|
||||
transfersTotal = 0;
|
||||
transfersOffset = 0;
|
||||
settlementsTotal = 0;
|
||||
settlementsOffset = 0;
|
||||
interTotal = 0;
|
||||
interOffset = 0;
|
||||
exchangesTotal = 0;
|
||||
exchangesOffset = 0;
|
||||
|
||||
private denomKindMap = new Map<number, 'note' | 'coin'>();
|
||||
|
||||
reversingId: number | null = null;
|
||||
@@ -96,15 +115,55 @@ export class CashHistoryComponent implements OnInit {
|
||||
this.load();
|
||||
}
|
||||
|
||||
onPageChange(tab: 'transfers' | 'settlements' | 'inter' | 'exchanges', event: PageEvent): void {
|
||||
this.pageSize = event.pageSize;
|
||||
if (tab === 'transfers') {
|
||||
this.transfersOffset = event.pageIndex * event.pageSize;
|
||||
this.loadTransfers();
|
||||
}
|
||||
if (tab === 'settlements') {
|
||||
this.settlementsOffset = event.pageIndex * event.pageSize;
|
||||
this.loadSettlements();
|
||||
}
|
||||
if (tab === 'inter') {
|
||||
this.interOffset = event.pageIndex * event.pageSize;
|
||||
this.loadInter();
|
||||
}
|
||||
if (tab === 'exchanges') {
|
||||
this.exchangesOffset = event.pageIndex * event.pageSize;
|
||||
this.loadExchanges();
|
||||
}
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.transfersOffset = 0;
|
||||
this.settlementsOffset = 0;
|
||||
this.interOffset = 0;
|
||||
this.exchangesOffset = 0;
|
||||
this.loading = true;
|
||||
forkJoin({
|
||||
transfers: this.service.getCashTransfers().pipe(catchError(() => of([]))),
|
||||
settlements: this.service.getCashSettlements().pipe(catchError(() => of([]))),
|
||||
inter: this.service.getInterCashierTransfers().pipe(catchError(() => of([]))),
|
||||
exchanges: this.service.getCashExchanges().pipe(catchError(() => of([])))
|
||||
transfers: this.service
|
||||
.getCashTransfers(0, this.pageSize)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))),
|
||||
settlements: this.service
|
||||
.getCashSettlements(0, this.pageSize)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))),
|
||||
inter: this.service
|
||||
.getInterCashierTransfers(0, this.pageSize)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))),
|
||||
exchanges: this.service
|
||||
.getCashExchanges(0, this.pageSize)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
|
||||
}).subscribe(({ transfers, settlements, inter, exchanges }) => {
|
||||
this.transfers = (transfers as any[]).map((t) => ({
|
||||
this.transfersTotal = transfers.totalFilteredRecords ?? 0;
|
||||
this.settlementsTotal = settlements.totalFilteredRecords ?? 0;
|
||||
this.interTotal = inter.totalFilteredRecords ?? 0;
|
||||
this.exchangesTotal = exchanges.totalFilteredRecords ?? 0;
|
||||
const transferItems = transfers.pageItems ?? [];
|
||||
const settlementItems = settlements.pageItems ?? [];
|
||||
const interItems = inter.pageItems ?? [];
|
||||
const exchangeItems = exchanges.pageItems ?? [];
|
||||
this.transfers = (transferItems as any[]).map((t) => ({
|
||||
id: t.id,
|
||||
date: t.transferDate,
|
||||
fromParty: t.fromParty,
|
||||
@@ -119,7 +178,7 @@ export class CashHistoryComponent implements OnInit {
|
||||
reverseReason: t.reverseReason ?? null,
|
||||
type: 'transfer'
|
||||
}));
|
||||
this.settlements = (settlements as any[]).map((s) => ({
|
||||
this.settlements = (settlementItems as any[]).map((s) => ({
|
||||
id: s.id,
|
||||
date: s.settlementDate,
|
||||
fromParty: s.fromParty,
|
||||
@@ -135,7 +194,7 @@ export class CashHistoryComponent implements OnInit {
|
||||
reverseReason: s.reverseReason ?? null,
|
||||
type: 'settlement'
|
||||
}));
|
||||
this.interCashier = (inter as any[]).map((i) => ({
|
||||
this.interCashier = (interItems as any[]).map((i) => ({
|
||||
id: i.id,
|
||||
date: i.transferDate,
|
||||
fromParty: i.fromParty,
|
||||
@@ -150,7 +209,7 @@ export class CashHistoryComponent implements OnInit {
|
||||
reverseReason: i.reverseReason ?? null,
|
||||
type: 'inter'
|
||||
}));
|
||||
this.exchanges = (exchanges as any[]).map((e) => ({
|
||||
this.exchanges = (exchangeItems as any[]).map((e) => ({
|
||||
id: e.id,
|
||||
date: e.exchangeDate,
|
||||
fromParty: e.accountNo ? `Account ${e.accountNo}` : 'Walk-in Customer',
|
||||
@@ -169,6 +228,103 @@ export class CashHistoryComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private loadTransfers(): void {
|
||||
this.service
|
||||
.getCashTransfers(this.transfersOffset, this.pageSize)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
|
||||
.subscribe((res: any) => {
|
||||
this.transfersTotal = res.totalFilteredRecords ?? 0;
|
||||
this.transfers = (res.pageItems ?? []).map((t: any) => ({
|
||||
id: t.id,
|
||||
date: t.transferDate,
|
||||
fromParty: t.fromParty,
|
||||
toParty: t.toParty,
|
||||
amount: t.transferAmount,
|
||||
note: t.note,
|
||||
isReversed: t.isReversed,
|
||||
performedByUsername: t.performedByUsername,
|
||||
createdAt: t.createdAt,
|
||||
reversedBy: t.reversedBy ?? null,
|
||||
reversedAt: t.reversedAt ?? null,
|
||||
reverseReason: t.reverseReason ?? null,
|
||||
type: 'transfer'
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private loadSettlements(): void {
|
||||
this.service
|
||||
.getCashSettlements(this.settlementsOffset, this.pageSize)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
|
||||
.subscribe((res: any) => {
|
||||
this.settlementsTotal = res.totalFilteredRecords ?? 0;
|
||||
this.settlements = (res.pageItems ?? []).map((s: any) => ({
|
||||
id: s.id,
|
||||
date: s.settlementDate,
|
||||
fromParty: s.fromParty,
|
||||
fromCashierId: s.fromCashierId ?? undefined,
|
||||
toParty: s.toParty,
|
||||
amount: s.settlementAmount,
|
||||
note: s.note,
|
||||
isReversed: s.isReversed,
|
||||
performedByUsername: s.performedByUsername,
|
||||
createdAt: s.createdAt,
|
||||
reversedBy: s.reversedBy ?? null,
|
||||
reversedAt: s.reversedAt ?? null,
|
||||
reverseReason: s.reverseReason ?? null,
|
||||
type: 'settlement'
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private loadInter(): void {
|
||||
this.service
|
||||
.getInterCashierTransfers(this.interOffset, this.pageSize)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
|
||||
.subscribe((res: any) => {
|
||||
this.interTotal = res.totalFilteredRecords ?? 0;
|
||||
this.interCashier = (res.pageItems ?? []).map((i: any) => ({
|
||||
id: i.id,
|
||||
date: i.transferDate,
|
||||
fromParty: i.fromParty,
|
||||
toParty: i.toParty,
|
||||
amount: i.transferAmount,
|
||||
note: i.note,
|
||||
isReversed: i.isReversed,
|
||||
performedByUsername: i.performedByUsername,
|
||||
createdAt: i.createdAt,
|
||||
reversedBy: i.reversedBy ?? null,
|
||||
reversedAt: i.reversedAt ?? null,
|
||||
reverseReason: i.reverseReason ?? null,
|
||||
type: 'inter'
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private loadExchanges(): void {
|
||||
this.service
|
||||
.getCashExchanges(this.exchangesOffset, this.pageSize)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
|
||||
.subscribe((res: any) => {
|
||||
this.exchangesTotal = res.totalFilteredRecords ?? 0;
|
||||
this.exchanges = (res.pageItems ?? []).map((e: any) => ({
|
||||
id: e.id,
|
||||
date: e.exchangeDate,
|
||||
fromParty: e.accountNo ? `Account ${e.accountNo}` : 'Walk-in Customer',
|
||||
toParty: e.performedByUsername || '—',
|
||||
amount: e.exchangeAmount,
|
||||
note: e.note,
|
||||
isReversed: e.isReversed ?? false,
|
||||
performedByUsername: e.performedByUsername,
|
||||
createdAt: e.createdAt,
|
||||
reversedBy: e.reversedBy ?? null,
|
||||
reversedAt: e.reversedAt ?? null,
|
||||
reverseReason: e.reverseReason ?? null,
|
||||
type: 'exchange'
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
viewDetail(row: TxnRow): void {
|
||||
this.detail = {
|
||||
row,
|
||||
|
||||
@@ -18,34 +18,44 @@
|
||||
</header>
|
||||
|
||||
<div class="pg-toolbar pg-toolbar--mat">
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>Teller (optional)</mat-label>
|
||||
<mat-select [value]="selectedTellerId" (selectionChange)="onTellerSelect($event.value)">
|
||||
<mat-option [value]="null">All Tellers</mat-option>
|
||||
@for (t of tellers; track t.id) {
|
||||
<mat-option [value]="t.id">{{ t.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- Teller + Cashier filters — hidden for cashier role (auto-loads own position) -->
|
||||
@if (!isCashierMode) {
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>Teller (optional)</mat-label>
|
||||
<mat-select [value]="selectedTellerId" (selectionChange)="onTellerSelect($event.value)">
|
||||
<mat-option [value]="null">All Tellers</mat-option>
|
||||
@for (t of tellers; track t.id) {
|
||||
<mat-option [value]="t.id">{{ t.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>Cashier (optional)</mat-label>
|
||||
<mat-select [value]="selectedCashierId" (selectionChange)="selectedCashierId = $event.value">
|
||||
<mat-option [value]="null">All Cashiers</mat-option>
|
||||
@for (c of cashiers; track c.id) {
|
||||
<mat-option [value]="c.id">{{ c.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>Cashier (optional)</mat-label>
|
||||
<mat-select [value]="selectedCashierId" (selectionChange)="selectedCashierId = $event.value">
|
||||
<mat-option [value]="null">All Cashiers</mat-option>
|
||||
@for (c of cashiers; track c.id) {
|
||||
<mat-option [value]="c.id">{{ c.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>As of Date</mat-label>
|
||||
<input matInput type="date" [value]="asOfDate" (change)="asOfDate = $any($event.target).value" />
|
||||
</mat-form-field>
|
||||
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="load()" [disabled]="loading">
|
||||
<mat-icon>bar_chart</mat-icon> Load Position
|
||||
</button>
|
||||
@if (isCashierMode) {
|
||||
<!-- Cashier mode: just a refresh button -->
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="loadMe()" [disabled]="loading">
|
||||
<mat-icon>refresh</mat-icon> Refresh
|
||||
</button>
|
||||
} @else {
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="load()" [disabled]="loading">
|
||||
<mat-icon>bar_chart</mat-icon> Load Position
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (errorMsg) {
|
||||
|
||||
@@ -16,6 +16,7 @@ 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;
|
||||
@@ -54,6 +55,7 @@ interface CashierPos {
|
||||
})
|
||||
export class CashierPositionComponent implements OnInit {
|
||||
private service = inject(CashierService);
|
||||
private authService = inject(AuthenticationService);
|
||||
|
||||
tellers: { id: number; name: string }[] = [];
|
||||
cashiers: { id: number; label: string }[] = [];
|
||||
@@ -65,15 +67,64 @@ export class CashierPositionComponent implements OnInit {
|
||||
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.service
|
||||
.getTellers()
|
||||
.pipe(catchError(() => of([] as any[])))
|
||||
.subscribe((list) => {
|
||||
this.tellers = (list || []).map((t: any) => ({ id: t.id, name: t.name }));
|
||||
});
|
||||
|
||||
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 }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
|
||||
@@ -82,6 +82,16 @@ const routes: Routes = [
|
||||
path: 'share-collection',
|
||||
component: ShareCollectionComponent,
|
||||
data: { title: 'Share Collection', breadcrumb: 'Share Collection' }
|
||||
},
|
||||
{
|
||||
path: 'transaction-entry',
|
||||
loadComponent: () =>
|
||||
import('./transaction-entry/transaction-entry.component').then((m) => m.TransactionEntryComponent),
|
||||
data: {
|
||||
title: 'Cashier Transaction Entry',
|
||||
breadcrumb: 'Transaction Entry',
|
||||
hideShellLayout: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
|
||||
/** HTTP service for the Cashier menu features. */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -24,8 +25,9 @@ export class CashierService {
|
||||
createCashExchange(data: any): Observable<any> {
|
||||
return this.http.post('/cashexchange', data);
|
||||
}
|
||||
getCashExchanges(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/cashexchange');
|
||||
getCashExchanges(offset = 0, limit = 20): Observable<any> {
|
||||
const params = new HttpParams().set('offset', offset).set('limit', limit);
|
||||
return this.http.get<any>('/cashexchange', { params });
|
||||
}
|
||||
getCashExchange(id: number): Observable<any> {
|
||||
return this.http.get(`/cashexchange/${id}`);
|
||||
@@ -49,8 +51,9 @@ export class CashierService {
|
||||
createCashTransfer(data: any): Observable<any> {
|
||||
return this.http.post('/cashtransfer', data);
|
||||
}
|
||||
getCashTransfers(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/cashtransfer');
|
||||
getCashTransfers(offset = 0, limit = 20): Observable<any> {
|
||||
const params = new HttpParams().set('offset', offset).set('limit', limit);
|
||||
return this.http.get<any>('/cashtransfer', { params });
|
||||
}
|
||||
getCashTransferById(id: number): Observable<any> {
|
||||
return this.http.get(`/cashtransfer/${id}`);
|
||||
@@ -63,8 +66,9 @@ export class CashierService {
|
||||
createCashSettlement(data: any): Observable<any> {
|
||||
return this.http.post('/cashsettlement', data);
|
||||
}
|
||||
getCashSettlements(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/cashsettlement');
|
||||
getCashSettlements(offset = 0, limit = 20): Observable<any> {
|
||||
const params = new HttpParams().set('offset', offset).set('limit', limit);
|
||||
return this.http.get<any>('/cashsettlement', { params });
|
||||
}
|
||||
getCashSettlementById(id: number): Observable<any> {
|
||||
return this.http.get(`/cashsettlement/${id}`);
|
||||
@@ -77,8 +81,9 @@ export class CashierService {
|
||||
createInterCashierTransfer(data: any): Observable<any> {
|
||||
return this.http.post('/cashintercashier', data);
|
||||
}
|
||||
getInterCashierTransfers(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/cashintercashier');
|
||||
getInterCashierTransfers(offset = 0, limit = 20): Observable<any> {
|
||||
const params = new HttpParams().set('offset', offset).set('limit', limit);
|
||||
return this.http.get<any>('/cashintercashier', { params });
|
||||
}
|
||||
getInterCashierById(id: number): Observable<any> {
|
||||
return this.http.get(`/cashintercashier/${id}`);
|
||||
@@ -157,6 +162,15 @@ export class CashierService {
|
||||
return this.http.get('/cashreconciliation/expected/me', { params });
|
||||
}
|
||||
|
||||
// ── Current cashier identity ──
|
||||
// Returns cashierId + cashierName for the logged-in user, or null if not a cashier.
|
||||
getCurrentCashierInfo(): Observable<{ cashierId: number; cashierName: string } | null> {
|
||||
return this.getCashierPositionForMe().pipe(
|
||||
map((pos: any) => (pos?.cashierId ? { cashierId: pos.cashierId, cashierName: pos.cashierName ?? '' } : null)),
|
||||
catchError(() => of(null))
|
||||
);
|
||||
}
|
||||
|
||||
// ── Payment Types ──
|
||||
getPaymentTypes(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/paymenttypes');
|
||||
|
||||
@@ -52,5 +52,15 @@
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<mat-paginator
|
||||
[length]="totalRecords"
|
||||
[pageSize]="pageSize"
|
||||
[pageIndex]="pageIndex"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
showFirstLastButtons
|
||||
(page)="onPageChange($event)"
|
||||
>
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { CashierService } from '../cashier.service';
|
||||
|
||||
/** Lists recorded cash exchanges (Currency Exchange Detail). */
|
||||
@@ -21,25 +22,48 @@ import { CashierService } from '../cashier.service';
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
MatIconModule
|
||||
MatIconModule,
|
||||
MatPaginatorModule
|
||||
]
|
||||
})
|
||||
export class CurrencyExchangeDetailComponent implements OnInit {
|
||||
private service = inject(CashierService);
|
||||
|
||||
exchanges: any[] = [];
|
||||
totalRecords = 0;
|
||||
pageSize = 20;
|
||||
pageIndex = 0;
|
||||
pageSizeOptions = [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
100
|
||||
];
|
||||
loading = true;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.service.getCashExchanges().subscribe({
|
||||
next: (list) => {
|
||||
this.exchanges = list || [];
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.service.getCashExchanges(this.pageIndex * this.pageSize, this.pageSize).subscribe({
|
||||
next: (res: any) => {
|
||||
this.exchanges = res.pageItems ?? [];
|
||||
this.totalRecords = res.totalFilteredRecords ?? 0;
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.exchanges = [];
|
||||
this.totalRecords = 0;
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
this.pageIndex = event.pageIndex;
|
||||
this.pageSize = event.pageSize;
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="te-root">
|
||||
<!-- ── ENTRY GRID shown when no account loaded yet ────────────────── -->
|
||||
@if (!accountDetails && !loadingAccount) {
|
||||
<div class="te-grid-wrap">
|
||||
<table class="te-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Typ</th>
|
||||
<th>Br.</th>
|
||||
<th>Tr.Tp.</th>
|
||||
<th>Actyp</th>
|
||||
<th>A/c No.</th>
|
||||
<th>Ecs A/c No.</th>
|
||||
<th>Db./Cr.</th>
|
||||
<th>Cheque No.</th>
|
||||
<th>Amount</th>
|
||||
<th>Description</th>
|
||||
<th>Tok.No.</th>
|
||||
<th>Cheq.Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of rows; track $index; let i = $index) {
|
||||
<tr [class.te-row--selected]="selectedRow === row" (click)="onRowSelect(row, i)">
|
||||
<td><input class="te-cell te-cell--xs" [(ngModel)]="row.typ" (click)="$event.stopPropagation()" /></td>
|
||||
<td>
|
||||
<input
|
||||
class="te-cell te-cell--xs"
|
||||
type="number"
|
||||
[(ngModel)]="row.br"
|
||||
(click)="$event.stopPropagation()"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<select class="te-cell te-cell--sm" [(ngModel)]="row.trTp" (click)="$event.stopPropagation()">
|
||||
<option value=""></option>
|
||||
@for (t of trTpOptions; track t) {
|
||||
<option [value]="t">{{ t }}</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select class="te-cell te-cell--sm" [(ngModel)]="row.acTyp" (click)="$event.stopPropagation()">
|
||||
@for (a of acTypOptions; track a) {
|
||||
<option [value]="a">{{ a }}</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
class="te-cell te-cell--md"
|
||||
[(ngModel)]="row.acNo"
|
||||
(click)="$event.stopPropagation()"
|
||||
(blur)="onAcNoBlur(row, i)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input class="te-cell te-cell--lg" [(ngModel)]="row.ecsAcNo" (click)="$event.stopPropagation()" />
|
||||
</td>
|
||||
<td>
|
||||
<select class="te-cell te-cell--sm" [(ngModel)]="row.dbCr" (click)="$event.stopPropagation()">
|
||||
<option value=""></option>
|
||||
@for (d of dbCrOptions; track d) {
|
||||
<option [value]="d">{{ d }}</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input class="te-cell te-cell--md" [(ngModel)]="row.chequeNo" (click)="$event.stopPropagation()" />
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
class="te-cell te-cell--md"
|
||||
type="number"
|
||||
[(ngModel)]="row.amount"
|
||||
(click)="$event.stopPropagation()"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input class="te-cell te-cell--lg" [(ngModel)]="row.description" (click)="$event.stopPropagation()" />
|
||||
</td>
|
||||
<td><input class="te-cell te-cell--sm" [(ngModel)]="row.tokNo" (click)="$event.stopPropagation()" /></td>
|
||||
<td>
|
||||
<input
|
||||
class="te-cell te-cell--sm"
|
||||
[(ngModel)]="row.cheqDate"
|
||||
placeholder="dd/mm/yy"
|
||||
(click)="$event.stopPropagation()"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="te-grid-footer">
|
||||
<span class="te-eff-date"
|
||||
>Eff. Dt : <strong>{{ today }}</strong></span
|
||||
>
|
||||
<button class="te-add-btn" type="button" (click)="addRow()" title="Add row">+</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── STATUS ────────────────────────────────────────────────────────── -->
|
||||
@if (loadingAccount) {
|
||||
<div class="te-loading">Loading account details…</div>
|
||||
}
|
||||
@if (accountError && !loadingAccount) {
|
||||
<div class="te-error">{{ accountError }}</div>
|
||||
}
|
||||
|
||||
<!-- ── ACCOUNT DETAIL ─────────────────────────────────────────────────── -->
|
||||
@if (accountDetails && !loadingAccount) {
|
||||
<div class="te-detail">
|
||||
<!-- ROW 1: Personal | Joint + Nominees | Other + Balances -->
|
||||
<div class="te-detail-top">
|
||||
<!-- Personal Information -->
|
||||
<div class="te-panel">
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
Personal Information
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Branch</span><span class="te-value">{{ officeName }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">A/c No.</span><span class="te-value te-mono">{{ accountDetails.accountNo }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Name</span><span class="te-value te-value-name">{{ clientName }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">S/D/W/H/O</span><span class="te-value">{{ guardianName }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Address</span><span class="te-value te-value-wrap">{{ clientAddress }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Email</span><span class="te-value">{{ clientEmail }}</span>
|
||||
</div>
|
||||
<div class="te-field-row">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Last Txn</span><span class="te-value">{{ lastTransactionDate }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Type</span
|
||||
><span class="te-value">{{ accountDetails.depositType?.value ?? selectedRow?.acTyp ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="te-field-row">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Opening Dt</span><span class="te-value">{{ openingDate }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Status</span
|
||||
><span class="te-value">{{ accountDetails.status?.value ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">CIF No.</span><span class="te-value te-mono">{{ cifNumber }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Joint Holders + Nominees -->
|
||||
<div class="te-panel">
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
Joint A/c Holders
|
||||
@if (accountDetails?.modeOfOperation) {
|
||||
<span class="te-mandate-badge">{{ modeOfOperationLabel }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
@if (jointHolders.length === 0) {
|
||||
<span class="te-dim">No joint holders</span>
|
||||
}
|
||||
@for (jh of jointHolders; track jh.id ?? $index) {
|
||||
<div class="te-joint-row">
|
||||
<span class="te-joint-name">{{ jh.displayName }}</span>
|
||||
<span class="te-badge" [class.te-badge--primary]="jh.isPrimary || jh.primary">
|
||||
{{ jh.role ?? (jh.isPrimary || jh.primary ? 'Primary' : 'Co-Holder') }}
|
||||
</span>
|
||||
@if (jh.sharePercentage) {
|
||||
<span class="te-dim">{{ jh.sharePercentage }}%</span>
|
||||
}
|
||||
@if (jh.signingAuthority) {
|
||||
<span
|
||||
class="te-sign-badge"
|
||||
[class.te-sign-badge--mandatory]="jh.signingAuthority?.toLowerCase() === 'mandatory'"
|
||||
[class.te-sign-badge--optional]="jh.signingAuthority?.toLowerCase() === 'optional'"
|
||||
>
|
||||
{{ jh.signingAuthority }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="te-panel__header te-panel__header--mt">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
Nominees
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
@if (nominees.length === 0) {
|
||||
<span class="te-dim">No nominees</span>
|
||||
}
|
||||
@for (n of nominees; track n.id ?? $index) {
|
||||
<div class="te-joint-row">
|
||||
<span class="te-joint-name">{{ n.nomineeDisplayName ?? '—' }}</span>
|
||||
<span class="te-dim">{{ n.relationshipName ?? '—' }}</span>
|
||||
@if (n.percentageShare) {
|
||||
<span class="te-badge">{{ n.percentageShare }}%</span>
|
||||
}
|
||||
@if (n.nomineeIsMinor) {
|
||||
<span class="te-badge" style="background: #fff7ed; color: #c2410c; border-color: #fed7aa">Minor</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other Info + KYC + Account Balances -->
|
||||
<div class="te-panel">
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
Other Information
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
<div class="te-field">
|
||||
<span class="te-label">Phone No.</span><span class="te-value">{{ clientPhone }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Client ID</span><span class="te-value te-mono">{{ accountDetails.clientId }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">PAN</span><span class="te-value te-mono">{{ panNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">KYC Status</span>
|
||||
<span class="te-value" [style.color]="kycDetails?.kycStatus === 300 ? '#059669' : '#f59e0b'">{{
|
||||
kycStatus
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">CKYC No.</span><span class="te-value te-mono">{{ ckycNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Aadhaar</span><span class="te-value te-mono">{{ aadhaarNumber }}</span>
|
||||
</div>
|
||||
<div class="te-field">
|
||||
<span class="te-label">Product</span>
|
||||
<span class="te-value">{{
|
||||
accountDetails.savingsProductName ||
|
||||
accountDetails.depositProductName ||
|
||||
accountDetails.productName ||
|
||||
'—'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="te-panel__header te-panel__header--mt">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="1" x2="12" y2="23" />
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||||
</svg>
|
||||
Account Information
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-num">1.</span><span class="te-bal-label">Current Balance</span>
|
||||
<span class="te-bal-value te-bal-value--cr"
|
||||
>{{ currencySymbol }}{{ currentBalance | number: '1.2-2' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-num">2.</span><span class="te-bal-label">Today's Clg. Amount</span>
|
||||
<span class="te-bal-value">{{ currencySymbol }}{{ todayClgAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-num">3.</span><span class="te-bal-label">Clear Balance (2-3)</span>
|
||||
<span class="te-bal-value te-bal-value--cr"
|
||||
>{{ currencySymbol }}{{ clearBalance | number: '1.2-2' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-num">4.</span><span class="te-bal-label">Freeze Amount</span>
|
||||
<span class="te-bal-value">{{ currencySymbol }}{{ freezeAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-num">5.</span><span class="te-bal-label">Withdrawable Amt.</span>
|
||||
<span class="te-bal-value te-bal-value--cr"
|
||||
>{{ currencySymbol }}{{ withdrawableAmount | number: '1.2-2' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NET BALANCE BAR -->
|
||||
<div class="te-balance-bar">
|
||||
<div class="te-bal-item">
|
||||
<span class="te-bal-item__label">Net Balance</span>
|
||||
<span class="te-bal-item__val te-bal-item__val--cr te-bal-item__val--big">
|
||||
{{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} CR
|
||||
</span>
|
||||
</div>
|
||||
<div class="te-bal-item">
|
||||
<span class="te-bal-item__label">A/c Type</span>
|
||||
<span class="te-bal-item__val">{{
|
||||
accountDetails.depositType?.value || accountDetails.accountType || selectedRow?.acTyp || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="te-bal-item">
|
||||
<span class="te-bal-item__label">Interest Rate</span>
|
||||
<span class="te-bal-item__val">{{ accountDetails.nominalAnnualInterestRate ?? 0 | number: '1.2-2' }}%</span>
|
||||
</div>
|
||||
<div class="te-bal-item">
|
||||
<span class="te-bal-item__label">Currency</span>
|
||||
<span class="te-bal-item__val">{{ accountDetails.currency?.code ?? 'INR' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM: Basic Info -->
|
||||
<div class="te-panel">
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
Basic Information
|
||||
</div>
|
||||
<div class="te-panel__body te-basic-grid">
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-label">Opening Balance</span
|
||||
><span class="te-bal-value">{{ currencySymbol }}{{ currentBalance | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-label">Cash Hold</span><span class="te-bal-value">0.00</span>
|
||||
</div>
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-label">Transfer Hold</span><span class="te-bal-value">0.00</span>
|
||||
</div>
|
||||
<div class="te-balance-row">
|
||||
<span class="te-bal-label">Clearing Hold</span><span class="te-bal-value">0.00</span>
|
||||
</div>
|
||||
<div class="te-balance-row te-balance-row--total">
|
||||
<span class="te-bal-label">Bal. After This Trans.</span>
|
||||
<span class="te-bal-value te-bal-value--cr">{{ currencySymbol }}{{ clearBalance | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── DENOMINATION + OPENING/CLOSING ──────────────────────────── -->
|
||||
<div class="te-denom-wrap">
|
||||
<!-- Opening Balance -->
|
||||
<div class="te-panel te-panel--denom">
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
Opening Position — {{ today }}
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
@if (!openingLoaded) {
|
||||
<span class="te-dim">Loading…</span>
|
||||
}
|
||||
@if (openingLoaded && openingDenoms.length === 0) {
|
||||
<span class="te-dim">No opening position found</span>
|
||||
}
|
||||
@if (openingLoaded && openingDenoms.length > 0) {
|
||||
<table class="te-denom-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Denomination</th>
|
||||
<th class="num">Opening Count</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (d of sortedOpeningDenoms; track d.denominationId) {
|
||||
@if (d.count > 0) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="te-denom-chip" [class.te-denom-chip--coin]="d.kind === 'coin'"
|
||||
>₹{{ d.denominationValue }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="num">{{ d.count }}</td>
|
||||
<td class="num te-cr">₹{{ d.count * d.denominationValue | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>Total</strong></td>
|
||||
<td></td>
|
||||
<td class="num te-cr">
|
||||
<strong>₹{{ openingTotal | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Denomination Entry -->
|
||||
<div class="te-panel te-panel--denom">
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="7" width="20" height="14" rx="2" />
|
||||
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" />
|
||||
</svg>
|
||||
Denomination Entry
|
||||
@if (denominationTotal > 0) {
|
||||
<span class="te-mandate-badge">₹{{ denominationTotal | number: '1.2-2' }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
<table class="te-denom-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Denomination</th>
|
||||
<th class="num">Count</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (d of allDenominations; track d.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="te-denom-chip" [class.te-denom-chip--coin]="d.kind === 'coin'">
|
||||
{{ d.label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="num">
|
||||
<div class="te-denom-stepper">
|
||||
<button type="button" (click)="bumpDenom(d, -1)" [disabled]="countFor(d) === 0">−</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
[value]="countFor(d) || ''"
|
||||
placeholder="0"
|
||||
(input)="onDenomChange(d, $any($event.target).value)"
|
||||
/>
|
||||
<button type="button" (click)="bumpDenom(d, 1)">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="num te-cr">
|
||||
{{ subtotalFor(d) > 0 ? (subtotalFor(d) | number: '1.2-2') : '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>Total</strong></td>
|
||||
<td></td>
|
||||
<td class="num te-cr">
|
||||
<strong>₹{{ denominationTotal | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@if (denominationTotal > 0) {
|
||||
<div class="te-denom-actions">
|
||||
<button class="te-btn te-btn--primary" type="button" (click)="applyDenominationTotal()">
|
||||
Apply ₹{{ denominationTotal | number: '1.2-2' }} to Amount
|
||||
</button>
|
||||
<button class="te-btn" type="button" (click)="clearDenominations()">Clear</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Closing Position -->
|
||||
<div class="te-panel te-panel--denom">
|
||||
<div class="te-panel__header">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
Closing Position (Preview)
|
||||
</div>
|
||||
<div class="te-panel__body">
|
||||
@if (closingDenoms.length === 0) {
|
||||
<span class="te-dim">Enter denomination to see closing position</span>
|
||||
}
|
||||
@if (closingDenoms.length > 0) {
|
||||
<table class="te-denom-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Denomination</th>
|
||||
<th class="num">Opening</th>
|
||||
<th class="num">{{ selectedRow?.dbCr === 'Cr' ? '+Deposit' : '-Withdrawal' }}</th>
|
||||
<th class="num">Closing</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (d of sortedClosingDenoms; track d.denominationValue + d.kind) {
|
||||
<tr [class.te-closing-row--neg]="d.closingCount < 0">
|
||||
<td>
|
||||
<span class="te-denom-chip" [class.te-denom-chip--coin]="d.kind === 'coin'"
|
||||
>₹{{ d.denominationValue }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="num">{{ d.openCount }}</td>
|
||||
<td class="num" [style.color]="selectedRow?.dbCr === 'Cr' ? '#059669' : '#dc2626'">
|
||||
{{ selectedRow?.dbCr === 'Cr' ? '+' : '-' }}{{ d.entered }}
|
||||
</td>
|
||||
<td class="num" [class.te-cr]="d.closingCount >= 0" [class.te-dr]="d.closingCount < 0">
|
||||
{{ d.closingCount }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>Total</strong></td>
|
||||
<td class="num">₹{{ openingTotal | number: '1.2-2' }}</td>
|
||||
<td class="num" [style.color]="selectedRow?.dbCr === 'Cr' ? '#059669' : '#dc2626'">
|
||||
{{ selectedRow?.dbCr === 'Cr' ? '+' : '-' }}₹{{ denominationTotal | number: '1.2-2' }}
|
||||
</td>
|
||||
<td class="num" [class.te-cr]="closingTotal >= 0" [class.te-dr]="closingTotal < 0">
|
||||
<strong>₹{{ closingTotal | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── ENTRY GRID (bottom) ─────────────────────────────────────── -->
|
||||
<div class="te-grid-wrap">
|
||||
<table class="te-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Typ</th>
|
||||
<th>Br.</th>
|
||||
<th>Tr.Tp.</th>
|
||||
<th>Actyp</th>
|
||||
<th>A/c No.</th>
|
||||
<th>Ecs A/c No.</th>
|
||||
<th>Db./Cr.</th>
|
||||
<th>Cheque No.</th>
|
||||
<th>Amount</th>
|
||||
<th>Description</th>
|
||||
<th>Tok.No.</th>
|
||||
<th>Cheq.Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of rows; track $index; let i = $index) {
|
||||
<tr [class.te-row--selected]="selectedRow === row" (click)="onRowSelect(row, i)">
|
||||
<td><input class="te-cell te-cell--xs" [(ngModel)]="row.typ" /></td>
|
||||
<td><input class="te-cell te-cell--xs" type="number" [(ngModel)]="row.br" /></td>
|
||||
<td>
|
||||
<select class="te-cell te-cell--sm" [(ngModel)]="row.trTp">
|
||||
<option value=""></option>
|
||||
@for (t of trTpOptions; track t) {
|
||||
<option [value]="t">{{ t }}</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select class="te-cell te-cell--sm" [(ngModel)]="row.acTyp">
|
||||
@for (a of acTypOptions; track a) {
|
||||
<option [value]="a">{{ a }}</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td><input class="te-cell te-cell--md" [(ngModel)]="row.acNo" (blur)="onAcNoBlur(row, i)" /></td>
|
||||
<td><input class="te-cell te-cell--lg" [(ngModel)]="row.ecsAcNo" /></td>
|
||||
<td>
|
||||
<select class="te-cell te-cell--sm" [(ngModel)]="row.dbCr">
|
||||
<option value=""></option>
|
||||
@for (d of dbCrOptions; track d) {
|
||||
<option [value]="d">{{ d }}</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td><input class="te-cell te-cell--md" [(ngModel)]="row.chequeNo" /></td>
|
||||
<td><input class="te-cell te-cell--md" type="number" [(ngModel)]="row.amount" /></td>
|
||||
<td><input class="te-cell te-cell--lg" [(ngModel)]="row.description" /></td>
|
||||
<td><input class="te-cell te-cell--sm" [(ngModel)]="row.tokNo" /></td>
|
||||
<td><input class="te-cell te-cell--sm" [(ngModel)]="row.cheqDate" placeholder="dd/mm/yy" /></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="te-grid-footer">
|
||||
<span class="te-eff-date"
|
||||
>Eff. Dt : <strong>{{ today }}</strong></span
|
||||
>
|
||||
<button class="te-add-btn" type="button" (click)="addRow()" title="Add row">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ACTION BUTTONS -->
|
||||
<div class="te-actions">
|
||||
<button class="te-btn te-btn--exit" type="button" (click)="onExit()">Exit</button>
|
||||
<button class="te-btn" type="button" (click)="onCancel()">Cancel</button>
|
||||
<button class="te-btn" type="button">Drop</button>
|
||||
<button class="te-btn" type="button" (click)="onSearch()">Search</button>
|
||||
<button class="te-btn te-btn--mode" type="button">Change Tr.Mode</button>
|
||||
<button class="te-btn te-btn--primary" type="button" (click)="onSave()" [disabled]="saving">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
{{ saving ? 'Posting…' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,839 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
font-family: Inter, Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// ── Root wrapper ──────────────────────────────────────────────────────────────
|
||||
|
||||
.te-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
// ── Entry Grid Card ───────────────────────────────────────────────────────────
|
||||
|
||||
.te-grid-wrap {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
.te-grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
|
||||
th {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
padding: 7px 8px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
border-right: 1px solid #334155;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border-right: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding: 3px 4px;
|
||||
background: #fff;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
tr.te-row--selected td {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--mini th {
|
||||
font-size: 10px;
|
||||
padding: 5px 6px;
|
||||
background: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
.te-cell {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 5px;
|
||||
background: #f8fafc;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 12px;
|
||||
padding: 4px 7px;
|
||||
color: #1e293b;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 2px rgb(59 130 246 / 15%);
|
||||
}
|
||||
|
||||
&--xs {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
&--sm {
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
&--md {
|
||||
width: 88px;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
width: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
select.te-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.te-grid-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.te-eff-date {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
|
||||
strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
.te-add-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: #0ea5e9;
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s;
|
||||
box-shadow: 0 2px 6px rgb(14 165 233 / 35%);
|
||||
|
||||
&:hover {
|
||||
background: #0284c7;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Status messages ──────────────────────────────────────────────────────────
|
||||
|
||||
.te-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: te-spin 0.7s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes te-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.te-error {
|
||||
padding: 10px 16px;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
color: #dc2626;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// ── Main Detail Panel ─────────────────────────────────────────────────────────
|
||||
|
||||
.te-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.te-detail-top {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 0.9fr 1.1fr;
|
||||
gap: 10px;
|
||||
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (width <= 700px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Panel Card ────────────────────────────────────────────────────────────────
|
||||
|
||||
.te-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 14px;
|
||||
background: linear-gradient(135deg, #1e293b, #334155);
|
||||
color: #e2e8f0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
|
||||
&--mt {
|
||||
margin-top: 0;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: linear-gradient(135deg, #334155, #475569);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 10px 14px;
|
||||
|
||||
&--center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Field rows ────────────────────────────────────────────────────────────────
|
||||
|
||||
.te-field {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px dotted #f1f5f9;
|
||||
padding-bottom: 4px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.te-field-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.te-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.te-label {
|
||||
min-width: 88px;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.te-value {
|
||||
color: #1e293b;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
|
||||
&-name {
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&-wrap {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.te-mono {
|
||||
font-family: 'Roboto Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.te-dim {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
// ── Joint holders / Nominees ──────────────────────────────────────────────────
|
||||
|
||||
.te-joint-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 12px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.te-joint-name {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.te-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
border: 1px solid #e2e8f0;
|
||||
white-space: nowrap;
|
||||
|
||||
&--primary {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
.te-mandate-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border: 1px solid #fde68a;
|
||||
margin-left: 8px;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.te-sign-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
|
||||
&--mandatory {
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
border: 1px solid #fdba74;
|
||||
}
|
||||
|
||||
&--optional {
|
||||
background: #f0fdf4;
|
||||
color: #15803d;
|
||||
border: 1px solid #86efac;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Balance rows ──────────────────────────────────────────────────────────────
|
||||
|
||||
.te-balance-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px dotted #f1f5f9;
|
||||
font-size: 12px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--total {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
border-bottom: none;
|
||||
font-weight: 700;
|
||||
padding-top: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.te-bal-num {
|
||||
width: 18px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.te-bal-label {
|
||||
flex: 1;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.te-bal-value {
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
min-width: 100px;
|
||||
color: #1e293b;
|
||||
|
||||
&--cr {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&--dr {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Net Balance Bar ───────────────────────────────────────────────────────────
|
||||
|
||||
.te-balance-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
.te-bal-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
font-size: 12px;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__val {
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&--cr {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&--big {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Denomination Section ──────────────────────────────────────────────────────
|
||||
|
||||
.te-denom-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (width <= 700px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.te-denom-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
text-align: left;
|
||||
|
||||
&.num {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid #f8fafc;
|
||||
color: #1e293b;
|
||||
font-size: 12px;
|
||||
|
||||
&.num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
tfoot td {
|
||||
background: #f1f5f9;
|
||||
border-top: 2px solid #e2e8f0;
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Section header row inside denomination table ──────────────────────────────
|
||||
|
||||
.te-denom-section-row td {
|
||||
padding: 4px 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
|
||||
&.te-denom-section-row__label {
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
}
|
||||
}
|
||||
|
||||
.te-denom-section-row--coin td.te-denom-section-row__label {
|
||||
color: #92400e;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.te-denom-section-label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
border-bottom: 1px solid #dbeafe;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding: 4px 10px;
|
||||
|
||||
&--coin {
|
||||
color: #92400e;
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
}
|
||||
|
||||
.te-denom-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 48px;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #ecfeff, #cffafe);
|
||||
color: #155e75;
|
||||
border: 1px solid #a5f3fc;
|
||||
|
||||
&--coin {
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
padding: 0;
|
||||
background: linear-gradient(135deg, #fef9c3, #fde68a);
|
||||
color: #854d0e;
|
||||
border-color: #fcd34d;
|
||||
}
|
||||
}
|
||||
|
||||
.te-denom-stepper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: auto;
|
||||
|
||||
button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #0ea5e9;
|
||||
border-color: #0ea5e9;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
background: #fff;
|
||||
outline: none;
|
||||
appearance: textfield;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:focus {
|
||||
border-color: #0ea5e9;
|
||||
box-shadow: 0 0 0 2px rgb(14 165 233 / 15%);
|
||||
}
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.te-denom-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.te-cr {
|
||||
color: #059669 !important;
|
||||
}
|
||||
|
||||
.te-dr {
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
|
||||
.te-closing-row--neg td {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
// ── Basic Info horizontal layout ─────────────────────────────────────────────
|
||||
|
||||
.te-basic-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
|
||||
.te-balance-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-right: 1px solid #f1f5f9;
|
||||
border-bottom: none;
|
||||
padding: 10px 16px;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&--total {
|
||||
border-top: none;
|
||||
padding-top: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.te-bal-label {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.te-bal-value {
|
||||
font-size: 15px;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Action Buttons ────────────────────────────────────────────────────────────
|
||||
|
||||
.te-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
.te-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: linear-gradient(135deg, #0ea5e9, #0284c7);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 2px 8px rgb(14 165 233 / 30%);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #0284c7, #0369a1);
|
||||
}
|
||||
}
|
||||
|
||||
&--exit {
|
||||
background: #fff;
|
||||
color: #ef4444;
|
||||
border-color: #fecaca;
|
||||
|
||||
&:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
|
||||
&--mode {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 2px 8px rgb(139 92 246 / 30%);
|
||||
min-width: 130px;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* 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 { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { CashierService } from '../cashier.service';
|
||||
|
||||
interface TxnRow {
|
||||
typ: string;
|
||||
br: number;
|
||||
trTp: string;
|
||||
acTyp: string;
|
||||
acNo: string;
|
||||
ecsAcNo: string;
|
||||
dbCr: string;
|
||||
chequeNo: string;
|
||||
amount: number | null;
|
||||
description: string;
|
||||
tokNo: string;
|
||||
cheqDate: string;
|
||||
}
|
||||
|
||||
interface Denomination {
|
||||
id: number;
|
||||
value: number;
|
||||
kind: 'note' | 'coin';
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-transaction-entry',
|
||||
templateUrl: './transaction-entry.component.html',
|
||||
styleUrls: ['./transaction-entry.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class TransactionEntryComponent implements OnInit {
|
||||
private http = inject(HttpClient);
|
||||
private cashierService = inject(CashierService);
|
||||
|
||||
today = '';
|
||||
rows: TxnRow[] = [];
|
||||
selectedRow: TxnRow | null = null;
|
||||
loadingAccount = false;
|
||||
accountError = '';
|
||||
|
||||
// Full account + client data
|
||||
accountDetails: any = null; // savings account API response
|
||||
clientDetails: any = null; // client personal details
|
||||
kycDetails: any = null; // KYC (PAN, Aadhaar, CKYC)
|
||||
nominees: any[] = []; // nominee list
|
||||
jointHolders: any[] = []; // joint holders
|
||||
clientAddresses: any[] = []; // client addresses
|
||||
|
||||
acTypOptions = [
|
||||
'SD',
|
||||
'CA',
|
||||
'FD',
|
||||
'RD',
|
||||
'SHR',
|
||||
'CC'
|
||||
];
|
||||
dbCrOptions = [
|
||||
'Dr',
|
||||
'Cr'
|
||||
];
|
||||
trTpOptions = [
|
||||
'C',
|
||||
'D',
|
||||
'T'
|
||||
];
|
||||
|
||||
// ── Denomination ──────────────────────────────────────────────────────────
|
||||
notes: Denomination[] = [];
|
||||
coins: Denomination[] = [];
|
||||
denominationCounts: Record<string, number> = {};
|
||||
|
||||
// ── Opening / Closing position ────────────────────────────────────────────
|
||||
openingDenoms: any[] = [];
|
||||
openingTotal = 0;
|
||||
openingLoaded = false;
|
||||
|
||||
// ── Transaction saving state ──────────────────────────────────────────────
|
||||
saving = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
const d = new Date();
|
||||
this.today = `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
||||
this.addRow();
|
||||
this.loadDenominations();
|
||||
this.loadOpeningPosition();
|
||||
}
|
||||
|
||||
private loadDenominations(): void {
|
||||
this.cashierService
|
||||
.getActiveDenominations()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((list: any[]) => {
|
||||
const rows: Denomination[] = (list || []).map((d: any) => ({
|
||||
id: d.id,
|
||||
value: d.denomination ?? d.value,
|
||||
kind: d.kind as 'note' | 'coin',
|
||||
label: '₹' + (d.denomination ?? d.value)
|
||||
}));
|
||||
const order = (a: Denomination, b: Denomination) => b.value - a.value;
|
||||
this.notes = rows.filter((r) => r.kind === 'note').sort(order);
|
||||
this.coins = rows.filter((r) => r.kind === 'coin').sort(order);
|
||||
});
|
||||
}
|
||||
|
||||
private loadOpeningPosition(): void {
|
||||
const d = new Date();
|
||||
const asOfDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
this.cashierService
|
||||
.getCashierPositionForMe(asOfDate)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
this.openingDenoms = res?.denominations ?? [];
|
||||
this.openingTotal = Number(res?.totalAmount ?? 0);
|
||||
this.openingLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Denomination helpers ──────────────────────────────────────────────────
|
||||
|
||||
denomKey(d: Denomination): string {
|
||||
return `${d.kind}:${d.value}`;
|
||||
}
|
||||
|
||||
countFor(d: Denomination): number {
|
||||
return this.denominationCounts[this.denomKey(d)] || 0;
|
||||
}
|
||||
|
||||
subtotalFor(d: Denomination): number {
|
||||
return this.countFor(d) * d.value;
|
||||
}
|
||||
|
||||
onDenomChange(d: Denomination, raw: any): void {
|
||||
this.denominationCounts[this.denomKey(d)] = Math.max(0, Math.floor(Number(raw) || 0));
|
||||
}
|
||||
|
||||
bumpDenom(d: Denomination, delta: number): void {
|
||||
this.denominationCounts[this.denomKey(d)] = Math.max(0, this.countFor(d) + delta);
|
||||
}
|
||||
|
||||
get notesTotal(): number {
|
||||
return this.notes.reduce((s, d) => s + this.subtotalFor(d), 0);
|
||||
}
|
||||
get coinsTotal(): number {
|
||||
return this.coins.reduce((s, d) => s + this.subtotalFor(d), 0);
|
||||
}
|
||||
get denominationTotal(): number {
|
||||
return this.notesTotal + this.coinsTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denomination Entry order: ALL notes (desc) first, then ALL coins (desc).
|
||||
* This matches how cashiers physically count cash — notes first, coins after.
|
||||
* Opening/Closing panels use value-based interleaving (reporting format).
|
||||
*/
|
||||
get allDenominations(): Denomination[] {
|
||||
const desc = (a: Denomination, b: Denomination) => b.value - a.value;
|
||||
return [
|
||||
...[...this.notes].sort(desc),
|
||||
...[...this.coins].sort(desc)
|
||||
];
|
||||
}
|
||||
|
||||
// ── Opening / Closing ─────────────────────────────────────────────────────
|
||||
|
||||
get openingNotes(): any[] {
|
||||
return this.openingDenoms.filter((d) => d.kind !== 'coin');
|
||||
}
|
||||
get openingCoins(): any[] {
|
||||
return this.openingDenoms.filter((d) => d.kind === 'coin');
|
||||
}
|
||||
|
||||
/** Opening sorted: all notes (desc) then all coins (desc) */
|
||||
get sortedOpeningDenoms(): any[] {
|
||||
const desc = (a: any, b: any) => b.denominationValue - a.denominationValue;
|
||||
const notes = this.openingDenoms.filter((d) => d.kind !== 'coin').sort(desc);
|
||||
const coins = this.openingDenoms.filter((d) => d.kind === 'coin').sort(desc);
|
||||
return [
|
||||
...notes,
|
||||
...coins
|
||||
];
|
||||
}
|
||||
|
||||
/** Closing sorted: all notes (desc) then all coins (desc) */
|
||||
get sortedClosingDenoms(): any[] {
|
||||
const desc = (a: any, b: any) => b.denominationValue - a.denominationValue;
|
||||
const notes = this.closingDenoms.filter((d) => d.kind !== 'coin').sort(desc);
|
||||
const coins = this.closingDenoms.filter((d) => d.kind === 'coin').sort(desc);
|
||||
return [
|
||||
...notes,
|
||||
...coins
|
||||
];
|
||||
}
|
||||
|
||||
get closingTotal(): number {
|
||||
const row = this.selectedRow;
|
||||
const sign = row?.dbCr === 'Cr' ? 1 : -1; // Cr = Deposit (+), Dr = Withdrawal (-)
|
||||
return this.openingTotal + this.denominationTotal * sign;
|
||||
}
|
||||
|
||||
get closingDenoms(): any[] {
|
||||
const openMap = new Map<string, any>();
|
||||
for (const d of this.openingDenoms) {
|
||||
openMap.set(`${d.kind}:${d.denominationValue}`, d);
|
||||
}
|
||||
const sign = this.selectedRow?.dbCr === 'Cr' ? 1 : -1;
|
||||
const result: any[] = [];
|
||||
for (const denom of [
|
||||
...this.notes,
|
||||
...this.coins
|
||||
]) {
|
||||
const key = this.denomKey(denom);
|
||||
const open = openMap.get(key);
|
||||
const openCount = open?.count ?? 0;
|
||||
const entered = this.countFor(denom);
|
||||
if (openCount === 0 && entered === 0) continue;
|
||||
result.push({
|
||||
denominationValue: denom.value,
|
||||
kind: denom.kind,
|
||||
openCount,
|
||||
entered,
|
||||
closingCount: openCount + entered * sign
|
||||
});
|
||||
}
|
||||
return result.sort((a, b) => b.denominationValue - a.denominationValue);
|
||||
}
|
||||
|
||||
applyDenominationTotal(): void {
|
||||
if (this.selectedRow) {
|
||||
this.selectedRow.amount = this.denominationTotal;
|
||||
}
|
||||
}
|
||||
|
||||
clearDenominations(): void {
|
||||
this.denominationCounts = {};
|
||||
}
|
||||
|
||||
addRow(): void {
|
||||
this.rows.push({
|
||||
typ: '~',
|
||||
br: 1,
|
||||
trTp: '',
|
||||
acTyp: 'SD',
|
||||
acNo: '',
|
||||
ecsAcNo: '',
|
||||
dbCr: '',
|
||||
chequeNo: '',
|
||||
amount: null,
|
||||
description: '',
|
||||
tokNo: '',
|
||||
cheqDate: ''
|
||||
});
|
||||
}
|
||||
|
||||
onAcNoBlur(row: TxnRow, index: number): void {
|
||||
if (!row.acNo.trim()) return;
|
||||
this.selectedRow = row;
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
|
||||
onRowSelect(row: TxnRow, index: number): void {
|
||||
this.selectedRow = row;
|
||||
// Only reload if account number changed or nothing is loaded yet
|
||||
const alreadyLoaded = this.accountDetails && this.accountDetails.accountNo === row.acNo.trim();
|
||||
if (row.acNo.trim() && !alreadyLoaded) {
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
}
|
||||
|
||||
private loadAccountDetails(acNo: string, acTyp: string): void {
|
||||
this.loadingAccount = true;
|
||||
this.accountDetails = null;
|
||||
this.clientDetails = null;
|
||||
this.kycDetails = null;
|
||||
this.nominees = [];
|
||||
this.jointHolders = [];
|
||||
this.clientAddresses = [];
|
||||
this.accountError = '';
|
||||
|
||||
const cfg = this.accountTypeConfig(acTyp);
|
||||
|
||||
// Step 1: search by accountNo on the correct endpoint for this account type
|
||||
this.http
|
||||
.get<any>(`${cfg.list}?accountNo=${encodeURIComponent(acNo)}&offset=0&limit=1`)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
const items: any[] = res?.pageItems ?? (Array.isArray(res) ? res : []);
|
||||
if (items.length > 0 && items[0].id) {
|
||||
this.fetchAllDetails(items[0].id, acNo, cfg);
|
||||
return;
|
||||
}
|
||||
// Fallback: global search
|
||||
this.http
|
||||
.get<any>(`/search?query=${encodeURIComponent(acNo)}&resource=${cfg.search}`)
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((found: any) => {
|
||||
const list: any[] = Array.isArray(found) ? found : [];
|
||||
if (list.length > 0 && list[0].entityId) {
|
||||
this.fetchAllDetails(list[0].entityId, acNo, cfg);
|
||||
} else {
|
||||
this.loadingAccount = false;
|
||||
this.accountError = 'Account not found for: ' + acNo;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Maps UI account type → { listEndpoint, detailEndpoint, nomineeType, searchResource } */
|
||||
private accountTypeConfig(acTyp: string): { list: string; detail: string; nominee: string; search: string } {
|
||||
const t = acTyp?.toUpperCase();
|
||||
if (t === 'CA' || t === 'CB' || t === 'CC') {
|
||||
return { list: '/currentaccounts', detail: '/currentaccounts', nominee: 'CB', search: 'currentaccount' };
|
||||
}
|
||||
if (t === 'FD') {
|
||||
return { list: '/fixeddepositaccounts', detail: '/fixeddepositaccounts', nominee: 'FD', search: 'fixeddeposit' };
|
||||
}
|
||||
if (t === 'RD') {
|
||||
return {
|
||||
list: '/recurringdepositaccounts',
|
||||
detail: '/recurringdepositaccounts',
|
||||
nominee: 'RD',
|
||||
search: 'recurringdeposit'
|
||||
};
|
||||
}
|
||||
// Default: SD / SB — Savings
|
||||
return { list: '/savingsaccounts', detail: '/savingsaccounts', nominee: 'SB', search: 'saving' };
|
||||
}
|
||||
|
||||
/** Maps UI account type code → backend accountType code for nominee filter */
|
||||
private toNomineeAccountType(acTyp: string): string {
|
||||
return this.accountTypeConfig(acTyp).nominee;
|
||||
}
|
||||
|
||||
/** Parallel fetch: account (with associations) + client + KYC + nominees */
|
||||
private fetchAllDetails(
|
||||
accountId: number,
|
||||
acNo: string,
|
||||
cfg: { list: string; detail: string; nominee: string; search: string }
|
||||
): void {
|
||||
this.http
|
||||
.get<any>(`${cfg.detail}/${accountId}?associations=all`)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((acc: any) => {
|
||||
if (!acc) {
|
||||
this.loadingAccount = false;
|
||||
this.accountError = 'Could not load account details.';
|
||||
return;
|
||||
}
|
||||
this.accountDetails = acc;
|
||||
this.jointHolders = acc.jointHolders ?? [];
|
||||
|
||||
const clientId = acc.clientId?.toString();
|
||||
if (!clientId) {
|
||||
this.loadingAccount = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const acType = cfg.nominee;
|
||||
|
||||
// Parallel: client + KYC + nominees + addresses + family members
|
||||
forkJoin({
|
||||
client: this.http.get<any>(`/clients/${clientId}`).pipe(catchError(() => of(null))),
|
||||
kyc: this.http.get<any>(`/clients/${clientId}/kyc`).pipe(catchError(() => of([]))),
|
||||
nominees: this.http
|
||||
.get<any>(`/clients/${clientId}/nominees?accountNo=${acNo}&accountType=${acType}`)
|
||||
.pipe(catchError(() => of([]))),
|
||||
addresses: this.http.get<any>(`/client/${clientId}/addresses`).pipe(catchError(() => of([]))),
|
||||
family: this.http.get<any>(`/clients/${clientId}/familymembers`).pipe(catchError(() => of([])))
|
||||
}).subscribe(({ client, kyc, nominees, addresses, family }) => {
|
||||
this.loadingAccount = false;
|
||||
this.clientDetails = { ...client, familyMembers: Array.isArray(family) ? family : [] };
|
||||
const kycArr: any[] = Array.isArray(kyc) ? kyc : kyc ? [kyc] : [];
|
||||
this.kycDetails = kycArr[0] ?? null;
|
||||
this.nominees = Array.isArray(nominees) ? nominees : [];
|
||||
this.clientAddresses = Array.isArray(addresses) ? addresses : [];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Balance getters ────────────────────────────────────────────────────────
|
||||
|
||||
get currentBalance(): number {
|
||||
return this.accountDetails?.summary?.accountBalance ?? 0;
|
||||
}
|
||||
|
||||
get clearBalance(): number {
|
||||
return this.accountDetails?.summary?.availableBalance ?? this.currentBalance;
|
||||
}
|
||||
|
||||
get todayClgAmount(): number {
|
||||
return this.accountDetails?.summary?.totalDepositsPostMaturityLevel ?? 0;
|
||||
}
|
||||
|
||||
get freezeAmount(): number {
|
||||
return this.accountDetails?.summary?.holdAmountBalance ?? 0;
|
||||
}
|
||||
|
||||
get withdrawableAmount(): number {
|
||||
const avail = this.accountDetails?.summary?.availableBalance ?? this.currentBalance;
|
||||
return Math.max(0, avail - this.freezeAmount);
|
||||
}
|
||||
|
||||
get currencySymbol(): string {
|
||||
return this.accountDetails?.currency?.displaySymbol ?? '₹';
|
||||
}
|
||||
|
||||
get modeOfOperationLabel(): string {
|
||||
const modeMap: Record<string, string> = {
|
||||
FORMER_OR_SURVIVOR: 'Former or Survivor',
|
||||
EITHER_OR_SURVIVOR: 'Either or Survivor',
|
||||
ANYONE_OR_SURVIVOR: 'Any One or Survivor',
|
||||
JOINTLY: 'Both Jointly',
|
||||
SINGLE: 'Single',
|
||||
OTHER: 'Other'
|
||||
};
|
||||
const mode = this.accountDetails?.modeOfOperation ?? '';
|
||||
return modeMap[mode] ?? mode.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
// ── Client detail getters ─────────────────────────────────────────────────
|
||||
|
||||
get clientName(): string {
|
||||
return this.clientDetails?.displayName ?? this.accountDetails?.clientName ?? '—';
|
||||
}
|
||||
|
||||
get guardianName(): string {
|
||||
// S/D/W/H/O derived from family members
|
||||
// API returns: firstName, middleName, lastName, relationship
|
||||
const family: any[] = this.clientDetails?.familyMembers ?? [];
|
||||
if (!family.length) return this.clientDetails?.guardianClientName ?? '—';
|
||||
|
||||
const priority = [
|
||||
'FATHER',
|
||||
'MOTHER',
|
||||
'HUSBAND',
|
||||
'WIFE',
|
||||
'SPOUSE'
|
||||
];
|
||||
const prefixMap: Record<string, string> = {
|
||||
FATHER: 'S/O',
|
||||
MOTHER: 'D/O',
|
||||
HUSBAND: 'W/O',
|
||||
WIFE: 'H/O',
|
||||
SPOUSE: 'W/O'
|
||||
};
|
||||
|
||||
for (const rel of priority) {
|
||||
const member = family.find((m: any) => (m.relationship ?? m.relationshipName ?? '').toUpperCase() === rel);
|
||||
if (member) {
|
||||
// API uses camelCase: firstName, middleName, lastName
|
||||
const name = [
|
||||
member.firstName,
|
||||
member.middleName,
|
||||
member.lastName
|
||||
]
|
||||
.filter((v) => v && v.trim())
|
||||
.join(' ')
|
||||
.trim();
|
||||
return name ? `${prefixMap[rel] ?? rel} ${name}` : (prefixMap[rel] ?? rel);
|
||||
}
|
||||
}
|
||||
// Fallback: first family member
|
||||
const m = family[0];
|
||||
const name = [
|
||||
m.firstName,
|
||||
m.middleName,
|
||||
m.lastName
|
||||
]
|
||||
.filter((v) => v && v.trim())
|
||||
.join(' ')
|
||||
.trim();
|
||||
return name || '—';
|
||||
}
|
||||
|
||||
get clientAddress(): string {
|
||||
// From /client/{id}/addresses — AddressData fields: addressLine1, townVillage, city, stateName, postalCode
|
||||
if (this.clientAddresses.length > 0) {
|
||||
const a = this.clientAddresses[0];
|
||||
return [
|
||||
a.addressLine1,
|
||||
a.addressLine2,
|
||||
a.addressLine3,
|
||||
a.townVillage,
|
||||
a.city,
|
||||
a.countyDistrict,
|
||||
a.stateName ?? a.stateProvinceLabel,
|
||||
a.postalCode
|
||||
]
|
||||
.filter((v) => v && v.toString().trim())
|
||||
.join(', ');
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
|
||||
get clientPhone(): string {
|
||||
const mob = this.clientDetails?.mobileNo ?? this.accountDetails?.clientMobileNo;
|
||||
const alt = this.clientDetails?.alternateMobileNo;
|
||||
return (
|
||||
[
|
||||
mob,
|
||||
alt
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ') || '—'
|
||||
);
|
||||
}
|
||||
|
||||
get clientEmail(): string {
|
||||
return this.clientDetails?.emailAddress ?? '—';
|
||||
}
|
||||
|
||||
get officeName(): string {
|
||||
return this.accountDetails?.officeName ?? this.clientDetails?.officeName ?? '—';
|
||||
}
|
||||
|
||||
get openingDate(): string {
|
||||
const d = this.accountDetails?.timeline?.submittedOnDate;
|
||||
return d ? `${d[2]}/${d[1]}/${d[0]}` : '—';
|
||||
}
|
||||
|
||||
get lastTransactionDate(): string {
|
||||
const d = this.accountDetails?.lastActiveTransactionDate;
|
||||
return d ? `${d[2]}/${d[1]}/${d[0]}` : '—';
|
||||
}
|
||||
|
||||
get panNumber(): string {
|
||||
return this.kycDetails?.panNumber ?? this.kycDetails?.pan_number ?? '—';
|
||||
}
|
||||
|
||||
get aadhaarNumber(): string {
|
||||
return this.kycDetails?.aadhaarNumberMasked ?? this.kycDetails?.aadhaar_number_masked ?? 'XXXX/XXXX/XXXX';
|
||||
}
|
||||
|
||||
get ckycNumber(): string {
|
||||
return this.kycDetails?.ckycNumber ?? this.kycDetails?.ckyc_number ?? '—';
|
||||
}
|
||||
|
||||
get kycStatus(): string {
|
||||
const s = this.kycDetails?.kycStatus ?? this.kycDetails?.kyc_status;
|
||||
if (!s) return '—';
|
||||
const map: Record<number, string> = { 100: 'Pending', 200: 'In Progress', 300: 'Verified', 400: 'Rejected' };
|
||||
return map[s] ?? `KYC-${s}`;
|
||||
}
|
||||
|
||||
get cifNumber(): string {
|
||||
return this.clientDetails?.cifNumber ?? this.clientDetails?.cif_number ?? '—';
|
||||
}
|
||||
|
||||
onSave(): void {
|
||||
const row = this.selectedRow;
|
||||
if (!row || !row.acNo.trim() || !this.accountDetails) return;
|
||||
|
||||
const amount = row.amount ?? this.denominationTotal;
|
||||
if (!amount || amount <= 0) {
|
||||
alert('Please enter amount or denomination.');
|
||||
return;
|
||||
}
|
||||
|
||||
const isDeposit = row.dbCr === 'Cr';
|
||||
const command = isDeposit ? 'deposit' : 'withdrawal';
|
||||
const accountId = this.accountDetails.id;
|
||||
const cfg = this.accountTypeConfig(row.acTyp);
|
||||
|
||||
const today = new Date();
|
||||
const txnDate = `${String(today.getDate()).padStart(2, '0')}/${String(today.getMonth() + 1).padStart(2, '0')}/${today.getFullYear()}`;
|
||||
|
||||
const payload: any = {
|
||||
transactionDate: txnDate,
|
||||
transactionAmount: amount,
|
||||
dateFormat: 'dd/MM/yyyy',
|
||||
locale: 'en',
|
||||
note: row.description || null
|
||||
};
|
||||
|
||||
if (row.chequeNo) payload['checkNumber'] = row.chequeNo;
|
||||
|
||||
if (this.denominationTotal > 0) {
|
||||
payload['denominationBreakdown'] = [
|
||||
...this.notes,
|
||||
...this.coins
|
||||
]
|
||||
.filter((d) => this.countFor(d) > 0)
|
||||
.map((d) => ({
|
||||
denominationId: d.id,
|
||||
denominationValue: d.value,
|
||||
kind: d.kind,
|
||||
count: this.countFor(d),
|
||||
subtotal: this.subtotalFor(d)
|
||||
}));
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
const params = new HttpParams().set('command', command);
|
||||
this.http
|
||||
.post(`${cfg.detail}/${accountId}/transactions`, payload, { params })
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.developerMessage ?? 'Transaction failed.';
|
||||
alert(msg);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
alert(`Transaction posted. Ref: ${res.resourceId ?? res.transactionId ?? 'N/A'}`);
|
||||
this.clearDenominations();
|
||||
this.loadOpeningPosition();
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.accountDetails = null;
|
||||
this.clientDetails = null;
|
||||
this.kycDetails = null;
|
||||
this.nominees = [];
|
||||
this.jointHolders = [];
|
||||
this.clientAddresses = [];
|
||||
this.accountError = '';
|
||||
this.selectedRow = null;
|
||||
this.rows = [];
|
||||
this.addRow();
|
||||
}
|
||||
|
||||
onSearch(): void {
|
||||
if (this.selectedRow?.acNo.trim()) {
|
||||
this.loadAccountDetails(this.selectedRow.acNo.trim(), this.selectedRow.acTyp);
|
||||
}
|
||||
}
|
||||
|
||||
onExit(): void {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
@@ -388,6 +388,30 @@ export class AuthenticationService {
|
||||
return JSON.parse(this.storage.getItem(this.credentialsStorageKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true ONLY for regular cashier role (not Head Cashier).
|
||||
* Regular cashier → full-screen transaction entry, no nav.
|
||||
* Head Cashier → normal CBS interface, all menus visible.
|
||||
*/
|
||||
isCashierUser(): boolean {
|
||||
const creds = this.getCredentials();
|
||||
if (!creds || !creds.permissions) return false;
|
||||
// Super-admin / all-functions users are never cashier-only
|
||||
if (creds.permissions.includes('ALL_FUNCTIONS')) return false;
|
||||
if (creds.roles && Array.isArray(creds.roles)) {
|
||||
const roleNames: string[] = creds.roles.map((r: any) => (r.name || r.role || '').toLowerCase().trim());
|
||||
// Head Cashier gets full access — NOT a cashier-only user
|
||||
if (roleNames.some((n) => n.includes('head cashier') || n === 'head_cashier')) {
|
||||
return false;
|
||||
}
|
||||
// Regular CASHIER role → cashier-only mode
|
||||
if (roleNames.some((n) => n === 'cashier')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user credentials.
|
||||
*
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<!-- Application Shell -->
|
||||
<mat-sidenav-container id="mifosx-shell-container" autosize>
|
||||
<!-- Sidenav -->
|
||||
<!-- Sidenav — always rendered so #sidenav ref exists; closed for cashier mode -->
|
||||
<mat-sidenav
|
||||
#sidenav
|
||||
class="sidebar-panel"
|
||||
@@ -18,15 +18,17 @@
|
||||
}"
|
||||
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
|
||||
[mode]="(isHandset$ | async) ? 'over' : 'side'"
|
||||
[opened]="(isHandset$ | async) === false"
|
||||
[opened]="!hideShellLayout && (isHandset$ | async) === false"
|
||||
>
|
||||
<mifosx-sidenav [sidenavCollapsed]="sidenavCollapsed"></mifosx-sidenav>
|
||||
@if (!hideShellLayout) {
|
||||
<mifosx-sidenav [sidenavCollapsed]="sidenavCollapsed"></mifosx-sidenav>
|
||||
}
|
||||
</mat-sidenav>
|
||||
|
||||
<!-- Content -->
|
||||
<mat-sidenav-content class="sidenav">
|
||||
<!-- Sidebar Toggle Handle (hidden on small/handset screens) -->
|
||||
@if ((isHandset$ | async) === false) {
|
||||
<mat-sidenav-content class="sidenav" [class.sidenav--fullscreen]="hideShellLayout">
|
||||
<!-- Sidebar Toggle Handle — hidden in cashier full-screen mode -->
|
||||
@if (!hideShellLayout && (isHandset$ | async) === false) {
|
||||
<div
|
||||
class="sidebar-handle"
|
||||
[class.collapsed]="sidenavCollapsed"
|
||||
@@ -52,9 +54,78 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Toolbar -->
|
||||
<!-- Toolbar always visible (shows CBSNexora, Branch, Session info) -->
|
||||
<mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar>
|
||||
|
||||
<!-- Cashier simplified menu bar — visible only in cashier full-screen mode -->
|
||||
@if (hideShellLayout) {
|
||||
<nav class="cashier-nav">
|
||||
<a
|
||||
class="cashier-nav__item cashier-nav__item--primary"
|
||||
[routerLink]="['/cashier/transaction-entry']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="keyboard"></fa-icon> Transaction Entry
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item"
|
||||
[routerLink]="['/cashier/share-collection']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="hand-holding-usd"></fa-icon> Share Collection
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item"
|
||||
[routerLink]="['/cashier/cash-exchange']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="exchange"></fa-icon> Cash Exchange
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item"
|
||||
[routerLink]="['/cashier/exchange-detail']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="globe"></fa-icon> Exchange Detail
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item"
|
||||
[routerLink]="['/cashier/inter-cashier']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="exchange-alt"></fa-icon> Inter-Cashier
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item"
|
||||
[routerLink]="['/cashier/excess-report']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="exclamation-circle"></fa-icon> Excess Report
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item"
|
||||
[routerLink]="['/cashier/cashier-position']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="wallet"></fa-icon> Position
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item"
|
||||
[routerLink]="['/cashier/cash-history']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="history"></fa-icon> Cash History
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item"
|
||||
[routerLink]="['/cashier/closing-report']"
|
||||
routerLinkActive="cashier-nav__item--active"
|
||||
>
|
||||
<fa-icon icon="file-alt"></fa-icon> Closing Report
|
||||
</a>
|
||||
</nav>
|
||||
}
|
||||
|
||||
<!-- Live KPI ticker items — referenced twice below for seamless marquee loop -->
|
||||
<ng-template #kpiTickerItems>
|
||||
<span class="ti"><span class="ti-k">CASH</span><span class="ti-v amber">₹14,82,500</span></span>
|
||||
@@ -206,133 +277,141 @@
|
||||
════════ END OLD ORDER ════════
|
||||
-->
|
||||
|
||||
<div class="main-header">
|
||||
<!-- Row 1 — Client Master · CFCFRMS · Approvals · Cashier · Clearing · Deposits · Advances · Share · NPA · General Ledger · Sub Ledger -->
|
||||
<div class="nav-row r1">
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="institutionMenu" *mifosxHasPermission="'MENU_INSTITUTION'">
|
||||
<fa-icon icon="users-cog" class="ni-ico"></fa-icon>Client Master
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" routerLink="/cfcfrms" *mifosxHasPermission="'SIDEBAR_CFCFRMS'">
|
||||
<fa-icon icon="user-shield" class="ni-ico"></fa-icon>CFCFRMS
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
routerLink="/checker-inbox-and-tasks/checker-inbox"
|
||||
*mifosxHasPermission="'MENU_TASKS'"
|
||||
>
|
||||
<fa-icon icon="check" class="ni-ico"></fa-icon>Approvals
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="cashierMenu" *mifosxHasPermission="'MENU_CASHIER'">
|
||||
<fa-icon icon="money-bill" class="ni-ico"></fa-icon>Cashier
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="clearingMenu" *mifosxHasPermission="'MENU_CLEARING'">
|
||||
<fa-icon icon="exchange" class="ni-ico"></fa-icon>Clearing
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="depositsMenu" *mifosxHasPermission="'MENU_DEPOSITS'">
|
||||
<fa-icon icon="piggy-bank" class="ni-ico"></fa-icon>Deposits
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu" *mifosxHasPermission="'MENU_ADVANCES'">
|
||||
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="shareMenu" *mifosxHasPermission="'MENU_SHARE'">
|
||||
<fa-icon icon="coins" class="ni-ico"></fa-icon>Share
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="npaMenu" *mifosxHasPermission="'MENU_NPA'">
|
||||
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="glMenuOpen = !glMenuOpen; subLedgerOpen = false"
|
||||
*mifosxHasPermission="'MENU_GENERAL_LEDGER'"
|
||||
>
|
||||
<fa-icon icon="book-open" class="ni-ico"></fa-icon>General Ledger
|
||||
<fa-icon [icon]="glMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="subLedgerOpen = !subLedgerOpen; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_SUB_LEDGER'"
|
||||
>
|
||||
<fa-icon icon="book" class="ni-ico"></fa-icon>Sub Ledger
|
||||
<fa-icon [icon]="subLedgerOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
@if (!hideShellLayout) {
|
||||
<div class="main-header">
|
||||
<!-- Row 1 — Client Master · CFCFRMS · Approvals · Cashier · Clearing · Deposits · Advances · Share · NPA · General Ledger · Sub Ledger -->
|
||||
<div class="nav-row r1">
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="institutionMenu" *mifosxHasPermission="'MENU_INSTITUTION'">
|
||||
<fa-icon icon="users-cog" class="ni-ico"></fa-icon>Client Master
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" routerLink="/cfcfrms" *mifosxHasPermission="'SIDEBAR_CFCFRMS'">
|
||||
<fa-icon icon="user-shield" class="ni-ico"></fa-icon>CFCFRMS
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
routerLink="/checker-inbox-and-tasks/checker-inbox"
|
||||
*mifosxHasPermission="'MENU_TASKS'"
|
||||
>
|
||||
<fa-icon icon="check" class="ni-ico"></fa-icon>Approvals
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="cashierMenu" *mifosxHasPermission="'MENU_CASHIER'">
|
||||
<fa-icon icon="money-bill" class="ni-ico"></fa-icon>Cashier
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="clearingMenu" *mifosxHasPermission="'MENU_CLEARING'">
|
||||
<fa-icon icon="exchange" class="ni-ico"></fa-icon>Clearing
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="depositsMenu" *mifosxHasPermission="'MENU_DEPOSITS'">
|
||||
<fa-icon icon="piggy-bank" class="ni-ico"></fa-icon>Deposits
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu" *mifosxHasPermission="'MENU_ADVANCES'">
|
||||
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="shareMenu" *mifosxHasPermission="'MENU_SHARE'">
|
||||
<fa-icon icon="coins" class="ni-ico"></fa-icon>Share
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="npaMenu" *mifosxHasPermission="'MENU_NPA'">
|
||||
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="glMenuOpen = !glMenuOpen; subLedgerOpen = false"
|
||||
*mifosxHasPermission="'MENU_GENERAL_LEDGER'"
|
||||
>
|
||||
<fa-icon icon="book-open" class="ni-ico"></fa-icon>General Ledger
|
||||
<fa-icon [icon]="glMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="subLedgerOpen = !subLedgerOpen; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_SUB_LEDGER'"
|
||||
>
|
||||
<fa-icon icon="book" class="ni-ico"></fa-icon>Sub Ledger
|
||||
<fa-icon [icon]="subLedgerOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Row 2 — Accounting · CKYC · Insurance · Digital · HRM · Admin · RBI · Reports · Others · Reconciliation · Documents Hub -->
|
||||
<div class="nav-row r2">
|
||||
<a class="ni" tabindex="0" routerLink="/accounting" *mifosxHasPermission="'MENU_ACCOUNTING'">
|
||||
<fa-icon icon="money-bill-alt" class="ni-ico"></fa-icon>Accounting
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="ckycMenu" *mifosxHasPermission="'MENU_CKYC_MGMT'">
|
||||
<fa-icon icon="fingerprint" class="ni-ico"></fa-icon>CKYC
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="insuranceMenu" *mifosxHasPermission="'MENU_INSURANCE'">
|
||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Insurance
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="digitalMenu" *mifosxHasPermission="'MENU_DIGITAL'">
|
||||
<fa-icon icon="mobile-alt" class="ni-ico"></fa-icon>Digital
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="hrmMenu" *mifosxHasPermission="'MENU_HRM'">
|
||||
<fa-icon icon="money-check" class="ni-ico"></fa-icon>HRM
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="adminMenu" *mifosxHasPermission="'MENU_ADMIN'">
|
||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Admin
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="rbiMenuOpen = !rbiMenuOpen; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_RBI'"
|
||||
>
|
||||
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
|
||||
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="reportsMenu" *mifosxHasPermission="'MENU_REPORTS'">
|
||||
<fa-icon icon="chart-bar" class="ni-ico"></fa-icon>Reports
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="othersMenu" *mifosxHasPermission="'MENU_OTHERS'">
|
||||
<fa-icon icon="folder-open" class="ni-ico"></fa-icon>Others
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
[matMenuTriggerFor]="reconciliationMenu"
|
||||
*mifosxHasPermission="'MENU_CONFIG_WIZARD'"
|
||||
>
|
||||
<fa-icon icon="sync-alt" class="ni-ico"></fa-icon>Reconciliation
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="letterMgmtMenu" *mifosxHasPermission="'MENU_LETTER_MGMT'">
|
||||
<fa-icon icon="envelope" class="ni-ico"></fa-icon>Documents Hub
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2 — Accounting · CKYC · Insurance · Digital · HRM · Admin · RBI · Reports · Others · Reconciliation · Documents Hub -->
|
||||
<div class="nav-row r2">
|
||||
<a class="ni" tabindex="0" routerLink="/accounting" *mifosxHasPermission="'MENU_ACCOUNTING'">
|
||||
<fa-icon icon="money-bill-alt" class="ni-ico"></fa-icon>Accounting
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="ckycMenu" *mifosxHasPermission="'MENU_CKYC_MGMT'">
|
||||
<fa-icon icon="fingerprint" class="ni-ico"></fa-icon>CKYC
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="insuranceMenu" *mifosxHasPermission="'MENU_INSURANCE'">
|
||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Insurance
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="digitalMenu" *mifosxHasPermission="'MENU_DIGITAL'">
|
||||
<fa-icon icon="mobile-alt" class="ni-ico"></fa-icon>Digital
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="hrmMenu" *mifosxHasPermission="'MENU_HRM'">
|
||||
<fa-icon icon="money-check" class="ni-ico"></fa-icon>HRM
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="adminMenu" *mifosxHasPermission="'MENU_ADMIN'">
|
||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Admin
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="rbiMenuOpen = !rbiMenuOpen; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_RBI'"
|
||||
>
|
||||
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
|
||||
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="reportsMenu" *mifosxHasPermission="'MENU_REPORTS'">
|
||||
<fa-icon icon="chart-bar" class="ni-ico"></fa-icon>Reports
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="othersMenu" *mifosxHasPermission="'MENU_OTHERS'">
|
||||
<fa-icon icon="folder-open" class="ni-ico"></fa-icon>Others
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="reconciliationMenu" *mifosxHasPermission="'MENU_CONFIG_WIZARD'">
|
||||
<fa-icon icon="sync-alt" class="ni-ico"></fa-icon>Reconciliation
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="letterMgmtMenu" *mifosxHasPermission="'MENU_LETTER_MGMT'">
|
||||
<fa-icon icon="envelope" class="ni-ico"></fa-icon>Documents Hub
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<!-- LIVE KPI TICKER (below the menu — pauses on hover) -->
|
||||
<div class="kpi-ticker" role="marquee" aria-label="Live key-performance ticker">
|
||||
<span class="kpi-live">
|
||||
<span class="kpi-live-dot"></span>
|
||||
LIVE
|
||||
</span>
|
||||
<div class="kpi-track">
|
||||
<!-- Render the item block twice so the marquee loops seamlessly. -->
|
||||
<ng-container *ngTemplateOutlet="kpiTickerItems"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="kpiTickerItems"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LIVE KPI TICKER (below the menu — pauses on hover) -->
|
||||
<div class="kpi-ticker" role="marquee" aria-label="Live key-performance ticker">
|
||||
<span class="kpi-live">
|
||||
<span class="kpi-live-dot"></span>
|
||||
LIVE
|
||||
</span>
|
||||
<div class="kpi-track">
|
||||
<!-- Render the item block twice so the marquee loops seamlessly. -->
|
||||
<ng-container *ngTemplateOutlet="kpiTickerItems"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="kpiTickerItems"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- end @if (!hideShellLayout) -->
|
||||
|
||||
<!-- Row 1 Menus -->
|
||||
<mat-menu #institutionMenu="matMenu" [overlapTrigger]="false" class="nav-dropdown">
|
||||
@@ -728,6 +807,9 @@
|
||||
<button mat-menu-item tabindex="0" [routerLink]="['/cashier/share-collection']">
|
||||
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Share Collection
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0" [routerLink]="['/cashier/transaction-entry']">
|
||||
<fa-icon icon="keyboard" class="menu-item-icon"></fa-icon> Transaction Entry
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #depositsMenu="matMenu" class="deposits-mega-panel" [overlapTrigger]="false">
|
||||
@@ -1607,7 +1689,9 @@
|
||||
|
||||
<!-- Content -->
|
||||
<mifosx-content></mifosx-content>
|
||||
<!-- Footer -->
|
||||
<mifosx-footer [styleClass]="'main-page'"></mifosx-footer>
|
||||
<!-- Footer — hidden in cashier full-screen mode -->
|
||||
@if (!hideShellLayout) {
|
||||
<mifosx-footer [styleClass]="'main-page'"></mifosx-footer>
|
||||
}
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
||||
@@ -87,10 +87,72 @@ $bar: 190px;
|
||||
$foo: $bar * 8;
|
||||
$color: #e2e4ec;
|
||||
|
||||
// ── Cashier simplified menu bar ──────────────────────────────────────────────
|
||||
.cashier-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
background: #1a2744;
|
||||
padding: 4px 12px;
|
||||
border-bottom: 2px solid #0ea5e9;
|
||||
|
||||
&__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
white-space: nowrap;
|
||||
|
||||
fa-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #223060;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #0ea5e9;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: #1353a0;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
|
||||
&:hover {
|
||||
background: #1a6ac8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
|
||||
&--fullscreen {
|
||||
margin-left: 0 !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
|
||||
mifosx-content {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Constrain routed page content so tables/cards shrink when sidebar opens
|
||||
mifosx-content {
|
||||
display: block;
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router, NavigationEnd } from '@angular/router';
|
||||
import { AuthenticationService } from '../authentication/authentication.service';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { map, filter } from 'rxjs/operators';
|
||||
|
||||
/** Custom Services */
|
||||
import { ProgressBarService } from '../progress-bar/progress-bar.service';
|
||||
@@ -60,6 +62,8 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
private progressBarService = inject(ProgressBarService);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
private dialog = inject(MatDialog);
|
||||
private router = inject(Router);
|
||||
private authService = inject(AuthenticationService);
|
||||
|
||||
/** Subscription to breakpoint observer for handset. */
|
||||
isHandset$: Observable<boolean> = this.breakpointObserver
|
||||
@@ -71,6 +75,9 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
progressBarMode: string;
|
||||
/** Subscription to progress bar. */
|
||||
progressBar$: Subscription;
|
||||
/** Hides nav/sidebar for full-screen cashier routes. */
|
||||
hideShellLayout = false;
|
||||
private routeSub: Subscription;
|
||||
|
||||
/** Sub Ledger mega menu state */
|
||||
subLedgerOpen = false;
|
||||
@@ -110,6 +117,14 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
this.progressBarMode = mode;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
|
||||
// Cashier users: always hide shell layout (role-based, not URL-based)
|
||||
const checkLayout = () => {
|
||||
this.hideShellLayout = this.authService.isCashierUser();
|
||||
this.cdr.detectChanges();
|
||||
};
|
||||
this.routeSub = this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe(() => checkLayout());
|
||||
setTimeout(() => checkLayout(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,5 +272,8 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
if (this.progressBar$) {
|
||||
this.progressBar$.unsubscribe();
|
||||
}
|
||||
if (this.routeSub) {
|
||||
this.routeSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,11 @@ export class HomeComponent implements OnInit, AfterViewInit {
|
||||
* Set Form.
|
||||
*/
|
||||
ngOnInit() {
|
||||
// Cashier users bypass the home dashboard entirely
|
||||
if (this.authenticationService.isCashierUser()) {
|
||||
this.router.navigate(['/cashier/transaction-entry'], { replaceUrl: true });
|
||||
return;
|
||||
}
|
||||
const credentials = this.authenticationService.getCredentials();
|
||||
this.username = credentials.username;
|
||||
this.tenant = this.tenantIdentifier();
|
||||
|
||||
@@ -50,6 +50,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { M3IconComponent } from '../shared/m3-ui/m3-icon/m3-icon.component';
|
||||
|
||||
import { VersionService } from '../system/version.service';
|
||||
import { AuthenticationService } from '../core/authentication/authentication.service';
|
||||
|
||||
/**
|
||||
* Login component.
|
||||
@@ -220,6 +221,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
private dialog = inject(MatDialog);
|
||||
private twoFactorDialogRef: MatDialogRef<TwoFactorAuthenticationComponent> | null = null;
|
||||
private router = inject(Router);
|
||||
private authService = inject(AuthenticationService);
|
||||
|
||||
private versionService = inject(VersionService);
|
||||
|
||||
@@ -283,7 +285,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
this.resetPassword = false;
|
||||
this.twoFactorAuthenticationRequired = false;
|
||||
this.closeTwoFactorDialog();
|
||||
this.router.navigate(['/'], { replaceUrl: true });
|
||||
// Cashier users go directly to the full-screen transaction entry dashboard
|
||||
const destination = this.authService.isCashierUser() ? ['/cashier/transaction-entry'] : ['/'];
|
||||
this.router.navigate(destination, { replaceUrl: true });
|
||||
} else if (alertType === 'Tenant Changed') {
|
||||
this.updateLogo();
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260602',
|
||||
'hash': '3be2f962'
|
||||
'version': '260603',
|
||||
'hash': '5f7de01b'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user