From 01e86173c1f250fbb5f66533f7417293c6896c9f Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 6 Aug 2026 19:27:51 +0530 Subject: [PATCH] feat(shares): close share account directly on withdrawal approval; drop separate closure-request flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the auto-submit-closure-request behavior with an immediate account close, in the same approval action, when a withdrawal brings shares to zero — matching the backend's new single-step close-on- approval flow. Removes the now-unneeded Share Closure Request feature entirely: dedicated create/list components (Clerk Dashboard + Approval Center), routes, nav links, and the "eligible for closure" banner on the Share Account view. Also adds a clear "this will close the account" notice wherever a withdrawal would zero out the account's shares — the standalone Share Withdrawal Request page (submit + review) and Transaction Entry's Share Redemption Transfer confirm dialog — styled as an amber warning callout rather than a plain info line. Trims that confirm dialog to just the required details (dropped the redundant ECS A/C No. line and the validation-checklist block) and compacts its overall size. --- src/app/cashier/cashier-routing.module.ts | 16 - .../cashier/clerk-share-closure.service.ts | 53 --- .../transaction-entry.component.html | 9 + .../transaction-entry.component.scss | 101 +++-- .../transaction-entry.component.ts | 33 +- .../my-share-closure-requests.component.html | 69 ---- .../my-share-closure-requests.component.ts | 112 ------ .../share-closure-request.component.html | 355 ------------------ .../share-closure-request.component.ts | 336 ----------------- .../share-withdrawal-request.component.html | 19 +- .../share-withdrawal-request.component.scss | 47 +++ .../share-withdrawal-request.component.ts | 4 +- src/app/core/shell/shell.component.html | 11 - .../shares-account-view.component.html | 7 - .../shares-account-view.component.scss | 30 -- .../share-closure-requests.component.html | 82 ---- .../share-closure-requests.component.ts | 128 ------- .../checker-inbox-and-tasks.component.html | 9 - src/app/tasks/tasks-routing.module.ts | 19 - 19 files changed, 169 insertions(+), 1271 deletions(-) delete mode 100644 src/app/cashier/clerk-share-closure.service.ts delete mode 100644 src/app/clerk/my-share-closure-requests/my-share-closure-requests.component.html delete mode 100644 src/app/clerk/my-share-closure-requests/my-share-closure-requests.component.ts delete mode 100644 src/app/clerk/share-closure-request/share-closure-request.component.html delete mode 100644 src/app/clerk/share-closure-request/share-closure-request.component.ts delete mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/share-closure-requests/share-closure-requests.component.html delete mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/share-closure-requests/share-closure-requests.component.ts diff --git a/src/app/cashier/cashier-routing.module.ts b/src/app/cashier/cashier-routing.module.ts index c8c184925..e6fafaf2d 100644 --- a/src/app/cashier/cashier-routing.module.ts +++ b/src/app/cashier/cashier-routing.module.ts @@ -78,22 +78,6 @@ const routes: Routes = [ ), data: { title: 'My Share Requests', breadcrumb: 'My Share Requests' } }, - { - path: 'share-closure-request', - loadComponent: () => - import('../clerk/share-closure-request/share-closure-request.component').then( - (m) => m.ShareClosureRequestComponent - ), - data: { title: 'New Share Closure Request', breadcrumb: 'New Share Closure Request', mode: 'create' } - }, - { - path: 'share-closure-requests', - loadComponent: () => - import('../clerk/my-share-closure-requests/my-share-closure-requests.component').then( - (m) => m.MyShareClosureRequestsComponent - ), - data: { title: 'My Share Closure Requests', breadcrumb: 'My Share Closure Requests' } - }, { path: 'account-facilities', loadComponent: () => diff --git a/src/app/cashier/clerk-share-closure.service.ts b/src/app/cashier/clerk-share-closure.service.ts deleted file mode 100644 index ca57131b8..000000000 --- a/src/app/cashier/clerk-share-closure.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * 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 { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable } from 'rxjs'; - -/** HTTP service for the Share Closure maker-checker workflow — mirrors - * ClerkShareWithdrawalService, adapted for closing an already-zero-share account. */ -@Injectable({ providedIn: 'root' }) -export class ClerkShareClosureService { - private http = inject(HttpClient); - - submit(data: any): Observable { - return this.http.post('/clerk/share-closure-requests', data); - } - - getMyRequests(status?: string, offset = 0, limit = 20, fromDate?: string, toDate?: string): Observable { - let params = new HttpParams().set('offset', String(offset)).set('limit', String(limit)); - if (status) params = params.set('status', status); - if (fromDate) params = params.set('fromDate', fromDate); - if (toDate) params = params.set('toDate', toDate); - return this.http.get('/clerk/share-closure-requests/my', { params }); - } - - getPendingRequests(status = 'PENDING', offset = 0, limit = 20, fromDate?: string, toDate?: string): Observable { - let params = new HttpParams().set('status', status).set('offset', String(offset)).set('limit', String(limit)); - if (fromDate) params = params.set('fromDate', fromDate); - if (toDate) params = params.set('toDate', toDate); - return this.http.get('/clerk/share-closure-requests', { params }); - } - - getById(id: number): Observable { - return this.http.get(`/clerk/share-closure-requests/${id}`); - } - - validate(id: number): Observable { - return this.http.post(`/clerk/share-closure-requests/${id}/validate`, {}); - } - - approve(id: number): Observable { - return this.http.post(`/clerk/share-closure-requests/${id}/approve`, {}); - } - - reject(id: number, reason: string): Observable { - return this.http.post(`/clerk/share-closure-requests/${id}/reject`, { reason }); - } -} diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index 0b62d5029..4cd957977 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -1924,6 +1924,15 @@ @for (line of alertModal.lines; track $index) { @if ($index === 0) {

