Compare commits

...
11 Commits
Author SHA1 Message Date
nitesh 4281a596af fix: restore remaining content dropped by the dev merge (comprehensive audit)
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
Systematic file-by-file audit of demo-backup against dev to find any
remaining instances of the same silent-drop pattern from the earlier
merge (git's 3-way auto-resolve keeping demo-backup's side wherever it
had independently touched nearby lines, even when dev's side was a
genuine, unrelated addition). Restored:

- clients.module.ts: missing ClientFamilyMemberChildDialogComponent
  registration (component existed but was never wired in)
- client-family-member-dialog: full children-management flow (checkbox,
  form array, child dialog integration, view/edit summary blocks)
- edit-client: FIR-exception flow for inaccessible old mobile numbers,
  and a hardened detectSensitiveFieldChanges() (null-guard, phone-digit
  normalization, dirty-checked comparisons to avoid false-positive
  Verify-Contact-Change prompts)
- shell.component.ts: withdrawal-review URL matcher extended to cover
  the Share Withdrawal Request review route
- fiu.component.html: AML alert status filter and monthly CTR reports
  table UI (backing logic already existed unused in the .ts)
- Shares module: Passbook tab (component existed but had no route or
  tab link), running-balance/resolution columns on the transactions
  tab, subscript-sizing fixes and a pill-style filter-bar redesign on
  Share Holders
- Savings/Share product accounting steps: Payment-Channel-to-Fund-
  Source mapping section, and Share Reference account restructuring
- pending-client-edits: email status badges + "View Email" button
  (dialog component existed but nothing on this list triggered it)

