From 0cc79ccd57eca3a8daeecc9a67410b1400f058d9 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Wed, 15 Jul 2026 16:08:43 +0530 Subject: [PATCH] feat(clerk): add clerk-facing Cheque Book screen and shell nav wiring Adds a customer-account-scoped Cheque Book view under Clerk > Account Facilities (list + leaf detail, with stop-payment/report-lost/mark-used/ return actions), reachable via a new hover dropdown on the Account Facilities nav item. Also refreshes the Account Facilities page layout, and carries over a few chequebook service/detail-dialog fixes from the back-office module (used-leaf display, leaf-lookup dialog polish). Co-Authored-By: Claude Sonnet 5 --- src/app/cashier/cashier-routing.module.ts | 12 + .../cheque-book-detail.component.html | 7 +- .../cheque-book-detail.component.scss | 4 + .../cheque-book-detail.component.ts | 12 +- .../leaf-detail-dialog.component.html | 10 +- src/app/chequebook/chequebook.service.ts | 5 + .../leaf-lookup/leaf-lookup.component.html | 10 +- .../leaf-lookup/leaf-lookup.component.scss | 5 + .../account-facilities.component.scss | 269 +++++++---------- .../account-facilities.component.ts | 113 ++++--- .../cheque-book-detail.component.scss | 281 ++++++++++++++++++ .../cheque-book-detail.component.ts | 207 +++++++++++++ .../cheque-book/cheque-book.component.scss | 257 ++++++++++++++++ .../cheque-book/cheque-book.component.ts | 228 ++++++++++++++ .../leaf-action-dialog.component.scss | 87 ++++++ .../leaf-action-dialog.component.ts | 145 +++++++++ src/app/core/shell/shell.component.html | 33 +- src/app/core/shell/shell.component.scss | 63 ++++ src/app/core/shell/shell.component.ts | 9 + src/assets/translations/en-US.json | 2 + 20 files changed, 1517 insertions(+), 242 deletions(-) create mode 100644 src/app/clerk/cheque-book/cheque-book-detail.component.scss create mode 100644 src/app/clerk/cheque-book/cheque-book-detail.component.ts create mode 100644 src/app/clerk/cheque-book/cheque-book.component.scss create mode 100644 src/app/clerk/cheque-book/cheque-book.component.ts create mode 100644 src/app/clerk/cheque-book/leaf-action-dialog/leaf-action-dialog.component.scss create mode 100644 src/app/clerk/cheque-book/leaf-action-dialog/leaf-action-dialog.component.ts diff --git a/src/app/cashier/cashier-routing.module.ts b/src/app/cashier/cashier-routing.module.ts index c9e818ee7..86568fc2a 100644 --- a/src/app/cashier/cashier-routing.module.ts +++ b/src/app/cashier/cashier-routing.module.ts @@ -70,6 +70,18 @@ const routes: Routes = [ ), data: { title: 'Account Facilities', breadcrumb: 'Account Facilities' } }, + { + path: 'account-facilities/cheque-book', + loadComponent: () => + import('../clerk/cheque-book/cheque-book.component').then((m) => m.ClerkChequeBookComponent), + data: { title: 'Cheque Book', breadcrumb: 'Cheque Book' } + }, + { + path: 'account-facilities/cheque-book/:id', + loadComponent: () => + import('../clerk/cheque-book/cheque-book-detail.component').then((m) => m.ClerkChequeBookDetailComponent), + data: { title: 'Cheque Book Detail', breadcrumb: 'Cheque Book Detail' } + }, { path: 'photo-signature', loadComponent: () => diff --git a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.html b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.html index 0afd956de..cf657e658 100644 --- a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.html +++ b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.html @@ -73,6 +73,9 @@ + } @@ -130,7 +133,9 @@ ? 'warn' : l.status === 'LOST' ? 'bad' - : 'neutral' + : l.status === 'RETURNED' + ? 'returned' + : 'neutral' " (click)="selectLeaf(l)" > diff --git a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.scss b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.scss index a994d762f..0334282b6 100644 --- a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.scss +++ b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.scss @@ -118,6 +118,10 @@ mat-form-field { &.neutral { color: #757575; } + + &.returned { + color: #9333ea; + } } .empty-state { diff --git a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.ts b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.ts index db69eb49f..2a95702db 100644 --- a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.ts +++ b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.ts @@ -21,13 +21,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { ChequeBookService } from '../chequebook.service'; import { LeafDetailDialogComponent } from './leaf-detail-dialog/leaf-detail-dialog.component'; -type LeafAction = 'stoppayment' | 'revokestoppayment' | 'reportlost' | 'cancelleaf' | 'markused'; +type LeafAction = 'stoppayment' | 'revokestoppayment' | 'reportlost' | 'cancelleaf' | 'markused' | 'return'; /** - * One issued cheque book: its leaf register, and the five leaf-range actions (stop-payment, revoke, - * report-lost, cancel-leaf, mark-used) — each defaults to the whole book when no range is given. + * One issued cheque book: its leaf register, and the six leaf-range actions (stop-payment, revoke, + * report-lost, cancel-leaf, mark-used, return) — each defaults to the whole book when no range is given. * "Mark as used" is a manual record (this CBS has no clearing/presentment integration to detect it itself) - * — a branch sets it once it sees a leaf clear on the bank/clearing statement. + * — a branch sets it once it sees a leaf clear on the bank/clearing statement. "Return" flags a leaf as + * presented-and-dishonoured (insufficient funds, signature mismatch, etc.) — terminal, like report-lost. */ @Component({ selector: 'mifosx-cheque-book-detail', @@ -112,7 +113,8 @@ export class ChequeBookDetailComponent implements OnInit { revokestoppayment: (id, p) => this.chequeBookService.revokeStopPayment(id, p), reportlost: (id, p) => this.chequeBookService.reportBookLost(id, p), cancelleaf: (id, p) => this.chequeBookService.cancelLeaf(id, p), - markused: (id, p) => this.chequeBookService.markUsed(id, p) + markused: (id, p) => this.chequeBookService.markUsed(id, p), + return: (id, p) => this.chequeBookService.returnBook(id, p) }; const call = actionCalls[this.activeAction](this.bookId, payload); diff --git a/src/app/chequebook/cheque-book-detail/leaf-detail-dialog/leaf-detail-dialog.component.html b/src/app/chequebook/cheque-book-detail/leaf-detail-dialog/leaf-detail-dialog.component.html index 0712eb0e1..2a1470c61 100644 --- a/src/app/chequebook/cheque-book-detail/leaf-detail-dialog/leaf-detail-dialog.component.html +++ b/src/app/chequebook/cheque-book-detail/leaf-detail-dialog/leaf-detail-dialog.component.html @@ -18,7 +18,9 @@ ? 'warn' : data.status === 'LOST' ? 'bad' - : 'neutral' + : data.status === 'RETURNED' + ? 'returned' + : 'neutral' " >{{ data.leafSerialNo }} @@ -57,6 +59,12 @@ >{{ data.usedReason }} } + @if (data.returnReason) { +
+ {{ 'labels.inputs.Return Reason' | translate }}{{ data.returnReason }} +
+ } diff --git a/src/app/chequebook/chequebook.service.ts b/src/app/chequebook/chequebook.service.ts index c18fa2db0..371362194 100644 --- a/src/app/chequebook/chequebook.service.ts +++ b/src/app/chequebook/chequebook.service.ts @@ -166,6 +166,11 @@ export class ChequeBookService { return this.http.post(`/chequebooks/${bookId}`, payload, { params: new HttpParams().set('command', 'markused') }); } + /** Flag leaves as returned/dishonoured (insufficient funds, signature mismatch, etc.) — terminal, like reportlost. */ + returnBook(bookId: string | number, payload: any): Observable { + return this.http.post(`/chequebooks/${bookId}`, payload, { params: new HttpParams().set('command', 'return') }); + } + // ----- Pending requests (bespoke maker-checker for series/stock/book creation) ----- /** diff --git a/src/app/chequebook/leaf-lookup/leaf-lookup.component.html b/src/app/chequebook/leaf-lookup/leaf-lookup.component.html index 9f3b775b2..280ce6243 100644 --- a/src/app/chequebook/leaf-lookup/leaf-lookup.component.html +++ b/src/app/chequebook/leaf-lookup/leaf-lookup.component.html @@ -53,7 +53,9 @@ ? 'warn' : result.status === 'LOST' ? 'bad' - : 'neutral' + : result.status === 'RETURNED' + ? 'returned' + : 'neutral' " >{{ result.status }} @@ -117,6 +119,12 @@ >{{ result.usedReason }} } + @if (result.status === 'RETURNED') { +
+ {{ 'labels.inputs.Return Reason' | translate }}{{ result.returnReason }} +
+ } diff --git a/src/app/chequebook/leaf-lookup/leaf-lookup.component.scss b/src/app/chequebook/leaf-lookup/leaf-lookup.component.scss index 35b4d10b8..822668cad 100644 --- a/src/app/chequebook/leaf-lookup/leaf-lookup.component.scss +++ b/src/app/chequebook/leaf-lookup/leaf-lookup.component.scss @@ -116,6 +116,11 @@ background: #e0e0e0; color: #757575; } + + &.returned { + background: #f3e8fd; + color: #9333ea; + } } .empty-state { diff --git a/src/app/clerk/account-facilities/account-facilities.component.scss b/src/app/clerk/account-facilities/account-facilities.component.scss index 020d40e68..125be056f 100644 --- a/src/app/clerk/account-facilities/account-facilities.component.scss +++ b/src/app/clerk/account-facilities/account-facilities.component.scss @@ -11,116 +11,79 @@ } .af-page { + background: #f8fafc; min-height: calc(100vh - 68px); - background: #f0f4f8; - padding: 24px 28px; display: flex; flex-direction: column; - gap: 20px; + gap: 12px; } -// ── Hero ────────────────────────────────────────────────────────────────────── +// ── Header ────────────────────────────────────────────────────────────────────── -.af-hero { +.af-header { 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; + justify-content: space-between; + padding: 12px 20px; + background: #fff; + border-bottom: 1px solid #e2e8f0; - &::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; + &__left { display: flex; align-items: center; - justify-content: center; - flex-shrink: 0; - backdrop-filter: blur(8px); + gap: 10px; } - &__text { - flex: 1; + &__icon { + color: #0f4f3c; + flex-shrink: 0; } &__title { - font-size: 22px; - font-weight: 800; - letter-spacing: -0.01em; - margin: 0 0 5px; + font-size: 15px; + font-weight: 700; + color: #0d1b2a; } - &__stats { - display: flex; - gap: 14px; - align-items: center; + &__sub { + font-size: 12px; + color: #64748b; + margin-top: 1px; } &__stat { - display: flex; - align-items: center; - gap: 5px; - font-size: 12.5px; - color: rgb(255 255 255 / 75%); + font-weight: 700; - strong { - color: #fff; - font-weight: 700; + &--green { + color: #16a34a; } - &--green strong { - color: #86efac; + &--red { + color: #dc2626; } - - &--red strong { - color: #fca5a5; - } - } - - &__sep { - width: 1px; - height: 14px; - background: rgb(255 255 255 / 25%); } } -.af-back-btn { +.af-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; + gap: 6px; + border-radius: 7px; + font-family: inherit; font-weight: 600; cursor: pointer; - transition: all 0.2s; - white-space: nowrap; - backdrop-filter: blur(8px); + border: 1px solid #e2e8f0; + transition: background 0.15s; + height: 30px; + padding: 0 12px; + font-size: 12.5px; + background: #fff; + color: #0d1b2a; &:hover { - background: rgb(255 255 255 / 22%); - transform: translateX(-2px); + background: #f0fdf4; + border-color: #0f4f3c; + color: #0f4f3c; } } @@ -132,12 +95,12 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 14px; - padding: 60px 20px; + gap: 10px; + padding: 40px 20px; &__icon { - width: 88px; - height: 88px; + width: 64px; + height: 64px; background: #e2e8f0; border: 1px solid #cbd5e1; border-radius: 50%; @@ -147,80 +110,87 @@ } &__title { - font-size: 18px; + font-size: 15px; font-weight: 700; color: #1e293b; - margin: 6px 0 0; + margin: 4px 0 0; } &__sub { - font-size: 13.5px; + font-size: 12.5px; color: #64748b; text-align: center; margin: 0; } &__btn { - margin-top: 10px; - padding: 10px 24px; - background: #2563eb; + margin-top: 6px; + padding: 8px 20px; + background: #0f4f3c; border: none; - border-radius: 10px; + border-radius: 8px; color: #fff; - font-size: 13px; + font-size: 12.5px; font-weight: 600; cursor: pointer; transition: background 0.15s; &:hover { - background: #1d4ed8; + background: #0a3d2e; } } } +// ── Body ──────────────────────────────────────────────────────────────────────── + +.af-body { + display: flex; + flex-direction: column; + gap: 12px; + padding: 0 20px 16px; +} + // ── 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%); + border-radius: 10px; + padding: 12px 16px; &__top { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 12px; + margin-bottom: 8px; } &__label { - font-size: 11.5px; - font-weight: 600; + font-size: 11px; + font-weight: 700; color: #64748b; text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.06em; } &__fraction { - font-size: 13px; + font-size: 12.5px; font-weight: 700; - color: #16a34a; + color: #0f4f3c; } } .af-progress-bar { - height: 10px; + height: 7px; background: #e2e8f0; border-radius: 999px; overflow: hidden; &__fill { height: 100%; - background: linear-gradient(90deg, #16a34a, #22c55e); + background: #16a34a; border-radius: 999px; - transition: width 0.5s ease; - box-shadow: 0 0 8px rgb(34 197 94 / 35%); + transition: width 0.4s ease; } } @@ -229,65 +199,53 @@ .af-fac-card { background: #fff; border: 1px solid #e2e8f0; - border-radius: 14px; + border-radius: 10px; 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; + padding: 9px 16px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + color: #64748b; + font-size: 11px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.06em; } } .af-pills { display: flex; flex-wrap: wrap; - gap: 10px; - padding: 20px 22px; + gap: 7px; + padding: 12px 16px; } .af-pill { display: inline-flex; align-items: center; - gap: 6px; - padding: 7px 15px; + gap: 5px; + padding: 4px 11px; border-radius: 999px; - font-size: 13px; + font-size: 12px; font-weight: 600; - border: 1.5px solid; - transition: - transform 0.15s, - box-shadow 0.15s; + border: 1px solid; cursor: default; &__dot { - width: 7px; - height: 7px; + width: 6px; + height: 6px; border-radius: 50%; flex-shrink: 0; } &--on { background: #f0fdf4; - border-color: #22c55e; - color: #15803d; + border-color: #86efac; + color: #166534; .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%); } } @@ -307,63 +265,46 @@ .af-stats-row { display: grid; grid-template-columns: 1fr 1fr; - gap: 14px; + gap: 10px; } .af-stat-card { - border-radius: 14px; - padding: 20px 24px; + border-radius: 10px; + padding: 10px 14px; display: flex; - align-items: center; - gap: 16px; + align-items: baseline; + gap: 7px; border: 1px solid; - box-shadow: 0 1px 4px rgb(0 0 0 / 6%); + font-size: 12px; &__icon { - width: 44px; - height: 44px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - font-size: 20px; + font-size: 12px; + font-weight: 700; } &__num { - font-size: 30px; + font-size: 17px; font-weight: 800; line-height: 1; - display: block; } &__label { - font-size: 12px; + font-size: 11px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.08em; - display: block; - margin-top: 3px; + letter-spacing: 0.05em; opacity: 0.75; } &--green { - background: linear-gradient(135deg, #f0fdf4, #dcfce7); + background: #f0fdf4; border-color: #86efac; - color: #15803d; - - .af-stat-card__icon { - background: #bbf7d0; - } + color: #166534; } &--red { - background: linear-gradient(135deg, #f8fafc, #f1f5f9); - border-color: #cbd5e1; - color: #475569; - - .af-stat-card__icon { - background: #e2e8f0; - } + background: #f8fafc; + border-color: #e2e8f0; + color: #64748b; } } diff --git a/src/app/clerk/account-facilities/account-facilities.component.ts b/src/app/clerk/account-facilities/account-facilities.component.ts index 6f887f97b..9457ef896 100644 --- a/src/app/clerk/account-facilities/account-facilities.component.ts +++ b/src/app/clerk/account-facilities/account-facilities.component.ts @@ -18,34 +18,37 @@ import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service'; styleUrl: './account-facilities.component.scss', template: `
- -
-
- + +
+
+ -
-
-