{{ line }}

+ } @else if (line.startsWith('⚠')) { +
+ + + + + + {{ line.slice(1).trim() }} +
} @else if (line.includes(':') && $index < alertModal.lines.length - 1) {
{{ line.split(':')[0].trim() }} diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.scss b/src/app/cashier/transaction-entry/transaction-entry.component.scss index b1c5387e4..00c01b31c 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.scss +++ b/src/app/cashier/transaction-entry/transaction-entry.component.scss @@ -2003,12 +2003,12 @@ select.te-cell { .te-alert-box { display: flex; - width: 480px; - max-width: 96vw; - border-radius: 16px; + width: 400px; + max-width: 94vw; + border-radius: 14px; overflow: hidden; box-shadow: - 0 32px 80px rgb(0 0 0 / 35%), + 0 24px 60px rgb(0 0 0 / 32%), 0 0 0 1px rgb(255 255 255 / 8%); animation: te-pop-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); @@ -2018,32 +2018,35 @@ select.te-cell { flex-direction: column; align-items: center; justify-content: center; - gap: 10px; - width: 76px; + gap: 8px; + width: 56px; flex-shrink: 0; - padding: 24px 0; + padding: 16px 0; } &__sidebar-icon { - width: 40px; - height: 40px; + width: 32px; + height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: rgb(255 255 255 / 20%); + box-shadow: + 0 0 0 1px rgb(255 255 255 / 15%), + inset 0 1px 2px rgb(255 255 255 / 25%); svg { - width: 22px; - height: 22px; + width: 17px; + height: 17px; stroke: #fff; } } &__sidebar-type { - font-size: 8px; + font-size: 7px; font-weight: 900; - letter-spacing: 0.12em; + letter-spacing: 0.1em; color: rgb(255 255 255 / 70%); text-transform: uppercase; writing-mode: vertical-lr; @@ -2053,6 +2056,7 @@ select.te-cell { // ── Right content ───────────────────────────────────────────────────────── &__content { flex: 1; + min-width: 0; background: #fff; display: flex; flex-direction: column; @@ -2062,12 +2066,12 @@ select.te-cell { display: flex; align-items: flex-start; justify-content: space-between; - padding: 20px 20px 12px; + padding: 14px 16px 8px; border-bottom: 1px solid #f1f5f9; h3 { margin: 0; - font-size: 15px; + font-size: 13.5px; font-weight: 800; color: #0f172a; letter-spacing: -0.02em; @@ -2091,52 +2095,87 @@ select.te-cell { &__lines { flex: 1; - padding: 14px 20px; + max-height: 60vh; + overflow-y: auto; + padding: 10px 16px; } &__desc { - margin: 0 0 10px; - font-size: 12.5px; + margin: 0 0 8px; + font-size: 11.5px; color: #64748b; - line-height: 1.55; + line-height: 1.5; } &__kv { display: flex; align-items: center; justify-content: space-between; - padding: 7px 12px; - margin: 4px 0; + gap: 10px; + padding: 5px 10px; + margin: 3px 0; background: #f8fafc; - border-radius: 8px; + border-radius: 7px; border-left: 3px solid #e2e8f0; + transition: background-color 0.15s; + + &:hover { + background: #f1f5f9; + } } &__kv-k { - font-size: 12px; + font-size: 11px; font-weight: 700; color: #334155; } &__kv-v { - font-size: 12px; + font-size: 11px; font-weight: 600; color: #475569; font-variant-numeric: tabular-nums; + text-align: right; } &__foot { - margin: 10px 0 0; - font-size: 11px; + margin: 8px 0 0; + font-size: 10px; color: #94a3b8; font-style: italic; + line-height: 1.4; + } + + &__callout { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 8px 0; + padding: 8px 10px; + background: #fffbeb; + border: 1px solid #fde68a; + border-left: 3px solid #d97706; + border-radius: 9px; + font-size: 11px; + font-weight: 600; + color: #92400e; + line-height: 1.45; + animation: te-pop-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); + + svg { + width: 14px; + height: 14px; + flex-shrink: 0; + margin-top: 1px; + color: #d97706; + } } &__actions { display: flex; justify-content: flex-end; gap: 8px; - padding: 14px 20px; + padding: 10px 16px; border-top: 1px solid #f1f5f9; background: #fafafa; } @@ -2204,11 +2243,11 @@ select.te-cell { display: inline-flex; align-items: center; justify-content: center; - padding: 8px 20px; - min-width: 90px; - border-radius: 8px; + padding: 7px 16px; + min-width: 76px; + border-radius: 7px; border: none; - font-size: 12.5px; + font-size: 12px; font-weight: 700; cursor: pointer; transition: diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts index 7895c5865..f1ab705b0 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.ts +++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts @@ -807,6 +807,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { // Per-row linked savings account number, keyed by the Share account's own acNo — used to make // sure a Share-to-linked-account Bank Transfer actually credits the right account. rowLinkedSavingsAcNo: Record = {}; + // Per-row shares currently held, keyed by acNo — lets submitShareRedemptionTransfer() warn the + // clerk when a withdrawal will bring the account's holding to zero (closing it on approval). + rowShareHoldings: Record = {}; // Full account + client data accountDetails: any = null; @@ -1645,26 +1648,31 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { }; const sharePrice = this.rowSharePrices[shareRow.acNo] ?? 0; + const currentHolding = this.rowShareHoldings[shareRow.acNo] ?? 0; + const remainingShares = currentHolding - shares; + const willCloseAccount = remainingShares <= 0; const confirmLines = [ `Share Account: ${shareRow.acNo} · Client: ${this.rowAccountNames[shareRow.acNo] ?? '—'}`, `Shares to Withdraw: ${shares} @ ₹${sharePrice.toLocaleString('en-IN', { minimumFractionDigits: 2 })} = ₹${(shares * sharePrice).toLocaleString('en-IN', { minimumFractionDigits: 2 })}`, + `Remaining Shares After Withdrawal: ${Math.max(remainingShares, 0)}`, + ...(willCloseAccount + ? ['⚠ All shares withdrawn — this account will be CLOSED automatically when the Manager approves.'] + : []), `Board Resolution No.: ${payload.resolutionNumber}`, `Date of Meeting: ${payload.meetingDate}`, `Settlement Credit To: ${linkedAcNo}`, `Settlement Payment Type: ${settlementPaymentType?.name ?? '—'}`, - ...(settlementRowSnapshot.ecsAccountNo ? [`ECS A/C No.: ${settlementRowSnapshot.ecsAccountNo}`] : []), + ...(settlementRowSnapshot.ecsAccountNo && settlementRowSnapshot.ecsAccountNo !== linkedAcNo + ? [`ECS A/C No.: ${settlementRowSnapshot.ecsAccountNo}`] + : []), ...(settlementRowSnapshot.chequeNo ? [`Cheque No.: ${settlementRowSnapshot.chequeNo}`] : []), ...(settlementRowSnapshot.chequeDate ? [`Cheque Date: ${settlementRowSnapshot.chequeDate}`] : []), ...(settlementRowSnapshot.tokenNo ? [`Token No.: ${settlementRowSnapshot.tokenNo}`] : []), ...(settlementRowSnapshot.description ? [`CR Row Remarks: ${settlementRowSnapshot.description}`] : []), '', - 'Validation checks:', - ` ✓ Share account and linked savings account both loaded`, - ` ✓ CR account matches this Share account's linked savings account`, - ` ✓ Settlement Payment Type is non-cash`, - ` ✓ Board Resolution Number and Date of Meeting provided`, - '', - 'Pending Manager approval — redemption and settlement both post together on approval.' + willCloseAccount + ? 'Pending Manager approval — redemption, settlement and account closure all post together on approval.' + : 'Pending Manager approval — redemption and settlement both post together on approval.' ]; this.showConfirmAlert('info', 'Confirm Share Withdrawal Request', confirmLines, 'Submit for Approval', () => this.doSubmitShareRedemptionTransfer(payload, shareRow, shares, linkedAcNo) @@ -1695,9 +1703,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { .subscribe((res: any) => { this.saving = false; if (res) { + const willCloseAccount = (this.rowShareHoldings[shareRow.acNo] ?? 0) - shares <= 0; this.showAlert('success', 'Share Withdrawal Submitted', [ `${shares} share(s) from ${shareRow.acNo} → settlement to ${linkedAcNo} on approval.`, - 'Pending Manager approval. Track status in "My Share Requests".' + willCloseAccount + ? 'Pending Manager approval — this will close the share account, since all shares will be withdrawn. Track status in "My Share Requests".' + : 'Pending Manager approval. Track status in "My Share Requests".' ]); this.rows = []; this.rowAccountIds = {}; @@ -1705,6 +1716,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { this.rowSharePrices = {}; this.rowClientIds = {}; this.rowLinkedSavingsAcNo = {}; + this.rowShareHoldings = {}; this.addRow(); this.selectedRow = this.rows[0]; this.accountDetails = null; @@ -2108,6 +2120,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { // rowSharePrices[row.acNo]) reads via row.acNo, so this must match or every lookup misses. this.rowSharePrices[this.selectedRow.acNo] = Number(acc.currentMarketPrice) || 0; this.rowLinkedSavingsAcNo[this.selectedRow.acNo] = acc.savingsAccountNumber ?? ''; + this.rowShareHoldings[this.selectedRow.acNo] = Number( + acc.summary?.totalApprovedShares ?? acc.totalApprovedShares ?? 0 + ); this.syncShareLinkedAmounts(); } return true; diff --git a/src/app/clerk/my-share-closure-requests/my-share-closure-requests.component.html b/src/app/clerk/my-share-closure-requests/my-share-closure-requests.component.html deleted file mode 100644 index 01f19ed07..000000000 --- a/src/app/clerk/my-share-closure-requests/my-share-closure-requests.component.html +++ /dev/null @@ -1,69 +0,0 @@ - - -
-
-
-
CLERK · SHARE CLOSURE
-

My Share Closure Requests

-

Share account closure requests you have submitted, and their approval status.

-
-
- -
-
- -
-
- -
- - - - - - - - - - - - @if (loading) { - - - - } @else if (loaded && requests.length === 0) { - - - - } @else { - @for (r of requests; track r.id) { - - - - - - - - } - } - -
Account NoClientClosed DateSubmitted AtStatus
Loading…
No share closure requests found.
- {{ r.account_no }} - {{ r.client_name }}{{ formatDateTime(r.closed_date) }}{{ formatDateTime(r.submitted_at) }} - {{ statusLabel(r.status) }} -
-
-
diff --git a/src/app/clerk/my-share-closure-requests/my-share-closure-requests.component.ts b/src/app/clerk/my-share-closure-requests/my-share-closure-requests.component.ts deleted file mode 100644 index 302e95b54..000000000 --- a/src/app/clerk/my-share-closure-requests/my-share-closure-requests.component.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * 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 { FormsModule } from '@angular/forms'; -import { Router, RouterModule } from '@angular/router'; -import { catchError } from 'rxjs/operators'; -import { of } from 'rxjs'; - -import { ClerkShareClosureService } from '../../cashier/clerk-share-closure.service'; - -/** Clerk Dashboard "My Share Closure Requests" — lists the share account closure requests this - * clerk has submitted and their approval status. Mirrors MyShareWithdrawalRequestsComponent. */ -@Component({ - selector: 'mifosx-my-share-closure-requests', - templateUrl: './my-share-closure-requests.component.html', - styleUrls: [ - '../../cashier/cashier-shared.scss', - '../clerk-shared.scss' - ], - standalone: true, - imports: [ - CommonModule, - FormsModule, - RouterModule - ] -}) -export class MyShareClosureRequestsComponent implements OnInit { - private service = inject(ClerkShareClosureService); - private router = inject(Router); - - requests: any[] = []; - totalRecords = 0; - loading = false; - loaded = false; - - filterStatus = ''; - - get statusOptions(): { value: string; label: string }[] { - return [ - { value: '', label: 'All' }, - { value: 'PENDING', label: 'Pending' }, - { value: 'APPROVED', label: 'Approved' }, - { value: 'REJECTED', label: 'Rejected' } - ]; - } - - ngOnInit(): void { - this.load(); - } - - load(): void { - this.loading = true; - this.service - .getMyRequests(this.filterStatus || undefined, 0, 50) - .pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))) - .subscribe((res: any) => { - this.requests = res?.pageItems ?? []; - this.totalRecords = Number(res?.totalFilteredRecords ?? 0); - this.loading = false; - this.loaded = true; - }); - } - - onFilterChange(): void { - this.load(); - } - - newRequest(): void { - this.router.navigate(['/clerk/share-closure-request']); - } - - statusClass(status: string): string { - const map: Record = { - PENDING: 'inv-badge--active', - APPROVED: 'inv-badge--matured', - REJECTED: 'inv-badge--written_off' - }; - return `inv-badge ${map[status] ?? ''}`; - } - - statusLabel(status: string): string { - const map: Record = { - PENDING: 'Pending Approval', - APPROVED: 'Approved', - REJECTED: 'Rejected' - }; - return map[status] ?? status; - } - - formatDateTime(val: any): string { - if (!val) return '—'; - try { - const d = new Date(val); - return d.toLocaleString('en-IN', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } catch { - return String(val); - } - } -} diff --git a/src/app/clerk/share-closure-request/share-closure-request.component.html b/src/app/clerk/share-closure-request/share-closure-request.component.html deleted file mode 100644 index 898b9f092..000000000 --- a/src/app/clerk/share-closure-request/share-closure-request.component.html +++ /dev/null @@ -1,355 +0,0 @@ - - -
-
-
-
{{ isReview ? 'CHECKER · APPROVAL' : 'CLERK · SHARE CLOSURE' }}
-

{{ pageTitle }}

-

{{ pageSubtitle }}

-
-
- -
-
- -
- -
- @if (!isReview) { - - } - @if (account) { - - - - } - @if (loadingAccount) { -
Loading account…
- } - @if (account && heldShares !== 0 && !isReview) { -
- This account still holds {{ heldShares }} share(s) — all shares must be withdrawn first via the Share - Withdrawal workflow before it becomes eligible for closure. -
- } -
-
- - @if (isReview && reviewRequest) { -
- -
-
- KYC Status - - {{ reviewRequest.client_kyc_status_label }} - -
-
- Risk Category - - {{ reviewRequest.client_risk_category_label }} - -
-
- Open AML Alerts - - {{ reviewRequest.client_open_aml_alerts || 0 }} - -
-
-
- } - - @if (account) { -
- -
- - -
- - @if (!isReview) { -
- -
- } -
- } - - @if (isReview && reviewRequest?.status === 'PENDING') { -
- -
-
- fact_check -
-
-
Run a live backend validation
-
- Re-checks shares held, account status, active loan/guarantee and client compliance before you approve. -
-
- -
- -
- - -
-
- } - - @if (showValidateModal) { -
-
-
-
- fact_check - Validate Share Closure Request -
- -
- -
-

- Re-checking closure of {{ account?.accountNo }} ({{ account?.clientName }}) against the - live account, loan/guarantee and compliance state — not just what was captured at submission time. -

- - @if (backendChecks && !validating && !validateAllPassed) { -
- warning -
- Some checks need review before approving. - Scroll down to see which ones failed and why. -
-
- } - -
    - @for (step of validateSteps; track step.label; let i = $index) { -
  1. -
    - @if (step.status === 'done') { - check - } @else if (step.status === 'active') { - autorenew - } @else if (step.status === 'failed') { - close - } @else { - {{ i + 1 }} - } -
    -
    -
    {{ step.label }}
    -
    {{ step.detail }}
    -
    -
  2. - } -
- - @if (validating && validateSteps.length === 0) { -
Connecting to server…
- } - - @if (backendChecks && !validating && validateAllPassed) { -
- verified - All checks passed — safe to approve. -
- } -
- - -
-
- } - - @if (isReview && activityLog.length > 0) { -
- - - - - - - - - - - - @for (entry of activityLog; track entry.timestamp) { - - - - - - - } - -
TimestampActionUserNote
{{ entry.timestamp | date: 'medium' }}{{ entry.action }}{{ entry.user }}{{ entry.note || '—' }}
-
- } - - @if (showConfirmDialog) { -
-
-

Confirm Share Closure Request

-

Review the details below before submitting for Bank Manager approval.

- - - - - - - - - - - - - - - - - - - -
Account No / Client{{ account?.accountNo }} — {{ account?.clientName }}
Shares Held{{ heldShares }}
Closed Date{{ closedDate }}
Note{{ note || '—' }}
- - - - - @for (check of verificationChecks; track check.label) { - - - - - } - -
{{ check.label }} - {{ check.pass ? 'Pass — ' : 'Review — ' }}{{ check.detail }} -
- -
- - -
-
-
- } - - @if (showRejectDialog) { -
-
-

Reject Share Closure Request

- -
- - -
-
-
- } -
diff --git a/src/app/clerk/share-closure-request/share-closure-request.component.ts b/src/app/clerk/share-closure-request/share-closure-request.component.ts deleted file mode 100644 index afa98ee11..000000000 --- a/src/app/clerk/share-closure-request/share-closure-request.component.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * 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, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { MatIconModule } from '@angular/material/icon'; -import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; -import { catchError } from 'rxjs/operators'; -import { of } from 'rxjs'; - -import { SharesService } from '../../shares/shares.service'; -import { ClerkShareClosureService } from '../../cashier/clerk-share-closure.service'; -import { SearchSelectComponent, SearchSelectOption } from '../../shared/search-select/search-select.component'; - -export type ShareClosureMode = 'create' | 'review'; - -interface ActivityEntry { - timestamp: string; - action: string; - user: string; - note?: string; -} - -/** - * Share Account Closure request — maker screen in the Clerk Dashboard. A clerk finds a member's - * Share Account that already holds zero shares (all shares already withdrawn), enters the closed - * date plus an optional note, and submits for Bank Manager approval. This is the ONLY entry point - * a share account closure should go through — approval server-side executes the real Fineract - * "close" command (see ClerkShareClosureRequestApiResource on the backend). - */ -@Component({ - selector: 'mifosx-share-closure-request', - templateUrl: './share-closure-request.component.html', - styleUrls: [ - '../../cashier/cashier-shared.scss', - '../clerk-shared.scss', - '../share-withdrawal-request/share-withdrawal-request.component.scss' - ], - standalone: true, - imports: [ - CommonModule, - FormsModule, - RouterModule, - MatIconModule, - MatSnackBarModule, - SearchSelectComponent - ] -}) -export class ShareClosureRequestComponent implements OnInit { - private route = inject(ActivatedRoute); - private router = inject(Router); - private sharesService = inject(SharesService); - private closureService = inject(ClerkShareClosureService); - private snackBar = inject(MatSnackBar); - - /** When embedded (e.g. opened as a popup from a list screen instead of routed to directly), mode - * and requestId come from these inputs rather than the route, and success/close actions emit - * events instead of navigating away. */ - @Input() embedded = false; - @Input() overrideMode: ShareClosureMode | null = null; - @Input() overrideRequestId: number | null = null; - @Output() closeRequested = new EventEmitter(); - @Output() actionCompleted = new EventEmitter(); - - mode: ShareClosureMode = 'create'; - requestId: number | null = null; - - submitting = false; - loadingAccount = false; - - selectedAccountId = ''; - accountOptions: SearchSelectOption[] = []; - - account: any = null; - - closedDate = ''; - note = ''; - - activityLog: ActivityEntry[] = []; - reviewRequest: any = null; - showRejectDialog = false; - rejectionReason = ''; - showConfirmDialog = false; - - showValidateModal = false; - validating = false; - validateAllPassed = false; - backendChecks: { label: string; pass: boolean; detail: string }[] | null = null; - validateSteps: { label: string; detail: string; pass: boolean; status: 'pending' | 'active' | 'done' | 'failed' }[] = - []; - - get heldShares(): number { - return Number(this.account?.summary?.totalApprovedShares ?? this.account?.totalApprovedShares ?? 0); - } - - get isFormValid(): boolean { - if (!this.account) return false; - if (this.heldShares !== 0) return false; - if (!this.closedDate) return false; - return true; - } - - /** Shown to the clerk before submit and to the manager before approval, so both can verify the same checks. */ - get verificationChecks(): { label: string; pass: boolean; detail: string }[] { - return [ - { label: 'No shares currently held', pass: this.heldShares === 0, detail: `${this.heldShares} share(s) held` }, - { label: 'Closed Date provided', pass: !!this.closedDate, detail: this.closedDate || 'Missing' } - ]; - } - - get isReview(): boolean { - return this.mode === 'review'; - } - - get pageTitle(): string { - return this.isReview ? 'Review Share Closure Request' : 'New Share Closure Request'; - } - - get pageSubtitle(): string { - return this.isReview - ? "Approve or reject the clerk's share account closure request." - : 'Close a Share Account whose shares have already been fully withdrawn, for Bank Manager approval.'; - } - - ngOnInit(): void { - this.mode = this.overrideMode ?? (this.route.snapshot.data?.['mode'] as ShareClosureMode) ?? 'create'; - const idParam = this.overrideRequestId ?? this.route.snapshot.params?.['id']; - this.requestId = idParam ? Number(idParam) : null; - - const today = new Date(); - this.closedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; - - this.sharesService - .getShareAccountSummaries() - .pipe(catchError(() => of([]))) - .subscribe((rows: any[]) => { - this.accountOptions = (rows || []) - .filter((r) => (r.status?.value || '').toLowerCase() === 'active' && Number(r.totalApprovedShares ?? 0) === 0) - .map((r) => ({ - value: String(r.id), - label: `${r.accountNo} — ${r.clientName} (0 shares held)`, - searchText: `${r.accountNo} ${r.clientName}` - })); - }); - - if (this.requestId && this.mode === 'review') { - this.loadReviewRequest(this.requestId); - } - } - - onAccountSelected(accountId: string): void { - this.selectedAccountId = accountId; - if (!accountId) { - this.account = null; - return; - } - this.loadingAccount = true; - this.sharesService - .getSharesAccountData(accountId, false) - .pipe(catchError(() => of(null))) - .subscribe((acc: any) => { - this.account = acc; - this.loadingAccount = false; - }); - } - - private loadReviewRequest(id: number): void { - this.closureService.getById(id).subscribe({ - next: (req: any) => { - this.reviewRequest = req; - this.closedDate = req.closed_date ? String(req.closed_date).split('T')[0].split(' ')[0] : this.closedDate; - this.note = req.note ?? ''; - - if (req.activity_log) { - try { - this.activityLog = JSON.parse(req.activity_log); - } catch { - this.activityLog = []; - } - } - - this.sharesService - .getSharesAccountData(String(req.share_account_id), false) - .pipe(catchError(() => of(null))) - .subscribe((acc: any) => (this.account = acc)); - }, - error: () => this.snackBar.open('Failed to load share closure request.', 'Close', { duration: 5000 }) - }); - } - - /** "Review before Save" per requirement — opens the confirm dialog instead of submitting directly. */ - openConfirmDialog(): void { - if (!this.isFormValid || this.submitting) { - return; - } - this.showConfirmDialog = true; - } - - cancelConfirmDialog(): void { - this.showConfirmDialog = false; - } - - confirmSubmit(): void { - this.showConfirmDialog = false; - this.submitting = true; - const payload = { - shareAccountId: Number(this.selectedAccountId), - accountNo: this.account?.accountNo, - clientId: this.account?.clientId, - clientName: this.account?.clientName, - closedDate: this.closedDate, - note: this.note - }; - this.closureService.submit(payload).subscribe({ - next: () => { - this.snackBar.open('Share closure request submitted for approval.', 'Close', { duration: 4000 }); - if (this.embedded) { - this.actionCompleted.emit(); - } else { - this.router.navigate(['/clerk/dashboard']); - } - }, - error: (err) => { - this.submitting = false; - this.snackBar.open(err?.error?.defaultUserMessage || 'Could not submit the request.', 'Close', { - duration: 6000 - }); - } - }); - } - - openValidateModal(): void { - if (!this.reviewRequest) return; - this.showValidateModal = true; - this.runBackendValidation(); - } - - closeValidateModal(): void { - if (this.validating) return; - this.showValidateModal = false; - } - - /** Re-checks live backend state (shares still zero, account still active, no active loan/guarantee, - * compliance standing) rather than relying on data already loaded in the browser — catches drift - * since submission. Results are revealed one step at a time for the pipeline-style animation. */ - runBackendValidation(): void { - if (!this.reviewRequest || this.validating) return; - this.validating = true; - this.backendChecks = null; - this.validateSteps = []; - this.closureService.validate(this.reviewRequest.id).subscribe({ - next: (res: any) => { - const checks = (res?.checks ?? []) as { label: string; pass: boolean; detail: string }[]; - this.backendChecks = checks; - this.validateAllPassed = !!res?.allPassed; - this.validateSteps = checks.map((c) => ({ ...c, status: 'pending' as const })); - this.revealValidateSteps(0); - }, - error: (err) => { - this.validating = false; - this.snackBar.open(err?.error?.defaultUserMessage || 'Could not run backend validation.', 'Close', { - duration: 6000 - }); - } - }); - } - - private revealValidateSteps(index: number): void { - if (index >= this.validateSteps.length) { - this.validating = false; - return; - } - this.validateSteps[index].status = 'active'; - setTimeout(() => { - this.validateSteps[index].status = this.validateSteps[index].pass ? 'done' : 'failed'; - this.revealValidateSteps(index + 1); - }, 320); - } - - approveReview(): void { - if (!this.reviewRequest) return; - this.closureService.approve(this.reviewRequest.id).subscribe({ - next: () => { - this.snackBar.open('Share closure approved — account closed.', 'Close', { duration: 6000 }); - if (this.embedded) { - this.actionCompleted.emit(); - } else { - this.router.navigate(['/checker-inbox-and-tasks/share-closure-requests']); - } - }, - error: (err) => - this.snackBar.open(err?.error?.defaultUserMessage || 'Could not approve the request.', 'Close', { - duration: 6000 - }) - }); - } - - openRejectDialog(): void { - this.showRejectDialog = true; - this.rejectionReason = ''; - } - - confirmRejectReview(): void { - if (!this.reviewRequest || !this.rejectionReason.trim()) return; - this.closureService.reject(this.reviewRequest.id, this.rejectionReason.trim()).subscribe({ - next: () => { - this.showRejectDialog = false; - this.snackBar.open('Share closure request rejected.', 'Close', { duration: 4000 }); - if (this.embedded) { - this.actionCompleted.emit(); - } else { - this.router.navigate(['/checker-inbox-and-tasks/share-closure-requests']); - } - }, - error: (err) => - this.snackBar.open(err?.error?.defaultUserMessage || 'Could not reject the request.', 'Close', { - duration: 6000 - }) - }); - } - - goBack(): void { - if (this.embedded) { - this.closeRequested.emit(); - return; - } - this.router.navigate(this.isReview ? ['/checker-inbox-and-tasks/share-closure-requests'] : ['/clerk/dashboard']); - } -} diff --git a/src/app/clerk/share-withdrawal-request/share-withdrawal-request.component.html b/src/app/clerk/share-withdrawal-request/share-withdrawal-request.component.html index f8c536f33..b47ead9e9 100644 --- a/src/app/clerk/share-withdrawal-request/share-withdrawal-request.component.html +++ b/src/app/clerk/share-withdrawal-request/share-withdrawal-request.component.html @@ -148,6 +148,16 @@
+ @if (account && remainingShares === 0) { +
+ lock + + This withdraws all shares on this account. Approving it will + close the share account automatically — no separate closure step is needed. + +
+ } + @if (!isReview) {
- -
- - - - - - - - - - - - - - - @if (loading) { - - - - } @else if (loaded && requests.length === 0) { - - - - } @else { - @for (r of requests; track r.id) { - - - - - - - - - - - } - } - -
Account NoClientClosed DateClerkManagerSubmitted AtStatus
Loading…
No {{ filterStatus | lowercase }} share closure requests.
- {{ r.account_no }} - {{ r.client_name }}{{ formatDateTime(r.closed_date) }}{{ r.clerk_name }}{{ r.manager_name || r.manager_username || '—' }}{{ formatDateTime(r.submitted_at) }} - {{ statusLabel(r.status) }} - - -
- - -@if (showReviewModal && reviewRequestId) { -
-
- -
-
-} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/share-closure-requests/share-closure-requests.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/share-closure-requests/share-closure-requests.component.ts deleted file mode 100644 index a9ccfd6fa..000000000 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/share-closure-requests/share-closure-requests.component.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * 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 { FormsModule } from '@angular/forms'; -import { catchError } from 'rxjs/operators'; -import { of } from 'rxjs'; - -import { ClerkShareClosureService } from '../../../cashier/clerk-share-closure.service'; -import { ShareClosureRequestComponent } from '../../../clerk/share-closure-request/share-closure-request.component'; - -/** Approval Center — Share Closure Requests tab. A Bank Manager (checker) reviews, approves or - * rejects share account closure requests submitted from the Clerk Dashboard. Mirrors - * ShareWithdrawalRequestsComponent. Clicking "Review" opens the review screen in a popup on top of - * this list, rather than navigating away, so approving/rejecting drops the manager straight back - * into the (now refreshed) list. */ -@Component({ - selector: 'mifosx-share-closure-requests', - templateUrl: './share-closure-requests.component.html', - styleUrls: [ - '../../../cashier/cashier-shared.scss', - '../../../clerk/clerk-shared.scss', - '../share-withdrawal-requests/share-withdrawal-requests.component.scss' - ], - standalone: true, - imports: [ - CommonModule, - FormsModule, - ShareClosureRequestComponent - ] -}) -export class ShareClosureRequestsComponent implements OnInit { - private service = inject(ClerkShareClosureService); - - requests: any[] = []; - totalRecords = 0; - loading = false; - loaded = false; - - filterStatus = 'PENDING'; - - showReviewModal = false; - reviewRequestId: number | null = null; - - get statusOptions(): { value: string; label: string }[] { - return [ - { value: 'PENDING', label: 'Pending' }, - { value: 'APPROVED', label: 'Approved' }, - { value: 'REJECTED', label: 'Rejected' } - ]; - } - - ngOnInit(): void { - this.load(); - } - - load(): void { - this.loading = true; - this.service - .getPendingRequests(this.filterStatus, 0, 50) - .pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))) - .subscribe((res: any) => { - this.requests = res?.pageItems ?? []; - this.totalRecords = Number(res?.totalFilteredRecords ?? 0); - this.loading = false; - this.loaded = true; - }); - } - - onFilterChange(): void { - this.load(); - } - - review(req: any): void { - this.reviewRequestId = req.id; - this.showReviewModal = true; - } - - closeReviewModal(): void { - this.showReviewModal = false; - this.reviewRequestId = null; - } - - onReviewActionCompleted(): void { - this.closeReviewModal(); - this.load(); - } - - statusClass(status: string): string { - const map: Record = { - PENDING: 'inv-badge--active', - APPROVED: 'inv-badge--matured', - REJECTED: 'inv-badge--written_off' - }; - return `inv-badge ${map[status] ?? ''}`; - } - - statusLabel(status: string): string { - const map: Record = { - PENDING: 'Pending', - APPROVED: 'Approved', - REJECTED: 'Rejected' - }; - return map[status] ?? status; - } - - formatDateTime(val: any): string { - if (!val) return '—'; - try { - const d = new Date(val); - return d.toLocaleString('en-IN', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } catch { - return String(val); - } - } -} diff --git a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html index 438a1f49c..4210bba1d 100644 --- a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html +++ b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html @@ -144,15 +144,6 @@ Share Withdrawal Requests
- - - Share Closure Requests - - import('../clerk/share-closure-request/share-closure-request.component').then( - (m) => m.ShareClosureRequestComponent - ), - data: { title: 'Review Share Closure Request', breadcrumb: 'Review Share Closure', mode: 'review' } - } - ] - }, { path: 'checker-inbox-and-tasks/disbursement-requests', children: [