feat(clerk): add dedicated nav pages, state restore, and fullscreen ESC fix

- Add ClerkUiStateService with full account state save/restore (no double network call on back navigation)
- Add Photo & Signature, Standing Instructions, Today's Activity as dedicated nav pages
- Remove photo/signature, standing instructions, linked accounts, today's activity panels from main dashboard
- Add ESC key to exit fullscreen; clicking Clerk nav immediately restores fullscreen without re-navigating
- Compact clerk dashboard UI with reduced font sizes and padding throughout
- Add CashierOnlyGuard for route protection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-09 12:49:06 +05:30
co-authored by Claude Sonnet 4.6
parent f7d9259eb7
commit 40a436dabe
20 changed files with 3708 additions and 1020 deletions
+42
View File
@@ -10,6 +10,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Route } from '../core/route/route.service';
import { CashierOnlyGuard } from '../core/authentication/cashier-only.guard';
import { CashExchangeEntryComponent } from './cash-exchange-entry/cash-exchange-entry.component';
import { CurrencyExchangeDetailComponent } from './currency-exchange-detail/currency-exchange-detail.component';
import { CashTransferComponent } from './cash-transfer/cash-transfer.component';
@@ -24,6 +25,46 @@ import { ShareCollectionComponent } from './share-collection/share-collection.co
const routes: Routes = [
Route.withShell([
{
path: 'clerk',
data: { title: 'Clerk', breadcrumb: 'Clerk' },
children: [
{
path: 'dashboard',
loadComponent: () =>
import('../clerk/clerk-dashboard/clerk-dashboard.component').then((m) => m.ClerkDashboardComponent),
data: { title: 'Clerk Dashboard', breadcrumb: 'Clerk Dashboard' }
},
{
path: 'account-facilities',
loadComponent: () =>
import('../clerk/account-facilities/account-facilities.component').then(
(m) => m.AccountFacilitiesComponent
),
data: { title: 'Account Facilities', breadcrumb: 'Account Facilities' }
},
{
path: 'photo-signature',
loadComponent: () =>
import('../clerk/photo-signature/photo-signature.component').then((m) => m.PhotoSignatureComponent),
data: { title: 'Photo & Signature', breadcrumb: 'Photo & Signature' }
},
{
path: 'standing-instructions',
loadComponent: () =>
import('../clerk/standing-instructions/standing-instructions.component').then(
(m) => m.StandingInstructionsComponent
),
data: { title: 'Standing Instructions', breadcrumb: 'Standing Instructions' }
},
{
path: 'today-activity',
loadComponent: () =>
import('../clerk/today-activity/today-activity.component').then((m) => m.TodayActivityComponent),
data: { title: "Today's Activity", breadcrumb: "Today's Activity" }
}
]
},
{
path: 'cashier',
data: { title: 'Cashier', breadcrumb: 'Cashier' },
@@ -85,6 +126,7 @@ const routes: Routes = [
},
{
path: 'transaction-entry',
canActivate: [CashierOnlyGuard],
loadComponent: () =>
import('./transaction-entry/transaction-entry.component').then((m) => m.TransactionEntryComponent),
data: {
+63
View File
@@ -0,0 +1,63 @@
/**
* 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 { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
export interface ClerkSavedAccount {
accountDetails: any;
clientDetails: any;
kycDetails: any;
nominees: any[];
jointHolders: any[];
clientAddresses: any[];
nonPersonDetails: any;
clientPhotoUrl: string | null;
signatureUrl: string | null;
accountNotes: any[];
standingInstructions: any[];
todayTransactions: any[];
linkedAccounts: any;
sameDayNeftRtgs: number;
sameDayChequeClearing: number;
selectedRow: any;
}
export interface ClerkUiState {
accountLoaded: boolean;
facilities: { label: string; active: boolean }[];
savedRow: { acNo: string; acTyp: string } | null;
savedAccount: ClerkSavedAccount | null;
}
@Injectable({ providedIn: 'root' })
export class ClerkUiStateService {
private readonly _state = new BehaviorSubject<ClerkUiState>({
accountLoaded: false,
facilities: [],
savedRow: null,
savedAccount: null
});
readonly state$ = this._state.asObservable();
get snapshot(): ClerkUiState {
return this._state.value;
}
setFacilities(facilities: { label: string; active: boolean }[], acNo: string, acTyp: string): void {
this._state.next({ ...this._state.value, accountLoaded: true, facilities, savedRow: { acNo, acTyp } });
}
saveFullState(account: ClerkSavedAccount): void {
this._state.next({ ...this._state.value, savedAccount: account });
}
clear(): void {
this._state.next({ accountLoaded: false, facilities: [], savedRow: null, savedAccount: null });
}
}
File diff suppressed because it is too large Load Diff
@@ -8,10 +8,9 @@
:host {
display: block;
min-height: 100vh;
background: #f0f2f5;
font-family: Inter, Roboto, sans-serif;
font-size: 13px;
font-size: 12px;
}
// ── Root wrapper ──────────────────────────────────────────────────────────────
@@ -19,8 +18,8 @@
.te-root {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 14px;
gap: 6px;
padding: 6px 10px;
}
// ── Day-status blocking banners ───────────────────────────────────────────────
@@ -60,6 +59,223 @@
}
}
// ── Cashier compact account info strip ───────────────────────────────────────
.te-acs {
display: flex;
align-items: center;
background: #fff;
border: 1px solid #dce4ef;
border-radius: 8px;
overflow: hidden;
min-height: 44px;
}
.te-acs-id {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
flex-shrink: 0;
}
.te-acs-photo {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
border: 2px solid #dce4ef;
&--ph {
background: #f0f4f9;
display: flex;
align-items: center;
justify-content: center;
}
&--clickable {
cursor: zoom-in;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:hover {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgb(37 99 235 / 18%);
}
}
}
.te-acs-nameblock {
display: flex;
flex-direction: column;
gap: 1px;
}
.te-acs-name {
font-size: 12px;
font-weight: 700;
color: #0d1b2e;
white-space: nowrap;
}
.te-acs-acno {
font-size: 10px;
font-family: 'Roboto Mono', monospace;
color: #5a7089;
letter-spacing: 0.4px;
}
.te-acs-sep {
width: 1px;
align-self: stretch;
background: #dce4ef;
flex-shrink: 0;
}
.te-acs-meta {
display: flex;
align-items: center;
gap: 6px;
padding: 0 10px;
flex-shrink: 0;
}
.te-acs-tag {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 7px;
border-radius: 10px;
font-size: 10px;
font-weight: 700;
background: #f0fdf4;
color: #16a34a;
border: 1px solid #bbf7d0;
&--blocked {
background: #fef2f2;
color: #dc2626;
border-color: #fecaca;
}
&--dormant {
background: #fffbeb;
color: #d97706;
border-color: #fde68a;
}
}
.te-acs-kv {
font-size: 11px;
color: #4a607a;
white-space: nowrap;
b {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
color: #8aa0b8;
margin-right: 2px;
}
}
.te-acs-bals {
display: flex;
align-items: stretch;
flex: 1;
}
.te-acs-b {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 5px 10px;
border-right: 1px solid #f0f4f9;
gap: 1px;
&:last-child {
border-right: none;
}
&--main {
background: #f8fbff;
}
&--after {
background: #f0fdf6;
}
}
.te-acs-b-l {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #8aa0b8;
white-space: nowrap;
}
.te-acs-b-v {
font-size: 11.5px;
font-weight: 700;
color: #1a2a3e;
font-variant-numeric: tabular-nums;
white-space: nowrap;
&--cr {
color: #059669;
}
}
.te-acs-alerts {
display: flex;
align-items: center;
gap: 4px;
padding: 0 10px;
flex-shrink: 0;
}
.te-acs-alert {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-weight: 600;
border-radius: 4px;
padding: 3px 7px;
&--danger {
background: #fef2f2;
color: #991b1b;
svg {
stroke: #dc2626;
}
}
&--warning {
background: #fffbeb;
color: #92400e;
svg {
stroke: #d97706;
}
}
&--info {
background: #eff6ff;
color: #1e40af;
svg {
stroke: #3b82f6;
}
}
}
// ── CBS Blue Transaction Entry ────────────────────────────────────────────────
$ws-ink: #0d1b2e; // nav bar dark navy
@@ -80,11 +296,11 @@ $ws-fld-bd: #c8d8eb;
.ws-wrap {
background: $ws-canvas;
border-radius: 12px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 1px 3px rgb(13 27 46 / 8%),
0 8px 24px -10px rgb(13 27 46 / 18%);
0 1px 3px rgb(13 27 46 / 6%),
0 4px 14px -8px rgb(13 27 46 / 14%);
border: 1px solid $ws-border;
font-family: 'Hanken Grotesk', Inter, sans-serif;
}
@@ -94,27 +310,27 @@ $ws-fld-bd: #c8d8eb;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 18px;
height: 48px;
padding: 0 12px;
height: 32px;
background: linear-gradient(90deg, #0d1b2e 0%, #162542 100%);
border-bottom: 2px solid $ws-accent;
&__left {
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
}
&__right {
display: flex;
align-items: center;
gap: 14px;
gap: 10px;
}
&__icon {
width: 28px;
height: 28px;
border-radius: 7px;
width: 22px;
height: 22px;
border-radius: 6px;
background: $ws-accent;
display: flex;
align-items: center;
@@ -122,12 +338,14 @@ $ws-fld-bd: #c8d8eb;
svg {
stroke: #fff;
width: 12px;
height: 12px;
}
}
&__title {
font-family: Newsreader, Georgia, serif;
font-size: 16px;
font-size: 14px;
font-weight: 500;
color: #e8f0fb;
letter-spacing: 0.01em;
@@ -136,8 +354,8 @@ $ws-fld-bd: #c8d8eb;
&__effdt {
display: flex;
align-items: center;
gap: 7px;
font-size: 11.5px;
gap: 6px;
font-size: 11px;
color: #7a95b0;
svg {
@@ -146,14 +364,14 @@ $ws-fld-bd: #c8d8eb;
}
&__date {
height: 28px;
padding: 0 10px;
height: 24px;
padding: 0 8px;
border: 1px solid #2a4060;
border-radius: 6px;
border-radius: 5px;
background: #0a1628;
color: #e8f0fb;
font-family: 'Hanken Grotesk', monospace;
font-size: 12px;
font-size: 11px;
font-weight: 600;
outline: none;
@@ -166,9 +384,9 @@ $ws-fld-bd: #c8d8eb;
.ws-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 7px 15px;
border-radius: 8px;
gap: 4px;
padding: 5px 12px;
border-radius: 7px;
border: none;
font-family: 'Hanken Grotesk', sans-serif;
font-size: 12px;
@@ -243,7 +461,7 @@ $ws-fld-bd: #c8d8eb;
background: $ws-accent;
color: #fff;
font-weight: 700;
padding: 8px 20px;
padding: 6px 16px;
box-shadow: 0 2px 8px rgb(232 119 34 / 30%);
&:hover:not(:disabled) {
@@ -259,20 +477,20 @@ $ws-fld-bd: #c8d8eb;
// ── Row cards ────────────────────────────────────────────────────────────────
.ws-rows {
padding: 12px 14px;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 10px;
gap: 6px;
}
.ws-card {
display: flex;
align-items: flex-start;
gap: 10px;
gap: 6px;
background: $ws-card;
border: 1px solid $ws-border;
border-radius: 12px;
padding: 12px 14px 12px 10px;
border-radius: 10px;
padding: 6px 8px;
cursor: pointer;
transition:
border-color 0.15s,
@@ -293,17 +511,17 @@ $ws-fld-bd: #c8d8eb;
&__badge {
flex-shrink: 0;
width: 26px;
height: 26px;
width: 22px;
height: 22px;
border-radius: 50%;
background: $ws-ink;
color: #c8daf0;
font-size: 11px;
font-size: 10px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-top: 22px;
margin-top: 16px;
}
&__body {
@@ -354,7 +572,7 @@ $ws-fld-bd: #c8d8eb;
gap: 0;
&--mt {
margin-top: 10px;
margin-top: 6px;
}
}
@@ -372,7 +590,7 @@ $ws-fld-bd: #c8d8eb;
&__fields {
display: flex;
align-items: flex-end;
gap: 8px;
gap: 6px;
flex-wrap: nowrap;
height: 100%;
align-content: flex-end;
@@ -390,7 +608,7 @@ $ws-fld-bd: #c8d8eb;
.ws-vdivider {
width: 1px;
background: $ws-divider;
margin: 18px 10px 0;
margin: 12px 8px 0;
flex-shrink: 0;
}
@@ -471,14 +689,14 @@ $ws-fld-bd: #c8d8eb;
}
&__ctl {
height: 34px;
padding: 0 9px;
height: 26px;
padding: 0 7px;
border: 1px solid $ws-fld-bd;
border-radius: 8px;
border-radius: 6px;
background: $ws-fld-bg;
color: $ws-text;
font-family: 'Hanken Grotesk', sans-serif;
font-size: 13px;
font-size: 11.5px;
outline: none;
width: 100%;
transition:
@@ -523,9 +741,9 @@ select.ws-f__ctl {
display: flex;
align-items: center;
border: 1px solid $ws-fld-bd;
border-radius: 8px;
border-radius: 7px;
background: $ws-fld-bg;
height: 34px;
height: 30px;
transition:
border-color 0.15s,
box-shadow 0.15s;
@@ -553,8 +771,8 @@ select.ws-f__ctl {
}
.ws-amt-prefix {
padding: 0 6px 0 9px;
font-size: 13px;
padding: 0 5px 0 8px;
font-size: 12px;
color: $ws-muted;
font-weight: 600;
flex-shrink: 0;
@@ -566,10 +784,10 @@ select.ws-f__ctl {
border: none;
background: transparent;
font-family: 'Hanken Grotesk', monospace;
font-size: 13px;
font-size: 12px;
font-weight: 600;
text-align: right;
padding-right: 9px;
padding-right: 8px;
color: $ws-text;
outline: none;
font-variant-numeric: tabular-nums;
@@ -578,8 +796,8 @@ select.ws-f__ctl {
// ── Segmented DR/CR toggle ───────────────────────────────────────────────────
.ws-seg {
display: flex;
height: 34px;
border-radius: 8px;
height: 30px;
border-radius: 7px;
overflow: hidden;
border: 1px solid $ws-fld-bd;
@@ -624,7 +842,7 @@ select.ws-f__ctl {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 18px;
padding: 7px 14px;
background: linear-gradient(180deg, #f4f7fc, #edf2f9);
border-top: 1px solid $ws-divider;
@@ -903,24 +1121,50 @@ select.te-cell {
// ── Status messages ──────────────────────────────────────────────────────────
.te-loading {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 14px 16px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
color: #64748b;
font-size: 13px;
justify-content: center;
gap: 14px;
background: rgb(241 245 249 / 88%);
backdrop-filter: blur(6px);
z-index: 999;
&::before {
content: '';
width: 16px;
height: 16px;
border: 2px solid #e2e8f0;
border-top-color: #3b82f6;
&__spinner {
position: relative;
width: 56px;
height: 56px;
svg {
animation: te-spin 1s linear infinite;
}
}
&__arc {
transform-origin: 28px 28px;
}
&__pulse {
position: absolute;
inset: 6px;
border-radius: 50%;
animation: te-spin 0.7s linear infinite;
background: radial-gradient(circle, rgb(37 99 235 / 12%), transparent 70%);
animation: te-pulse 1.4s ease-in-out infinite;
}
&__title {
font-size: 15px;
font-weight: 700;
color: #1e293b;
letter-spacing: -0.01em;
}
&__sub {
font-size: 12.5px;
color: #94a3b8;
margin-top: -6px;
}
}
@@ -930,6 +1174,19 @@ select.te-cell {
}
}
@keyframes te-pulse {
0%,
100% {
opacity: 0.4;
transform: scale(0.9);
}
50% {
opacity: 1;
transform: scale(1.1);
}
}
.te-error {
padding: 10px 16px;
background: #fef2f2;
@@ -945,13 +1202,13 @@ select.te-cell {
.te-detail {
display: flex;
flex-direction: column;
gap: 10px;
gap: 5px;
}
.te-detail-top {
display: grid;
grid-template-columns: 1.1fr 0.9fr 1.1fr;
gap: 10px;
gap: 7px;
@media (width <= 1100px) {
grid-template-columns: 1fr 1fr;
@@ -974,11 +1231,11 @@ select.te-cell {
&__header {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
gap: 5px;
padding: 4px 8px;
background: linear-gradient(135deg, #1e293b, #334155);
color: #e2e8f0;
font-size: 11px;
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
@@ -991,13 +1248,13 @@ select.te-cell {
}
&__body {
padding: 10px 14px;
padding: 4px 8px;
&--center {
display: flex;
align-items: center;
justify-content: center;
min-height: 60px;
min-height: 32px;
}
}
}
@@ -1007,11 +1264,11 @@ select.te-cell {
.te-field {
display: flex;
align-items: flex-start;
gap: 6px;
margin-bottom: 5px;
font-size: 12px;
gap: 5px;
margin-bottom: 3px;
font-size: 11px;
border-bottom: 1px dotted #f1f5f9;
padding-bottom: 4px;
padding-bottom: 3px;
&:last-child {
border-bottom: none;
@@ -1021,7 +1278,7 @@ select.te-cell {
.te-field-row {
display: flex;
gap: 10px;
gap: 8px;
.te-field {
flex: 1;
@@ -1029,9 +1286,9 @@ select.te-cell {
}
.te-label {
min-width: 88px;
min-width: 76px;
color: #64748b;
font-size: 11px;
font-size: 10.5px;
font-weight: 600;
white-space: nowrap;
padding-top: 1px;
@@ -1039,14 +1296,14 @@ select.te-cell {
.te-value {
color: #1e293b;
font-size: 12px;
font-size: 11px;
font-weight: 500;
flex: 1;
&-name {
font-weight: 700;
color: #0f172a;
font-size: 13px;
font-size: 12px;
}
&-wrap {
@@ -1069,10 +1326,10 @@ select.te-cell {
.te-joint-row {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 8px;
gap: 6px;
padding: 5px 6px;
border-bottom: 1px solid #f1f5f9;
font-size: 12px;
font-size: 11px;
border-radius: 6px;
margin-bottom: 2px;
@@ -1225,9 +1482,9 @@ select.te-cell {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
padding: 3px 0;
border-bottom: 1px dotted #f1f5f9;
font-size: 12px;
font-size: 11px;
&:last-child {
border-bottom: none;
@@ -1252,7 +1509,7 @@ select.te-cell {
.te-bal-label {
flex: 1;
color: #475569;
font-size: 12px;
font-size: 11px;
}
.te-bal-value {
@@ -1288,8 +1545,8 @@ select.te-cell {
.te-bal-item {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 18px;
gap: 5px;
padding: 7px 14px;
border-right: 1px solid #e2e8f0;
&:last-child {
@@ -1312,7 +1569,7 @@ select.te-cell {
&__val {
font-weight: 800;
font-size: 13px;
font-size: 12px;
color: #1e293b;
font-variant-numeric: tabular-nums;
letter-spacing: 0.01em;
@@ -1322,7 +1579,7 @@ select.te-cell {
}
&--big {
font-size: 16px;
font-size: 14px;
}
}
}
@@ -2246,7 +2503,7 @@ select.te-cell {
.te-denom-wrap {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
gap: 6px;
@media (width <= 1100px) {
grid-template-columns: 1fr 1fr;
@@ -2260,16 +2517,16 @@ select.te-cell {
.te-denom-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
font-size: 11px;
th {
background: #f1f5f9;
color: #64748b;
font-size: 10px;
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 5px 10px;
padding: 3px 8px;
border-bottom: 1px solid #e2e8f0;
text-align: left;
@@ -2279,10 +2536,10 @@ select.te-cell {
}
td {
padding: 5px 10px;
padding: 2px 8px;
border-bottom: 1px solid #f8fafc;
color: #1e293b;
font-size: 12px;
font-size: 11px;
&.num {
text-align: right;
@@ -2301,8 +2558,8 @@ select.te-cell {
tfoot td {
background: #f1f5f9;
border-top: 2px solid #e2e8f0;
font-size: 12px;
padding: 6px 10px;
font-size: 11px;
padding: 4px 8px;
}
}
@@ -2349,11 +2606,11 @@ select.te-cell {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
height: 24px;
padding: 0 8px;
border-radius: 6px;
font-size: 12px;
min-width: 36px;
height: 20px;
padding: 0 6px;
border-radius: 5px;
font-size: 11px;
font-weight: 700;
background: linear-gradient(135deg, #ecfeff, #cffafe);
color: #155e75;
@@ -2361,10 +2618,11 @@ select.te-cell {
&--coin {
border-radius: 50%;
width: 32px;
height: 32px;
min-width: 32px;
width: 24px;
height: 24px;
min-width: 24px;
padding: 0;
font-size: 10px;
background: linear-gradient(135deg, #fef9c3, #fde68a);
color: #854d0e;
border-color: #fcd34d;
@@ -2374,16 +2632,16 @@ select.te-cell {
.te-denom-stepper {
display: inline-flex;
align-items: center;
gap: 6px;
gap: 4px;
margin-left: auto;
button {
width: 28px;
height: 28px;
width: 22px;
height: 22px;
border-radius: 50%;
border: 1.5px solid #e2e8f0;
background: #f8fafc;
font-size: 16px;
font-size: 14px;
font-weight: 600;
color: #475569;
cursor: pointer;
@@ -2408,12 +2666,12 @@ select.te-cell {
}
input {
width: 64px;
height: 32px;
width: 50px;
height: 24px;
border: 1.5px solid #e2e8f0;
border-radius: 8px;
border-radius: 6px;
text-align: center;
font-size: 14px;
font-size: 11px;
font-weight: 700;
color: #1e293b;
background: #fff;
@@ -2441,9 +2699,9 @@ select.te-cell {
.te-denom-actions {
display: flex;
gap: 8px;
margin-top: 10px;
padding-top: 10px;
gap: 6px;
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid #e2e8f0;
}
@@ -2470,7 +2728,7 @@ select.te-cell {
align-items: flex-start;
border-right: 1px solid #f1f5f9;
border-bottom: none;
padding: 10px 16px;
padding: 6px 12px;
&:last-child {
border-right: none;
@@ -2478,18 +2736,18 @@ select.te-cell {
&--total {
border-top: none;
padding-top: 10px;
padding-top: 6px;
margin-top: 0;
}
.te-bal-label {
font-size: 11px;
font-size: 10px;
color: #94a3b8;
margin-bottom: 4px;
margin-bottom: 2px;
}
.te-bal-value {
font-size: 15px;
font-size: 13px;
min-width: unset;
}
}
@@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, inject } from '@angular/core';
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient, HttpParams } from '@angular/common/http';
@@ -14,6 +14,8 @@ import { catchError } from 'rxjs/operators';
import { forkJoin, of } from 'rxjs';
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CashierService } from '../cashier.service';
import { AuthenticationService } from '../../core/authentication/authentication.service';
import { ClerkUiStateService, ClerkSavedAccount } from '../clerk-ui-state.service';
interface TxnRow {
typ: string;
@@ -621,10 +623,12 @@ export class ImagePreviewDialogComponent {
MatDialogModule
]
})
export class TransactionEntryComponent implements OnInit {
export class TransactionEntryComponent implements OnInit, OnDestroy {
private http = inject(HttpClient);
private cashierService = inject(CashierService);
private authService = inject(AuthenticationService);
private dialog = inject(MatDialog);
private clerkUiState = inject(ClerkUiStateService);
today = '';
rows: TxnRow[] = [];
@@ -748,6 +752,56 @@ export class TransactionEntryComponent implements OnInit {
.getPaymentTypes()
.pipe(catchError(() => of([])))
.subscribe((types: any[]) => (this.paymentTypes = types ?? []));
if (this.isClerk) {
const saved = this.clerkUiState.snapshot.savedAccount;
if (saved) {
this.accountDetails = saved.accountDetails;
this.clientDetails = saved.clientDetails;
this.kycDetails = saved.kycDetails;
this.nominees = saved.nominees;
this.jointHolders = saved.jointHolders;
this.clientAddresses = saved.clientAddresses;
this.nonPersonDetails = saved.nonPersonDetails;
this.clientPhotoUrl = saved.clientPhotoUrl;
this.signatureUrl = saved.signatureUrl;
this.accountNotes = saved.accountNotes;
this.standingInstructions = saved.standingInstructions;
this.todayTransactions = saved.todayTransactions;
this.linkedAccounts = saved.linkedAccounts;
this.sameDayNeftRtgs = saved.sameDayNeftRtgs;
this.sameDayChequeClearing = saved.sameDayChequeClearing;
this.selectedRow = saved.selectedRow;
if (saved.accountDetails) {
const acNo = saved.accountDetails.accountNo ?? '';
const acTyp = this.clerkUiState.snapshot.savedRow?.acTyp ?? '';
this.rows[0] = { ...this.rows[0], acNo, acTyp };
}
}
}
}
ngOnDestroy(): void {
if (this.isClerk && this.accountDetails) {
this.clerkUiState.saveFullState({
accountDetails: this.accountDetails,
clientDetails: this.clientDetails,
kycDetails: this.kycDetails,
nominees: this.nominees,
jointHolders: this.jointHolders,
clientAddresses: this.clientAddresses,
nonPersonDetails: this.nonPersonDetails,
clientPhotoUrl: this.clientPhotoUrl,
signatureUrl: this.signatureUrl,
accountNotes: this.accountNotes,
standingInstructions: this.standingInstructions,
todayTransactions: this.todayTransactions,
linkedAccounts: this.linkedAccounts,
sameDayNeftRtgs: this.sameDayNeftRtgs,
sameDayChequeClearing: this.sameDayChequeClearing,
selectedRow: this.selectedRow
});
}
}
private loadDenominations(): void {
@@ -1010,7 +1064,7 @@ export class TransactionEntryComponent implements OnInit {
// returns all accounts as an array (e.g. recurringdepositaccounts)
const match = allItems.find((x: any) => x.accountNo === acNo) ?? allItems[0];
if (match?.id) {
this.fetchAllDetails(match.id, acNo, cfg, token);
this.fetchAllDetails(match.id, acNo, cfg, token, acTyp);
return;
}
// Fallback: global search
@@ -1021,7 +1075,7 @@ export class TransactionEntryComponent implements OnInit {
if (token !== this.searchToken) return;
const list: any[] = Array.isArray(found) ? found : [];
if (list.length > 0 && list[0].entityId) {
this.fetchAllDetails(list[0].entityId, acNo, cfg, token);
this.fetchAllDetails(list[0].entityId, acNo, cfg, token, acTyp);
} else {
this.loadingAccount = false;
this.accountError = 'Account not found for: ' + acNo;
@@ -1107,7 +1161,8 @@ export class TransactionEntryComponent implements OnInit {
txnBase: string;
txnSearchSupported: boolean;
},
token: number
token: number,
acTyp: string = ''
): void {
this.http
.get<any>(`${cfg.detail}/${accountId}?associations=all`)
@@ -1127,6 +1182,7 @@ export class TransactionEntryComponent implements OnInit {
}
this.accountDetails = acc;
this.jointHolders = acc.jointHolders ?? [];
if (this.isClerk) this.clerkUiState.setFacilities(this.facilities, acNo, acTyp);
// Compute same-day compliance indicators from included transactions
this.computeSameDayNeftRtgs(acc.transactions);
this.computeSameDayChequeClearing(acc.transactions);
@@ -1263,11 +1319,6 @@ export class TransactionEntryComponent implements OnInit {
if (acc.withHoldTax)
alerts.push({ type: 'info', message: `TDS Applicable — Tax Group: ${acc.taxGroupData?.name ?? ''}` });
// Notes
(this.accountNotes ?? []).forEach((n: any) => {
if (n.note) alerts.push({ type: 'info', message: n.note });
});
// Sanctions (from KYC) — DB stores 'YES'/'NO' strings, not booleans
const isYes = (v: any) => v === true || v === 'YES' || v === 'yes' || v === 'Y';
if (isYes(this.kycDetails?.bannedEntityMatch))
@@ -1423,6 +1474,10 @@ export class TransactionEntryComponent implements OnInit {
if (this.selectedRow && !this.rows.includes(this.selectedRow)) this.selectedRow = null;
}
get isClerk(): boolean {
return this.authService.isClerkUser();
}
get isNonIndividual(): boolean {
// legalForm.id=1 → Individual, legalForm.id=2 → Non-Individual (Entity)
const lf = this.clientDetails?.legalForm?.id ?? this.clientDetails?.legalFormId;
@@ -1996,6 +2051,7 @@ export class TransactionEntryComponent implements OnInit {
}
onCancel(): void {
this.clerkUiState.clear();
this.accountDetails = null;
this.clientDetails = null;
this.kycDetails = null;