Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4281a596af | ||
|
|
9f9c9bfab6 | ||
|
|
513e863bba | ||
|
|
13333088d7 | ||
|
|
89bb76455e | ||
|
|
7e69468995 | ||
|
|
63ab9643ce | ||
|
|
8255c8398d | ||
|
|
14c5f5070d | ||
|
|
b609f19e9f | ||
|
|
7f836f6f71 |
+1
-1
@@ -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.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 & 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 & 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 & 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 {
|
||||
|
||||
+91
-79
@@ -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>
|
||||
|
||||
+40
-22
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-4
@@ -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>
|
||||
}
|
||||
|
||||
+54
-26
@@ -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}$/)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+46
-37
@@ -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.
|
||||
|
||||
+1
-28
@@ -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" />
|
||||
|
||||
|
||||
-24
@@ -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();
|
||||
|
||||
|
||||
@@ -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 & 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 & 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&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 & 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 & 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 & 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 -->
|
||||
|
||||
@@ -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>₹{{ 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'
|
||||
}.`
|
||||
);
|
||||
|
||||
|
||||
+522
-522
File diff suppressed because it is too large
Load Diff
@@ -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':
|
||||
|
||||
+14
-7
@@ -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 {
|
||||
|
||||
+2
-6
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+82
@@ -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>
|
||||
}
|
||||
+128
@@ -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>
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260805',
|
||||
'hash': '8c7d8287'
|
||||
'version': '260806',
|
||||
'hash': 'ddb215de'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user