Account Facilities

- @if (accountLoaded) { -
- - {{ activeCount }} Active - - - - {{ facilities.length - activeCount }} Inactive - - - - {{ facilities.length }} Total - -
- } +
+
Account Facilities
+ @if (accountLoaded) { +
+ {{ activeCount }} active + · + {{ facilities.length - activeCount }} inactive + · {{ facilities.length }} total +
+ } @else { +
View all facilities enabled for the loaded account.
+ } +
@if (!embedded) { - +
+ + @if (loading) { +
Loading cheque book…
+ } @else { +
+ +
+
+
+ Account Type + {{ book.accountType }} +
+
+ Office + {{ book.officeName }} +
+
+ Series + {{ book.seriesCode ?? '—' }} +
+
+ Leaf Range + + {{ book.rangeStart ? book.rangeStart + ' – ' + book.rangeEnd : '—' }} + +
+
+ Quantity + {{ book.noOfBooks }} book{{ book.noOfBooks === 1 ? '' : 's' }} +
+
+ Status + {{ book.status }} +
+
+ Issue Date + {{ book.issueDate ?? '—' }} +
+
+
+ + + @if (book.status === 'ISSUED') { +
+ + + + + + +
+ } + + +
+ @if (leaves.length === 0) { +
No leaves to display
+ } @else { +
+ @for (l of leaves; track l.leafSerialNo) { + + } +
+ } +
+
+ } +
+ ` +}) +export class ClerkChequeBookDetailComponent implements OnInit { + private route = inject(ActivatedRoute); + private router = inject(Router); + private chequeBookService = inject(ChequeBookService); + private dialog = inject(MatDialog); + + bookId!: string; + book: any = {}; + leaves: any[] = []; + loading = false; + + ngOnInit(): void { + this.bookId = this.route.snapshot.paramMap.get('id') as string; + this.loadAll(); + } + + loadAll(): void { + this.loading = true; + this.chequeBookService.getBook(this.bookId).subscribe((data: any) => { + this.book = data || {}; + this.loading = false; + }); + this.chequeBookService.getLeaves(this.bookId).subscribe((data: any[]) => { + this.leaves = data || []; + }); + } + + selectLeaf(leaf: any): void { + this.dialog.open(LeafDetailDialogComponent, { data: leaf, width: '420px' }); + } + + openAction(action: LeafAction): void { + const ref = this.dialog.open(ClerkLeafActionDialogComponent, { + width: '480px', + data: { bookId: this.bookId, action } + }); + ref.afterClosed().subscribe((result) => { + if (result?.success) { + this.loadAll(); + } + }); + } + + goBack(): void { + this.router.navigate(['/clerk/account-facilities/cheque-book']); + } +} diff --git a/src/app/clerk/cheque-book/cheque-book.component.scss b/src/app/clerk/cheque-book/cheque-book.component.scss new file mode 100644 index 000000000..5d12b094f --- /dev/null +++ b/src/app/clerk/cheque-book/cheque-book.component.scss @@ -0,0 +1,257 @@ +/** + * 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; +} + +.cb-page { + background: #f8fafc; + min-height: calc(100vh - 68px); + display: flex; + flex-direction: column; + gap: 16px; +} + +// ── Header ────────────────────────────────────────────────────────────────────── + +.cb-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + background: #fff; + border-bottom: 1px solid #e2e8f0; + + &__left { + display: flex; + align-items: center; + gap: 12px; + } + + &__icon { + color: #0f4f3c; + flex-shrink: 0; + } + + &__title { + font-size: 15px; + font-weight: 700; + color: #0d1b2a; + } + + &__sub { + font-size: 12px; + color: #64748b; + margin-top: 2px; + } +} + +.cb-btn { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 7px; + font-family: inherit; + font-weight: 600; + cursor: pointer; + border: 1px solid #e2e8f0; + transition: background 0.15s; + height: 32px; + padding: 0 12px; + font-size: 13px; + background: #fff; + color: #0d1b2a; + + &:hover { + background: #f0fdf4; + border-color: #0f4f3c; + color: #0f4f3c; + } +} + +// ── Empty state ──────────────────────────────────────────────────────────────── + +.cb-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: #0f4f3c; + border: none; + border-radius: 10px; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + &:hover { + background: #0a3d2e; + } + } +} + +// ── Warning banner ───────────────────────────────────────────────────────────── + +.cb-warn-banner { + margin: 0 20px; + background: #fffbeb; + border: 1px solid #fde68a; + color: #92400e; + font-size: 13px; + font-weight: 600; + padding: 12px 18px; + border-radius: 10px; +} + +.cb-status-msg { + padding: 40px 20px; + text-align: center; + color: #64748b; + font-size: 13.5px; + + &--error { + color: #dc2626; + } +} + +.cb-no-books { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 40px 20px; + color: #94a3b8; + font-size: 13px; +} + +// ── Table card ───────────────────────────────────────────────────────────────── + +.cb-table-card { + margin: 0 20px; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; +} + +.cb-table-wrap { + overflow-x: auto; +} + +.cb-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th, + td { + padding: 10px 14px; + text-align: left; + border-bottom: 1px solid #f1f5f9; + } + + th { + font-size: 11px; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.06em; + background: #f8fafc; + white-space: nowrap; + } + + tbody tr:last-child td { + border-bottom: none; + } +} + +.cb-row { + cursor: pointer; + transition: background 0.1s; + + &:hover td { + background: #f0fdf4; + } +} + +.cb-cell--num { + color: #64748b; + font-family: 'Courier New', monospace; +} + +.cb-cell--date { + color: #64748b; + font-size: 12px; +} + +.cb-cell--action { + color: #0f4f3c; + font-weight: 700; + font-size: 12px; + white-space: nowrap; +} + +.cb-pill { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + white-space: nowrap; + + &--ok { + background: #dcfce7; + color: #166534; + } + + &--bad { + background: #fee2e2; + color: #991b1b; + } + + &--neutral { + background: #fef3c7; + color: #92400e; + } +} diff --git a/src/app/clerk/cheque-book/cheque-book.component.ts b/src/app/clerk/cheque-book/cheque-book.component.ts new file mode 100644 index 000000000..b09bfaae5 --- /dev/null +++ b/src/app/clerk/cheque-book/cheque-book.component.ts @@ -0,0 +1,228 @@ +/** + * 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, Input, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service'; +import { ChequeBookService } from '../../chequebook/chequebook.service'; + +/** Read-only cheque-book view for the account currently loaded on the Clerk Dashboard — + * reached via the "Cheque Book" entry in the Account Facilities dropdown. */ +@Component({ + selector: 'mifosx-clerk-cheque-book', + standalone: true, + imports: [CommonModule], + styleUrl: './cheque-book.component.scss', + template: ` +
+ +
+
+ + + + +
+
Cheque Book
+ @if (accountLoaded) { +
+ {{ accountNo }} + @if (clientName) { +  · {{ clientName }} + } + @if (books.length > 0) { +  · {{ books.length }} book{{ books.length === 1 ? '' : 's' }} + } +
+ } @else { +
View cheque books issued for the loaded account.
+ } +
+
+ @if (!embedded) { + + } +
+ + @if (!accountLoaded) { +
+
+ + + + +
+

No Account Loaded

+

Search an account on the Clerk Dashboard first.

+ @if (!embedded) { + + } +
+ } @else { + @if (!facilityActive) { +
Cheque Book facility is not active for this account.
+ } + + @if (loading) { +
Loading cheque books…
+ } @else if (error) { +
{{ error }}
+ } @else if (books.length === 0) { +
+ + + + + No cheque book issued for this account +
+ } @else { +
+
+ + + + + + + + + + + + + @for (b of books; track b.id) { + + + + + + + + + } + +
SeriesBooksLeavesStatusRequested
{{ b.seriesCode }}{{ b.noOfBooks }}{{ b.totalLeaves ?? '—' }} + {{ b.status }} + {{ b.requestedDate ?? '—' }}View ›
+
+
+ } + } +
+ ` +}) +export class ClerkChequeBookComponent implements OnInit { + private clerkUiState = inject(ClerkUiStateService); + private chequeBookService = inject(ChequeBookService); + private router = inject(Router); + + @Input() embedded = false; + + accountLoaded = false; + accountNo = ''; + clientName = ''; + facilityActive = true; + books: any[] = []; + loading = false; + error = ''; + + ngOnInit(): void { + const s = this.clerkUiState.snapshot; + this.accountLoaded = s.accountLoaded; + if (!this.accountLoaded) { + return; + } + + this.accountNo = s.savedAccount?.accountDetails?.accountNo ?? s.savedRow?.acNo ?? ''; + const clientDetails = s.savedAccount?.clientDetails; + this.clientName = clientDetails ? `${clientDetails.firstname ?? ''} ${clientDetails.lastname ?? ''}`.trim() : ''; + const facility = s.facilities.find((f) => f.label === 'Cheque Book'); + this.facilityActive = facility ? facility.active : true; + + const accountType = this.toChequeAccountType(s.savedRow?.acTyp); + const accountId = s.savedAccount?.accountDetails?.id; + if (accountId) { + this.loadBooks(accountType, accountId); + } else if (this.accountNo) { + this.resolveAccountId(accountType, this.accountNo); + } + } + + /** Cheque-book series/books are only ever SAVINGS or CURRENT — CA/CB is the clerk's UI code for current accounts. */ + private toChequeAccountType(acTyp?: string): string { + const t = (acTyp || '').toUpperCase(); + return t === 'CA' || t === 'CB' ? 'CURRENT' : 'SAVINGS'; + } + + private resolveAccountId(accountType: string, accountNo: string): void { + this.loading = true; + this.chequeBookService.searchAccountByNo(accountType, accountNo).subscribe({ + next: (res: any) => { + const items = res?.pageItems ?? (Array.isArray(res) ? res : []); + const match = items.find((i: any) => i.accountNo === accountNo) ?? (items.length === 1 ? items[0] : null); + if (match) { + this.loadBooks(accountType, match.id); + } else { + this.loading = false; + this.error = 'Could not resolve this account for cheque book lookup'; + } + }, + error: () => { + this.loading = false; + this.error = 'Could not resolve this account for cheque book lookup'; + } + }); + } + + private loadBooks(accountType: string, accountId: string | number): void { + this.loading = true; + this.chequeBookService.getBooks(accountType, accountId, undefined, undefined, 0, 50).subscribe({ + next: (res: any) => { + this.books = res?.pageItems ?? (Array.isArray(res) ? res : []); + this.loading = false; + }, + error: () => { + this.loading = false; + this.error = 'Failed to load cheque book details'; + } + }); + } + + viewBook(book: any): void { + this.router.navigate([ + '/clerk/account-facilities/cheque-book', + book.id + ]); + } + + goBack(): void { + this.router.navigate(['/clerk/dashboard']); + } +} diff --git a/src/app/clerk/cheque-book/leaf-action-dialog/leaf-action-dialog.component.scss b/src/app/clerk/cheque-book/leaf-action-dialog/leaf-action-dialog.component.scss new file mode 100644 index 000000000..1a06f7b4a --- /dev/null +++ b/src/app/clerk/cheque-book/leaf-action-dialog/leaf-action-dialog.component.scss @@ -0,0 +1,87 @@ +/** + * 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/. + */ + +.la-form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 14px; + padding-top: 4px; +} + +.la-field { + display: flex; + flex-direction: column; + gap: 4px; + + &__lbl { + font-size: 11px; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + &__input { + height: 36px; + padding: 0 10px; + border: 1px solid #e2e8f0; + border-radius: 7px; + font-size: 13px; + font-family: inherit; + color: #0d1b2a; + + &:focus { + outline: none; + border-color: #0f4f3c; + } + } + + &__hint { + font-size: 11px; + color: #94a3b8; + } +} + +.la-btn { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 7px; + font-family: inherit; + font-weight: 600; + cursor: pointer; + border: 1px solid #e2e8f0; + transition: background 0.15s; + height: 34px; + padding: 0 16px; + font-size: 13px; + background: #fff; + color: #0d1b2a; + + &:hover { + background: #f0fdf4; + border-color: #0f4f3c; + color: #0f4f3c; + } + + &--primary { + background: #0f4f3c; + border-color: #0f4f3c; + color: #fff; + + &:hover { + background: #0a3d2e; + color: #fff; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } +} diff --git a/src/app/clerk/cheque-book/leaf-action-dialog/leaf-action-dialog.component.ts b/src/app/clerk/cheque-book/leaf-action-dialog/leaf-action-dialog.component.ts new file mode 100644 index 000000000..330ded096 --- /dev/null +++ b/src/app/clerk/cheque-book/leaf-action-dialog/leaf-action-dialog.component.ts @@ -0,0 +1,145 @@ +/** + * 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, inject } from '@angular/core'; +import { ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialogTitle, + MatDialogContent, + MatDialogActions +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Observable } from 'rxjs'; +import { ChequeBookService } from '../../../chequebook/chequebook.service'; + +export type LeafAction = 'stoppayment' | 'revokestoppayment' | 'reportlost' | 'cancelleaf' | 'markused' | 'return'; + +const ACTION_TITLES: Record = { + stoppayment: 'Stop Payment', + revokestoppayment: 'Revoke Stop Payment', + reportlost: 'Report Lost', + cancelleaf: 'Cancel Leaf', + markused: 'Mark as Used', + return: 'Flag Returned' +}; + +/** Popup for the five leaf-range actions on a clerk-viewed cheque book — kept as its own dialog so the + * action form doesn't push the leaf grid around and always has full width to lay out its fields. */ +@Component({ + selector: 'mifosx-clerk-leaf-action-dialog', + standalone: true, + imports: [ + ReactiveFormsModule, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions + ], + styleUrl: './leaf-action-dialog.component.scss', + template: ` +

{{ title }}

+
+
+
+ + + @if (data.action !== 'revokestoppayment') { + + } +
+
+
+ + + + + ` +}) +export class ClerkLeafActionDialogComponent { + dialogRef = inject>(MatDialogRef); + data: { bookId: string; action: LeafAction } = inject(MAT_DIALOG_DATA); + private formBuilder = inject(UntypedFormBuilder); + private chequeBookService = inject(ChequeBookService); + private snackBar = inject(MatSnackBar); + + private notify(message: string, isError = false): void { + this.snackBar.open(message, 'Dismiss', { duration: isError ? 6000 : 3000 }); + } + + busy = false; + actionForm: UntypedFormGroup; + + get title(): string { + return ACTION_TITLES[this.data.action]; + } + + constructor() { + this.actionForm = this.formBuilder.group({ + fromLeafSeq: [''], + toLeafSeq: [''], + reason: [ + '', + this.data.action === 'revokestoppayment' || this.data.action === 'markused' ? [] : Validators.required + ] + }); + } + + submit(): void { + if (this.actionForm.invalid || this.busy) { + return; + } + this.busy = true; + const raw = this.actionForm.value; + const payload: any = { locale: 'en' }; + if (raw.fromLeafSeq !== '' && raw.fromLeafSeq != null) payload.fromLeafSeq = raw.fromLeafSeq; + if (raw.toLeafSeq !== '' && raw.toLeafSeq != null) payload.toLeafSeq = raw.toLeafSeq; + if (raw.reason) payload.reason = raw.reason; + + const actionCalls: Record Observable> = { + stoppayment: (id, p) => this.chequeBookService.stopPayment(id, p), + revokestoppayment: (id, p) => this.chequeBookService.revokeStopPayment(id, p), + reportlost: (id, p) => this.chequeBookService.reportBookLost(id, p), + cancelleaf: (id, p) => this.chequeBookService.cancelLeaf(id, p), + markused: (id, p) => this.chequeBookService.markUsed(id, p), + return: (id, p) => this.chequeBookService.returnBook(id, p) + }; + + actionCalls[this.data.action](this.data.bookId, payload).subscribe({ + next: () => { + this.busy = false; + this.notify('Action applied'); + this.dialogRef.close({ success: true }); + }, + error: () => { + this.busy = false; + this.notify( + 'Action failed — check the leaf range and that the leaves are in a state that allows this action', + true + ); + } + }); + } +} diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 25db734cf..e1e4d9659 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -100,17 +100,28 @@ My Disbursements @if (clerkAccountLoaded) { - - - - - - Account Facilities - +
+ + + + + + Account Facilities + + +
+ @for (f of clerkFacilities; track f.label) { + + + {{ f.label }} + + } +
+