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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-09 12:49:06 +05:30
co-authored by Claude Sonnet 4.6
parent f7d9259eb7
commit 40a436dabe
20 changed files with 3708 additions and 1020 deletions
@@ -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;
}
}
+140 -66
View File
@@ -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 &amp; 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>
+43
View File
@@ -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;
}
}
+41 -3
View File
@@ -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();
}
}
}