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;
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
.af-page {
|
||||
min-height: calc(100vh - 68px);
|
||||
background: #f0f4f8;
|
||||
padding: 24px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
// ── Hero ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
.af-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 40%, #2563eb 100%);
|
||||
border-radius: 16px;
|
||||
padding: 22px 28px;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 32px rgb(37 99 235 / 35%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgb(255 255 255 / 5%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__icon-wrap {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgb(255 255 255 / 15%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
&__stats {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12.5px;
|
||||
color: rgb(255 255 255 / 75%);
|
||||
|
||||
strong {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&--green strong {
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
&--red strong {
|
||||
color: #fca5a5;
|
||||
}
|
||||
}
|
||||
|
||||
&__sep {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: rgb(255 255 255 / 25%);
|
||||
}
|
||||
}
|
||||
|
||||
.af-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 20px;
|
||||
background: rgb(255 255 255 / 12%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 22%);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ────────────────────────────────────────────────────────────────
|
||||
|
||||
.af-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 60px 20px;
|
||||
|
||||
&__icon {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
background: #e2e8f0;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-top: 10px;
|
||||
padding: 10px 24px;
|
||||
background: #2563eb;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Progress card ──────────────────────────────────────────────────────────────
|
||||
|
||||
.af-progress-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 18px 22px;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
&__fraction {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #16a34a;
|
||||
}
|
||||
}
|
||||
|
||||
.af-progress-bar {
|
||||
height: 10px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
|
||||
&__fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #16a34a, #22c55e);
|
||||
border-radius: 999px;
|
||||
transition: width 0.5s ease;
|
||||
box-shadow: 0 0 8px rgb(34 197 94 / 35%);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Facilities card ────────────────────────────────────────────────────────────
|
||||
|
||||
.af-fac-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 12px 22px;
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
}
|
||||
|
||||
.af-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.af-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 15px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: 1.5px solid;
|
||||
transition:
|
||||
transform 0.15s,
|
||||
box-shadow 0.15s;
|
||||
cursor: default;
|
||||
|
||||
&__dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--on {
|
||||
background: #f0fdf4;
|
||||
border-color: #22c55e;
|
||||
color: #15803d;
|
||||
|
||||
.af-pill__dot {
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 5px rgb(34 197 94 / 50%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgb(34 197 94 / 20%);
|
||||
}
|
||||
}
|
||||
|
||||
&--off {
|
||||
background: #f8fafc;
|
||||
border-color: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
|
||||
.af-pill__dot {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stats row ─────────────────────────────────────────────────────────────────
|
||||
|
||||
.af-stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.af-stat-card {
|
||||
border-radius: 14px;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border: 1px solid;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__num {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&--green {
|
||||
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
|
||||
border-color: #86efac;
|
||||
color: #15803d;
|
||||
|
||||
.af-stat-card__icon {
|
||||
background: #bbf7d0;
|
||||
}
|
||||
}
|
||||
|
||||
&--red {
|
||||
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
||||
border-color: #cbd5e1;
|
||||
color: #475569;
|
||||
|
||||
.af-stat-card__icon {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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 { Router } from '@angular/router';
|
||||
import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-account-facilities',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
styleUrl: './account-facilities.component.scss',
|
||||
template: `
|
||||
<div class="af-page">
|
||||
<!-- Hero -->
|
||||
<div class="af-hero">
|
||||
<div class="af-hero__icon-wrap">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="af-hero__text">
|
||||
<h1 class="af-hero__title">Account Facilities</h1>
|
||||
@if (accountLoaded) {
|
||||
<div class="af-hero__stats">
|
||||
<span class="af-hero__stat af-hero__stat--green">
|
||||
<strong>{{ activeCount }}</strong> Active
|
||||
</span>
|
||||
<span class="af-hero__sep"></span>
|
||||
<span class="af-hero__stat af-hero__stat--red">
|
||||
<strong>{{ facilities.length - activeCount }}</strong> Inactive
|
||||
</span>
|
||||
<span class="af-hero__sep"></span>
|
||||
<span class="af-hero__stat">
|
||||
<strong>{{ facilities.length }}</strong> Total
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button class="af-back-btn" (click)="goBack()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (!accountLoaded) {
|
||||
<!-- Empty state -->
|
||||
<div class="af-empty">
|
||||
<div class="af-empty__icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.3">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="af-empty__title">No Account Loaded</p>
|
||||
<p class="af-empty__sub">Search an account on the Clerk Dashboard first.</p>
|
||||
<button class="af-empty__btn" (click)="goBack()">Go to Dashboard</button>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Progress bar -->
|
||||
<div class="af-progress-card">
|
||||
<div class="af-progress-card__top">
|
||||
<span class="af-progress-card__label">Facility Coverage</span>
|
||||
<span class="af-progress-card__fraction">{{ activeCount }} / {{ facilities.length }}</span>
|
||||
</div>
|
||||
<div class="af-progress-bar">
|
||||
<div class="af-progress-bar__fill" [style.width.%]="(activeCount / facilities.length) * 100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Facilities pills -->
|
||||
<div class="af-fac-card">
|
||||
<div class="af-fac-card__header">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
All Facilities
|
||||
</div>
|
||||
<div class="af-pills">
|
||||
@for (f of facilities; track f.label) {
|
||||
<span class="af-pill" [class.af-pill--on]="f.active" [class.af-pill--off]="!f.active">
|
||||
<span class="af-pill__dot"></span>
|
||||
{{ f.label }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats row -->
|
||||
<div class="af-stats-row">
|
||||
<div class="af-stat-card af-stat-card--green">
|
||||
<div class="af-stat-card__icon">✓</div>
|
||||
<div>
|
||||
<span class="af-stat-card__num">{{ activeCount }}</span>
|
||||
<span class="af-stat-card__label">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="af-stat-card af-stat-card--red">
|
||||
<div class="af-stat-card__icon">✗</div>
|
||||
<div>
|
||||
<span class="af-stat-card__num">{{ facilities.length - activeCount }}</span>
|
||||
<span class="af-stat-card__label">Inactive</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class AccountFacilitiesComponent implements OnInit {
|
||||
private clerkUiState = inject(ClerkUiStateService);
|
||||
private router = inject(Router);
|
||||
|
||||
accountLoaded = false;
|
||||
facilities: { label: string; active: boolean }[] = [];
|
||||
|
||||
get activeCount(): number {
|
||||
return this.facilities.filter((f) => f.active).length;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clerkUiState.state$.subscribe((s) => {
|
||||
this.accountLoaded = s.accountLoaded;
|
||||
this.facilities = s.facilities;
|
||||
});
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.router.navigate(['/clerk/dashboard']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import { TransactionEntryComponent } from '../../cashier/transaction-entry/transaction-entry.component';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-clerk-dashboard',
|
||||
standalone: true,
|
||||
imports: [TransactionEntryComponent],
|
||||
template: `<mifosx-transaction-entry></mifosx-transaction-entry>`
|
||||
})
|
||||
export class ClerkDashboardComponent {}
|
||||
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
.ps-page {
|
||||
min-height: calc(100vh - 68px);
|
||||
background: #f0f4f8;
|
||||
padding: 24px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
// ── Hero ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
.ps-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 40%, #2563eb 100%);
|
||||
border-radius: 16px;
|
||||
padding: 22px 28px;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 32px rgb(37 99 235 / 35%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgb(255 255 255 / 5%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__icon-wrap {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgb(255 255 255 / 15%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12.5px;
|
||||
color: rgb(255 255 255 / 75%);
|
||||
}
|
||||
|
||||
&__ac {
|
||||
font-weight: 700;
|
||||
color: #bfdbfe;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__sep {
|
||||
width: 1px;
|
||||
height: 13px;
|
||||
background: rgb(255 255 255 / 30%);
|
||||
}
|
||||
|
||||
&__name {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
}
|
||||
|
||||
.ps-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 20px;
|
||||
background: rgb(255 255 255 / 12%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 22%);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ────────────────────────────────────────────────────────────────
|
||||
|
||||
.ps-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 60px 20px;
|
||||
|
||||
&__icon {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
background: #e2e8f0;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-top: 10px;
|
||||
padding: 10px 24px;
|
||||
background: #2563eb;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cards row ─────────────────────────────────────────────────────────────────
|
||||
|
||||
.ps-cards-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
|
||||
@media (width <= 680px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.ps-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 7%);
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 12px 22px;
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 280px;
|
||||
|
||||
&--sig {
|
||||
background: repeating-linear-gradient(-45deg, #f8fafc, #f8fafc 4px, #fff 4px, #fff 12px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image display ──────────────────────────────────────────────────────────────
|
||||
|
||||
.ps-img-wrap {
|
||||
position: relative;
|
||||
cursor: zoom-in;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgb(0 0 0 / 12%);
|
||||
max-width: 240px;
|
||||
max-height: 240px;
|
||||
|
||||
&--sig {
|
||||
max-width: 320px;
|
||||
max-height: 160px;
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
}
|
||||
|
||||
&:hover .ps-img__overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ps-img {
|
||||
display: block;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
object-fit: cover;
|
||||
|
||||
&--sig {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.ps-img__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgb(15 23 42 / 55%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.ps-no-img {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// ── Lightbox ──────────────────────────────────────────────────────────────────
|
||||
|
||||
.ps-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(2 6 23 / 82%);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(6px);
|
||||
|
||||
&__inner {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 24px 80px rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
&__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 18px;
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
&__close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
transition: color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__img {
|
||||
display: block;
|
||||
max-width: 90vw;
|
||||
max-height: calc(90vh - 48px);
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 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 { Router } from '@angular/router';
|
||||
import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-photo-signature',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
styleUrl: './photo-signature.component.scss',
|
||||
template: `
|
||||
<div class="ps-page">
|
||||
<!-- Hero -->
|
||||
<div class="ps-hero">
|
||||
<div class="ps-hero__icon-wrap">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ps-hero__text">
|
||||
<h1 class="ps-hero__title">Photo & Signature</h1>
|
||||
@if (accountLoaded) {
|
||||
<div class="ps-hero__meta">
|
||||
<span class="ps-hero__ac">{{ accountNo }}</span>
|
||||
@if (clientName) {
|
||||
<span class="ps-hero__sep"></span>
|
||||
<span class="ps-hero__name">{{ clientName }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button class="ps-back-btn" (click)="goBack()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (!accountLoaded) {
|
||||
<!-- Empty state -->
|
||||
<div class="ps-empty">
|
||||
<div class="ps-empty__icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.3">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="ps-empty__title">No Account Loaded</p>
|
||||
<p class="ps-empty__sub">Search an account on the Clerk Dashboard first.</p>
|
||||
<button class="ps-empty__btn" (click)="goBack()">Go to Dashboard</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ps-cards-row">
|
||||
<!-- Photo card -->
|
||||
<div class="ps-card">
|
||||
<div class="ps-card__header">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<polyline points="21 15 16 10 5 21" />
|
||||
</svg>
|
||||
Client Photo
|
||||
</div>
|
||||
<div class="ps-card__body">
|
||||
@if (photoUrl) {
|
||||
<div class="ps-img-wrap" (click)="preview(photoUrl, 'Client Photo')">
|
||||
<img [src]="photoUrl" alt="Client Photo" class="ps-img" />
|
||||
<div class="ps-img__overlay">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
<line x1="11" y1="8" x2="11" y2="14" />
|
||||
<line x1="8" y1="11" x2="14" y2="11" />
|
||||
</svg>
|
||||
Click to enlarge
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ps-no-img">
|
||||
<svg width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.2">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
<span>No photo on file</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature card -->
|
||||
<div class="ps-card">
|
||||
<div class="ps-card__header">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
|
||||
</svg>
|
||||
Signature
|
||||
</div>
|
||||
<div class="ps-card__body ps-card__body--sig">
|
||||
@if (signatureUrl) {
|
||||
<div class="ps-img-wrap ps-img-wrap--sig" (click)="preview(signatureUrl, 'Signature')">
|
||||
<img [src]="signatureUrl" alt="Signature" class="ps-img ps-img--sig" />
|
||||
<div class="ps-img__overlay">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
<line x1="11" y1="8" x2="11" y2="14" />
|
||||
<line x1="8" y1="11" x2="14" y2="11" />
|
||||
</svg>
|
||||
Click to enlarge
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ps-no-img">
|
||||
<svg width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.2">
|
||||
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
|
||||
</svg>
|
||||
<span>No signature on file</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Lightbox preview -->
|
||||
@if (previewSrc) {
|
||||
<div class="ps-lightbox" (click)="closePreview()">
|
||||
<div class="ps-lightbox__inner" (click)="$event.stopPropagation()">
|
||||
<div class="ps-lightbox__bar">
|
||||
<span>{{ previewLabel }}</span>
|
||||
<button class="ps-lightbox__close" (click)="closePreview()">✕</button>
|
||||
</div>
|
||||
<img [src]="previewSrc" [alt]="previewLabel" class="ps-lightbox__img" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class PhotoSignatureComponent implements OnInit {
|
||||
private clerkUiState = inject(ClerkUiStateService);
|
||||
private router = inject(Router);
|
||||
|
||||
accountLoaded = false;
|
||||
photoUrl: string | null = null;
|
||||
signatureUrl: string | null = null;
|
||||
accountNo = '';
|
||||
clientName = '';
|
||||
|
||||
previewSrc: string | null = null;
|
||||
previewLabel = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
const s = this.clerkUiState.snapshot;
|
||||
this.accountLoaded = s.accountLoaded;
|
||||
if (s.savedAccount) {
|
||||
this.photoUrl = s.savedAccount.clientPhotoUrl;
|
||||
this.signatureUrl = s.savedAccount.signatureUrl;
|
||||
this.accountNo = s.savedAccount.accountDetails?.accountNo ?? s.savedRow?.acNo ?? '';
|
||||
const cd = s.savedAccount.clientDetails;
|
||||
this.clientName = cd ? `${cd.firstname ?? ''} ${cd.lastname ?? ''}`.trim() : '';
|
||||
}
|
||||
}
|
||||
|
||||
preview(src: string, label: string): void {
|
||||
this.previewSrc = src;
|
||||
this.previewLabel = label;
|
||||
}
|
||||
|
||||
closePreview(): void {
|
||||
this.previewSrc = null;
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.router.navigate(['/clerk/dashboard']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
.si-page {
|
||||
min-height: calc(100vh - 68px);
|
||||
background: #f0f4f8;
|
||||
padding: 24px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
// ── Hero ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
.si-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 40%, #2563eb 100%);
|
||||
border-radius: 16px;
|
||||
padding: 22px 28px;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 32px rgb(37 99 235 / 35%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgb(255 255 255 / 5%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__icon-wrap {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgb(255 255 255 / 15%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12.5px;
|
||||
color: rgb(255 255 255 / 75%);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__ac {
|
||||
font-weight: 700;
|
||||
color: #bfdbfe;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__sep {
|
||||
width: 1px;
|
||||
height: 13px;
|
||||
background: rgb(255 255 255 / 30%);
|
||||
}
|
||||
|
||||
&__name {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
|
||||
&__count {
|
||||
color: rgb(255 255 255 / 75%);
|
||||
|
||||
strong {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.si-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 20px;
|
||||
background: rgb(255 255 255 / 12%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 22%);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ────────────────────────────────────────────────────────────────
|
||||
|
||||
.si-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 60px 20px;
|
||||
|
||||
&__icon {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
background: #e2e8f0;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-top: 10px;
|
||||
padding: 10px 24px;
|
||||
background: #2563eb;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stats row ─────────────────────────────────────────────────────────────────
|
||||
|
||||
.si-stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.si-stat {
|
||||
border-radius: 14px;
|
||||
padding: 18px 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
border: 1px solid;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
|
||||
&__icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__num {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&--blue {
|
||||
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||||
border-color: #93c5fd;
|
||||
color: #1d4ed8;
|
||||
|
||||
.si-stat__icon {
|
||||
background: #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
&--green {
|
||||
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
|
||||
border-color: #86efac;
|
||||
color: #15803d;
|
||||
|
||||
.si-stat__icon {
|
||||
background: #bbf7d0;
|
||||
}
|
||||
}
|
||||
|
||||
&--grey {
|
||||
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
||||
border-color: #cbd5e1;
|
||||
color: #475569;
|
||||
|
||||
.si-stat__icon {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Table card ─────────────────────────────────────────────────────────────────
|
||||
|
||||
.si-table-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 7%);
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 12px 22px;
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
}
|
||||
|
||||
.si-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.si-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
|
||||
thead tr {
|
||||
background: #f8fafc;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #334155;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tbody tr:hover td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
.si-row {
|
||||
&--active td {
|
||||
background: #fafffe;
|
||||
}
|
||||
}
|
||||
|
||||
.si-cell--num {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.si-cell--name {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.si-cell--amt {
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.si-cell--ac {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.si-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
border: 1px solid;
|
||||
|
||||
&--active {
|
||||
background: #f0fdf4;
|
||||
border-color: #22c55e;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
background: #f8fafc;
|
||||
border-color: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 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 { Router } from '@angular/router';
|
||||
import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-standing-instructions',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
styleUrl: './standing-instructions.component.scss',
|
||||
template: `
|
||||
<div class="si-page">
|
||||
<!-- Hero -->
|
||||
<div class="si-hero">
|
||||
<div class="si-hero__icon-wrap">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
||||
<polyline points="7 23 3 19 7 15" />
|
||||
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="si-hero__text">
|
||||
<h1 class="si-hero__title">Standing Instructions</h1>
|
||||
@if (accountLoaded) {
|
||||
<div class="si-hero__meta">
|
||||
<span class="si-hero__ac">{{ accountNo }}</span>
|
||||
@if (clientName) {
|
||||
<span class="si-hero__sep"></span>
|
||||
<span class="si-hero__name">{{ clientName }}</span>
|
||||
}
|
||||
<span class="si-hero__sep"></span>
|
||||
<span class="si-hero__count">
|
||||
<strong>{{ instructions.length }}</strong> instruction{{ instructions.length !== 1 ? 's' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button class="si-back-btn" (click)="goBack()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (!accountLoaded) {
|
||||
<div class="si-empty">
|
||||
<div class="si-empty__icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.3">
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
||||
<polyline points="7 23 3 19 7 15" />
|
||||
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="si-empty__title">No Account Loaded</p>
|
||||
<p class="si-empty__sub">Search an account on the Clerk Dashboard first.</p>
|
||||
<button class="si-empty__btn" (click)="goBack()">Go to Dashboard</button>
|
||||
</div>
|
||||
} @else if (instructions.length === 0) {
|
||||
<div class="si-empty">
|
||||
<div class="si-empty__icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.3">
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
||||
<polyline points="7 23 3 19 7 15" />
|
||||
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="si-empty__title">No Standing Instructions</p>
|
||||
<p class="si-empty__sub">This account has no standing instructions set up.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Stats row -->
|
||||
<div class="si-stats-row">
|
||||
<div class="si-stat si-stat--blue">
|
||||
<div class="si-stat__icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
||||
<polyline points="7 23 3 19 7 15" />
|
||||
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span class="si-stat__num">{{ instructions.length }}</span>
|
||||
<span class="si-stat__label">Total</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="si-stat si-stat--green">
|
||||
<div class="si-stat__icon">✓</div>
|
||||
<div>
|
||||
<span class="si-stat__num">{{ activeCount }}</span>
|
||||
<span class="si-stat__label">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="si-stat si-stat--grey">
|
||||
<div class="si-stat__icon">—</div>
|
||||
<div>
|
||||
<span class="si-stat__num">{{ instructions.length - activeCount }}</span>
|
||||
<span class="si-stat__label">Inactive</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions table -->
|
||||
<div class="si-table-card">
|
||||
<div class="si-table-card__header">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
||||
<polyline points="7 23 3 19 7 15" />
|
||||
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
</svg>
|
||||
All Standing Instructions
|
||||
</div>
|
||||
<div class="si-table-wrap">
|
||||
<table class="si-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Amount</th>
|
||||
<th>From Account</th>
|
||||
<th>To Account</th>
|
||||
<th>Frequency</th>
|
||||
<th>Valid Till</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (si of instructions; track si.id) {
|
||||
<tr class="si-row" [class.si-row--active]="si.status?.value === 'Active'">
|
||||
<td class="si-cell--num">{{ $index + 1 }}</td>
|
||||
<td class="si-cell--name">{{ si.name ?? '—' }}</td>
|
||||
<td class="si-cell--amt">₹{{ si.amount ?? 0 | number: '1.2-2' }}</td>
|
||||
<td class="si-cell--ac">{{ si.fromAccountId ?? '—' }}</td>
|
||||
<td class="si-cell--ac">{{ si.toAccountId ?? '—' }}</td>
|
||||
<td>{{ si.recurrenceType?.value ?? si.instructionType?.value ?? '—' }}</td>
|
||||
<td>{{ si.validTill ?? si.endDate ?? '—' }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="si-badge"
|
||||
[class.si-badge--active]="si.status?.value === 'Active'"
|
||||
[class.si-badge--inactive]="si.status?.value !== 'Active'"
|
||||
>
|
||||
{{ si.status?.value ?? '—' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class StandingInstructionsComponent implements OnInit {
|
||||
private clerkUiState = inject(ClerkUiStateService);
|
||||
private router = inject(Router);
|
||||
|
||||
accountLoaded = false;
|
||||
instructions: any[] = [];
|
||||
accountNo = '';
|
||||
clientName = '';
|
||||
|
||||
get activeCount(): number {
|
||||
return this.instructions.filter((si) => si.status?.value === 'Active').length;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const s = this.clerkUiState.snapshot;
|
||||
this.accountLoaded = s.accountLoaded;
|
||||
if (s.savedAccount) {
|
||||
this.instructions = s.savedAccount.standingInstructions ?? [];
|
||||
this.accountNo = s.savedAccount.accountDetails?.accountNo ?? s.savedRow?.acNo ?? '';
|
||||
const cd = s.savedAccount.clientDetails;
|
||||
this.clientName = cd ? `${cd.firstname ?? ''} ${cd.lastname ?? ''}`.trim() : '';
|
||||
}
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.router.navigate(['/clerk/dashboard']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
.ta-page {
|
||||
min-height: calc(100vh - 68px);
|
||||
background: #f0f4f8;
|
||||
padding: 24px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
// ── Hero ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
.ta-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 40%, #2563eb 100%);
|
||||
border-radius: 16px;
|
||||
padding: 22px 28px;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 32px rgb(37 99 235 / 35%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgb(255 255 255 / 5%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__icon-wrap {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgb(255 255 255 / 15%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12.5px;
|
||||
color: rgb(255 255 255 / 75%);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__ac {
|
||||
font-weight: 700;
|
||||
color: #bfdbfe;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__sep {
|
||||
width: 1px;
|
||||
height: 13px;
|
||||
background: rgb(255 255 255 / 30%);
|
||||
}
|
||||
|
||||
&__name {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
|
||||
&__count {
|
||||
strong {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ta-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 20px;
|
||||
background: rgb(255 255 255 / 12%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 22%);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
.ta-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 60px 20px;
|
||||
|
||||
&__icon {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
background: #e2e8f0;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-top: 10px;
|
||||
padding: 10px 24px;
|
||||
background: #2563eb;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── AML Compliance row ────────────────────────────────────────────────────────
|
||||
|
||||
.ta-aml-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.ta-aml-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 18px 22px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&--alert {
|
||||
border-color: #fca5a5;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&__val {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
font-family: monospace;
|
||||
|
||||
&--alert {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
&__warn {
|
||||
font-size: 11px;
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Table card ─────────────────────────────────────────────────────────────────
|
||||
|
||||
.ta-table-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 7%);
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 12px 22px;
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
}
|
||||
|
||||
.ta-badge {
|
||||
margin-left: 6px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.ta-no-txn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 40px 20px;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ta-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ta-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
|
||||
thead tr {
|
||||
background: #f8fafc;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #334155;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tbody tr:hover td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
.ta-row--cr {
|
||||
td {
|
||||
background: rgb(240 253 244 / 50%);
|
||||
}
|
||||
}
|
||||
|
||||
.ta-row--dr {
|
||||
td {
|
||||
background: rgb(255 245 245 / 50%);
|
||||
}
|
||||
}
|
||||
|
||||
.ta-cell--num {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.ta-cell--date {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ta-cell--ref {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.ta-cell--amt {
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.ta-cell--cr {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.ta-cell--dr {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.ta-type-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
border: 1px solid;
|
||||
|
||||
&--cr {
|
||||
background: #f0fdf4;
|
||||
border-color: #22c55e;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
&--dr {
|
||||
background: #fff5f5;
|
||||
border-color: #fca5a5;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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 { Router } from '@angular/router';
|
||||
import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-today-activity',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
styleUrl: './today-activity.component.scss',
|
||||
template: `
|
||||
<div class="ta-page">
|
||||
<!-- Hero -->
|
||||
<div class="ta-hero">
|
||||
<div class="ta-hero__icon-wrap">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<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>
|
||||
</div>
|
||||
<div class="ta-hero__text">
|
||||
<h1 class="ta-hero__title">Today's Activity</h1>
|
||||
@if (accountLoaded) {
|
||||
<div class="ta-hero__meta">
|
||||
<span class="ta-hero__ac">{{ accountNo }}</span>
|
||||
@if (clientName) {
|
||||
<span class="ta-hero__sep"></span>
|
||||
<span class="ta-hero__name">{{ clientName }}</span>
|
||||
}
|
||||
@if (transactions.length > 0) {
|
||||
<span class="ta-hero__sep"></span>
|
||||
<span class="ta-hero__count"
|
||||
><strong>{{ transactions.length }}</strong> txn today</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button class="ta-back-btn" (click)="goBack()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (!accountLoaded) {
|
||||
<div class="ta-empty">
|
||||
<div class="ta-empty__icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.3">
|
||||
<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>
|
||||
</div>
|
||||
<p class="ta-empty__title">No Account Loaded</p>
|
||||
<p class="ta-empty__sub">Search an account on the Clerk Dashboard first.</p>
|
||||
<button class="ta-empty__btn" (click)="goBack()">Go to Dashboard</button>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- AML Compliance cards -->
|
||||
<div class="ta-aml-row">
|
||||
<div class="ta-aml-card" [class.ta-aml-card--alert]="neftRtgs > 200000">
|
||||
<div class="ta-aml-card__icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ta-aml-card__body">
|
||||
<span class="ta-aml-card__label">Today's NEFT / RTGS</span>
|
||||
<span class="ta-aml-card__val" [class.ta-aml-card__val--alert]="neftRtgs > 200000">
|
||||
₹{{ neftRtgs | number: '1.2-2' }}
|
||||
</span>
|
||||
@if (neftRtgs > 200000) {
|
||||
<span class="ta-aml-card__warn">Exceeds ₹2,00,000 limit</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ta-aml-card" [class.ta-aml-card--alert]="chequeClearing > 0">
|
||||
<div class="ta-aml-card__icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ta-aml-card__body">
|
||||
<span class="ta-aml-card__label">Cheque In Clearing</span>
|
||||
<span class="ta-aml-card__val" [class.ta-aml-card__val--alert]="chequeClearing > 0">
|
||||
₹{{ chequeClearing | number: '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transactions table -->
|
||||
<div class="ta-table-card">
|
||||
<div class="ta-table-card__header">
|
||||
<svg width="13" height="13" 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>
|
||||
Transactions Today
|
||||
@if (transactions.length > 0) {
|
||||
<span class="ta-badge">{{ transactions.length }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (transactions.length === 0) {
|
||||
<div class="ta-no-txn">
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.3">
|
||||
<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>
|
||||
<span>No transactions today</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ta-table-wrap">
|
||||
<table class="ta-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Type</th>
|
||||
<th>Amount</th>
|
||||
<th>Date</th>
|
||||
<th>Reference</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of transactions; track t.id) {
|
||||
<tr
|
||||
[class.ta-row--cr]="t.transactionType?.deposit"
|
||||
[class.ta-row--dr]="!t.transactionType?.deposit"
|
||||
>
|
||||
<td class="ta-cell--num">{{ $index + 1 }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="ta-type-badge"
|
||||
[class.ta-type-badge--cr]="t.transactionType?.deposit"
|
||||
[class.ta-type-badge--dr]="!t.transactionType?.deposit"
|
||||
>
|
||||
{{ t.transactionType?.value ?? '—' }}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
class="ta-cell--amt"
|
||||
[class.ta-cell--cr]="t.transactionType?.deposit"
|
||||
[class.ta-cell--dr]="!t.transactionType?.deposit"
|
||||
>
|
||||
{{ t.transactionType?.deposit ? '+' : '−' }}₹{{ t.amount ?? 0 | number: '1.2-2' }}
|
||||
</td>
|
||||
<td class="ta-cell--date">{{ t.date ?? '—' }}</td>
|
||||
<td class="ta-cell--ref">{{ t.id ?? '—' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class TodayActivityComponent implements OnInit {
|
||||
private clerkUiState = inject(ClerkUiStateService);
|
||||
private router = inject(Router);
|
||||
|
||||
accountLoaded = false;
|
||||
transactions: any[] = [];
|
||||
neftRtgs = 0;
|
||||
chequeClearing = 0;
|
||||
accountNo = '';
|
||||
clientName = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
const s = this.clerkUiState.snapshot;
|
||||
this.accountLoaded = s.accountLoaded;
|
||||
if (s.savedAccount) {
|
||||
this.transactions = s.savedAccount.todayTransactions ?? [];
|
||||
this.neftRtgs = s.savedAccount.sameDayNeftRtgs ?? 0;
|
||||
this.chequeClearing = s.savedAccount.sameDayChequeClearing ?? 0;
|
||||
this.accountNo = s.savedAccount.accountDetails?.accountNo ?? s.savedRow?.acNo ?? '';
|
||||
const cd = s.savedAccount.clientDetails;
|
||||
this.clientName = cd ? `${cd.firstname ?? ''} ${cd.lastname ?? ''}`.trim() : '';
|
||||
}
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.router.navigate(['/clerk/dashboard']);
|
||||
}
|
||||
}
|
||||
@@ -388,6 +388,14 @@ export class AuthenticationService {
|
||||
return JSON.parse(this.storage.getItem(this.credentialsStorageKey));
|
||||
}
|
||||
|
||||
/** Returns true when the logged-in user has the "Clerk" role. */
|
||||
isClerkUser(): boolean {
|
||||
const creds = this.getCredentials();
|
||||
if (!creds || !creds.roles) return false;
|
||||
const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim());
|
||||
return roleNames.some((n) => n === 'clerk');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true ONLY for regular cashier role (not Head Cashier).
|
||||
* Regular cashier → full-screen transaction entry, no nav.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthenticationService } from './authentication.service';
|
||||
|
||||
/**
|
||||
* Restricts /cashier/transaction-entry to users with the Cashier role only.
|
||||
* Clerk and other roles are redirected to /home.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CashierOnlyGuard {
|
||||
private router = inject(Router);
|
||||
private authService = inject(AuthenticationService);
|
||||
|
||||
canActivate(): boolean {
|
||||
if (this.authService.isCashierUser()) {
|
||||
return true;
|
||||
}
|
||||
this.router.navigate(['/home'], { replaceUrl: true });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -57,73 +57,138 @@
|
||||
<!-- Toolbar always visible (shows CBSNexora, Branch, Session info) -->
|
||||
<mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar>
|
||||
|
||||
<!-- Cashier simplified menu bar — visible only in cashier full-screen mode -->
|
||||
<!-- Simplified menu bar — cashier OR clerk fullscreen 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>
|
||||
<!-- Clerk Dashboard fullscreen nav — only visible at /clerk/dashboard -->
|
||||
@if (currentUrl.includes('/clerk/')) {
|
||||
<nav class="cashier-nav cashier-nav--clerk">
|
||||
<a
|
||||
class="cashier-nav__item cashier-nav__item--primary"
|
||||
[class.cashier-nav__item--active]="currentUrl.includes('/clerk/dashboard')"
|
||||
[routerLink]="['/clerk/dashboard']"
|
||||
>
|
||||
<fa-icon icon="id-card"></fa-icon> Clerk Dashboard
|
||||
</a>
|
||||
@if (clerkAccountLoaded) {
|
||||
<a
|
||||
class="cashier-nav__item cashier-nav__item--sec"
|
||||
[class.cashier-nav__item--active]="currentUrl.includes('/clerk/account-facilities')"
|
||||
[routerLink]="['/clerk/account-facilities']"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
Account Facilities
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item cashier-nav__item--sec"
|
||||
[class.cashier-nav__item--active]="currentUrl.includes('/clerk/photo-signature')"
|
||||
[routerLink]="['/clerk/photo-signature']"
|
||||
>
|
||||
<svg width="12" height="12" 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>
|
||||
Photo & Signature
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item cashier-nav__item--sec"
|
||||
[class.cashier-nav__item--active]="currentUrl.includes('/clerk/standing-instructions')"
|
||||
[routerLink]="['/clerk/standing-instructions']"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
||||
<polyline points="7 23 3 19 7 15" />
|
||||
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
</svg>
|
||||
Standing Instructions
|
||||
</a>
|
||||
<a
|
||||
class="cashier-nav__item cashier-nav__item--sec"
|
||||
[class.cashier-nav__item--active]="currentUrl.includes('/clerk/today-activity')"
|
||||
[routerLink]="['/clerk/today-activity']"
|
||||
>
|
||||
<svg width="12" height="12" 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>
|
||||
Today's Activity
|
||||
</a>
|
||||
}
|
||||
<span class="cashier-nav__esc-hint" (click)="onEscapeKey()"> <kbd>ESC</kbd> Show Header </span>
|
||||
</nav>
|
||||
} @else {
|
||||
<!-- Cashier fullscreen nav -->
|
||||
<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 -->
|
||||
@@ -300,6 +365,15 @@
|
||||
<fa-icon icon="money-bill" class="ni-ico"></fa-icon>Cashier
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
routerLink="/clerk/dashboard"
|
||||
(click)="hideShellLayout = true"
|
||||
*mifosxHasPermission="'MENU_CLERK'"
|
||||
>
|
||||
<fa-icon icon="id-card" class="ni-ico"></fa-icon>Clerk
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="clearingMenu" *mifosxHasPermission="'MENU_CLEARING'">
|
||||
<fa-icon icon="exchange" class="ni-ico"></fa-icon>Clearing
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
|
||||
@@ -135,6 +135,49 @@ $color: #e2e4ec;
|
||||
background: #1a6ac8;
|
||||
}
|
||||
}
|
||||
|
||||
&--sec {
|
||||
font-size: 11.5px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid rgb(255 255 255 / 12%);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__esc-hint {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #2a3a5e;
|
||||
transition:
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
|
||||
kbd {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #e2e8f0;
|
||||
border-color: #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
&--clerk {
|
||||
border-bottom-color: #e87722;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
/** Angular Imports */
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, HostListener, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router, NavigationEnd } from '@angular/router';
|
||||
import { AuthenticationService } from '../authentication/authentication.service';
|
||||
import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
@@ -64,6 +65,7 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
private dialog = inject(MatDialog);
|
||||
private router = inject(Router);
|
||||
private authService = inject(AuthenticationService);
|
||||
private clerkUiStateService = inject(ClerkUiStateService);
|
||||
|
||||
/** Subscription to breakpoint observer for handset. */
|
||||
isHandset$: Observable<boolean> = this.breakpointObserver
|
||||
@@ -79,6 +81,11 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
hideShellLayout = false;
|
||||
private routeSub: Subscription;
|
||||
|
||||
/** Account Facilities — populated from ClerkUiStateService when account loads */
|
||||
clerkAccountLoaded = false;
|
||||
clerkFacilities: { label: string; active: boolean }[] = [];
|
||||
private clerkStateSub: Subscription;
|
||||
|
||||
/** Sub Ledger mega menu state */
|
||||
subLedgerOpen = false;
|
||||
subLedgerSearch = '';
|
||||
@@ -118,13 +125,27 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
|
||||
// Cashier users: always hide shell layout (role-based, not URL-based)
|
||||
// Cashier-only users: hide shell. Clerk/dashboard: also fullscreen by default.
|
||||
const checkLayout = () => {
|
||||
this.hideShellLayout = this.authService.isCashierUser();
|
||||
const url = this.router.url;
|
||||
const isCashierOnly = this.authService.isCashierUser() && !this.authService.isClerkUser();
|
||||
const isClerkRoute =
|
||||
url.includes('/clerk/dashboard') ||
|
||||
url.includes('/clerk/account-facilities') ||
|
||||
url.includes('/clerk/photo-signature') ||
|
||||
url.includes('/clerk/standing-instructions') ||
|
||||
url.includes('/clerk/today-activity');
|
||||
this.hideShellLayout = isCashierOnly || isClerkRoute;
|
||||
this.cdr.detectChanges();
|
||||
};
|
||||
this.routeSub = this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe(() => checkLayout());
|
||||
setTimeout(() => checkLayout(), 0);
|
||||
|
||||
this.clerkStateSub = this.clerkUiStateService.state$.subscribe((s) => {
|
||||
this.clerkAccountLoaded = s.accountLoaded;
|
||||
this.clerkFacilities = s.facilities;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,6 +162,20 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
/** Expose current URL to template for route-based conditionals. */
|
||||
get currentUrl(): string {
|
||||
return this.router.url;
|
||||
}
|
||||
|
||||
/** ESC key — exit fullscreen at Clerk Dashboard, restoring full shell header. */
|
||||
@HostListener('document:keydown.escape')
|
||||
onEscapeKey(): void {
|
||||
if (this.hideShellLayout && this.router.url.includes('/clerk/')) {
|
||||
this.hideShellLayout = false;
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
openConfigWizard(): void {
|
||||
// Placeholder — config wizard is handled by toolbar component
|
||||
}
|
||||
@@ -275,5 +310,8 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
if (this.routeSub) {
|
||||
this.routeSub.unsubscribe();
|
||||
}
|
||||
if (this.clerkStateSub) {
|
||||
this.clerkStateSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260608',
|
||||
'hash': 'f6404708'
|
||||
'version': '260609',
|
||||
'hash': 'f7d9259e'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user