Everything else that still differs from dev was individually verified
as either demo-backup's own independent work (Investment/Loan
Origination/NPA/RBI-OSS modules, the Approval Center tab-nav redesign,
AML/compliance review features, bank-name rebrand in demo seed data)
or purely cosmetic formatting differences — left untouched.
2026-08-06 16:35:52 +05:30
nitesh 9f9c9bfab6 feat(cashier): restore Share Redemption Transfer support in Transaction Entry
This entire feature — submitting a governed Share Withdrawal Request
directly from a Bank Transfer's DR(Share)/CR(linked savings) row pair
— was silently dropped from demo-backup during the dev merge's auto-
resolve, even though the feature files it depends on (clerk-share-
withdrawal.service.ts) were present. Restores: paymentTypesForRow()
(hides cash payment types for Share rows and their settlement CR
row), the SHARE NO./RESOLUTION NO./MEETING DATE fields (replacing
AMOUNT/CHEQUE NO./CHEQ. DATE for Share rows), rowSharePrices/
rowClientIds/rowLinkedSavingsAcNo tracking, submitShareRedemptionTransfer()
+ its confirm/submit flow, syncShareLinkedAmounts() auto-fill, the
Share-aware ecsAcNo/currentBalance logic, the Other Information panel's
Share-specific fields (No. of Shares Held, Market Price/Share,
Resolution No., Linked Savings A/C No.), and the SHR guard blocking
the generic Deposit/Withdrawal Slip flow for Share accounts.
2026-08-06 16:03:31 +05:30
nitesh 513e863bba fix(cashier): skip today's-transactions/standing-instructions calls for Share accounts
Share accounts have no /accounts/share/{id}/transactions REST
sub-resource (transactions come embedded via ?associations=all) and
Share isn't a valid standing-instruction source account type on the
backend — hitting either endpoint 404s / errors. Restores the
txnFetchUnsupported/siFetchUnsupported config flags (present on dev,
lost in the earlier dev-merge's silent auto-resolve) that skip both
calls for Share, fixing the 404 seen when opening a Share account in
Transaction Entry.
2026-08-06 15:30:30 +05:30
nitesh 13333088d7 fix(cashier): fix Share account lookup false-negative in Transaction Entry
The partial/suffix match fallback was gated on "exactly one row came
back from the list endpoint", but Share's /accounts/share list has no
accountNo server-side filter at all — it always returns the full
unfiltered list, so that guard could never pass and any non-exact
account number search (e.g. shortened digits) reported "Account not
found" even when the account existed. This fix (present on dev, lost
in the earlier dev-merge's silent auto-resolve) drops the count gate
and tries suffix-match then substring-match unconditionally.
2026-08-06 15:23:28 +05:30
nitesh 89bb76455e fix: restore content silently dropped from the dev merge (KYC exception, TDS reports, Form 121)
The demo-backup merge's git 3-way auto-resolve dropped several hunks
without flagging conflicts, wherever demo had independently touched
adjacent lines: the KYC Exception feature (client-kyc-step, kyc-tab,
create-client, clients.service — checkbox, derivation logic, banners,
contact-log endpoints), TDS report routes/menu links (reports-routing,
shell), the Form 121 client tab (clients-routing, clients-view), the
kyc-management exceptions dashboard, and the Form 121/15G-H register's
real-data view integration. Restored by diffing demo-backup against
dev per file and re-applying each genuine gap while preserving
demo-backup's own independent work (Approval Center tab style, Loan
Origination/Investment menus, form-97-98/risk-review report routes).
2026-08-06 15:12:06 +05:30
nitesh 7e69468995 fix: remove duplicate declarations introduced by dev merge
Both dev and demo-backup independently added suppressMonthlyAnnualLink
and isHeadOfficeUser() at different points in their respective history,
so the merge's auto-resolve kept both copies instead of deduping,
causing TS2300/TS2393 build failures.
2026-08-06 14:31:13 +05:30
nitesh 63ab9643ce fix(headers): add missing MPL-2.0 license headers on merged files
Files restored from dev during the demo-backup merge were missing the
standard license header (dev's copies predate that fix, which had only
been applied on the demo line previously). Also fixed pre-existing lint
debt surfaced by the edit: wrong component selector prefix and
constructor-injection pattern in client-family-member-child-dialog.
2026-08-06 14:21:06 +05:30
nitesh 8255c8398d Merge dev into demo-backup
Brings in all of dev's latest content not present on demo-backup
(Share Withdrawal/Closure feature, TDS reports, KYC exception tracking,
client-edit-email-dialog, form121 tab, search-select) while preserving
demo-backup's own unique changes (wrapping Approval Center tab nav,
Loan Origination/Investment mega-menus). 36 files that a prior "ours
wins" conflict resolution had silently dropped were restored explicitly
from dev's tip.
2026-08-06 14:16:22 +05:30
nitesh 14c5f5070d Revert demo to pre-2026-08-06 state (rollback today's push)
Reverts all commits pushed to demo today (a5124ca3, 858c277a, a1a50ab8,
f72abb09, fcf66657) back to f8c532f7 as a single forward commit, without
rewriting history. All reverted content (Share Withdrawal/Closure feature,
TDS reports, KYC exception tracking, client-edit-email-dialog, form121 tab,
search-select) remains intact on dev and was verified identical before
this revert.
2026-08-06 13:39:40 +05:30
nitesh b609f19e9f Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora into dev
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
2026-08-06 11:45:46 +05:30
niteshandClaude Sonnet 5 7f836f6f71 feat(shares): add Share Account Closure maker-checker workflow
Clerk submits a closure request only once a share account holds zero
shares; manager validates (shares still zero, account active, no active
loan/guarantee, KYC/AML standing, maker!=checker) and approves, which
executes the real Fineract "close" command. The direct native "Close"
action is removed from the account menu — it silently force-redeemed any
remaining shares, bypassing the withdrawal approval flow entirely — and
replaced with an "Eligible for Closure" banner. When a withdrawal approval
brings a share account's holding to zero, a closure request is now
auto-submitted (still requires manager approval) so the clerk doesn't have
to separately open "New Share Closure Request".

Also fixes an unrelated build break introduced by a merged commit: a
missing `suppressMonthlyAnnualLink` property declaration, and two files
(branch-scope.service.ts, gl-reports-data.ts) that existed on `demo` but
were never brought into `dev` alongside the new TDS report components
that depend on them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 11:43:56 +05:30
41 changed files with 2709 additions and 1283 deletions
+1 -1
View File
@@ -70,7 +70,7 @@
{
"type": "initial",
"maximumWarning": "15mb",
"maximumError": "16mb"
"maximumError": "17mb"
},
{
"type": "anyComponentStyle",
@@ -327,11 +327,8 @@ export class SearchJournalEntryComponent implements OnInit, OnDestroy, AfterView
},
error: (err: any) => {
this.marking.delete(entry.id);
// Fineract wraps the real reason in errors[0]; the outer defaultUserMessage
// is the generic "Errors contain reason for domain rule violation", which
// tells the reviewer nothing.
this.markError =
err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Could not mark this entry.';
err?.error?.defaultUserMessage || err?.error?.errors?.[0]?.defaultUserMessage || 'Could not mark this entry.';
}
});
}
+16
View File
@@ -78,6 +78,22 @@ 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: () =>
@@ -0,0 +1,53 @@
/**
* 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<any> {
return this.http.post('/clerk/share-closure-requests', data);
}
getMyRequests(status?: string, offset = 0, limit = 20, fromDate?: string, toDate?: string): Observable<any> {
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<any> {
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<any> {
return this.http.get(`/clerk/share-closure-requests/${id}`);
}
validate(id: number): Observable<any> {
return this.http.post(`/clerk/share-closure-requests/${id}/validate`, {});
}
approve(id: number): Observable<any> {
return this.http.post(`/clerk/share-closure-requests/${id}/approve`, {});
}
reject(id: number, reason: string): Observable<any> {
return this.http.post(`/clerk/share-closure-requests/${id}/reject`, { reason });
}
}
@@ -0,0 +1,69 @@
<!--
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/.
-->
<div class="pg">
<header class="pg-head">
<div>
<div class="pg-eyebrow">CLERK · SHARE CLOSURE</div>
<h1 class="pg-title">My Share Closure Requests</h1>
<p class="pg-sub">Share account closure requests you have submitted, and their approval status.</p>
</div>
<div class="pg-toolbar">
<button type="button" class="pg-btn pg-btn--primary" (click)="newRequest()">New Share Closure Request</button>
</div>
</header>
<div class="pg-card">
<div class="inv-form-grid" style="padding: 12px 16px">
<label class="inv-form-field">
<span>Status</span>
<select [(ngModel)]="filterStatus" (change)="onFilterChange()">
@for (opt of statusOptions; track opt.value) {
<option [value]="opt.value">{{ opt.label }}</option>
}
</select>
</label>
</div>
<table class="pg-table">
<thead>
<tr>
<th>Account No</th>
<th>Client</th>
<th>Closed Date</th>
<th>Submitted At</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@if (loading) {
<tr>
<td colspan="5" class="pg-empty">Loading…</td>
</tr>
} @else if (loaded && requests.length === 0) {
<tr>
<td colspan="5" class="pg-empty">No share closure requests found.</td>
</tr>
} @else {
@for (r of requests; track r.id) {
<tr>
<td>
<span class="pg-chip">{{ r.account_no }}</span>
</td>
<td>{{ r.client_name }}</td>
<td>{{ formatDateTime(r.closed_date) }}</td>
<td>{{ formatDateTime(r.submitted_at) }}</td>
<td>
<span [class]="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,112 @@
/**
* 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<string, string> = {
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<string, string> = {
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);
}
}
}
@@ -0,0 +1,355 @@
<!--
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/.
-->
<div class="pg">
<header class="pg-head">
<div>
<div class="pg-eyebrow">{{ isReview ? 'CHECKER · APPROVAL' : 'CLERK · SHARE CLOSURE' }}</div>
<h1 class="pg-title">{{ pageTitle }}</h1>
<p class="pg-sub">{{ pageSubtitle }}</p>
</div>
<div class="pg-toolbar">
<button type="button" class="pg-btn" (click)="goBack()">
<mat-icon>arrow_back</mat-icon>
{{ isReview ? 'Back to Requests' : 'My Share Closure Requests' }}
</button>
</div>
</header>
<div class="pg-card">
<div class="pos-section-label">Share Account</div>
<div class="inv-form-grid" style="padding: 16px">
@if (!isReview) {
<label class="inv-form-field" style="grid-column: span 2">
<span>* Search Account No. / Client Name (only zero-share accounts are eligible)</span>
<mifosx-search-select
[options]="accountOptions"
[(ngModel)]="selectedAccountId"
(ngModelChange)="onAccountSelected($event)"
placeholder="Search share account no. or client name…"
></mifosx-search-select>
</label>
}
@if (account) {
<label class="inv-form-field">
<span>Account No</span>
<input type="text" [value]="account.accountNo" readonly />
</label>
<label class="inv-form-field">
<span>Client</span>
<input type="text" [value]="account.clientName" readonly />
</label>
<label class="inv-form-field">
<span>Shares Held</span>
<input type="text" [value]="heldShares" readonly />
</label>
}
@if (loadingAccount) {
<div class="pg-empty">Loading account…</div>
}
@if (account && heldShares !== 0 && !isReview) {
<div class="pg-empty" style="grid-column: span 2; color: #b42318">
This account still holds {{ heldShares }} share(s) — all shares must be withdrawn first via the Share
Withdrawal workflow before it becomes eligible for closure.
</div>
}
</div>
</div>
@if (isReview && reviewRequest) {
<div class="pg-card">
<div class="pos-section-label">Client Compliance</div>
<div class="inv-form-grid" style="padding: 16px">
<div class="inv-form-field">
<span>KYC Status</span>
<span
class="swr-chip"
[class.swr-chip--ok]="reviewRequest.client_kyc_verified"
[class.swr-chip--warn]="!reviewRequest.client_kyc_verified"
>
{{ reviewRequest.client_kyc_status_label }}
</span>
</div>
<div class="inv-form-field">
<span>Risk Category</span>
<span
class="swr-chip"
[class.swr-chip--ok]="!reviewRequest.client_risk_high"
[class.swr-chip--warn]="reviewRequest.client_risk_high"
>
{{ reviewRequest.client_risk_category_label }}
</span>
</div>
<div class="inv-form-field">
<span>Open AML Alerts</span>
<span
class="swr-chip"
[class.swr-chip--ok]="!reviewRequest.client_open_aml_alerts"
[class.swr-chip--warn]="reviewRequest.client_open_aml_alerts > 0"
>
{{ reviewRequest.client_open_aml_alerts || 0 }}
</span>
</div>
</div>
</div>
}
@if (account) {
<div class="pg-card">
<div class="pos-section-label">Closure Details</div>
<div class="inv-form-grid" style="padding: 16px">
<label class="inv-form-field">
<span>* Closed Date</span>
<input type="date" [(ngModel)]="closedDate" [disabled]="isReview" />
</label>
<label class="inv-form-field" style="grid-column: span 2">
<span>Note (optional)</span>
<input
type="text"
[(ngModel)]="note"
[disabled]="isReview"
placeholder="e.g. Member exit — all shares redeemed"
/>
</label>
</div>
@if (!isReview) {
<div class="inv-form-actions">
<button
type="button"
class="pg-btn pg-btn--primary"
[disabled]="!isFormValid || submitting"
(click)="openConfirmDialog()"
>
{{ submitting ? 'Submitting…' : 'Review &amp; Submit' }}
</button>
</div>
}
</div>
}
@if (isReview && reviewRequest?.status === 'PENDING') {
<div class="pg-card">
<div class="pos-section-label">Verification</div>
<div class="swrv-trigger">
<div class="swrv-trigger__icon">
<mat-icon>fact_check</mat-icon>
</div>
<div class="swrv-trigger__text">
<div class="swrv-trigger__title">Run a live backend validation</div>
<div class="swrv-trigger__sub">
Re-checks shares held, account status, active loan/guarantee and client compliance before you approve.
</div>
</div>
<button type="button" class="pg-btn pg-btn--primary" (click)="openValidateModal()">
<mat-icon>play_circle</mat-icon>
Validate Request
</button>
</div>
<div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="approveReview()">
<mat-icon>check_circle</mat-icon>
Approve &amp; Close Account
</button>
<button type="button" class="pg-btn pg-btn--danger" (click)="openRejectDialog()">
<mat-icon>cancel</mat-icon>
Reject
</button>
</div>
</div>
}
@if (showValidateModal) {
<div class="swrv-modal">
<div class="swrv-modal__card">
<div class="swrv-modal__header">
<div class="swrv-modal__title">
<span class="swrv-modal__title-icon"><mat-icon>fact_check</mat-icon></span>
Validate Share Closure Request
</div>
<button type="button" class="swrv-modal__close" [disabled]="validating" (click)="closeValidateModal()">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="swrv-modal__body">
<p class="swrv-modal__intro">
Re-checking closure of <strong>{{ account?.accountNo }}</strong> ({{ account?.clientName }}) against the
live account, loan/guarantee and compliance state — not just what was captured at submission time.
</p>
@if (backendChecks && !validating && !validateAllPassed) {
<div class="swrv-modal__warn">
<mat-icon>warning</mat-icon>
<div>
<strong>Some checks need review before approving.</strong>
Scroll down to see which ones failed and why.
</div>
</div>
}
<ol class="swrv-steps">
@for (step of validateSteps; track step.label; let i = $index) {
<li
class="swrv-step"
[class.swrv-step--active]="step.status === 'active'"
[class.swrv-step--done]="step.status === 'done'"
[class.swrv-step--failed]="step.status === 'failed'"
>
<div class="swrv-step__indicator">
@if (step.status === 'done') {
<mat-icon>check</mat-icon>
} @else if (step.status === 'active') {
<mat-icon class="swrv-spin">autorenew</mat-icon>
} @else if (step.status === 'failed') {
<mat-icon>close</mat-icon>
} @else {
<span>{{ i + 1 }}</span>
}
</div>
<div class="swrv-step__content">
<div class="swrv-step__label">{{ step.label }}</div>
<div class="swrv-step__msg">{{ step.detail }}</div>
</div>
</li>
}
</ol>
@if (validating && validateSteps.length === 0) {
<div class="pg-empty">Connecting to server…</div>
}
@if (backendChecks && !validating && validateAllPassed) {
<div class="swrv-modal__result">
<mat-icon>verified</mat-icon>
<span>All checks passed — safe to approve.</span>
</div>
}
</div>
<div class="swrv-modal__footer">
@if (!validating && backendChecks) {
<button type="button" class="pg-btn" (click)="runBackendValidation()">
<mat-icon>refresh</mat-icon>
Re-run
</button>
}
@if (!validating && backendChecks && validateAllPassed) {
<button type="button" class="pg-btn pg-btn--primary" (click)="closeValidateModal(); approveReview()">
<mat-icon>check_circle</mat-icon>
Approve &amp; Close Account
</button>
}
<button type="button" class="pg-btn" [disabled]="validating" (click)="closeValidateModal()">
{{ validating ? 'Validating…' : 'Close' }}
</button>
</div>
</div>
</div>
}
@if (isReview && activityLog.length > 0) {
<div class="pg-card">
<div class="pos-section-label">Activity Log</div>
<table class="pg-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Action</th>
<th>User</th>
<th>Note</th>
</tr>
</thead>
<tbody>
@for (entry of activityLog; track entry.timestamp) {
<tr>
<td>{{ entry.timestamp | date: 'medium' }}</td>
<td>{{ entry.action }}</td>
<td>{{ entry.user }}</td>
<td>{{ entry.note || '—' }}</td>
</tr>
}
</tbody>
</table>
</div>
}
@if (showConfirmDialog) {
<div class="swr-modal-backdrop">
<div class="swr-modal">
<h3>Confirm Share Closure Request</h3>
<p class="pg-sub">Review the details below before submitting for Bank Manager approval.</p>
<table class="pg-table">
<tbody>
<tr>
<td>Account No / Client</td>
<td>{{ account?.accountNo }} — {{ account?.clientName }}</td>
</tr>
<tr>
<td>Shares Held</td>
<td>{{ heldShares }}</td>
</tr>
<tr>
<td>Closed Date</td>
<td>{{ closedDate }}</td>
</tr>
<tr>
<td>Note</td>
<td>{{ note || '—' }}</td>
</tr>
</tbody>
</table>
<div class="pos-section-label" style="margin-top: 12px">Validation Checks</div>
<table class="pg-table">
<tbody>
@for (check of verificationChecks; track check.label) {
<tr>
<td>{{ check.label }}</td>
<td [class.swr-check-pass]="check.pass" [class.swr-check-fail]="!check.pass">
{{ check.pass ? 'Pass — ' : 'Review — ' }}{{ check.detail }}
</td>
</tr>
}
</tbody>
</table>
<div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" [disabled]="submitting" (click)="confirmSubmit()">
{{ submitting ? 'Submitting…' : 'Confirm & Submit' }}
</button>
<button type="button" class="pg-btn" (click)="cancelConfirmDialog()">Back to Edit</button>
</div>
</div>
</div>
}
@if (showRejectDialog) {
<div class="swr-modal-backdrop">
<div class="swr-modal">
<h3>Reject Share Closure Request</h3>
<label class="inv-form-field">
<span>* Rejection Reason</span>
<textarea rows="3" [(ngModel)]="rejectionReason"></textarea>
</label>
<div class="inv-form-actions">
<button
type="button"
class="pg-btn pg-btn--danger"
[disabled]="!rejectionReason.trim()"
(click)="confirmRejectReview()"
>
Confirm Reject
</button>
<button type="button" class="pg-btn" (click)="showRejectDialog = false">Cancel</button>
</div>
</div>
</div>
}
</div>
@@ -0,0 +1,336 @@
/**
* 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<void>();
@Output() actionCompleted = new EventEmitter<void>();
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']);
}
}
@@ -361,11 +361,14 @@ export class ShareWithdrawalRequestComponent implements OnInit {
if (!this.reviewRequest) return;
this.wdService.approve(this.reviewRequest.id).subscribe({
next: (res: any) => {
const message =
let message =
res?.settlementAmount != null
? `Share withdrawal approved. ₹${Number(res.settlementAmount).toLocaleString('en-IN', { minimumFractionDigits: 2 })} credited to the linked savings account.`
: (res?.settlementWarning ?? 'Share withdrawal approved and processed.');
this.snackBar.open(message, 'Close', { duration: 6000 });
if (res?.autoClosureRequestId) {
message += ' All shares now withdrawn — a Share Closure Request was auto-submitted for manager review.';
}
this.snackBar.open(message, 'Close', { duration: 8000 });
if (this.embedded) {
this.actionCompleted.emit();
} else {
@@ -7,92 +7,104 @@
-->
<div class="fm-dialog">
<div class="fm-dialog__header" style="display: flex; align-items: center; gap: 8px;">
<div class="fm-dialog__header" style="display: flex; align-items: center; gap: 8px">
<mat-icon class="fm-dialog__icon">people</mat-icon>
<span style="font-weight: 500; font-size: 18px;">CHILDREN</span>
<span style="font-weight: 500; font-size: 18px">CHILDREN</span>
</div>
<mat-dialog-content>
<form [formGroup]="childrenForm">
<div formArrayName="children" style="margin-top: 12px; background: #fff; padding: 12px;">
<div formArrayName="children" style="margin-top: 12px; background: #fff; padding: 12px">
@for (child of children.controls; track $index) {
<div [formGroupName]="$index"
style="position: relative; padding: 24px 16px 16px; margin-top: 16px; margin-bottom: 24px; border: 1px solid #e0e0e0; border-radius: 8px; background: #fff;">
<div class="fm-grid">
<mat-form-field appearance="outline">
<mat-label>Child Full Name*</mat-label>
<input matInput formControlName="fullName" required placeholder="Child Full Name*" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Gender*</mat-label>
<mat-select formControlName="genderId" required>
@for (g of genderIdOptions; track g.id) {
<mat-option [value]="g.id">{{ g.name }}</mat-option>
<div
[formGroupName]="$index"
style="
position: relative;
padding: 24px 16px 16px;
margin-top: 16px;
margin-bottom: 24px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fff;
"
>
<div class="fm-grid">
<mat-form-field appearance="outline">
<mat-label>Child Full Name*</mat-label>
<input matInput formControlName="fullName" required placeholder="Child Full Name*" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Gender*</mat-label>
<mat-select formControlName="genderId" required>
@for (g of genderIdOptions; track g.id) {
<mat-option [value]="g.id">{{ g.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" (click)="childDobPicker.open()">
<mat-label>Date of Birth</mat-label>
<input matInput [max]="maxDate" [matDatepicker]="childDobPicker" formControlName="dateOfBirth" />
<mat-datepicker-toggle matSuffix [for]="childDobPicker"></mat-datepicker-toggle>
<mat-datepicker #childDobPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Nationality</mat-label>
<mat-select formControlName="nationality">
<mat-option value="INDIAN">Indian</mat-option>
<mat-option value="NRI">NRI</mat-option>
<mat-option value="FOREIGN_NATIONAL">Foreign National</mat-option>
<mat-option value="OTHER">Other</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Aadhaar Number (Optional)</mat-label>
<input
matInput
formControlName="aadhaarNumber"
maxlength="12"
placeholder="12-digit Aadhaar"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>PAN Number (Optional)</mat-label>
<input
matInput
formControlName="panNumber"
maxlength="10"
placeholder="ABCDE1234F"
style="text-transform: uppercase"
(input)="
$any(children.at($index))
.get('panNumber')
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
"
/>
@if (
$any(children.at($index)).get('panNumber')?.hasError('pattern') &&
$any(children.at($index)).get('panNumber')?.touched
) {
<mat-error>Invalid PAN format</mat-error>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" (click)="childDobPicker.open()">
<mat-label>Date of Birth</mat-label>
<input matInput [max]="maxDate" [matDatepicker]="childDobPicker" formControlName="dateOfBirth" />
<mat-datepicker-toggle matSuffix [for]="childDobPicker"></mat-datepicker-toggle>
<mat-datepicker #childDobPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Nationality</mat-label>
<mat-select formControlName="nationality">
<mat-option value="INDIAN">Indian</mat-option>
<mat-option value="NRI">NRI</mat-option>
<mat-option value="FOREIGN_NATIONAL">Foreign National</mat-option>
<mat-option value="OTHER">Other</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Aadhaar Number (Optional)</mat-label>
<input
matInput
formControlName="aadhaarNumber"
maxlength="12"
placeholder="12-digit Aadhaar"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>PAN Number (Optional)</mat-label>
<input
matInput
formControlName="panNumber"
maxlength="10"
placeholder="ABCDE1234F"
style="text-transform: uppercase"
(input)="
$any(children.at($index)).get('panNumber')
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
"
/>
@if (
$any(children.at($index)).get('panNumber')?.hasError('pattern') && $any(children.at($index)).get('panNumber')?.touched
) {
<mat-error>Invalid PAN format</mat-error>
}
</mat-form-field>
</mat-form-field>
</div>
</div>
</div>
@if ($index < children.controls.length - 1) { <div style="border-bottom: 1px dashed #ccc; margin-bottom: 16px;">
@if ($index < children.controls.length - 1) {
<div style="border-bottom: 1px dashed #ccc; margin-bottom: 16px"></div>
}
}
<div style="border-bottom: 1px dashed #ccc; margin-bottom: 16px"></div>
<button mat-stroked-button type="button" color="primary" (click)="addChild()">
<mat-icon>add</mat-icon> Add Another Child
</button>
</div>
}
}
<div style="border-bottom: 1px dashed #ccc; margin-bottom: 16px;"></div>
<button mat-stroked-button type="button" color="primary" (click)="addChild()">
<mat-icon>add</mat-icon> Add Another Child
</button>
</div>
</form>
</mat-dialog-content>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-stroked-button mat-dialog-close type="button">Cancel</button>
<button mat-flat-button color="primary" type="button" [disabled]="childrenForm.invalid" (click)="submit()">
Add Member
</button>
</mat-dialog-actions>
</div>
<mat-dialog-actions align="end">
<button mat-stroked-button mat-dialog-close type="button">Cancel</button>
<button mat-flat-button color="primary" type="button" [disabled]="childrenForm.invalid" (click)="submit()">
Add Member
</button>
</mat-dialog-actions>
</div>
@@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Inject, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
@@ -21,7 +21,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'cbs-client-family-member-child-dialog',
selector: 'mifosx-client-family-member-child-dialog',
templateUrl: './client-family-member-child-dialog.component.html',
styleUrls: ['../client-family-member-dialog/client-family-member-dialog.component.scss'],
standalone: true,
@@ -38,18 +38,17 @@ import { CommonModule } from '@angular/common';
]
})
export class ClientFamilyMemberChildDialogComponent implements OnInit {
dialogRef = inject(MatDialogRef<ClientFamilyMemberChildDialogComponent>);
data = inject(MAT_DIALOG_DATA);
private formBuilder = inject(UntypedFormBuilder);
childrenForm: UntypedFormGroup;
genderIdOptions: any[];
maxDate: Date = new Date();
constructor(
public dialogRef: MatDialogRef<ClientFamilyMemberChildDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private formBuilder: UntypedFormBuilder
) {
this.genderIdOptions = data.genderIdOptions;
constructor() {
this.genderIdOptions = this.data.genderIdOptions;
this.childrenForm = this.formBuilder.group({
children: this.formBuilder.array([])
});
@@ -58,15 +57,26 @@ export class ClientFamilyMemberChildDialogComponent implements OnInit {
ngOnInit() {
if (this.data.children && this.data.children.length > 0) {
this.data.children.forEach((child: any) => {
this.children.push(this.formBuilder.group({
id: [child.id],
fullName: [child.fullName, Validators.required],
genderId: [child.genderId, Validators.required],
dateOfBirth: [child.dateOfBirth ? new Date(child.dateOfBirth) : ''],
nationality: [child.nationality || 'INDIAN'],
aadhaarNumber: [child.aadhaarNumber || ''],
panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
}));
this.children.push(
this.formBuilder.group({
id: [child.id],
fullName: [
child.fullName,
Validators.required
],
genderId: [
child.genderId,
Validators.required
],
dateOfBirth: [child.dateOfBirth ? new Date(child.dateOfBirth) : ''],
nationality: [child.nationality || 'INDIAN'],
aadhaarNumber: [child.aadhaarNumber || ''],
panNumber: [
child.panNumber || '',
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
]
})
);
});
} else {
this.addChild();
@@ -80,12 +90,21 @@ export class ClientFamilyMemberChildDialogComponent implements OnInit {
createChildFormGroup(): UntypedFormGroup {
return this.formBuilder.group({
id: [''],
fullName: ['', Validators.required],
genderId: ['', Validators.required],
fullName: [
'',
Validators.required
],
genderId: [
'',
Validators.required
],
dateOfBirth: [''],
nationality: ['INDIAN'],
aadhaarNumber: [''],
panNumber: ['', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
panNumber: [
'',
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
]
});
}
@@ -102,5 +121,4 @@ export class ClientFamilyMemberChildDialogComponent implements OnInit {
this.dialogRef.close(this.children.value);
}
}
}
@@ -144,7 +144,7 @@
<span class="fm-readonly-label">Children</span>
<div class="fm-children-list">
@for (child of familyMemberForm.get('children')?.value; track $index) {
<div class="fm-child-item" style="font-size: 13px; color: #555; margin-top: 4px;">
<div class="fm-child-item" style="font-size: 13px; color: #555; margin-top: 4px">
<span>{{ child.fullName }}</span>
@if (child.dateOfBirth) {
<span> · DOB: {{ child.dateOfBirth | date: 'dd MMM yyyy' }}</span>
@@ -465,11 +465,17 @@
<!-- Children Section -->
@if (isSpouse) {
<div class="fm-section-label" style="margin-top: 16px;">Children</div>
<div class="fm-checkbox-row" style="display: flex; align-items: center; justify-content: space-between;">
<div class="fm-section-label" style="margin-top: 16px">Children</div>
<div class="fm-checkbox-row" style="display: flex; align-items: center; justify-content: space-between">
<mat-checkbox formControlName="hasChildren">Does this family member have a child/children?</mat-checkbox>
@if (familyMemberForm.get('hasChildren')?.value) {
<button mat-stroked-button type="button" color="primary" (click)="openChildrenDialog()" style="margin-left: 16px;">
<button
mat-stroked-button
type="button"
color="primary"
(click)="openChildrenDialog()"
style="margin-left: 16px"
>
<mat-icon>edit</mat-icon> Edit Children Details ({{ children.length }})
</button>
}
@@ -58,7 +58,7 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
private dialog = inject(MatDialog);
maxDate = new Date();
hasChildrenChecked = false;
familyMemberForm: UntypedFormGroup;
@@ -206,12 +206,21 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
this.children.push(
this.formBuilder.group({
id: [child.id],
fullName: [child.fullName, Validators.required],
genderId: [child.genderId, Validators.required],
fullName: [
child.fullName,
Validators.required
],
genderId: [
child.genderId,
Validators.required
],
dateOfBirth: [child.dateOfBirth ? new Date(child.dateOfBirth) : ''],
nationality: [child.nationality || 'INDIAN'],
aadhaarNumber: [child.aadhaarNumber || ''],
panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
panNumber: [
child.panNumber || '',
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
]
})
);
});
@@ -249,7 +258,7 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
children: this.formBuilder.array([])
});
this.familyMemberForm.get('hasChildren')?.valueChanges.subscribe(value => {
this.familyMemberForm.get('hasChildren')?.valueChanges.subscribe((value) => {
if (value && !this.hasChildrenChecked) {
this.openChildrenDialog();
} else if (!value) {
@@ -264,9 +273,10 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
}
async openChildrenDialog() {
// dynamically import the dialog to avoid circular dependency issues if any
const { ClientFamilyMemberChildDialogComponent } = await import('../client-family-member-child-dialog/client-family-member-child-dialog.component');
// Dynamically import the dialog to avoid a circular dependency between the two dialogs.
const { ClientFamilyMemberChildDialogComponent } =
await import('../client-family-member-child-dialog/client-family-member-child-dialog.component');
const dialogRef = this.dialog.open(ClientFamilyMemberChildDialogComponent, {
data: {
children: this.children.value,
@@ -278,24 +288,33 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
dialogRef.afterClosed().subscribe((result: any[]) => {
if (result) {
this.children.clear();
result.forEach(child => {
this.children.push(this.formBuilder.group({
id: [child.id],
fullName: [child.fullName, Validators.required],
genderId: [child.genderId, Validators.required],
dateOfBirth: [child.dateOfBirth],
nationality: [child.nationality || 'INDIAN'],
aadhaarNumber: [child.aadhaarNumber || ''],
panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
}));
result.forEach((child) => {
this.children.push(
this.formBuilder.group({
id: [child.id],
fullName: [
child.fullName,
Validators.required
],
genderId: [
child.genderId,
Validators.required
],
dateOfBirth: [child.dateOfBirth],
nationality: [child.nationality || 'INDIAN'],
aadhaarNumber: [child.aadhaarNumber || ''],
panNumber: [
child.panNumber || '',
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
]
})
);
});
this.hasChildrenChecked = true;
this.familyMemberForm.patchValue({ hasChildren: true }, { emitEvent: false });
} else {
if (this.children.length === 0) {
this.hasChildrenChecked = false;
this.familyMemberForm.patchValue({ hasChildren: false }, { emitEvent: false });
}
} else if (this.children.length === 0) {
this.hasChildrenChecked = false;
this.familyMemberForm.patchValue({ hasChildren: false }, { emitEvent: false });
}
});
}
@@ -303,12 +322,21 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
createChildFormGroup(): UntypedFormGroup {
return this.formBuilder.group({
id: [''],
fullName: ['', Validators.required],
genderId: ['', Validators.required],
fullName: [
'',
Validators.required
],
genderId: [
'',
Validators.required
],
dateOfBirth: [''],
nationality: ['INDIAN'],
aadhaarNumber: [''],
panNumber: ['', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
panNumber: [
'',
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
]
});
}
@@ -533,6 +533,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
screeningLogId: number | null = null;
/** Snapshot of the name that was last verified — used to invalidate verification when name changes */
private verifiedName: string = '';
/** True while prefillForEdit() is patching historical monthly/annual values — see linkMonthlyAnnual(). */
private suppressMonthlyAnnualLink = false;
/**
* @param {FormBuilder} formBuilder Form Builder
@@ -709,6 +711,12 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
const data = this.clientData;
this.loadExistingSeniorPwdDocuments();
// Loading an existing client's independently-saved monthly/annual values must not run
// through the auto-calc in linkMonthlyAnnual() — patching monthlyIncome then annualIncome
// (or vice versa) would otherwise have each overwrite the other via its valueChanges,
// instead of keeping the exact historical values as saved.
this.suppressMonthlyAnnualLink = true;
// Patch all base controls that exist on the form
this.createClientForm.patchValue({
officeId: data.officeId,
@@ -743,6 +751,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
? new Date(data.timeline.submittedOnDate)
: this.settingsService.businessDate
});
this.suppressMonthlyAnnualLink = false;
// Patch name fields (already created by buildDependencies via legalFormId subscription)
const legalFormId = data.legalForm?.id || LegalFormId.PERSON;
@@ -960,6 +969,37 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.linkIncomeToTurnover();
}
/**
* Auto-calculates turnover based on income (doubling monthly income for monthly turnover,
* and annual income for annual/yearly turnover).
*/
private linkIncomeToTurnover(): void {
const monthlyIncome = this.createClientForm.get('monthlyIncome');
const monthlyTurnover = this.createClientForm.get('monthlyTurnover');
const annualIncome = this.createClientForm.get('annualIncome');
const annualTurnover = this.createClientForm.get('annualTurnover');
if (monthlyIncome && monthlyTurnover) {
monthlyIncome.valueChanges.subscribe((value) => {
if (this.suppressMonthlyAnnualLink) {
return;
}
const num = parseFloat(value);
monthlyTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
});
}
if (annualIncome && annualTurnover) {
annualIncome.valueChanges.subscribe((value) => {
if (this.suppressMonthlyAnnualLink) {
return;
}
const num = parseFloat(value);
annualTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
});
}
}
/**
* Keeps a Monthly/Annual pair of amount fields in sync at a 12x ratio — typing into either
* one auto-fills the other. Uses { emitEvent: false } on the write side so the two
@@ -994,37 +1034,6 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
});
}
/**
* Auto-calculates turnover based on income (doubling monthly income for monthly turnover,
* and annual income for annual/yearly turnover).
*/
private linkIncomeToTurnover(): void {
const monthlyIncome = this.createClientForm.get('monthlyIncome');
const monthlyTurnover = this.createClientForm.get('monthlyTurnover');
const annualIncome = this.createClientForm.get('annualIncome');
const annualTurnover = this.createClientForm.get('annualTurnover');
if (monthlyIncome && monthlyTurnover) {
monthlyIncome.valueChanges.subscribe((value) => {
if (this.suppressMonthlyAnnualLink) {
return;
}
const num = parseFloat(value);
monthlyTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
});
}
if (annualIncome && annualTurnover) {
annualIncome.valueChanges.subscribe((value) => {
if (this.suppressMonthlyAnnualLink) {
return;
}
const num = parseFloat(value);
annualTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
});
}
}
/**
* Sets select dropdown options.
*/
@@ -2259,26 +2268,26 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
}
// Match monthly income ranges
if (rule.minMonthlyIncome != null && monthlyIncome < rule.minMonthlyIncome) {
if (rule.minMonthlyIncome != null && rule.minMonthlyIncome > 0 && monthlyIncome < rule.minMonthlyIncome) {
continue;
}
if (rule.maxMonthlyIncome != null && monthlyIncome > rule.maxMonthlyIncome) {
if (rule.maxMonthlyIncome != null && rule.maxMonthlyIncome > 0 && monthlyIncome > rule.maxMonthlyIncome) {
continue;
}
// Match annual income ranges (evaluated against totalAnnualIncome)
if (rule.minAnnualIncome != null && totalAnnualIncome < rule.minAnnualIncome) {
if (rule.minAnnualIncome != null && rule.minAnnualIncome > 0 && totalAnnualIncome < rule.minAnnualIncome) {
continue;
}
if (rule.maxAnnualIncome != null && totalAnnualIncome > rule.maxAnnualIncome) {
if (rule.maxAnnualIncome != null && rule.maxAnnualIncome > 0 && totalAnnualIncome > rule.maxAnnualIncome) {
continue;
}
// Match turnover ranges
if (rule.minAnnualTurnover != null && annualTurnover < rule.minAnnualTurnover) {
if (rule.minAnnualTurnover != null && rule.minAnnualTurnover > 0 && annualTurnover < rule.minAnnualTurnover) {
continue;
}
if (rule.maxAnnualTurnover != null && annualTurnover > rule.maxAnnualTurnover) {
if (rule.maxAnnualTurnover != null && rule.maxAnnualTurnover > 0 && annualTurnover > rule.maxAnnualTurnover) {
continue;
}
@@ -725,16 +725,36 @@
}
<!-- KYC Exception Flag -->
<div class="exception-flag-box" style="margin-top: 20px; padding: 16px; border: 1px solid #f59e0b; background: #fffbeb; border-radius: 8px; clear: both;">
<h5 style="margin: 0 0 12px 0; font-size: 14px; font-weight: 600; color: #b45309; display: flex; align-items: center; gap: 8px;">
<div
class="exception-flag-box"
style="
margin-top: 20px;
padding: 16px;
border: 1px solid #f59e0b;
background: #fffbeb;
border-radius: 8px;
clear: both;
"
>
<h5
style="
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #b45309;
display: flex;
align-items: center;
gap: 8px;
"
>
<fa-icon icon="exclamation-triangle"></fa-icon>
KYC Exception Flag
</h5>
<div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap">
<mat-checkbox formControlName="isException">Mark as KYC Exception</mat-checkbox>
@if (kycForm.get('isException')?.value) {
<mat-form-field appearance="outline" style="flex: 1; min-width: 280px; margin-bottom: -1.25em;">
<mat-form-field appearance="outline" style="flex: 1; min-width: 280px; margin-bottom: -1.25em">
<mat-label>Exception Reasons</mat-label>
<mat-select formControlName="exceptionReason" multiple>
@if (!isNonIndividual) {
@@ -1437,7 +1457,6 @@
</div>
</div>
<!-- ═══ SECTION: Photograph & Signature ═══ -->
<div class="section-block">
<h4 class="section-title">
@@ -1874,7 +1874,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
// or blank PAN/Aadhaar is an exception by definition, so the officer cannot forget to flag
// it. See deriveKycExceptionFields() for the raise / narrow / close rules.
...deriveKycExceptionFields({
panVerified: this.isNonIndividual ? this.entPanVerifyStatus === 'VERIFIED' : this.panVerifyStatus === 'VERIFIED',
panVerified: this.isNonIndividual
? this.entPanVerifyStatus === 'VERIFIED'
: this.panVerifyStatus === 'VERIFIED',
aadhaarVerified: this.aadhaarVerifyStatus === 'VERIFIED',
// Non-individual clients submit a dummy masked Aadhaar, so checking it would saddle every
// company with a permanent exception.
@@ -36,34 +36,7 @@
<strong>Status:</strong>
<span class="status-badge status-{{ dispatch.status | lowercase }}">{{ dispatch.status }}</span>
</p>
<!-- Action buttons: only shown for DISPATCHED / IN_TRANSIT, only for users with UPDATE_WELCOMEKIT permission -->
<ng-container *mifosxHasPermission="'UPDATE_WELCOMEKIT'">
<div
*ngIf="canUpdateStatus(dispatch.status)"
style="margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap;"
>
<button
mat-stroked-button
color="primary"
type="button"
[disabled]="isLoading"
(click)="updateStatus(dispatch, 'DELIVERED')"
>
✅ Mark as Delivered
</button>
<button
mat-stroked-button
color="warn"
type="button"
[disabled]="isLoading"
(click)="updateStatus(dispatch, 'RETURNED')"
>
↩ Mark as Returned
</button>
</div>
</ng-container>
</div><!-- end dispatch-tracking-panel -->
</div>
<hr *ngIf="activeDispatches.length > 0 && welcomeKitsData?.canDispatchNew" />
@@ -165,30 +165,6 @@ export class WelcomeKitDialogComponent implements OnInit {
});
}
canUpdateStatus(status: string): boolean {
return status === 'DISPATCHED' || status === 'IN_TRANSIT';
}
updateStatus(dispatch: any, newStatus: string) {
const label = newStatus === 'DELIVERED' ? 'Delivered' : 'Returned';
const confirmed = confirm(`Are you sure you want to mark this as ${label}? This action cannot be undone from the UI.`);
if (!confirmed) {
return;
}
this.isLoading = true;
this.clientsService.updateWelcomeKitStatus(dispatch.id, newStatus).subscribe({
next: () => {
this.snackBar.open(`Welcome Kit marked as ${label} successfully`, 'Close', { duration: 3000 });
this.fetchData();
},
error: () => {
this.isLoading = false;
this.snackBar.open(`Failed to mark Welcome Kit as ${label}`, 'Close', { duration: 3000 });
}
});
}
getDisplayAddress(dispatch: any): string {
const address = dispatch.deliveryAddress || '';
const name = this.welcomeKitsData?.clientName || '';
@@ -43,13 +43,26 @@
<!-- KYC Summary (read-only when not in edit mode) -->
@if (!isEditMode && kycData) {
@if (kycData.isException && !kycData.exceptionResolved) {
<div class="kyc-exception-banner" style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 16px; margin-bottom: 20px; display: flex; align-items: flex-start; gap: 12px;">
<mat-icon style="color: #d97706; margin-top: 2px;">warning</mat-icon>
<div
class="kyc-exception-banner"
style="
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
display: flex;
align-items: flex-start;
gap: 12px;
"
>
<mat-icon style="color: #d97706; margin-top: 2px">warning</mat-icon>
<div>
<strong style="color: #92400e; font-size: 14px;">KYC Exception Active</strong>
<p style="color: #b45309; font-size: 13px; margin: 4px 0 0 0;">
Account is open under a temporary KYC Exception. Reason: <strong>{{ kycData.exceptionReason || 'Other' }}</strong>.
Please contact the customer and complete verification as soon as possible.
<strong style="color: #92400e; font-size: 14px">KYC Exception Active</strong>
<p style="color: #b45309; font-size: 13px; margin: 4px 0 0 0">
Account is open under a temporary KYC Exception. Reason:
<strong>{{ kycData.exceptionReason || 'Other' }}</strong
>. Please contact the customer and complete verification as soon as possible.
</p>
</div>
</div>
@@ -754,18 +767,23 @@
</mat-form-field>
<!-- KYC Exception Flag -->
<div class="full-span exception-controls-row" style="display: flex; align-items: center; gap: 20px; margin-bottom: 12px; flex-wrap: wrap;">
<mat-checkbox formControlName="isException" style="margin-top: 8px;">Mark as KYC Exception</mat-checkbox>
<div
class="full-span exception-controls-row"
style="display: flex; align-items: center; gap: 20px; margin-bottom: 12px; flex-wrap: wrap"
>
<mat-checkbox formControlName="isException" style="margin-top: 8px">Mark as KYC Exception</mat-checkbox>
@if (kycForm.get('isException')?.value) {
<mat-form-field appearance="outline" style="flex: 1; min-width: 250px;">
<mat-form-field appearance="outline" style="flex: 1; min-width: 250px">
<mat-label>Exception Reasons</mat-label>
<mat-select formControlName="exceptionReason" multiple>
@if (!isNonIndividual) {
<mat-option value="PAN Card Verification Failed">PAN Card Verification Failed</mat-option>
<mat-option value="Aadhaar Verification Failed">Aadhaar Verification Failed</mat-option>
} @else {
<mat-option value="Enterprise PAN Verification Failed">Enterprise PAN Verification Failed</mat-option>
<mat-option value="Enterprise PAN Verification Failed"
>Enterprise PAN Verification Failed</mat-option
>
<mat-option value="GSTIN Verification Failed">GSTIN Verification Failed</mat-option>
<mat-option value="CIN Verification Failed">CIN Verification Failed</mat-option>
}
@@ -779,7 +797,6 @@
<mat-label>{{ 'labels.inputs.Remarks' | translate }}</mat-label>
<textarea matInput formControlName="remarks" rows="3"></textarea>
</mat-form-field>
</div>
@if (errorMessage) {
@@ -297,7 +297,8 @@ export class KycTabComponent implements OnInit, OnDestroy {
error: (err: any) => {
this.verifying[field] = false;
this.verificationStatus[field] = 'FAILED';
this.verificationError[field] = err?.error?.defaultUserMessage || `Failed to update ${field.toUpperCase()} number before verification.`;
this.verificationError[field] =
err?.error?.defaultUserMessage || `Failed to update ${field.toUpperCase()} number before verification.`;
}
});
} else {
@@ -224,7 +224,8 @@ export class EditClientComponent {
return this.detectSensitiveFieldChanges(this.clientDataAndTemplate);
}
/** Whether every OTP block required by the fields that actually changed has been verified (or FIR uploaded for lost mobile). */
/** Whether every OTP block required by the fields that actually changed has been verified
* (or the FIR exception has been uploaded in place of old-mobile OTP verification). */
get contactVerificationComplete(): boolean {
const changes = this.contactChanges;
if (changes.mobileChanged) {
@@ -239,6 +240,7 @@ export class EditClientComponent {
return true;
}
/** Toggle the "old mobile not accessible" exception — clears any in-progress old-mobile OTP. */
onOldMobileNotAccessibleToggle(checked?: boolean): void {
if (checked !== undefined) {
this.oldMobileNotAccessible = checked;
@@ -249,6 +251,7 @@ export class EditClientComponent {
}
}
/** Upload the FIR / police-complaint copy that substitutes for old-mobile OTP verification. */
onFirFileSelected(event: any): void {
const file: File = event.target?.files?.[0];
if (!file) return;
@@ -705,10 +708,21 @@ export class EditClientComponent {
const mobileControl = form.get('mobileNo');
const emailControl = form.get('emailAddress');
// Trimmed so browser autofill adding invisible leading/trailing whitespace never reads as a change
// Trimmed so browser autofill adding invisible leading/trailing whitespace never reads as
// a "change" — the masked preview shown in the Verify Contact Change panel only reveals the
// first couple of characters, so a whitespace-only difference is otherwise invisible to the
// user and just looks like the panel appeared for no reason. These trimmed-but-original-case
// values are what get returned/sent as the actual OTP destination and saved value — only the
// *comparison* (below) is further normalized for mobile/email, so we never silently rewrite the
// customer's saved value or the OTP destination.
const newMobile = (mobileControl?.value || '').toString().trim();
const newEmail = (emailControl?.value || '').toString().trim();
const newDobRaw = form.get('dateOfBirth')?.value;
// Local calendar date, NOT `.toISOString()` — that converts to UTC first, which silently
// shifts the date by a day in any positive-UTC-offset timezone (e.g. India, UTC+5:30:
// midnight local on the 15th becomes 18:30 UTC on the 14th). That made this comparison
// report a DOB "change" — falsely triggering the Re-KYC dialog — even when the user never
// touched the date field, purely because the Date object round-tripped through the picker.
const newDob = newDobRaw instanceof Date ? this.toLocalDateString(newDobRaw) : newDobRaw || '';
const oldMobile = (originalData.mobileNo || '').toString().trim();
@@ -733,10 +747,10 @@ export class EditClientComponent {
).trim();
const newName = buildName({
firstname: form.get('firstname')?.value || '',
middlename: form.get('middlename')?.value || '',
lastname: form.get('lastname')?.value || '',
fullname: form.get('fullname')?.value || ''
firstname: form?.get('firstname')?.value || '',
middlename: form?.get('middlename')?.value || '',
lastname: form?.get('lastname')?.value || '',
fullname: form?.get('fullname')?.value || ''
});
const oldName = buildName({
firstname: originalData.firstname || '',
@@ -750,9 +764,13 @@ export class EditClientComponent {
const newMobileNorm = normPhone(newMobile);
const oldMobileNorm = normPhone(oldMobile);
// Mobile/Email are considered changed ONLY when the control has been edited (dirty) AND normalized values differ
// Mobile/Email are considered changed ONLY when the control has been edited (dirty) AND
// normalized values differ — otherwise a re-render/reformat of the control by the framework
// (never actually touched by the user) would falsely surface the dual-OTP verification panel.
const mobileChanged =
!!oldMobileNorm && !!newMobileNorm && !!mobileControl?.dirty && newMobileNorm !== oldMobileNorm;
// Case-insensitive: emails are not case-sensitive in practice, and comparing raw case
// would flag a change (surfacing the dual-OTP panel) even when nothing meaningful changed.
const emailChanged =
!!oldEmail && !!newEmail && !!emailControl?.dirty && newEmail.toLowerCase() !== oldEmail.toLowerCase();
+167 -71
View File
@@ -112,6 +112,17 @@
</svg>
My Share Requests
</a>
<a
class="cashier-nav__item cashier-nav__item--sec"
[class.cashier-nav__item--active]="currentUrl.includes('/clerk/share-closure-request')"
[routerLink]="['/clerk/share-closure-requests']"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="4" y="11" width="16" height="9" rx="2" />
<path d="M8 11V7a4 4 0 0 1 8 0v4" />
</svg>
My Share Closures
</a>
@if (clerkAccountLoaded) {
<div class="cashier-nav__item-wrap">
<a
@@ -447,10 +458,6 @@
<fa-icon icon="piggy-bank" class="ni-ico"></fa-icon>Deposits
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="loanOriginationMenu" *mifosxHasPermission="'MENU_ADVANCES'">
<fa-icon icon="clipboard-check" class="ni-ico"></fa-icon>Loan Origination
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu" *mifosxHasPermission="'MENU_ADVANCES'">
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
@@ -463,6 +470,10 @@
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="loanOriginationMenu" *mifosxHasPermission="'MENU_ADVANCES'">
<fa-icon icon="clipboard-check" class="ni-ico"></fa-icon>Loan Origination
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a
class="ni"
tabindex="0"
@@ -700,6 +711,7 @@
<a
class="gl-link"
[class.hidden]="!matchesSubLedgerSearch('Cash / Clearing / Transfer')"
routerLink="/cashier/cash-clearing-transfer-summary"
(click)="subLedgerOpen = false"
><fa-icon icon="money-bill" class="gl-icon"></fa-icon> Cash / Clearing / Transfer</a
>
@@ -748,8 +760,11 @@
</div>
<div class="gl-group">
<div class="gl-group-title">Deposits</div>
<a class="gl-link" [class.hidden]="!matchesSubLedgerSearch('FD / RD Entry')" (click)="subLedgerOpen = false"
><fa-icon icon="piggy-bank" class="gl-icon"></fa-icon> FD / RD Entry</a
<a
class="gl-link"
[class.hidden]="!matchesSubLedgerSearch('FD / RD Entry')"
(click)="subLedgerOpen = false; openFdRdEntryDialog()"
><fa-icon icon="piggy-bank" class="gl-icon" style="color: #0f766e"></fa-icon> FD / RD Entry</a
>
<a class="gl-link" [class.hidden]="!matchesSubLedgerSearch('DDS Entry')" (click)="subLedgerOpen = false"
><fa-icon icon="coins" class="gl-icon"></fa-icon> DDS Entry</a
@@ -855,50 +870,72 @@
<div class="gl-columns">
<div class="gl-group">
<div class="gl-group-title">Financial Statements</div>
<a class="gl-link" [class.hidden]="!matchesGlSearch('Trial Balance')" (click)="glMenuOpen = false"
><fa-icon icon="balance-scale" class="gl-icon"></fa-icon> Trial Balance</a
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Trial Balance')"
routerLink="/gl/reports/trial-balance"
(click)="glMenuOpen = false"
><fa-icon icon="balance-scale" class="gl-icon" style="color: #b45309"></fa-icon> Trial Balance</a
>
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Profit and Loss')"
(click)="glMenuOpen = false; openAccountingReportsDialog('PNL')"
routerLink="/gl/reports/profit-and-loss"
(click)="glMenuOpen = false"
><fa-icon icon="chart-line" class="gl-icon" style="color: #b45309"></fa-icon> Profit and Loss</a
>
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Consolidate Balance Sheet')"
(click)="glMenuOpen = false; openAccountingReportsDialog('BALANCE-SHEET')"
><fa-icon icon="file-alt" class="gl-icon" style="color: #b45309"></fa-icon> Consolidate Balance Sheet</a
routerLink="/gl/reports/balance-sheet"
(click)="glMenuOpen = false"
><fa-icon icon="file-alt" class="gl-icon" style="color: #b45309"></fa-icon> Consolidated Balance Sheet</a
>
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Comparative Balance Sheet')"
(click)="glMenuOpen = false; openAccountingReportsDialog('COMPARATIVE-BS')"
routerLink="/gl/reports/comparative-balance-sheet"
(click)="glMenuOpen = false"
><fa-icon icon="balance-scale" class="gl-icon" style="color: #b45309"></fa-icon> Comparative Balance
Sheet</a
>
</div>
<div class="gl-group">
<div class="gl-group-title">GL Reports</div>
<a class="gl-link" [class.hidden]="!matchesGlSearch('GL Average Balance')" (click)="glMenuOpen = false"
><fa-icon icon="calculator" class="gl-icon"></fa-icon> GL Average Balance</a
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('GL Average Balance')"
routerLink="/gl/reports/gl-average-balance"
(click)="glMenuOpen = false"
><fa-icon icon="calculator" class="gl-icon" style="color: #b45309"></fa-icon> GL Average Balance</a
>
<a class="gl-link" [class.hidden]="!matchesGlSearch('GL Mapping for Report')" (click)="glMenuOpen = false"
><fa-icon icon="project-diagram" class="gl-icon"></fa-icon> GL Mapping for Report</a
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('GL Mapping for Report')"
routerLink="/gl/reports/gl-mapping"
(click)="glMenuOpen = false"
><fa-icon icon="project-diagram" class="gl-icon" style="color: #b45309"></fa-icon> GL Mapping for
Report</a
>
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Account wise Balance List')"
routerLink="/gl/reports/account-wise-balance"
(click)="glMenuOpen = false"
><fa-icon icon="list" class="gl-icon"></fa-icon> Account wise Balance List</a
><fa-icon icon="list" class="gl-icon" style="color: #b45309"></fa-icon> Account wise Balance List</a
>
<a class="gl-link" [class.hidden]="!matchesGlSearch('Distribution of Balance')" (click)="glMenuOpen = false"
><fa-icon icon="chart-pie" class="gl-icon"></fa-icon> Distribution of Balance</a
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Distribution of Balance')"
routerLink="/gl/reports/distribution-of-balance"
(click)="glMenuOpen = false"
><fa-icon icon="chart-pie" class="gl-icon" style="color: #b45309"></fa-icon> Distribution of Balance</a
>
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('GL Wise DR CR')"
(click)="glMenuOpen = false; openAccountingReportsDialog('GL-DR-CR')"
routerLink="/gl/reports/gl-wise-dr-cr"
(click)="glMenuOpen = false"
><fa-icon icon="project-diagram" class="gl-icon" style="color: #b45309"></fa-icon> GL Wise DR / CR
Report</a
>
@@ -908,28 +945,47 @@
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Client wise Balance Report')"
routerLink="/gl/reports/client-wise-balance"
(click)="glMenuOpen = false"
><fa-icon icon="users" class="gl-icon"></fa-icon> Client wise Balance Report</a
><fa-icon icon="users" class="gl-icon" style="color: #b45309"></fa-icon> Client wise Balance Report</a
>
<a class="gl-link" [class.hidden]="!matchesGlSearch('Contingent Expense')" (click)="glMenuOpen = false"
><fa-icon icon="exclamation-circle" class="gl-icon"></fa-icon> Contingent Expense</a
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Contingent Liabilities')"
routerLink="/gl/reports/contingent-liabilities"
(click)="glMenuOpen = false"
><fa-icon icon="exclamation-circle" class="gl-icon" style="color: #b45309"></fa-icon> Contingent
Liabilities</a
>
<a class="gl-link" [class.hidden]="!matchesGlSearch('Banker Head Report')" (click)="glMenuOpen = false"
><fa-icon icon="university" class="gl-icon"></fa-icon> Banker Head Report</a
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Banker Head Report')"
routerLink="/gl/reports/banker-head"
(click)="glMenuOpen = false"
><fa-icon icon="university" class="gl-icon" style="color: #b45309"></fa-icon> Banker Head Report</a
>
</div>
<div class="gl-group">
<div class="gl-group-title">Registers</div>
<a class="gl-link" [class.hidden]="!matchesGlSearch('Sundry Register')" (click)="glMenuOpen = false"
><fa-icon icon="clipboard-list" class="gl-icon"></fa-icon> Sundry Register</a
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Sundry Register')"
routerLink="/gl/reports/sundry-register"
(click)="glMenuOpen = false"
><fa-icon icon="clipboard-list" class="gl-icon" style="color: #b45309"></fa-icon> Sundry Register</a
>
<a class="gl-link" [class.hidden]="!matchesGlSearch('Voucher Register')" (click)="glMenuOpen = false"
><fa-icon icon="receipt" class="gl-icon"></fa-icon> Voucher Register</a
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Voucher Register')"
routerLink="/gl/reports/voucher-register"
(click)="glMenuOpen = false"
><fa-icon icon="receipt" class="gl-icon" style="color: #b45309"></fa-icon> Voucher Register</a
>
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Misc Expense Ledger')"
(click)="glMenuOpen = false; openAccountingReportsDialog('MISC-EXP')"
routerLink="/gl/reports/misc-expense-ledger"
(click)="glMenuOpen = false"
><fa-icon icon="receipt" class="gl-icon" style="color: #b45309"></fa-icon> Misc Expense Ledger</a
>
</div>
@@ -1202,7 +1258,7 @@
<fa-icon icon="user-slash" class="menu-item-icon" style="color: #c2410c"></fa-icon> Non-Customer Entry
</button>
<button mat-menu-item tabindex="0"><fa-icon icon="lock" class="menu-item-icon"></fa-icon> Locker</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item routerLink="/investment/bonds" tabindex="0">
<fa-icon icon="chart-line" class="menu-item-icon"></fa-icon> SLR and Non SLR Investment Report
</button>
<button mat-menu-item tabindex="0" (click)="openCicReportingDialog()">
@@ -1218,13 +1274,13 @@
<!-- Investment -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Investment</div>
<button mat-menu-item tabindex="0">
<button mat-menu-item routerLink="/investment/dashboard" tabindex="0">
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
<fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received &amp; Receivable Report
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
<fa-icon icon="piggy-bank" class="menu-item-icon"></fa-icon> Bucket Wise Investment
</button>
<button mat-menu-item routerLink="/deaf" tabindex="0">
@@ -1255,39 +1311,63 @@
<!-- NPA Reports -->
<div class="deposit-menu-group">
<div class="deposit-group-title">NPA Reports</div>
<button mat-menu-item tabindex="0">
<fa-icon icon="chart-line" class="menu-item-icon"></fa-icon> NPA Interest
<button mat-menu-item tabindex="0" routerLink="/npa/reports/interest">
<fa-icon icon="chart-line" class="menu-item-icon"></fa-icon> NPA Report
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/npa/reports/status">
<fa-icon icon="file-alt" class="menu-item-icon"></fa-icon> NPA Status Report
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/npa/reports/recovery">
<fa-icon icon="undo" class="menu-item-icon"></fa-icon> NPA Recovery Report
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/npa/reports/summary">
<fa-icon icon="clipboard-list" class="menu-item-icon"></fa-icon> NPA Summary Report
</button>
</div>
<!-- NPA Analysis -->
<div class="deposit-menu-group">
<div class="deposit-group-title">NPA Analysis</div>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/npa/reports/account-detail">
<fa-icon icon="list-alt" class="menu-item-icon"></fa-icon> NPA Account Detail Report
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/npa/reports/movement">
<fa-icon icon="exchange" class="menu-item-icon"></fa-icon> NPA Movement Report
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/npa/reports/provision">
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> NPA with Provision and Required Provision
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/undebited-interest">
<fa-icon icon="coins" class="menu-item-icon"></fa-icon> Undebited Interest Recovery Report
</button>
</div>
<!-- NPA Classification -->
<div class="deposit-menu-group">
<div class="deposit-group-title">NPA Classification</div>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/npa/reports/probable">
<fa-icon icon="exclamation-circle" class="menu-item-icon"></fa-icon> Probable NPA
</button>
<button mat-menu-item tabindex="0"><fa-icon icon="flag" class="menu-item-icon"></fa-icon> SMA Report</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/sma">
<fa-icon icon="flag" class="menu-item-icon"></fa-icon> SMA Report
</button>
</div>
<!-- Recovery Management -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Recovery Management</div>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/recovery-dashboard">
<fa-icon icon="tachometer-alt" class="menu-item-icon"></fa-icon> Recovery Management Dashboard
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/daywise-overdue">
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Day-wise Overdue Report
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/daywise-overdrawn">
<fa-icon icon="credit-card" class="menu-item-icon"></fa-icon> Day-wise Overdrawn Report
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/agri-gold-value">
<fa-icon icon="crop" class="menu-item-icon"></fa-icon> Agriculture / Gold Market Value Report
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/exception-check">
<fa-icon icon="balance-scale" class="menu-item-icon"></fa-icon> Exception Check — Security Value vs Loan
</button>
</div>
</div>
</mat-menu>
@@ -1353,27 +1433,27 @@
<!-- Core / Books -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Core &amp; Books</div>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/voucher">
<fa-icon icon="receipt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Voucher Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/sundry-dr-cr">
<fa-icon icon="random" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Sundry Dr / Cr Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/pnl-impact">
<fa-icon icon="chart-line" class="menu-item-icon" style="color: #0d9488"></fa-icon>
P&amp;L Impact Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/gl-reconciliation">
<fa-icon icon="book-open" class="menu-item-icon" style="color: #0d9488"></fa-icon>
GL Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/fixed-assets">
<fa-icon icon="cube" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Fixed Assets Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/inter-company">
<fa-icon icon="sitemap" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Inter-Company Reconciliation
</button>
@@ -1382,19 +1462,19 @@
<!-- Bank & Clearing -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Bank &amp; Clearing</div>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/bank-reco">
<fa-icon icon="university" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Bank Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/api-bank-reco">
<fa-icon icon="code-branch" class="menu-item-icon" style="color: #0d9488"></fa-icon>
API Based Bank Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/neft-rtgs">
<fa-icon icon="exchange" class="menu-item-icon" style="color: #0d9488"></fa-icon>
NEFT / RTGS Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/clearing-reco">
<fa-icon icon="hand-holding-usd" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Clearing Reconciliation
</button>
@@ -1403,15 +1483,15 @@
<!-- Digital / Payments -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Digital &amp; Payments</div>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/udir">
<fa-icon icon="mobile-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
UDIR (IMPS / UPI / ATM)
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/three-way-digital">
<fa-icon icon="project-diagram" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Three-way Digital Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/nach-reco">
<fa-icon icon="sync-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
NACH Reconciliation
</button>
@@ -1424,7 +1504,7 @@
<fa-icon icon="money-check-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
DD / Pay Order Reconciliation
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/share-member">
<fa-icon icon="coins" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Share Member Reconciliation
</button>
@@ -1433,11 +1513,11 @@
<!-- Compliance & Financials -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Compliance &amp; Financials</div>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/invoice-payment">
<fa-icon icon="file-invoice" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Invoice Payment
</button>
<button mat-menu-item tabindex="0">
<button mat-menu-item tabindex="0" routerLink="/reconciliation/tax-reco">
<fa-icon icon="percentage" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Tax Reconciliation
</button>
@@ -1549,6 +1629,9 @@
<button mat-menu-item tabindex="0">
<fa-icon icon="file-signature" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 98
</button>
<button mat-menu-item routerLink="/reports/form-97-98" tabindex="0">
<fa-icon icon="file-invoice" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 97/98 Txn Report
</button>
<button mat-menu-item routerLink="/letter-mgmt/form-15gh" tabindex="0">
<fa-icon icon="file-signature" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 121
</button>
@@ -1761,28 +1844,41 @@
>
<fa-icon icon="shield-alt" class="gl-icon"></fa-icon> AML
</a>
<a
class="gl-link"
[class.hidden]="!matchesRbiSearch('Risk Review Report')"
routerLink="/reports/risk-review"
(click)="rbiMenuOpen = false"
>
<fa-icon icon="user-shield" class="gl-icon"></fa-icon> Risk Review Report
</a>
</div>
<!-- OSS Reports -->
<div class="gl-group">
<div class="gl-group-title">OSS Reports</div>
<a class="gl-link" [class.hidden]="!matchesRbiSearch('OSS 1 XBRL/CIMS')" (click)="rbiMenuOpen = false">
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> OSS 1 XBRL/CIMS
<a
class="gl-link"
[class.hidden]="!matchesRbiSearch('OSS 1 XBRL/CIMS')"
routerLink="/rbi/oss1"
(click)="rbiMenuOpen = false"
>
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 1 XBRL/CIMS
</a>
<a
class="gl-link"
[class.hidden]="!matchesRbiSearch('OSS 2 XBRL')"
(click)="rbiMenuOpen = false; openCimsXbrlDialog('OSS-2')"
routerLink="/rbi/oss2"
(click)="rbiMenuOpen = false"
>
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 2 XBRL
</a>
<a class="gl-link" [class.hidden]="!matchesRbiSearch('OSS 3 XBRL/CIMS')" (click)="rbiMenuOpen = false">
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> OSS 3 XBRL/CIMS
</a>
<a class="gl-link" [class.hidden]="!matchesRbiSearch('OSS 4/OSS 5/OSS 6')" (click)="rbiMenuOpen = false">
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> OSS 4 / OSS 5 / OSS 6
</a>
<a class="gl-link" [class.hidden]="!matchesRbiSearch('OSS 7')" (click)="rbiMenuOpen = false">
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> OSS 7
<a
class="gl-link"
[class.hidden]="!matchesRbiSearch('OSS 3 XBRL/CIMS')"
routerLink="/rbi/oss3"
(click)="rbiMenuOpen = false"
>
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 3 XBRL/CIMS
</a>
</div>
<!-- CIMS / XBRL Returns -->
+54
View File
@@ -295,6 +295,53 @@
</div>
</div>
</div>
<!-- LIVE: monthly CTR reports (mechanical, threshold-based) -->
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>Monthly CTR Reports</h3>
<p>
Threshold: <strong>&#8377;{{ bank.cashThreshold | number }}</strong> per single txn / connected aggregate
per month
</p>
</div>
<div class="fiu-action-group">
<input class="fiu-inp" type="month" [(ngModel)]="ctrGenerateMonth" />
<button class="fiu-btn fiu-btn-g" *mifosxHasPermission="'REVIEW_AML_ALERT'" (click)="generateCtrReport()">
Generate
</button>
</div>
</div>
<div class="fiu-tbl-wrap">
<table class="fiu-tbl">
<thead>
<tr>
<th>Month</th>
<th>Generated On</th>
<th class="ta-c">Records</th>
<th>Status</th>
<th>Submitted On</th>
</tr>
</thead>
<tbody>
@for (r of amlCtrReports; track r.id) {
<tr>
<td>{{ r.reportMonth }}</td>
<td>{{ r.generatedOn | date: 'medium' }}</td>
<td class="ta-c">{{ r.recordCount }}</td>
<td>
<span [class]="amlStatusClass(r.status)">{{ r.status }}</span>
</td>
<td>{{ r.submittedOn ? (r.submittedOn | date: 'medium') : '—' }}</td>
</tr>
} @empty {
<tr>
<td colspan="5" class="fiu-empty">No CTR reports generated yet.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
@@ -307,6 +354,13 @@
<h3>Live AML Alerts (Awaiting Triage)</h3>
<p>Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate</p>
</div>
<select class="fiu-inp" [(ngModel)]="amlAlertStatusFilter" (ngModelChange)="loadAmlAlerts()">
<option value="">All statuses</option>
<option value="RAISED">Raised</option>
<option value="UNDER_REVIEW">Under Review</option>
<option value="WHITELISTED">Whitelisted</option>
<option value="ESCALATED">Escalated</option>
</select>
</div>
<div class="fiu-tbl-wrap">
<table class="fiu-tbl">
@@ -1717,9 +1717,11 @@
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 2px;
@@ -642,7 +642,12 @@ export class KycManagementComponent implements OnInit {
private mapException(item: any, resolved: boolean): KmClient {
return {
id: item.clientId?.toString(),
av: (item.clientDisplayName || 'Client').split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase(),
av: (item.clientDisplayName || 'Client')
.split(' ')
.map((n: string) => n[0])
.join('')
.substring(0, 2)
.toUpperCase(),
name: item.clientDisplayName,
meta: `${item.clientAccountNo || `CLI-${item.clientId}`} · Savings A/c · ${item.clientOfficeName || 'Bhopal Main'}`,
account: item.clientAccountNo || `CLI-${item.clientId}`,
@@ -710,17 +715,16 @@ export class KycManagementComponent implements OnInit {
}
}
get notYetContactedCount(): number {
return this.exceptionRows.filter(c => !c.contactHistory || c.contactHistory.length === 0).length;
return this.exceptionRows.filter((c) => !c.contactHistory || c.contactHistory.length === 0).length;
}
get panMismatchesCount(): number {
return this.exceptionRows.filter(c => c.exceptionReason?.includes('PAN')).length;
return this.exceptionRows.filter((c) => c.exceptionReason?.includes('PAN')).length;
}
get aadhaarFailuresCount(): number {
return this.exceptionRows.filter(c => c.exceptionReason?.includes('Aadhaar')).length;
return this.exceptionRows.filter((c) => c.exceptionReason?.includes('Aadhaar')).length;
}
showSection(section: 'alerts' | 'notif' | 'audit' | 'status' | 'exceptions'): void {
@@ -743,7 +747,7 @@ export class KycManagementComponent implements OnInit {
/** The channel currently picked in the dialog. */
get selectedChannel(): { value: string; label: string; sends: boolean; requires?: 'mobile' | 'email' } | undefined {
return this.contactChannels.find(c => c.value === this.contactChannel);
return this.contactChannels.find((c) => c.value === this.contactChannel);
}
/** True when saving will transmit a notice to the customer, not merely record one. */
@@ -787,7 +791,10 @@ export class KycManagementComponent implements OnInit {
.subscribe({
next: (log: any) => {
this.contactSaving = false;
client.contactHistory = [...(client.contactHistory || []), this.mapContactLog(log)];
client.contactHistory = [
...(client.contactHistory || []),
this.mapContactLog(log)
];
// The request succeeding does not mean the customer was reached — a gateway failure comes
// back as HTTP 200 with delivered=false, so report what actually happened.
@@ -884,7 +891,20 @@ export class KycManagementComponent implements OnInit {
const today = new Date();
const day = today.getDate().toString().padStart(2, '0');
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
const monthStr = months[today.getMonth()];
const year = today.getFullYear();
const dateStr = `${day} ${monthStr} ${year}`;
@@ -939,7 +959,7 @@ export class KycManagementComponent implements OnInit {
this.clientsService.updateClientKyc(clientId, kycId, resolvedKycData).subscribe({
next: () => {
const msg = isFullyResolved
const msg = isFullyResolved
? `KYC Exception fully resolved and completed for ${this.current?.name}`
: `KYC Exception partially resolved for ${this.current?.name}`;
this.toast(msg);
@@ -1069,10 +1069,12 @@ export function buildForm15ghPdf(r: Form15ghRecord, kind: Form15Kind): jsPDF {
y = paintPara(
c,
y,
`Declaration under Section ${kind === '15H' ? '197A(1C)' : '197A(1) / 197A(1A)'
} of the Income-tax Act, 1961, for non-deduction of tax at source on certain incomes — ${kind === '15H'
? 'applicable to resident individuals aged 60 years or above'
: 'applicable to resident individuals (other than companies / firms) below 60 years'
`Declaration under Section ${
kind === '15H' ? '197A(1C)' : '197A(1) / 197A(1A)'
} of the Income-tax Act, 1961, for non-deduction of tax at source on certain incomes — ${
kind === '15H'
? 'applicable to resident individuals aged 60 years or above'
: 'applicable to resident individuals (other than companies / firms) below 60 years'
}.`
);
@@ -52,304 +52,306 @@
@if (viewMode === 'demo') {
<!-- KPI strip -->
<div class="cert-kpis">
<div class="cert-kpi cert-kpi--blue">
<fa-icon icon="user" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ countGeneralBand() | number }}</div>
<div class="cert-kpi-l">General band (under 60)</div>
</div>
</div>
<div class="cert-kpi cert-kpi--amber">
<fa-icon icon="user-tie" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ countSeniorBand() | number }}</div>
<div class="cert-kpi-l">Senior citizen band (60+)</div>
</div>
</div>
<div class="cert-kpi cert-kpi--green">
<fa-icon icon="check-circle" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ countFiled() | number }}</div>
<div class="cert-kpi-l">Filed this FY</div>
</div>
</div>
<div class="cert-kpi cert-kpi--teal">
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">₹{{ totalInterest() | number: '1.0-0' }}</div>
<div class="cert-kpi-l">Projected Interest</div>
</div>
</div>
<div class="cert-kpi cert-kpi--green">
<fa-icon icon="shield-alt" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">₹{{ totalTdsSaved() | number: '1.0-0' }}</div>
<div class="cert-kpi-l">TDS Saved</div>
</div>
</div>
@if (countIneligible() > 0) {
<div class="cert-kpi cert-kpi--amber">
<fa-icon icon="exclamation-triangle" class="cert-kpi-ic"></fa-icon>
<div class="cert-kpis">
<div class="cert-kpi cert-kpi--blue">
<fa-icon icon="user" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ countIneligible() | number }}</div>
<div class="cert-kpi-l">Not Eligible</div>
<div class="cert-kpi-n">{{ countGeneralBand() | number }}</div>
<div class="cert-kpi-l">General band (under 60)</div>
</div>
</div>
}
</div>
<!-- Filters -->
<div class="cert-filters">
<div class="cert-filter">
<label>Customer / Customer ID / PAN</label>
<div class="cert-input-ic">
<fa-icon icon="search"></fa-icon>
<input type="text" [(ngModel)]="search" placeholder="e.g. CUST556677 or customer name" />
<div class="cert-kpi cert-kpi--amber">
<fa-icon icon="user-tie" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ countSeniorBand() | number }}</div>
<div class="cert-kpi-l">Senior citizen band (60+)</div>
</div>
</div>
<div class="cert-kpi cert-kpi--green">
<fa-icon icon="check-circle" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ countFiled() | number }}</div>
<div class="cert-kpi-l">Filed this FY</div>
</div>
</div>
<div class="cert-kpi cert-kpi--teal">
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">₹{{ totalInterest() | number: '1.0-0' }}</div>
<div class="cert-kpi-l">Projected Interest</div>
</div>
</div>
<div class="cert-kpi cert-kpi--green">
<fa-icon icon="shield-alt" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">₹{{ totalTdsSaved() | number: '1.0-0' }}</div>
<div class="cert-kpi-l">TDS Saved</div>
</div>
</div>
@if (countIneligible() > 0) {
<div class="cert-kpi cert-kpi--amber">
<fa-icon icon="exclamation-triangle" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ countIneligible() | number }}</div>
<div class="cert-kpi-l">Not Eligible</div>
</div>
</div>
}
</div>
<div class="cert-filter">
<label>Threshold Band</label>
<select [(ngModel)]="bandFilter">
<option value="">All bands</option>
<option value="GENERAL">General (below 60)</option>
<option value="SENIOR">Senior citizen (60+)</option>
</select>
<!-- Filters -->
<div class="cert-filters">
<div class="cert-filter">
<label>Customer / Customer ID / PAN</label>
<div class="cert-input-ic">
<fa-icon icon="search"></fa-icon>
<input type="text" [(ngModel)]="search" placeholder="e.g. CUST556677 or customer name" />
</div>
</div>
<div class="cert-filter">
<label>Threshold Band</label>
<select [(ngModel)]="bandFilter">
<option value="">All bands</option>
<option value="GENERAL">General (below 60)</option>
<option value="SENIOR">Senior citizen (60+)</option>
</select>
</div>
<div class="cert-filter">
<label>Status</label>
<select [(ngModel)]="statusFilter">
<option value="">All statuses</option>
<option value="FILED">Filed</option>
<option value="PENDING">Pending</option>
</select>
</div>
<div class="cert-filter">
<label>Eligibility</label>
<select [(ngModel)]="ineligibleOnly">
<option [ngValue]="false">All customers</option>
<option [ngValue]="true">Not eligible only</option>
</select>
</div>
@if (search || bandFilter || statusFilter || ineligibleOnly) {
<button type="button" class="cert-clear" (click)="clearFilters()">
<fa-icon icon="times"></fa-icon> Clear
</button>
}
</div>
<div class="cert-filter">
<label>Status</label>
<select [(ngModel)]="statusFilter">
<option value="">All statuses</option>
<option value="FILED">Filed</option>
<option value="PENDING">Pending</option>
</select>
</div>
<div class="cert-filter">
<label>Eligibility</label>
<select [(ngModel)]="ineligibleOnly">
<option [ngValue]="false">All customers</option>
<option [ngValue]="true">Not eligible only</option>
</select>
</div>
@if (search || bandFilter || statusFilter || ineligibleOnly) {
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
}
</div>
<!-- Table -->
<div class="cert-card">
<div class="cert-table-wrap">
<table class="cert-table">
<thead>
<tr>
<th>Customer · PAN</th>
<th>Form · Band</th>
<th>Age</th>
<th>Branch</th>
<th class="num">Est. Interest</th>
<th class="num">Threshold</th>
<th class="num">TDS Payable</th>
<th class="num">TDS Saved</th>
<th>Settlement</th>
<th>Status</th>
<th class="col-actions">Actions</th>
</tr>
</thead>
<tbody>
@for (row of filteredRows; track row.record.customerId) {
<tr [class.cert-row--warn]="!row.eligibility.eligible">
<td>
<div class="cert-cust-name">{{ row.record.customerName }}</div>
<div class="mono small muted">{{ row.record.customerId }} · PAN {{ row.record.customerPan }}</div>
<div class="small muted">DOB {{ row.record.dob | date: 'd MMM y' }}</div>
</td>
<td>
<span
class="cert-pill"
[class.cert-pill--blue]="row.band === 'GENERAL'"
[class.cert-pill--amber]="row.band === 'SENIOR'"
>
{{ row.form === '121' ? 'Form 121' : 'Form ' + row.form }}
</span>
<div class="small muted">{{ row.band === 'SENIOR' ? 'Senior citizen' : 'General' }}</div>
</td>
<td class="small">{{ row.age }} yrs</td>
<td class="small">{{ row.record.branch }}</td>
<td class="num">
<strong class="cert-interest">₹{{ row.record.estimatedInterest | number: '1.0-0' }}</strong>
</td>
<td class="num small muted">₹{{ row.schedule.threshold | number: '1.0-0' }}</td>
<td class="num">
@if (row.schedule.totalTds > 0) {
<strong>₹{{ row.schedule.totalTds | number: '1.0-0' }}</strong>
<div class="small muted">@ {{ row.schedule.rateApplied * 100 | number: '1.0-0' }}%</div>
} @else {
<span class="small muted">—</span>
}
</td>
<td class="num">
@if (row.tdsSaved > 0) {
<strong class="cert-interest">₹{{ row.tdsSaved | number: '1.0-0' }}</strong>
} @else {
<span class="small muted">—</span>
}
</td>
<td class="small">
<span class="cert-pill" [class.cert-pill--blue]="row.mode === 'QUARTERLY'">
{{ row.mode === 'QUARTERLY' ? 'Quarterly' : 'Year end' }}
</span>
<div class="small muted">product-driven</div>
</td>
<td>
@if (row.record.status === 'FILED') {
<span class="cert-pill cert-pill--green"> <fa-icon icon="check-circle"></fa-icon> Filed </span>
@if (row.record.filedOn) {
<div class="mono small muted">{{ row.record.filedOn | date: 'd MMM y' }}</div>
}
} @else {
<span class="cert-pill cert-pill--amber"> <fa-icon icon="hourglass-half"></fa-icon> Pending </span>
}
@if (!row.eligibility.eligible) {
<div class="cert-pill cert-pill--amber">
<fa-icon icon="exclamation-triangle"></fa-icon> Not eligible
</div>
}
</td>
<td class="col-actions">
<div class="cert-row-actions">
<button
type="button"
class="cert-icon-btn"
(click)="toggleExpanded(row)"
[matTooltip]="isExpanded(row) ? 'Hide quarter-wise' : 'Show quarter-wise'"
>
<fa-icon [icon]="isExpanded(row) ? 'chevron-up' : 'chevron-down'"></fa-icon>
</button>
<button
type="button"
class="cert-icon-btn primary"
(click)="viewCertificate(row)"
matTooltip="View / Print declaration"
>
<fa-icon icon="eye"></fa-icon>
</button>
<button
type="button"
class="cert-icon-btn"
(click)="downloadCertificate(row)"
matTooltip="Download PDF"
>
<fa-icon icon="download"></fa-icon>
</button>
</div>
</td>
<!-- Table -->
<div class="cert-card">
<div class="cert-table-wrap">
<table class="cert-table">
<thead>
<tr>
<th>Customer · PAN</th>
<th>Form · Band</th>
<th>Age</th>
<th>Branch</th>
<th class="num">Est. Interest</th>
<th class="num">Threshold</th>
<th class="num">TDS Payable</th>
<th class="num">TDS Saved</th>
<th>Settlement</th>
<th>Status</th>
<th class="col-actions">Actions</th>
</tr>
@if (isExpanded(row)) {
<tr class="cert-row--detail">
<td colspan="11">
</thead>
<tbody>
@for (row of filteredRows; track row.record.customerId) {
<tr [class.cert-row--warn]="!row.eligibility.eligible">
<td>
<div class="cert-cust-name">{{ row.record.customerName }}</div>
<div class="mono small muted">{{ row.record.customerId }} · PAN {{ row.record.customerPan }}</div>
<div class="small muted">DOB {{ row.record.dob | date: 'd MMM y' }}</div>
</td>
<td>
<span
class="cert-pill"
[class.cert-pill--blue]="row.band === 'GENERAL'"
[class.cert-pill--amber]="row.band === 'SENIOR'"
>
{{ row.form === '121' ? 'Form 121' : 'Form ' + row.form }}
</span>
<div class="small muted">{{ row.band === 'SENIOR' ? 'Senior citizen' : 'General' }}</div>
</td>
<td class="small">{{ row.age }} yrs</td>
<td class="small">{{ row.record.branch }}</td>
<td class="num">
<strong class="cert-interest">₹{{ row.record.estimatedInterest | number: '1.0-0' }}</strong>
</td>
<td class="num small muted">₹{{ row.schedule.threshold | number: '1.0-0' }}</td>
<td class="num">
@if (row.schedule.totalTds > 0) {
<strong>₹{{ row.schedule.totalTds | number: '1.0-0' }}</strong>
<div class="small muted">@ {{ row.schedule.rateApplied * 100 | number: '1.0-0' }}%</div>
} @else {
<span class="small muted">—</span>
}
</td>
<td class="num">
@if (row.tdsSaved > 0) {
<strong class="cert-interest">₹{{ row.tdsSaved | number: '1.0-0' }}</strong>
} @else {
<span class="small muted">—</span>
}
</td>
<td class="small">
<span class="cert-pill" [class.cert-pill--blue]="row.mode === 'QUARTERLY'">
{{ row.mode === 'QUARTERLY' ? 'Quarterly' : 'Year end' }}
</span>
<div class="small muted">product-driven</div>
</td>
<td>
@if (row.record.status === 'FILED') {
<span class="cert-pill cert-pill--green"> <fa-icon icon="check-circle"></fa-icon> Filed </span>
@if (row.record.filedOn) {
<div class="mono small muted">{{ row.record.filedOn | date: 'd MMM y' }}</div>
}
} @else {
<span class="cert-pill cert-pill--amber"> <fa-icon icon="hourglass-half"></fa-icon> Pending </span>
}
@if (!row.eligibility.eligible) {
<div class="cert-note cert-note--warn">
<fa-icon icon="exclamation-triangle"></fa-icon>
<div>
<strong>Declaration cannot be accepted</strong>
<ul>
@for (reason of row.eligibility.reasons; track reason) {
<li>{{ reason }}</li>
}
</ul>
</div>
</div>
}
@for (note of row.eligibility.unassessed; track note) {
<div class="cert-note">
<fa-icon icon="info-circle"></fa-icon>
<div>{{ note }}</div>
</div>
}
@for (note of row.schedule.notes; track note) {
<div class="cert-note">
<fa-icon icon="info-circle"></fa-icon>
<div>{{ note }}</div>
</div>
}
<table class="cert-table cert-table--inner">
<thead>
<tr>
<th>Quarter</th>
<th class="num">Interest</th>
<th class="num">Cumulative</th>
<th class="num">Liability to date</th>
<th class="num">TDS this quarter</th>
<th>Return due</th>
</tr>
</thead>
<tbody>
@for (line of row.schedule.lines; track line.quarter) {
<tr [class.cert-row--warn]="line.thresholdCrossedHere">
<td class="small">
{{ line.label }}
@if (line.thresholdCrossedHere) {
<span class="cert-pill cert-pill--amber">threshold crossed</span>
}
@if (line.declarationActive) {
<span class="cert-pill cert-pill--green">Form 121 active</span>
}
</td>
<td class="num">₹{{ line.interest | number: '1.0-0' }}</td>
<td class="num">₹{{ line.cumInterest | number: '1.0-0' }}</td>
<td class="num">₹{{ line.cumLiability | number: '1.0-0' }}</td>
<td class="num">
@if (line.tds !== 0) {
<strong>₹{{ line.tds | number: '1.0-0' }}</strong>
} @else {
<span class="small muted">—</span>
}
</td>
<td class="small muted">{{ line.dueDate }}</td>
</tr>
}
<tr>
<td class="small"><strong>Total</strong></td>
<td class="num">
<strong>₹{{ row.schedule.totalInterest | number: '1.0-0' }}</strong>
</td>
<td></td>
<td></td>
<td class="num">
<strong>₹{{ row.schedule.totalTds | number: '1.0-0' }}</strong>
</td>
<td></td>
</tr>
</tbody>
</table>
@if (row.schedule.shieldedInterest > 0) {
<div class="small muted">
₹{{ row.schedule.shieldedInterest | number: '1.0-0' }} paid without deduction on the strength of
the declaration — reportable in the quarterly {{ statute.statement }} return.
<div class="cert-pill cert-pill--amber">
<fa-icon icon="exclamation-triangle"></fa-icon> Not eligible
</div>
}
</td>
<td class="col-actions">
<div class="cert-row-actions">
<button
type="button"
class="cert-icon-btn"
(click)="toggleExpanded(row)"
[matTooltip]="isExpanded(row) ? 'Hide quarter-wise' : 'Show quarter-wise'"
>
<fa-icon [icon]="isExpanded(row) ? 'chevron-up' : 'chevron-down'"></fa-icon>
</button>
<button
type="button"
class="cert-icon-btn primary"
(click)="viewCertificate(row)"
matTooltip="View / Print declaration"
>
<fa-icon icon="eye"></fa-icon>
</button>
<button
type="button"
class="cert-icon-btn"
(click)="downloadCertificate(row)"
matTooltip="Download PDF"
>
<fa-icon icon="download"></fa-icon>
</button>
</div>
</td>
</tr>
@if (isExpanded(row)) {
<tr class="cert-row--detail">
<td colspan="11">
@if (!row.eligibility.eligible) {
<div class="cert-note cert-note--warn">
<fa-icon icon="exclamation-triangle"></fa-icon>
<div>
<strong>Declaration cannot be accepted</strong>
<ul>
@for (reason of row.eligibility.reasons; track reason) {
<li>{{ reason }}</li>
}
</ul>
</div>
</div>
}
@for (note of row.eligibility.unassessed; track note) {
<div class="cert-note">
<fa-icon icon="info-circle"></fa-icon>
<div>{{ note }}</div>
</div>
}
@for (note of row.schedule.notes; track note) {
<div class="cert-note">
<fa-icon icon="info-circle"></fa-icon>
<div>{{ note }}</div>
</div>
}
<table class="cert-table cert-table--inner">
<thead>
<tr>
<th>Quarter</th>
<th class="num">Interest</th>
<th class="num">Cumulative</th>
<th class="num">Liability to date</th>
<th class="num">TDS this quarter</th>
<th>Return due</th>
</tr>
</thead>
<tbody>
@for (line of row.schedule.lines; track line.quarter) {
<tr [class.cert-row--warn]="line.thresholdCrossedHere">
<td class="small">
{{ line.label }}
@if (line.thresholdCrossedHere) {
<span class="cert-pill cert-pill--amber">threshold crossed</span>
}
@if (line.declarationActive) {
<span class="cert-pill cert-pill--green">Form 121 active</span>
}
</td>
<td class="num">₹{{ line.interest | number: '1.0-0' }}</td>
<td class="num">₹{{ line.cumInterest | number: '1.0-0' }}</td>
<td class="num">₹{{ line.cumLiability | number: '1.0-0' }}</td>
<td class="num">
@if (line.tds !== 0) {
<strong>₹{{ line.tds | number: '1.0-0' }}</strong>
} @else {
<span class="small muted">—</span>
}
</td>
<td class="small muted">{{ line.dueDate }}</td>
</tr>
}
<tr>
<td class="small"><strong>Total</strong></td>
<td class="num">
<strong>₹{{ row.schedule.totalInterest | number: '1.0-0' }}</strong>
</td>
<td></td>
<td></td>
<td class="num">
<strong>₹{{ row.schedule.totalTds | number: '1.0-0' }}</strong>
</td>
<td></td>
</tr>
</tbody>
</table>
@if (row.schedule.shieldedInterest > 0) {
<div class="small muted">
₹{{ row.schedule.shieldedInterest | number: '1.0-0' }} paid without deduction on the strength of
the declaration — reportable in the quarterly {{ statute.statement }} return.
</div>
}
</td>
</tr>
}
}
@if (filteredRows.length === 0) {
<tr>
<td colspan="11" class="cert-empty">
<fa-icon icon="info-circle"></fa-icon>
No declarations match the current filters.
</td>
</tr>
}
}
@if (filteredRows.length === 0) {
<tr>
<td colspan="11" class="cert-empty">
<fa-icon icon="info-circle"></fa-icon>
No declarations match the current filters.
</td>
</tr>
}
</tbody>
</table>
</tbody>
</table>
</div>
</div>
</div>
}
}
</div>
@@ -16,9 +16,9 @@
max-width: 1400px;
padding: 20px 24px;
border-radius: 16px;
background: #ffffff;
background: #fff;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
box-sizing: border-box;
}
@@ -62,9 +62,17 @@
border-radius: 2px;
background: #1353a0;
&:nth-child(2) { opacity: 0.7; }
&:nth-child(3) { opacity: 0.4; }
&:nth-child(4) { opacity: 0.2; }
&:nth-child(2) {
opacity: 0.7;
}
&:nth-child(3) {
opacity: 0.4;
}
&:nth-child(4) {
opacity: 0.2;
}
}
}
}
@@ -118,7 +126,7 @@
.mat-mdc-form-field-flex {
height: 38px !important;
background: #ffffff !important;
background: #fff !important;
align-items: center;
}
@@ -142,7 +150,7 @@
}
}
button[mat-icon-button] {
[mat-icon-button] {
color: #64748b;
width: 36px;
height: 36px;
@@ -159,7 +167,7 @@
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
background: #ffffff;
background: #fff;
.table-loader {
height: 2px;
@@ -221,5 +229,5 @@
mat-paginator {
border-top: 1px solid #e2e8f0;
background: #ffffff;
background: #fff;
}
@@ -124,6 +124,13 @@
</mat-card-header>
<mat-card-content class="content">
@if (sharesAccountData.status?.value === 'Active' && (sharesAccountData.summary?.totalApprovedShares ?? 0) === 0) {
<div class="closure-eligible-banner">
<fa-icon icon="info-circle"></fa-icon>
<span>All shares have been withdrawn from this account — it is now eligible for closure.</span>
<a class="closure-eligible-banner__link" [routerLink]="['/clerk/share-closure-request']"> Request Closure </a>
</div>
}
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
<a
mat-tab-link
@@ -14,3 +14,33 @@
min-width: 60%;
font-weight: 400;
}
.closure-eligible-banner {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
margin: 0 0 12px;
background: #eaf2ff;
border: 1px solid rgb(52 87 213 / 25%);
border-left: 3px solid #3457d5;
border-radius: 8px;
font-size: 13px;
color: #1b3a8a;
fa-icon {
color: #3457d5;
}
&__link {
margin-left: auto;
font-weight: 700;
color: #3457d5;
text-decoration: none;
white-space: nowrap;
&:hover {
text-decoration: underline;
}
}
}
@@ -100,11 +100,11 @@ export class SharesButtonsConfiguration {
{
name: 'Share Certificate',
taskPermissionName: 'READ_SHAREACCOUNTPURCHASE'
},
{
name: 'Close',
taskPermissionName: 'CLOSE_SHAREACCOUNT'
}
// 'Close' is intentionally not offered here — closing a share account must go through
// the Clerk Dashboard maker-checker flow (Clerk Dashboard > My Share Closures), never a
// direct one-step action from this view. The native close command also force-redeems any
// remaining shares, bypassing the governed Share Withdrawal approval flow entirely.
];
break;
case 'Submitted and pending approval':
@@ -9,16 +9,22 @@
<h1 mat-dialog-title class="ced-title">
<span class="ced-title__icon">✉</span>
Approval Email
<div class="ced-status-row">
@if (data.emailSent) {
<span class="ced-chip ced-chip--sent"> <span class="ced-chip__dot"></span>Sent </span>
<span class="ced-chip ced-chip--sent">
<span class="ced-chip__dot"></span>Sent
</span>
} @else {
<span class="ced-chip ced-chip--notsent"> <span class="ced-chip__dot"></span>Not sent </span>
<span class="ced-chip ced-chip--notsent">
<span class="ced-chip__dot"></span>Not sent
</span>
}
@if (data.emailOpenedAt) {
<span class="ced-chip ced-chip--seen"> <fa-icon icon="eye" class="ced-chip__icon"></fa-icon>Seen </span>
<span class="ced-chip ced-chip--seen">
<fa-icon icon="eye" class="ced-chip__icon"></fa-icon>Seen
</span>
} @else {
<span class="ced-chip ced-chip--notseen">
<fa-icon icon="eye-slash" class="ced-chip__icon"></fa-icon>Not yet seen
@@ -28,15 +34,16 @@
</h1>
<div mat-dialog-content class="ced-content">
<!-- Classic Email Header (Gmail Style) -->
<div class="ced-gmail-header">
<h2 class="ced-gmail-subject">{{ data.emailSubject ?? '(No Subject)' }}</h2>
<div class="ced-gmail-meta">
<div class="ced-gmail-avatar">
<div class="ced-gmail-avatar-letter">C</div>
</div>
<div class="ced-gmail-meta-content">
<div class="ced-gmail-meta-top">
<span class="ced-gmail-sender-name">CBSNexora</span>
@@ -46,7 +53,7 @@
</div>
<div class="ced-gmail-meta-bottom">
<span class="ced-gmail-to">
to
to
@if (data.emailTo) {
{{ data.emailTo }}
} @else {
@@ -58,12 +58,8 @@ export class ClientEditEmailDialogComponent {
if (!isoDate) return '—';
const d = new Date(isoDate);
return d.toLocaleString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
day: '2-digit', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit', hour12: true
});
}
}
@@ -0,0 +1,82 @@
<!--
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/.
-->
<div class="pg-card">
<div class="pos-section-label">Share Closure Requests — Review, Approve or Reject</div>
<div class="inv-form-grid" style="padding: 12px 16px">
<label class="inv-form-field">
<span>Status</span>
<select [(ngModel)]="filterStatus" (change)="onFilterChange()">
@for (opt of statusOptions; track opt.value) {
<option [value]="opt.value">{{ opt.label }}</option>
}
</select>
</label>
<label class="inv-form-field" style="align-self: end">
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="loading" (click)="load()">Refresh</button>
</label>
</div>
<table class="pg-table">
<thead>
<tr>
<th>Account No</th>
<th>Client</th>
<th>Closed Date</th>
<th>Clerk</th>
<th>Manager</th>
<th>Submitted At</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
@if (loading) {
<tr>
<td colspan="8" class="pg-empty">Loading…</td>
</tr>
} @else if (loaded && requests.length === 0) {
<tr>
<td colspan="8" class="pg-empty">No {{ filterStatus | lowercase }} share closure requests.</td>
</tr>
} @else {
@for (r of requests; track r.id) {
<tr>
<td>
<span class="pg-chip">{{ r.account_no }}</span>
</td>
<td>{{ r.client_name }}</td>
<td>{{ formatDateTime(r.closed_date) }}</td>
<td>{{ r.clerk_name }}</td>
<td>{{ r.manager_name || r.manager_username || '—' }}</td>
<td>{{ formatDateTime(r.submitted_at) }}</td>
<td>
<span [class]="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
</td>
<td>
<button type="button" class="pg-btn pg-btn--ghost" (click)="review(r)">Review</button>
</td>
</tr>
}
}
</tbody>
</table>
</div>
@if (showReviewModal && reviewRequestId) {
<div class="swr-list-popup-backdrop" (click)="closeReviewModal()">
<div class="swr-list-popup-panel" (click)="$event.stopPropagation()">
<mifosx-share-closure-request
[embedded]="true"
[overrideMode]="'review'"
[overrideRequestId]="reviewRequestId"
(closeRequested)="closeReviewModal()"
(actionCompleted)="onReviewActionCompleted()"
></mifosx-share-closure-request>
</div>
</div>
}
@@ -0,0 +1,128 @@
/**
* 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<string, string> = {
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<string, string> = {
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);
}
}
}
@@ -20,207 +20,161 @@
</div>
</div>
<!-- Pill-style tab navigation -->
<nav mat-tab-nav-bar class="ac-tabs" [tabPanel]="tabPanel">
<!-- Pill-style tab navigation — wraps onto multiple rows so every section stays visible and
clickable at once, instead of being hidden behind prev/next scroll arrows. -->
<nav class="ac-tabs">
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./checker-inbox']"
routerLinkActive
#checkerInbox="routerLinkActive"
[active]="checkerInbox.isActive"
routerLink="./checker-inbox"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="inbox" class="ac-tab__ic"></fa-icon>
<span>Checker Inbox</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./client-approval']"
routerLinkActive
#clientApproval="routerLinkActive"
[active]="clientApproval.isActive"
routerLink="./client-approval"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'READ_CLIENT'"
>
<fa-icon icon="user-check" class="ac-tab__ic"></fa-icon>
<span>Client Approval</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./loan-approval']"
routerLinkActive
#loanApproval="routerLinkActive"
[active]="loanApproval.isActive"
*mifosxHasPermission="'READ_LOAN'"
>
<a class="ac-tab" routerLink="./loan-approval" routerLinkActive="ac-tab--active" *mifosxHasPermission="'READ_LOAN'">
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
<span>Loan Approval</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./loan-disbursal']"
routerLinkActive
#loanDisbursal="routerLinkActive"
[active]="loanDisbursal.isActive"
routerLink="./loan-disbursal"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'READ_LOAN'"
>
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
<span>Loan Disbursal</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./reschedule-loan']"
routerLinkActive
#rescheduleLoan="routerLinkActive"
[active]="rescheduleLoan.isActive"
routerLink="./reschedule-loan"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'RESCHEDULE_LOAN'"
>
<fa-icon icon="calendar-alt" class="ac-tab__ic"></fa-icon>
<span>Reschedule Loan</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./pending-account']"
routerLinkActive
#pendingAccount="routerLinkActive"
[active]="pendingAccount.isActive"
routerLink="./pending-account"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="hourglass-half" class="ac-tab__ic"></fa-icon>
<span>Pending Account</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./pending-savings-products']"
routerLinkActive
#pendingSavingsProducts="routerLinkActive"
[active]="pendingSavingsProducts.isActive"
routerLink="./pending-savings-products"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="piggy-bank" class="ac-tab__ic"></fa-icon>
<span>Pending Product Edits</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./pending-savings-account-edits']"
routerLinkActive
#pendingSavingsAccountEdits="routerLinkActive"
[active]="pendingSavingsAccountEdits.isActive"
routerLink="./pending-savings-account-edits"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
<span>Pending Account Edits</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./pending-share-account-edits']"
routerLinkActive
#pendingShareAccountEdits="routerLinkActive"
[active]="pendingShareAccountEdits.isActive"
routerLink="./pending-share-account-edits"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
<span>Pending Share Account Edits</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./pending-client-edits']"
routerLinkActive
#pendingClientEdits="routerLinkActive"
[active]="pendingClientEdits.isActive"
routerLink="./pending-client-edits"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
<span>Pending Client Edits</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./rejected-account']"
routerLinkActive
#rejectedAccount="routerLinkActive"
[active]="rejectedAccount.isActive"
routerLink="./rejected-account"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="ban" class="ac-tab__ic"></fa-icon>
<span>Reject Account</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./terminated-account']"
routerLinkActive
#terminatedAccount="routerLinkActive"
[active]="terminatedAccount.isActive"
routerLink="./terminated-account"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="times-circle" class="ac-tab__ic"></fa-icon>
<span>Terminated Account</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./withdrawal-requests']"
routerLinkActive
#withdrawalRequests="routerLinkActive"
[active]="withdrawalRequests.isActive"
routerLink="./withdrawal-requests"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
<span>Withdrawal Requests</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./share-withdrawal-requests']"
routerLinkActive
#shareWithdrawalRequests="routerLinkActive"
[active]="shareWithdrawalRequests.isActive"
routerLink="./share-withdrawal-requests"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
<span>Share Withdrawal Requests</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./deposit-requests']"
routerLinkActive
#depositRequests="routerLinkActive"
[active]="depositRequests.isActive"
routerLink="./share-closure-requests"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="lock" class="ac-tab__ic"></fa-icon>
<span>Share Closure Requests</span>
</a>
<a
class="ac-tab"
routerLink="./deposit-requests"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
<span>Deposit Requests</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./disbursement-requests']"
routerLinkActive
#disbursementRequests="routerLinkActive"
[active]="disbursementRequests.isActive"
routerLink="./disbursement-requests"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
<span>Disbursement & Transfers</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./chequebook-requests']"
routerLinkActive
#chequebookRequests="routerLinkActive"
[active]="chequebookRequests.isActive"
routerLink="./chequebook-requests"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'READ_CHEQUEBOOKPENDINGREQUEST'"
>
<fa-icon icon="money-check" class="ac-tab__ic"></fa-icon>
@@ -228,7 +182,7 @@
</a>
</nav>
<mat-tab-nav-panel #tabPanel>
<div class="ac-tab-panel">
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>
</div>
+19
View File
@@ -18,6 +18,7 @@ import { CheckerInboxAndTasksComponent } from './checker-inbox-and-tasks/checker
import { CheckerInboxComponent } from './checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component';
import { WithdrawalRequestsComponent } from './checker-inbox-and-tasks-tabs/withdrawal-requests/withdrawal-requests.component';
import { ShareWithdrawalRequestsComponent } from './checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component';
import { ShareClosureRequestsComponent } from './checker-inbox-and-tasks-tabs/share-closure-requests/share-closure-requests.component';
import { DepositRequestsComponent } from './checker-inbox-and-tasks-tabs/deposit-requests/deposit-requests.component';
import { DisbursementRequestsComponent } from './checker-inbox-and-tasks-tabs/disbursement-requests/disbursement-requests.component';
import { ClientApprovalComponent } from './checker-inbox-and-tasks-tabs/client-approval/client-approval.component';
@@ -165,6 +166,11 @@ const routes: Routes = [
component: ShareWithdrawalRequestsComponent,
data: { title: 'Share Withdrawal Requests' }
},
{
path: 'share-closure-requests',
component: ShareClosureRequestsComponent,
data: { title: 'Share Closure Requests' }
},
{
path: 'deposit-requests',
component: DepositRequestsComponent,
@@ -219,6 +225,19 @@ const routes: Routes = [
}
]
},
{
path: 'checker-inbox-and-tasks/share-closure-requests',
children: [
{
path: ':id/review',
loadComponent: () =>
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: [
+15 -16
View File
@@ -146,7 +146,8 @@ export function declarationStatement(fy: string, asAt: string = TDS_AS_AT): Decl
for (const row of declared) {
const acceptedOn = row.declarationAcceptedOn as string;
const key = cfg.declarationStatementFrequency === 'MONTHLY' ? acceptedOn.slice(0, 7) : `Q${quarterOf(acceptedOn)}`;
const key =
cfg.declarationStatementFrequency === 'MONTHLY' ? acceptedOn.slice(0, 7) : `Q${quarterOf(acceptedOn)}`;
if (!row.declarationUin) {
missingUinCount++;
@@ -181,22 +182,20 @@ export function declarationStatement(fy: string, asAt: string = TDS_AS_AT): Decl
const periods: DeclarationStatementPeriod[] = [...buckets.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(
([
.map(([
key,
lines
]) => {
const dueDate = declarationStatementDueDate(lines[0].acceptedOn, year);
return {
key,
lines
]) => {
const dueDate = declarationStatementDueDate(lines[0].acceptedOn, year);
return {
key,
label: cfg.declarationStatementFrequency === 'MONTHLY' ? monthLabel(`${key}-01`) : key,
dueDate,
overdue: dueDate < asAt,
lines,
totalPaidWithoutDeduction: lines.reduce((s, l) => s + l.paidWithoutDeduction, 0)
};
}
);
label: cfg.declarationStatementFrequency === 'MONTHLY' ? monthLabel(`${key}-01`) : key,
dueDate,
overdue: dueDate < asAt,
lines,
totalPaidWithoutDeduction: lines.reduce((s, l) => s + l.paidWithoutDeduction, 0)
};
});
return {
fy: year,
@@ -21,7 +21,7 @@ import {
MatRow
} from '@angular/material/table';
import { MatButton, MatIconButton } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatIcon } from '@angular/material/icon';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { MatTooltip } from '@angular/material/tooltip';
import { MatProgressBar } from '@angular/material/progress-bar';
@@ -52,7 +52,7 @@ import { RevokeDscDialogComponent } from '../revoke-dsc-dialog/revoke-dsc-dialog
MatRow,
MatButton,
MatIconButton,
MatIconModule,
MatIcon,
MatMenu,
MatMenuItem,
MatMenuTrigger,
+2 -2
View File
@@ -9,8 +9,8 @@
/* tslint:disable */
export default {
'mifos_x': {
'version': '260805',
'hash': '8c7d8287'
'version': '260806',
'hash': 'ddb215de'
},
'allow_switching_backend_instance': true
};