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:
co-authored by
Claude Sonnet 4.6
parent
f7d9259eb7
commit
40a436dabe
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user