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-group animationDuration="150ms">
|
||||||
<mat-tab label="Cash Transfers ({{ transfers.length }})">
|
<mat-tab label="Cash Transfers ({{ transfersTotal }})">
|
||||||
<div class="pg-tab-body">
|
<div class="pg-tab-body">
|
||||||
<ng-container *ngTemplateOutlet="txnTable; context: { rows: transfers }"></ng-container>
|
<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>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
<mat-tab label="Settlements ({{ settlements.length }})">
|
<mat-tab label="Settlements ({{ settlementsTotal }})">
|
||||||
<div class="pg-tab-body">
|
<div class="pg-tab-body">
|
||||||
<ng-container *ngTemplateOutlet="txnTable; context: { rows: settlements }"></ng-container>
|
<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>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
<mat-tab label="Inter-Cashier ({{ interCashier.length }})">
|
<mat-tab label="Inter-Cashier ({{ interTotal }})">
|
||||||
<div class="pg-tab-body">
|
<div class="pg-tab-body">
|
||||||
<ng-container *ngTemplateOutlet="txnTable; context: { rows: interCashier }"></ng-container>
|
<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>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
<mat-tab label="Cash Exchanges ({{ exchanges.length }})">
|
<mat-tab label="Cash Exchanges ({{ exchangesTotal }})">
|
||||||
<div class="pg-tab-body">
|
<div class="pg-tab-body">
|
||||||
<ng-container *ngTemplateOutlet="txnTable; context: { rows: exchanges }"></ng-container>
|
<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>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
|
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||||
import { forkJoin, of } from 'rxjs';
|
import { forkJoin, of } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
import { CashierService } from '../cashier.service';
|
import { CashierService } from '../cashier.service';
|
||||||
@@ -65,7 +66,8 @@ interface DetailView {
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatSnackBarModule,
|
MatSnackBarModule,
|
||||||
MatTabsModule
|
MatTabsModule,
|
||||||
|
MatPaginatorModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CashHistoryComponent implements OnInit {
|
export class CashHistoryComponent implements OnInit {
|
||||||
@@ -78,6 +80,23 @@ export class CashHistoryComponent implements OnInit {
|
|||||||
exchanges: TxnRow[] = [];
|
exchanges: TxnRow[] = [];
|
||||||
loading = false;
|
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'>();
|
private denomKindMap = new Map<number, 'note' | 'coin'>();
|
||||||
|
|
||||||
reversingId: number | null = null;
|
reversingId: number | null = null;
|
||||||
@@ -96,15 +115,55 @@ export class CashHistoryComponent implements OnInit {
|
|||||||
this.load();
|
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 {
|
load(): void {
|
||||||
|
this.transfersOffset = 0;
|
||||||
|
this.settlementsOffset = 0;
|
||||||
|
this.interOffset = 0;
|
||||||
|
this.exchangesOffset = 0;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
forkJoin({
|
forkJoin({
|
||||||
transfers: this.service.getCashTransfers().pipe(catchError(() => of([]))),
|
transfers: this.service
|
||||||
settlements: this.service.getCashSettlements().pipe(catchError(() => of([]))),
|
.getCashTransfers(0, this.pageSize)
|
||||||
inter: this.service.getInterCashierTransfers().pipe(catchError(() => of([]))),
|
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))),
|
||||||
exchanges: this.service.getCashExchanges().pipe(catchError(() => of([])))
|
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 }) => {
|
}).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,
|
id: t.id,
|
||||||
date: t.transferDate,
|
date: t.transferDate,
|
||||||
fromParty: t.fromParty,
|
fromParty: t.fromParty,
|
||||||
@@ -119,7 +178,7 @@ export class CashHistoryComponent implements OnInit {
|
|||||||
reverseReason: t.reverseReason ?? null,
|
reverseReason: t.reverseReason ?? null,
|
||||||
type: 'transfer'
|
type: 'transfer'
|
||||||
}));
|
}));
|
||||||
this.settlements = (settlements as any[]).map((s) => ({
|
this.settlements = (settlementItems as any[]).map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
date: s.settlementDate,
|
date: s.settlementDate,
|
||||||
fromParty: s.fromParty,
|
fromParty: s.fromParty,
|
||||||
@@ -135,7 +194,7 @@ export class CashHistoryComponent implements OnInit {
|
|||||||
reverseReason: s.reverseReason ?? null,
|
reverseReason: s.reverseReason ?? null,
|
||||||
type: 'settlement'
|
type: 'settlement'
|
||||||
}));
|
}));
|
||||||
this.interCashier = (inter as any[]).map((i) => ({
|
this.interCashier = (interItems as any[]).map((i) => ({
|
||||||
id: i.id,
|
id: i.id,
|
||||||
date: i.transferDate,
|
date: i.transferDate,
|
||||||
fromParty: i.fromParty,
|
fromParty: i.fromParty,
|
||||||
@@ -150,7 +209,7 @@ export class CashHistoryComponent implements OnInit {
|
|||||||
reverseReason: i.reverseReason ?? null,
|
reverseReason: i.reverseReason ?? null,
|
||||||
type: 'inter'
|
type: 'inter'
|
||||||
}));
|
}));
|
||||||
this.exchanges = (exchanges as any[]).map((e) => ({
|
this.exchanges = (exchangeItems as any[]).map((e) => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
date: e.exchangeDate,
|
date: e.exchangeDate,
|
||||||
fromParty: e.accountNo ? `Account ${e.accountNo}` : 'Walk-in Customer',
|
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 {
|
viewDetail(row: TxnRow): void {
|
||||||
this.detail = {
|
this.detail = {
|
||||||
row,
|
row,
|
||||||
|
|||||||
@@ -18,34 +18,44 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="pg-toolbar pg-toolbar--mat">
|
<div class="pg-toolbar pg-toolbar--mat">
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<!-- Teller + Cashier filters — hidden for cashier role (auto-loads own position) -->
|
||||||
<mat-label>Teller (optional)</mat-label>
|
@if (!isCashierMode) {
|
||||||
<mat-select [value]="selectedTellerId" (selectionChange)="onTellerSelect($event.value)">
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
<mat-option [value]="null">All Tellers</mat-option>
|
<mat-label>Teller (optional)</mat-label>
|
||||||
@for (t of tellers; track t.id) {
|
<mat-select [value]="selectedTellerId" (selectionChange)="onTellerSelect($event.value)">
|
||||||
<mat-option [value]="t.id">{{ t.name }}</mat-option>
|
<mat-option [value]="null">All Tellers</mat-option>
|
||||||
}
|
@for (t of tellers; track t.id) {
|
||||||
</mat-select>
|
<mat-option [value]="t.id">{{ t.name }}</mat-option>
|
||||||
</mat-form-field>
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
<mat-label>Cashier (optional)</mat-label>
|
<mat-label>Cashier (optional)</mat-label>
|
||||||
<mat-select [value]="selectedCashierId" (selectionChange)="selectedCashierId = $event.value">
|
<mat-select [value]="selectedCashierId" (selectionChange)="selectedCashierId = $event.value">
|
||||||
<mat-option [value]="null">All Cashiers</mat-option>
|
<mat-option [value]="null">All Cashiers</mat-option>
|
||||||
@for (c of cashiers; track c.id) {
|
@for (c of cashiers; track c.id) {
|
||||||
<mat-option [value]="c.id">{{ c.label }}</mat-option>
|
<mat-option [value]="c.id">{{ c.label }}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
<mat-label>As of Date</mat-label>
|
<mat-label>As of Date</mat-label>
|
||||||
<input matInput type="date" [value]="asOfDate" (change)="asOfDate = $any($event.target).value" />
|
<input matInput type="date" [value]="asOfDate" (change)="asOfDate = $any($event.target).value" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="load()" [disabled]="loading">
|
@if (isCashierMode) {
|
||||||
<mat-icon>bar_chart</mat-icon> Load Position
|
<!-- Cashier mode: just a refresh button -->
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
@if (errorMsg) {
|
@if (errorMsg) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { forkJoin, of } from 'rxjs';
|
import { forkJoin, of } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
import { CashierService } from '../cashier.service';
|
import { CashierService } from '../cashier.service';
|
||||||
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
|
|
||||||
interface DenomRow {
|
interface DenomRow {
|
||||||
denominationId: number;
|
denominationId: number;
|
||||||
@@ -54,6 +55,7 @@ interface CashierPos {
|
|||||||
})
|
})
|
||||||
export class CashierPositionComponent implements OnInit {
|
export class CashierPositionComponent implements OnInit {
|
||||||
private service = inject(CashierService);
|
private service = inject(CashierService);
|
||||||
|
private authService = inject(AuthenticationService);
|
||||||
|
|
||||||
tellers: { id: number; name: string }[] = [];
|
tellers: { id: number; name: string }[] = [];
|
||||||
cashiers: { id: number; label: string }[] = [];
|
cashiers: { id: number; label: string }[] = [];
|
||||||
@@ -65,15 +67,64 @@ export class CashierPositionComponent implements OnInit {
|
|||||||
results: CashierPos[] = [];
|
results: CashierPos[] = [];
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
|
|
||||||
|
// True when logged-in user is a regular cashier — hides teller/cashier filters
|
||||||
|
isCashierMode = false;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
this.asOfDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
this.asOfDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
this.service
|
|
||||||
.getTellers()
|
this.isCashierMode = this.authService.isCashierUser();
|
||||||
.pipe(catchError(() => of([] as any[])))
|
|
||||||
.subscribe((list) => {
|
if (this.isCashierMode) {
|
||||||
this.tellers = (list || []).map((t: any) => ({ id: t.id, name: t.name }));
|
// 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 {
|
onTellerSelect(tellerId: number): void {
|
||||||
|
|||||||
@@ -82,6 +82,16 @@ const routes: Routes = [
|
|||||||
path: 'share-collection',
|
path: 'share-collection',
|
||||||
component: ShareCollectionComponent,
|
component: ShareCollectionComponent,
|
||||||
data: { title: 'Share Collection', breadcrumb: 'Share Collection' }
|
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 { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
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. */
|
/** HTTP service for the Cashier menu features. */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -24,8 +25,9 @@ export class CashierService {
|
|||||||
createCashExchange(data: any): Observable<any> {
|
createCashExchange(data: any): Observable<any> {
|
||||||
return this.http.post('/cashexchange', data);
|
return this.http.post('/cashexchange', data);
|
||||||
}
|
}
|
||||||
getCashExchanges(): Observable<any[]> {
|
getCashExchanges(offset = 0, limit = 20): Observable<any> {
|
||||||
return this.http.get<any[]>('/cashexchange');
|
const params = new HttpParams().set('offset', offset).set('limit', limit);
|
||||||
|
return this.http.get<any>('/cashexchange', { params });
|
||||||
}
|
}
|
||||||
getCashExchange(id: number): Observable<any> {
|
getCashExchange(id: number): Observable<any> {
|
||||||
return this.http.get(`/cashexchange/${id}`);
|
return this.http.get(`/cashexchange/${id}`);
|
||||||
@@ -49,8 +51,9 @@ export class CashierService {
|
|||||||
createCashTransfer(data: any): Observable<any> {
|
createCashTransfer(data: any): Observable<any> {
|
||||||
return this.http.post('/cashtransfer', data);
|
return this.http.post('/cashtransfer', data);
|
||||||
}
|
}
|
||||||
getCashTransfers(): Observable<any[]> {
|
getCashTransfers(offset = 0, limit = 20): Observable<any> {
|
||||||
return this.http.get<any[]>('/cashtransfer');
|
const params = new HttpParams().set('offset', offset).set('limit', limit);
|
||||||
|
return this.http.get<any>('/cashtransfer', { params });
|
||||||
}
|
}
|
||||||
getCashTransferById(id: number): Observable<any> {
|
getCashTransferById(id: number): Observable<any> {
|
||||||
return this.http.get(`/cashtransfer/${id}`);
|
return this.http.get(`/cashtransfer/${id}`);
|
||||||
@@ -63,8 +66,9 @@ export class CashierService {
|
|||||||
createCashSettlement(data: any): Observable<any> {
|
createCashSettlement(data: any): Observable<any> {
|
||||||
return this.http.post('/cashsettlement', data);
|
return this.http.post('/cashsettlement', data);
|
||||||
}
|
}
|
||||||
getCashSettlements(): Observable<any[]> {
|
getCashSettlements(offset = 0, limit = 20): Observable<any> {
|
||||||
return this.http.get<any[]>('/cashsettlement');
|
const params = new HttpParams().set('offset', offset).set('limit', limit);
|
||||||
|
return this.http.get<any>('/cashsettlement', { params });
|
||||||
}
|
}
|
||||||
getCashSettlementById(id: number): Observable<any> {
|
getCashSettlementById(id: number): Observable<any> {
|
||||||
return this.http.get(`/cashsettlement/${id}`);
|
return this.http.get(`/cashsettlement/${id}`);
|
||||||
@@ -77,8 +81,9 @@ export class CashierService {
|
|||||||
createInterCashierTransfer(data: any): Observable<any> {
|
createInterCashierTransfer(data: any): Observable<any> {
|
||||||
return this.http.post('/cashintercashier', data);
|
return this.http.post('/cashintercashier', data);
|
||||||
}
|
}
|
||||||
getInterCashierTransfers(): Observable<any[]> {
|
getInterCashierTransfers(offset = 0, limit = 20): Observable<any> {
|
||||||
return this.http.get<any[]>('/cashintercashier');
|
const params = new HttpParams().set('offset', offset).set('limit', limit);
|
||||||
|
return this.http.get<any>('/cashintercashier', { params });
|
||||||
}
|
}
|
||||||
getInterCashierById(id: number): Observable<any> {
|
getInterCashierById(id: number): Observable<any> {
|
||||||
return this.http.get(`/cashintercashier/${id}`);
|
return this.http.get(`/cashintercashier/${id}`);
|
||||||
@@ -157,6 +162,15 @@ export class CashierService {
|
|||||||
return this.http.get('/cashreconciliation/expected/me', { params });
|
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 ──
|
// ── Payment Types ──
|
||||||
getPaymentTypes(): Observable<any[]> {
|
getPaymentTypes(): Observable<any[]> {
|
||||||
return this.http.get<any[]>('/paymenttypes');
|
return this.http.get<any[]>('/paymenttypes');
|
||||||
|
|||||||
@@ -52,5 +52,15 @@
|
|||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<mat-paginator
|
||||||
|
[length]="totalRecords"
|
||||||
|
[pageSize]="pageSize"
|
||||||
|
[pageIndex]="pageIndex"
|
||||||
|
[pageSizeOptions]="pageSizeOptions"
|
||||||
|
showFirstLastButtons
|
||||||
|
(page)="onPageChange($event)"
|
||||||
|
>
|
||||||
|
</mat-paginator>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Component, OnInit, inject } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||||
import { CashierService } from '../cashier.service';
|
import { CashierService } from '../cashier.service';
|
||||||
|
|
||||||
/** Lists recorded cash exchanges (Currency Exchange Detail). */
|
/** Lists recorded cash exchanges (Currency Exchange Detail). */
|
||||||
@@ -21,25 +22,48 @@ import { CashierService } from '../cashier.service';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
MatIconModule
|
MatIconModule,
|
||||||
|
MatPaginatorModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CurrencyExchangeDetailComponent implements OnInit {
|
export class CurrencyExchangeDetailComponent implements OnInit {
|
||||||
private service = inject(CashierService);
|
private service = inject(CashierService);
|
||||||
|
|
||||||
exchanges: any[] = [];
|
exchanges: any[] = [];
|
||||||
|
totalRecords = 0;
|
||||||
|
pageSize = 20;
|
||||||
|
pageIndex = 0;
|
||||||
|
pageSizeOptions = [
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
100
|
||||||
|
];
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.service.getCashExchanges().subscribe({
|
this.load();
|
||||||
next: (list) => {
|
}
|
||||||
this.exchanges = list || [];
|
|
||||||
|
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;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.exchanges = [];
|
this.exchanges = [];
|
||||||
|
this.totalRecords = 0;
|
||||||
this.loading = false;
|
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));
|
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.
|
* Sets the user credentials.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<!-- Application Shell -->
|
<!-- Application Shell -->
|
||||||
<mat-sidenav-container id="mifosx-shell-container" autosize>
|
<mat-sidenav-container id="mifosx-shell-container" autosize>
|
||||||
<!-- Sidenav -->
|
<!-- Sidenav — always rendered so #sidenav ref exists; closed for cashier mode -->
|
||||||
<mat-sidenav
|
<mat-sidenav
|
||||||
#sidenav
|
#sidenav
|
||||||
class="sidebar-panel"
|
class="sidebar-panel"
|
||||||
@@ -18,15 +18,17 @@
|
|||||||
}"
|
}"
|
||||||
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
|
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
|
||||||
[mode]="(isHandset$ | async) ? 'over' : 'side'"
|
[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>
|
</mat-sidenav>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<mat-sidenav-content class="sidenav">
|
<mat-sidenav-content class="sidenav" [class.sidenav--fullscreen]="hideShellLayout">
|
||||||
<!-- Sidebar Toggle Handle (hidden on small/handset screens) -->
|
<!-- Sidebar Toggle Handle — hidden in cashier full-screen mode -->
|
||||||
@if ((isHandset$ | async) === false) {
|
@if (!hideShellLayout && (isHandset$ | async) === false) {
|
||||||
<div
|
<div
|
||||||
class="sidebar-handle"
|
class="sidebar-handle"
|
||||||
[class.collapsed]="sidenavCollapsed"
|
[class.collapsed]="sidenavCollapsed"
|
||||||
@@ -52,9 +54,78 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar always visible (shows CBSNexora, Branch, Session info) -->
|
||||||
<mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar>
|
<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 -->
|
<!-- Live KPI ticker items — referenced twice below for seamless marquee loop -->
|
||||||
<ng-template #kpiTickerItems>
|
<ng-template #kpiTickerItems>
|
||||||
<span class="ti"><span class="ti-k">CASH</span><span class="ti-v amber">₹14,82,500</span></span>
|
<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 ════════
|
════════ END OLD ORDER ════════
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<div class="main-header">
|
@if (!hideShellLayout) {
|
||||||
<!-- Row 1 — Client Master · CFCFRMS · Approvals · Cashier · Clearing · Deposits · Advances · Share · NPA · General Ledger · Sub Ledger -->
|
<div class="main-header">
|
||||||
<div class="nav-row r1">
|
<!-- Row 1 — Client Master · CFCFRMS · Approvals · Cashier · Clearing · Deposits · Advances · Share · NPA · General Ledger · Sub Ledger -->
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="institutionMenu" *mifosxHasPermission="'MENU_INSTITUTION'">
|
<div class="nav-row r1">
|
||||||
<fa-icon icon="users-cog" class="ni-ico"></fa-icon>Client Master
|
<a class="ni" tabindex="0" [matMenuTriggerFor]="institutionMenu" *mifosxHasPermission="'MENU_INSTITUTION'">
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="users-cog" class="ni-ico"></fa-icon>Client Master
|
||||||
</a>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
<a class="ni" tabindex="0" routerLink="/cfcfrms" *mifosxHasPermission="'SIDEBAR_CFCFRMS'">
|
</a>
|
||||||
<fa-icon icon="user-shield" class="ni-ico"></fa-icon>CFCFRMS
|
<a class="ni" tabindex="0" routerLink="/cfcfrms" *mifosxHasPermission="'SIDEBAR_CFCFRMS'">
|
||||||
</a>
|
<fa-icon icon="user-shield" class="ni-ico"></fa-icon>CFCFRMS
|
||||||
<a
|
</a>
|
||||||
class="ni"
|
<a
|
||||||
tabindex="0"
|
class="ni"
|
||||||
routerLink="/checker-inbox-and-tasks/checker-inbox"
|
tabindex="0"
|
||||||
*mifosxHasPermission="'MENU_TASKS'"
|
routerLink="/checker-inbox-and-tasks/checker-inbox"
|
||||||
>
|
*mifosxHasPermission="'MENU_TASKS'"
|
||||||
<fa-icon icon="check" class="ni-ico"></fa-icon>Approvals
|
>
|
||||||
</a>
|
<fa-icon icon="check" class="ni-ico"></fa-icon>Approvals
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="cashierMenu" *mifosxHasPermission="'MENU_CASHIER'">
|
</a>
|
||||||
<fa-icon icon="money-bill" class="ni-ico"></fa-icon>Cashier
|
<a class="ni" tabindex="0" [matMenuTriggerFor]="cashierMenu" *mifosxHasPermission="'MENU_CASHIER'">
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="money-bill" class="ni-ico"></fa-icon>Cashier
|
||||||
</a>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="clearingMenu" *mifosxHasPermission="'MENU_CLEARING'">
|
</a>
|
||||||
<fa-icon icon="exchange" class="ni-ico"></fa-icon>Clearing
|
<a class="ni" tabindex="0" [matMenuTriggerFor]="clearingMenu" *mifosxHasPermission="'MENU_CLEARING'">
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="exchange" class="ni-ico"></fa-icon>Clearing
|
||||||
</a>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="depositsMenu" *mifosxHasPermission="'MENU_DEPOSITS'">
|
</a>
|
||||||
<fa-icon icon="piggy-bank" class="ni-ico"></fa-icon>Deposits
|
<a class="ni" tabindex="0" [matMenuTriggerFor]="depositsMenu" *mifosxHasPermission="'MENU_DEPOSITS'">
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="piggy-bank" class="ni-ico"></fa-icon>Deposits
|
||||||
</a>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu" *mifosxHasPermission="'MENU_ADVANCES'">
|
</a>
|
||||||
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
|
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu" *mifosxHasPermission="'MENU_ADVANCES'">
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
|
||||||
</a>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="shareMenu" *mifosxHasPermission="'MENU_SHARE'">
|
</a>
|
||||||
<fa-icon icon="coins" class="ni-ico"></fa-icon>Share
|
<a class="ni" tabindex="0" [matMenuTriggerFor]="shareMenu" *mifosxHasPermission="'MENU_SHARE'">
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="coins" class="ni-ico"></fa-icon>Share
|
||||||
</a>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="npaMenu" *mifosxHasPermission="'MENU_NPA'">
|
</a>
|
||||||
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
|
<a class="ni" tabindex="0" [matMenuTriggerFor]="npaMenu" *mifosxHasPermission="'MENU_NPA'">
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
|
||||||
</a>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
<a
|
</a>
|
||||||
class="ni"
|
<a
|
||||||
tabindex="0"
|
class="ni"
|
||||||
(click)="glMenuOpen = !glMenuOpen; subLedgerOpen = false"
|
tabindex="0"
|
||||||
*mifosxHasPermission="'MENU_GENERAL_LEDGER'"
|
(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>
|
<fa-icon icon="book-open" class="ni-ico"></fa-icon>General Ledger
|
||||||
</a>
|
<fa-icon [icon]="glMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||||
<a
|
</a>
|
||||||
class="ni"
|
<a
|
||||||
tabindex="0"
|
class="ni"
|
||||||
(click)="subLedgerOpen = !subLedgerOpen; glMenuOpen = false"
|
tabindex="0"
|
||||||
*mifosxHasPermission="'MENU_SUB_LEDGER'"
|
(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>
|
<fa-icon icon="book" class="ni-ico"></fa-icon>Sub Ledger
|
||||||
</a>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Row 2 — Accounting · CKYC · Insurance · Digital · HRM · Admin · RBI · Reports · Others · Reconciliation · Documents Hub -->
|
<!-- LIVE KPI TICKER (below the menu — pauses on hover) -->
|
||||||
<div class="nav-row r2">
|
<div class="kpi-ticker" role="marquee" aria-label="Live key-performance ticker">
|
||||||
<a class="ni" tabindex="0" routerLink="/accounting" *mifosxHasPermission="'MENU_ACCOUNTING'">
|
<span class="kpi-live">
|
||||||
<fa-icon icon="money-bill-alt" class="ni-ico"></fa-icon>Accounting
|
<span class="kpi-live-dot"></span>
|
||||||
</a>
|
LIVE
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="ckycMenu" *mifosxHasPermission="'MENU_CKYC_MGMT'">
|
</span>
|
||||||
<fa-icon icon="fingerprint" class="ni-ico"></fa-icon>CKYC
|
<div class="kpi-track">
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<!-- Render the item block twice so the marquee loops seamlessly. -->
|
||||||
</a>
|
<ng-container *ngTemplateOutlet="kpiTickerItems"></ng-container>
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="insuranceMenu" *mifosxHasPermission="'MENU_INSURANCE'">
|
<ng-container *ngTemplateOutlet="kpiTickerItems"></ng-container>
|
||||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Insurance
|
</div>
|
||||||
<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>
|
||||||
</div>
|
}
|
||||||
|
<!-- end @if (!hideShellLayout) -->
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- Row 1 Menus -->
|
<!-- Row 1 Menus -->
|
||||||
<mat-menu #institutionMenu="matMenu" [overlapTrigger]="false" class="nav-dropdown">
|
<mat-menu #institutionMenu="matMenu" [overlapTrigger]="false" class="nav-dropdown">
|
||||||
@@ -728,6 +807,9 @@
|
|||||||
<button mat-menu-item tabindex="0" [routerLink]="['/cashier/share-collection']">
|
<button mat-menu-item tabindex="0" [routerLink]="['/cashier/share-collection']">
|
||||||
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Share Collection
|
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Share Collection
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<mat-menu #depositsMenu="matMenu" class="deposits-mega-panel" [overlapTrigger]="false">
|
<mat-menu #depositsMenu="matMenu" class="deposits-mega-panel" [overlapTrigger]="false">
|
||||||
@@ -1607,7 +1689,9 @@
|
|||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<mifosx-content></mifosx-content>
|
<mifosx-content></mifosx-content>
|
||||||
<!-- Footer -->
|
<!-- Footer — hidden in cashier full-screen mode -->
|
||||||
<mifosx-footer [styleClass]="'main-page'"></mifosx-footer>
|
@if (!hideShellLayout) {
|
||||||
|
<mifosx-footer [styleClass]="'main-page'"></mifosx-footer>
|
||||||
|
}
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
</mat-sidenav-container>
|
</mat-sidenav-container>
|
||||||
|
|||||||
@@ -87,10 +87,72 @@ $bar: 190px;
|
|||||||
$foo: $bar * 8;
|
$foo: $bar * 8;
|
||||||
$color: #e2e4ec;
|
$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 {
|
.sidenav {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
position: relative;
|
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
|
// Constrain routed page content so tables/cards shrink when sidebar opens
|
||||||
mifosx-content {
|
mifosx-content {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { Router, NavigationEnd } from '@angular/router';
|
||||||
|
import { AuthenticationService } from '../authentication/authentication.service';
|
||||||
|
|
||||||
/** rxjs Imports */
|
/** rxjs Imports */
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map, filter } from 'rxjs/operators';
|
||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { ProgressBarService } from '../progress-bar/progress-bar.service';
|
import { ProgressBarService } from '../progress-bar/progress-bar.service';
|
||||||
@@ -60,6 +62,8 @@ export class ShellComponent implements OnInit, OnDestroy {
|
|||||||
private progressBarService = inject(ProgressBarService);
|
private progressBarService = inject(ProgressBarService);
|
||||||
private cdr = inject(ChangeDetectorRef);
|
private cdr = inject(ChangeDetectorRef);
|
||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
|
private router = inject(Router);
|
||||||
|
private authService = inject(AuthenticationService);
|
||||||
|
|
||||||
/** Subscription to breakpoint observer for handset. */
|
/** Subscription to breakpoint observer for handset. */
|
||||||
isHandset$: Observable<boolean> = this.breakpointObserver
|
isHandset$: Observable<boolean> = this.breakpointObserver
|
||||||
@@ -71,6 +75,9 @@ export class ShellComponent implements OnInit, OnDestroy {
|
|||||||
progressBarMode: string;
|
progressBarMode: string;
|
||||||
/** Subscription to progress bar. */
|
/** Subscription to progress bar. */
|
||||||
progressBar$: Subscription;
|
progressBar$: Subscription;
|
||||||
|
/** Hides nav/sidebar for full-screen cashier routes. */
|
||||||
|
hideShellLayout = false;
|
||||||
|
private routeSub: Subscription;
|
||||||
|
|
||||||
/** Sub Ledger mega menu state */
|
/** Sub Ledger mega menu state */
|
||||||
subLedgerOpen = false;
|
subLedgerOpen = false;
|
||||||
@@ -110,6 +117,14 @@ export class ShellComponent implements OnInit, OnDestroy {
|
|||||||
this.progressBarMode = mode;
|
this.progressBarMode = mode;
|
||||||
this.cdr.detectChanges();
|
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$) {
|
if (this.progressBar$) {
|
||||||
this.progressBar$.unsubscribe();
|
this.progressBar$.unsubscribe();
|
||||||
}
|
}
|
||||||
|
if (this.routeSub) {
|
||||||
|
this.routeSub.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ export class HomeComponent implements OnInit, AfterViewInit {
|
|||||||
* Set Form.
|
* Set Form.
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
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();
|
const credentials = this.authenticationService.getCredentials();
|
||||||
this.username = credentials.username;
|
this.username = credentials.username;
|
||||||
this.tenant = this.tenantIdentifier();
|
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 { M3IconComponent } from '../shared/m3-ui/m3-icon/m3-icon.component';
|
||||||
|
|
||||||
import { VersionService } from '../system/version.service';
|
import { VersionService } from '../system/version.service';
|
||||||
|
import { AuthenticationService } from '../core/authentication/authentication.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login component.
|
* Login component.
|
||||||
@@ -220,6 +221,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
private twoFactorDialogRef: MatDialogRef<TwoFactorAuthenticationComponent> | null = null;
|
private twoFactorDialogRef: MatDialogRef<TwoFactorAuthenticationComponent> | null = null;
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
private authService = inject(AuthenticationService);
|
||||||
|
|
||||||
private versionService = inject(VersionService);
|
private versionService = inject(VersionService);
|
||||||
|
|
||||||
@@ -283,7 +285,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
this.resetPassword = false;
|
this.resetPassword = false;
|
||||||
this.twoFactorAuthenticationRequired = false;
|
this.twoFactorAuthenticationRequired = false;
|
||||||
this.closeTwoFactorDialog();
|
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') {
|
} else if (alertType === 'Tenant Changed') {
|
||||||
this.updateLogo();
|
this.updateLogo();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260602',
|
'version': '260603',
|
||||||
'hash': '3be2f962'
|
'hash': '5f7de01b'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user