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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user