Compare commits
4
Commits
56f13ade67
...
29f1cdc646
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29f1cdc646 | ||
|
|
1efb520009 | ||
|
|
4ea833ff49 | ||
|
|
5dc284bd1c |
+32
-32
@@ -1232,38 +1232,6 @@
|
|||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="form-field">
|
|
||||||
<mat-label>Monthly Turnover</mat-label>
|
|
||||||
<span matPrefix class="rupee-prefix">₹</span>
|
|
||||||
<input matInput type="number" formControlName="monthlyTurnover" min="0" />
|
|
||||||
@if (
|
|
||||||
createClientForm.get('monthlyTurnover')?.hasError('pattern') &&
|
|
||||||
createClientForm.get('monthlyTurnover')?.touched
|
|
||||||
) {
|
|
||||||
<mat-error>Enter a valid amount</mat-error>
|
|
||||||
} @else if (
|
|
||||||
createClientForm.get('monthlyTurnover')?.hasError('min') && createClientForm.get('monthlyTurnover')?.touched
|
|
||||||
) {
|
|
||||||
<mat-error>Amount cannot be negative</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="form-field">
|
|
||||||
<mat-label>Yearly Turnover</mat-label>
|
|
||||||
<span matPrefix class="rupee-prefix">₹</span>
|
|
||||||
<input matInput type="number" formControlName="annualTurnover" min="0" />
|
|
||||||
@if (
|
|
||||||
createClientForm.get('annualTurnover')?.hasError('pattern') &&
|
|
||||||
createClientForm.get('annualTurnover')?.touched
|
|
||||||
) {
|
|
||||||
<mat-error>Enter a valid amount</mat-error>
|
|
||||||
} @else if (
|
|
||||||
createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched
|
|
||||||
) {
|
|
||||||
<mat-error>Amount cannot be negative</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<div class="full-width subsection-title">
|
<div class="full-width subsection-title">
|
||||||
<fa-icon icon="coins" class="subsection-icon"></fa-icon>
|
<fa-icon icon="coins" class="subsection-icon"></fa-icon>
|
||||||
Other Income
|
Other Income
|
||||||
@@ -1303,6 +1271,38 @@
|
|||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="form-field">
|
||||||
|
<mat-label>Monthly Turnover</mat-label>
|
||||||
|
<span matPrefix class="rupee-prefix">₹</span>
|
||||||
|
<input matInput type="number" formControlName="monthlyTurnover" min="0" />
|
||||||
|
@if (
|
||||||
|
createClientForm.get('monthlyTurnover')?.hasError('pattern') &&
|
||||||
|
createClientForm.get('monthlyTurnover')?.touched
|
||||||
|
) {
|
||||||
|
<mat-error>Enter a valid amount</mat-error>
|
||||||
|
} @else if (
|
||||||
|
createClientForm.get('monthlyTurnover')?.hasError('min') && createClientForm.get('monthlyTurnover')?.touched
|
||||||
|
) {
|
||||||
|
<mat-error>Amount cannot be negative</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="form-field">
|
||||||
|
<mat-label>Yearly Turnover</mat-label>
|
||||||
|
<span matPrefix class="rupee-prefix">₹</span>
|
||||||
|
<input matInput type="number" formControlName="annualTurnover" min="0" />
|
||||||
|
@if (
|
||||||
|
createClientForm.get('annualTurnover')?.hasError('pattern') &&
|
||||||
|
createClientForm.get('annualTurnover')?.touched
|
||||||
|
) {
|
||||||
|
<mat-error>Enter a valid amount</mat-error>
|
||||||
|
} @else if (
|
||||||
|
createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched
|
||||||
|
) {
|
||||||
|
<mat-error>Amount cannot be negative</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
<div class="full-width subsection-title">
|
<div class="full-width subsection-title">
|
||||||
<fa-icon icon="user-shield" class="subsection-icon"></fa-icon>
|
<fa-icon icon="user-shield" class="subsection-icon"></fa-icon>
|
||||||
Senior Citizen / Person with Disability
|
Senior Citizen / Person with Disability
|
||||||
|
|||||||
+35
@@ -42,6 +42,7 @@ import { ClientsService } from 'app/clients/clients.service';
|
|||||||
<strong>UN Security Council</strong> &
|
<strong>UN Security Council</strong> &
|
||||||
<strong>Financial Intelligence Unit - India (FIU-IND under UNSC Resolution 2231)</strong>
|
<strong>Financial Intelligence Unit - India (FIU-IND under UNSC Resolution 2231)</strong>
|
||||||
</p>
|
</p>
|
||||||
|
<p class="regulatory-note">{{ regulatoryBasis }}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (!verifying && !matchFound && !errorOccurred) {
|
@if (!verifying && !matchFound && !errorOccurred) {
|
||||||
@@ -69,7 +70,14 @@ import { ClientsService } from 'app/clients/clients.service';
|
|||||||
<span class="meta-label">Database</span>
|
<span class="meta-label">Database</span>
|
||||||
<span class="meta-value">UN SC Consolidated List</span>
|
<span class="meta-value">UN SC Consolidated List</span>
|
||||||
</div>
|
</div>
|
||||||
|
@if (listDataAsOf) {
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="meta-label">List Data As Of</span>
|
||||||
|
<span class="meta-value">{{ listDataAsOf }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="regulatory-note">{{ regulatoryBasis }}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (!verifying && matchFound) {
|
@if (!verifying && matchFound) {
|
||||||
@@ -96,11 +104,18 @@ import { ClientsService } from 'app/clients/clients.service';
|
|||||||
<span class="meta-label">Database</span>
|
<span class="meta-label">Database</span>
|
||||||
<span class="meta-value">UN SC Consolidated List</span>
|
<span class="meta-value">UN SC Consolidated List</span>
|
||||||
</div>
|
</div>
|
||||||
|
@if (listDataAsOf) {
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="meta-label">List Data As Of</span>
|
||||||
|
<span class="meta-value">{{ listDataAsOf }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="escalate-banner">
|
<div class="escalate-banner">
|
||||||
<fa-icon icon="exclamation-circle" class="escalate-icon"></fa-icon>
|
<fa-icon icon="exclamation-circle" class="escalate-icon"></fa-icon>
|
||||||
<span><strong>Escalate to Principal Officer immediately.</strong> Account opening blocked.</span>
|
<span><strong>Escalate to Principal Officer immediately.</strong> Account opening blocked.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="regulatory-note">{{ regulatoryBasis }}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (!verifying && errorOccurred) {
|
@if (!verifying && errorOccurred) {
|
||||||
@@ -207,6 +222,15 @@ import { ClientsService } from 'app/clients/clients.service';
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.regulatory-note {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9ca3af;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
.approve-card {
|
.approve-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -524,6 +548,10 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
|
|||||||
errorOccurred = false;
|
errorOccurred = false;
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
screeningLogId: number | null = null;
|
screeningLogId: number | null = null;
|
||||||
|
listDataAsOf: string | null = null;
|
||||||
|
|
||||||
|
regulatoryBasis =
|
||||||
|
'Screened as per Section 51-A of the Unlawful Activities (Prevention) Act, 1967 and the RBI Master Direction on KYC.';
|
||||||
|
|
||||||
screenedDate = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
screenedDate = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||||
progressValue = 0;
|
progressValue = 0;
|
||||||
@@ -624,6 +652,13 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
|
|||||||
'Unable to connect to sanctions screening service.';
|
'Unable to connect to sanctions screening service.';
|
||||||
} else {
|
} else {
|
||||||
this.screeningLogId = this.apiResult?.screeningLogId || null;
|
this.screeningLogId = this.apiResult?.screeningLogId || null;
|
||||||
|
this.listDataAsOf = this.apiResult?.listDataAsOf
|
||||||
|
? new Date(this.apiResult.listDataAsOf).toLocaleDateString('en-IN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric'
|
||||||
|
})
|
||||||
|
: null;
|
||||||
if (this.apiResult?.matchFound) {
|
if (this.apiResult?.matchFound) {
|
||||||
this.matchFound = true;
|
this.matchFound = true;
|
||||||
this.matchCount = this.apiResult.matchCount || 0;
|
this.matchCount = this.apiResult.matchCount || 0;
|
||||||
|
|||||||
+3
-2
@@ -173,8 +173,9 @@
|
|||||||
@if (selectedAddrProof.requiresDate) {
|
@if (selectedAddrProof.requiresDate) {
|
||||||
<mat-form-field appearance="outline" class="addr-ff">
|
<mat-form-field appearance="outline" class="addr-ff">
|
||||||
<mat-label>Issue / bill date</mat-label>
|
<mat-label>Issue / bill date</mat-label>
|
||||||
<span matPrefix class="addr-prefix"><mat-icon>event</mat-icon></span>
|
<input matInput [matDatepicker]="addrIssueDatePicker" formControlName="issueDate" />
|
||||||
<input matInput type="date" formControlName="issueDate" />
|
<mat-datepicker-toggle matSuffix [for]="addrIssueDatePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #addrIssueDatePicker></mat-datepicker>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+6
-3
@@ -20,6 +20,7 @@ import { ClientsService } from '../../clients.service';
|
|||||||
import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types';
|
import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types';
|
||||||
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
|
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
|
||||||
import { DocumentPreviewDialogComponent } from '../../shared/document-preview-dialog/document-preview-dialog.component';
|
import { DocumentPreviewDialogComponent } from '../../shared/document-preview-dialog/document-preview-dialog.component';
|
||||||
|
import { Dates } from 'app/core/utils/dates';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-client-india-address-step',
|
selector: 'mifosx-client-india-address-step',
|
||||||
@@ -41,6 +42,7 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
|||||||
private clientsService = inject(ClientsService);
|
private clientsService = inject(ClientsService);
|
||||||
private kycUpload = inject(KycUploadHelperService);
|
private kycUpload = inject(KycUploadHelperService);
|
||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
|
private dates = inject(Dates);
|
||||||
|
|
||||||
/** Client ID — when set, loads existing addresses for edit mode */
|
/** Client ID — when set, loads existing addresses for edit mode */
|
||||||
@Input() clientId: string = '';
|
@Input() clientId: string = '';
|
||||||
@@ -413,11 +415,12 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
|||||||
files.push({ slot: 'Back', file: this.addrFiles.back });
|
files.push({ slot: 'Back', file: this.addrFiles.back });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const rawIssueDate = this.permanentForm.get('issueDate')?.value;
|
||||||
return {
|
return {
|
||||||
proof: this.selectedAddrProof,
|
proof: this.selectedAddrProof,
|
||||||
proofId: this.permanentForm.get('proofId')?.value || '',
|
proofId: this.permanentForm.get('proofId')?.value || '',
|
||||||
documentKey: this.permanentForm.get('documentKey')?.value || '',
|
documentKey: this.permanentForm.get('documentKey')?.value || '',
|
||||||
issueDate: this.permanentForm.get('issueDate')?.value || '',
|
issueDate: rawIssueDate ? this.dates.getDate(rawIssueDate) : '',
|
||||||
files
|
files
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -560,13 +563,13 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
|||||||
proofId: matchedProof.id,
|
proofId: matchedProof.id,
|
||||||
documentKey: identifier.documentKey || ''
|
documentKey: identifier.documentKey || ''
|
||||||
});
|
});
|
||||||
// The description carries `Issue date: dd/mm/yyyy` when set on create (see
|
// The description carries `Issue date: yyyy-MM-dd` when set on create (see
|
||||||
// create-client.saveAddressProof) — pull it out so the form's issueDate field
|
// create-client.saveAddressProof) — pull it out so the form's issueDate field
|
||||||
// shows what was previously entered.
|
// shows what was previously entered.
|
||||||
const desc: string = identifier.description || '';
|
const desc: string = identifier.description || '';
|
||||||
const issueMatch = desc.match(/Issue date:\s*(.+)$/i);
|
const issueMatch = desc.match(/Issue date:\s*(.+)$/i);
|
||||||
if (issueMatch) {
|
if (issueMatch) {
|
||||||
this.permanentForm.patchValue({ issueDate: issueMatch[1].trim() });
|
this.permanentForm.patchValue({ issueDate: this.dates.parseDate(issueMatch[1].trim()) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -314,8 +314,24 @@
|
|||||||
.toggle-vis-btn {
|
.toggle-vis-btn {
|
||||||
width: 28px !important;
|
width: 28px !important;
|
||||||
height: 28px !important;
|
height: 28px !important;
|
||||||
|
padding: 0 !important;
|
||||||
font-size: 13px !important;
|
font-size: 13px !important;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
|
||||||
|
/* Material reserves a 48x48 invisible touch-target + ripple around every
|
||||||
|
mat-icon-button by default. Shrinking the button to 28px above doesn't
|
||||||
|
shrink that touch-target/ripple with it, so — since the suffix
|
||||||
|
container doesn't clip overflow — it visually spills out past the
|
||||||
|
field's own border, making the icon look misplaced/half-outside the box. */
|
||||||
|
::ng-deep .mat-mdc-button-touch-target {
|
||||||
|
width: 28px !important;
|
||||||
|
height: 28px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-button-persistent-ripple,
|
||||||
|
::ng-deep .mat-ripple {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verification Info Banner */
|
/* Verification Info Banner */
|
||||||
|
|||||||
+12
-3
@@ -347,9 +347,18 @@
|
|||||||
<i class="fa fa-calendar"></i> Issue / statement date
|
<i class="fa fa-calendar"></i> Issue / statement date
|
||||||
<span class="kyc-opt">(not older than 3 months)</span>
|
<span class="kyc-opt">(not older than 3 months)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="kyc-input-wrap kyc-input-wrap--date">
|
<div class="kyc-input-wrap kyc-input-wrap--date" (click)="uploadIssueDatePicker.open()">
|
||||||
<i class="fa fa-calendar-o kyc-input-wrap__icon"></i>
|
<input
|
||||||
<input type="date" class="kyc-input kyc-input--date" formControlName="issueDate" />
|
class="kyc-input kyc-input--date"
|
||||||
|
[matDatepicker]="uploadIssueDatePicker"
|
||||||
|
formControlName="issueDate"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle
|
||||||
|
class="kyc-input-wrap__calendar-toggle"
|
||||||
|
[for]="uploadIssueDatePicker"
|
||||||
|
></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #uploadIssueDatePicker></mat-datepicker>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-28
@@ -388,42 +388,32 @@ $red-900: #993c1d;
|
|||||||
padding-left: 38px;
|
padding-left: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Date input — match styling of other kyc inputs */
|
/* Date input — opens the Material calendar picker; styled to match other kyc inputs */
|
||||||
.kyc-input--date {
|
.kyc-input--date {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: $gray-900;
|
color: $gray-900;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding-right: 14px !important;
|
padding-left: 12px !important;
|
||||||
|
padding-right: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-calendar-picker-indicator {
|
.kyc-input-wrap--date .kyc-input-wrap__calendar-toggle {
|
||||||
opacity: 0.55;
|
position: absolute;
|
||||||
cursor: pointer;
|
right: 2px;
|
||||||
filter: grayscale(1);
|
top: 50%;
|
||||||
transition: opacity 0.15s;
|
transform: translateY(-50%);
|
||||||
|
color: $gray-500;
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-icon-button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover::-webkit-calendar-picker-indicator {
|
::ng-deep .mat-mdc-icon-button svg {
|
||||||
opacity: 0.9;
|
width: 18px;
|
||||||
}
|
height: 18px;
|
||||||
|
|
||||||
&::-webkit-datetime-edit {
|
|
||||||
color: $gray-900;
|
|
||||||
padding-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
color: $gray-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-datetime-edit-text {
|
|
||||||
color: $gray-500;
|
|
||||||
padding: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:invalid::-webkit-datetime-edit,
|
|
||||||
&:not(:focus):placeholder-shown::-webkit-datetime-edit {
|
|
||||||
color: $gray-500;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export const PROOF_META: Record<string, Partial<ProofType>> = {
|
|||||||
PENSION_PAYMENT_ORDER: {
|
PENSION_PAYMENT_ORDER: {
|
||||||
sides: 1,
|
sides: 1,
|
||||||
category: 'Financial & other',
|
category: 'Financial & other',
|
||||||
|
requiresDate: true,
|
||||||
info: 'Pension payment order — upload full document issued by pension authority.'
|
info: 'Pension payment order — upload full document issued by pension authority.'
|
||||||
},
|
},
|
||||||
CERT_INCORPORATION: {
|
CERT_INCORPORATION: {
|
||||||
|
|||||||
+80
-33
@@ -6,29 +6,73 @@
|
|||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@if (showData) {
|
<div class="cv-tab-content">
|
||||||
<div class="cv-tab-content">
|
<div class="cv-action-bar">
|
||||||
<div class="cv-action-bar">
|
<div class="cv-search-box">
|
||||||
<mat-form-field appearance="outline" class="cv-search-field">
|
<fa-icon icon="search"></fa-icon>
|
||||||
<mat-icon matPrefix>search</mat-icon>
|
<input
|
||||||
<input
|
#filterInput
|
||||||
#filterInput
|
type="text"
|
||||||
matInput
|
placeholder="{{ 'labels.text.Filter by name' | translate }}"
|
||||||
placeholder="{{ 'labels.text.Filter by name' | translate }}"
|
(keyup.enter)="applyFilter(filterInput.value)"
|
||||||
(keyup)="applyFilter(filterInput.value)"
|
/>
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
<div class="cv-action-buttons">
|
|
||||||
<button
|
|
||||||
mat-stroked-button
|
|
||||||
class="cv-btn cv-btn-approve"
|
|
||||||
*mifosxHasPermission="'ACTIVATE_CLIENT'"
|
|
||||||
(click)="approveClients()"
|
|
||||||
>
|
|
||||||
<fa-icon icon="check" class="m-r-10"></fa-icon>{{ 'labels.buttons.Approve' | translate }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cv-quick-filters">
|
||||||
|
@for (p of datePresets; track p.key) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cv-chip"
|
||||||
|
[class.cv-chip--active]="activePreset === p.key"
|
||||||
|
(click)="applyPreset(p.key)"
|
||||||
|
>
|
||||||
|
{{ p.label }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="cv-action-buttons">
|
||||||
|
@if (selection.selected.length > 0) {
|
||||||
|
<span class="cv-selected-count">{{ selection.selected.length }} selected</span>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
class="cv-btn cv-btn-approve"
|
||||||
|
*mifosxHasPermission="'ACTIVATE_CLIENT'"
|
||||||
|
[disabled]="!selection.hasValue()"
|
||||||
|
(click)="approveClients()"
|
||||||
|
>
|
||||||
|
<fa-icon icon="check" class="m-r-10"></fa-icon>{{ 'labels.buttons.Approve' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (loading) {
|
||||||
|
<div class="cv-empty">
|
||||||
|
<div class="cv-spinner"></div>
|
||||||
|
<p class="cv-empty__text">Loading pending clients…</p>
|
||||||
|
</div>
|
||||||
|
} @else if (loaded && !showData) {
|
||||||
|
<div class="cv-empty">
|
||||||
|
<div class="cv-empty__badge">
|
||||||
|
<fa-icon icon="user-check"></fa-icon>
|
||||||
|
</div>
|
||||||
|
@if (searchTerm) {
|
||||||
|
<p class="cv-empty__title">No matching clients</p>
|
||||||
|
<p class="cv-empty__text">
|
||||||
|
No pending client matches “{{ searchTerm }}”. Try
|
||||||
|
<a class="cv-empty__link" (click)="applyFilter(''); filterInput.value = ''">clearing the search</a>.
|
||||||
|
</p>
|
||||||
|
} @else if (activePreset !== 'all') {
|
||||||
|
<p class="cv-empty__title">No requests in this range</p>
|
||||||
|
<p class="cv-empty__text">
|
||||||
|
No client requests were submitted in this range. Try
|
||||||
|
<a class="cv-empty__link" (click)="applyPreset('all')">All requests</a> instead.
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<p class="cv-empty__title">{{ 'labels.text.No pending loans for approval' | translate }}</p>
|
||||||
|
<p class="cv-empty__text">Every client request has been reviewed — nothing waiting on you right now.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
@for (client of groupedClients | keyvalue; track client) {
|
@for (client of groupedClients | keyvalue; track client) {
|
||||||
<h3 class="cv-group-title">{{ client.key }}</h3>
|
<h3 class="cv-group-title">{{ client.key }}</h3>
|
||||||
@if (client.value.length > 0) {
|
@if (client.value.length > 0) {
|
||||||
@@ -60,8 +104,8 @@
|
|||||||
{{ element.displayName }}
|
{{ element.displayName }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="accountNumber">
|
<ng-container matColumnDef="clientId">
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Account Number' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client ID' | translate }}</th>
|
||||||
<td
|
<td
|
||||||
mat-cell
|
mat-cell
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
@@ -69,7 +113,7 @@
|
|||||||
[routerLink]="['../../clients', element.id]"
|
[routerLink]="['../../clients', element.id]"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
>
|
>
|
||||||
{{ element.accountNo }}
|
{{ element.id }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="staff">
|
<ng-container matColumnDef="staff">
|
||||||
@@ -94,12 +138,15 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!showData) {
|
<mat-paginator
|
||||||
<div class="empty-state">
|
class="cv-paginator"
|
||||||
<i class="fas fa-user-check empty-icon"></i>
|
[length]="totalRecords"
|
||||||
<p class="empty-text">{{ 'labels.text.No pending loans for approval' | translate }}</p>
|
[pageSize]="pageSize"
|
||||||
</div>
|
[pageSizeOptions]="pageSizeOptions"
|
||||||
}
|
[pageIndex]="pageIndex"
|
||||||
|
showFirstLastButtons
|
||||||
|
(page)="onPageChange($event)"
|
||||||
|
></mat-paginator>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|||||||
+207
-42
@@ -9,9 +9,14 @@
|
|||||||
@use '../../../shared/styles/list-layout' as *;
|
@use '../../../shared/styles/list-layout' as *;
|
||||||
|
|
||||||
$brand: #1353a0;
|
$brand: #1353a0;
|
||||||
|
$brand-tint: #eaf1fb;
|
||||||
|
$success: #16a34a;
|
||||||
|
$success-tint: #ecfdf5;
|
||||||
$border: #e2e8f0;
|
$border: #e2e8f0;
|
||||||
$text: #0f172a;
|
$text: #0f172a;
|
||||||
$text-muted: #64748b;
|
$text-muted: #64748b;
|
||||||
|
$surface: #fff;
|
||||||
|
$surface-muted: #f8fafc;
|
||||||
|
|
||||||
.cv-tab-content {
|
.cv-tab-content {
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
@@ -20,93 +25,252 @@ $text-muted: #64748b;
|
|||||||
.cv-action-bar {
|
.cv-action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 20px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: $surface;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-search-field {
|
.cv-search-box {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: $surface-muted;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
border-color 0.15s,
|
||||||
|
box-shadow 0.15s;
|
||||||
|
|
||||||
::ng-deep {
|
fa-icon {
|
||||||
.mat-mdc-form-field-flex {
|
color: $text-muted;
|
||||||
height: 32px;
|
font-size: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
color: $text;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mat-mdc-form-field-infix {
|
&:focus-within {
|
||||||
padding: 4px 0;
|
background: $surface;
|
||||||
min-height: 32px;
|
border-color: $brand;
|
||||||
}
|
box-shadow: 0 0 0 2px rgb(19 83 160 / 18%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mat-mdc-form-field-subscript-wrapper {
|
.cv-quick-filters {
|
||||||
height: 0;
|
display: flex;
|
||||||
min-height: 0;
|
align-items: center;
|
||||||
}
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mdc-notched-outline {
|
.cv-chip {
|
||||||
.mdc-notched-outline__leading {
|
display: inline-flex;
|
||||||
border-radius: 8px 0 0 8px;
|
align-items: center;
|
||||||
}
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'DM Sans', Roboto, sans-serif;
|
||||||
|
color: $text-muted;
|
||||||
|
background: $surface;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 17px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
color 0.15s,
|
||||||
|
border-color 0.15s,
|
||||||
|
box-shadow 0.15s;
|
||||||
|
|
||||||
.mdc-notched-outline__trailing {
|
&:hover {
|
||||||
border-radius: 0 8px 8px 0;
|
background: $surface-muted;
|
||||||
}
|
border-color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-mdc-form-field-icon-prefix {
|
&--active {
|
||||||
padding: 0 6px 0 8px;
|
background: $brand;
|
||||||
color: $text-muted;
|
border-color: $brand;
|
||||||
}
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgb(19 83 160 / 30%);
|
||||||
|
|
||||||
input {
|
&:hover {
|
||||||
font-size: 13px;
|
background: $brand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-action-buttons {
|
.cv-action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cv-selected-count {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $brand;
|
||||||
|
background: $brand-tint;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.cv-btn {
|
.cv-btn {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
height: 32px;
|
height: 34px;
|
||||||
padding: 0 14px;
|
padding: 0 16px;
|
||||||
font-family: 'DM Sans', Roboto, sans-serif;
|
font-family: 'DM Sans', Roboto, sans-serif;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
|
||||||
fa-icon {
|
fa-icon {
|
||||||
margin-right: 4px;
|
margin-right: 6px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-btn-approve {
|
.cv-btn-approve {
|
||||||
border-color: #16a34a;
|
border-color: $success;
|
||||||
color: #16a34a;
|
color: $success;
|
||||||
|
|
||||||
&:hover {
|
&:hover:not([disabled]) {
|
||||||
background: #f0fdf4;
|
background: $success-tint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-group-title {
|
.cv-group-title {
|
||||||
font-size: 14px;
|
display: flex;
|
||||||
font-weight: 600;
|
align-items: center;
|
||||||
color: $text;
|
gap: 6px;
|
||||||
margin: 20px 0 8px;
|
font-size: 13px;
|
||||||
padding-bottom: 4px;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: $text-muted;
|
||||||
|
margin: 22px 0 8px;
|
||||||
|
padding-bottom: 6px;
|
||||||
border-bottom: 1px solid #f1f5f9;
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $brand;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-group-table {
|
.cv-group-table {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid $border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-paginator {
|
||||||
|
margin-top: 8px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Empty / loading state — scoped locally so it doesn't touch the shared
|
||||||
|
// .empty-state used by every other checker-inbox tab. =====
|
||||||
|
.cv-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 56px 24px;
|
||||||
|
text-align: center;
|
||||||
|
background: $surface;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-empty__badge {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: $success-tint;
|
||||||
|
border: 1px solid #a7f3d0;
|
||||||
|
color: $success;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-empty__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-empty__text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-muted;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 360px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-empty__link {
|
||||||
|
color: $brand;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-spinner {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid $border;
|
||||||
|
border-top-color: $brand;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
animation: cv-spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cv-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 768px) {
|
@media (width <= 768px) {
|
||||||
@@ -121,5 +285,6 @@ $text-muted: #64748b;
|
|||||||
|
|
||||||
.cv-action-buttons {
|
.cv-action-buttons {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+109
-42
@@ -8,11 +8,9 @@
|
|||||||
|
|
||||||
/** Angular Imports */
|
/** Angular Imports */
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
|
||||||
import { SelectionModel } from '@angular/cdk/collections';
|
import { SelectionModel } from '@angular/cdk/collections';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
MatTableDataSource,
|
|
||||||
MatTable,
|
MatTable,
|
||||||
MatColumnDef,
|
MatColumnDef,
|
||||||
MatHeaderCellDef,
|
MatHeaderCellDef,
|
||||||
@@ -24,6 +22,7 @@ import {
|
|||||||
MatRowDef,
|
MatRowDef,
|
||||||
MatRow
|
MatRow
|
||||||
} from '@angular/material/table';
|
} from '@angular/material/table';
|
||||||
|
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
|
||||||
/** Dialog Imports */
|
/** Dialog Imports */
|
||||||
@@ -41,6 +40,12 @@ import { MatCheckbox } from '@angular/material/checkbox';
|
|||||||
import { AccountsFilterPipe } from '../../../pipes/accounts-filter.pipe';
|
import { AccountsFilterPipe } from '../../../pipes/accounts-filter.pipe';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checker-facing list of Clients pending approval.
|
||||||
|
* Pagination, name search, and submitted-date filtering all happen server-side via the
|
||||||
|
* standard /clients list API (offset/limit, displayName, fromDate/toDate) — mirrors
|
||||||
|
* PendingClientEditsComponent's load()/onPageChange()/applyPreset() pattern.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-client-approval',
|
selector: 'mifosx-client-approval',
|
||||||
templateUrl: './client-approval.component.html',
|
templateUrl: './client-approval.component.html',
|
||||||
@@ -60,53 +65,122 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
MatRowDef,
|
MatRowDef,
|
||||||
MatRow,
|
MatRow,
|
||||||
KeyValuePipe,
|
KeyValuePipe,
|
||||||
AccountsFilterPipe
|
AccountsFilterPipe,
|
||||||
|
MatPaginatorModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ClientApprovalComponent {
|
export class ClientApprovalComponent implements OnInit {
|
||||||
private route = inject(ActivatedRoute);
|
|
||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
private dateUtils = inject(Dates);
|
private dateUtils = inject(Dates);
|
||||||
private router = inject(Router);
|
|
||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private tasksService = inject(TasksService);
|
private tasksService = inject(TasksService);
|
||||||
|
|
||||||
/** Grouped Clients Data */
|
/** Current page of pending clients (from the server), grouped by office for display */
|
||||||
groupedClients: any;
|
groupedClients: any = {};
|
||||||
/** Checks to show the data */
|
/** True once the first page has loaded and it contains at least one client */
|
||||||
showData = false;
|
showData = false;
|
||||||
|
loading = false;
|
||||||
|
loaded = false;
|
||||||
|
/** Row Selection Data — selection lives across the whole session, not just the current page */
|
||||||
|
selection = new SelectionModel<any>(true, []);
|
||||||
/** Batch Requests */
|
/** Batch Requests */
|
||||||
batchRequests: any[];
|
batchRequests: any[];
|
||||||
/** Datasource */
|
|
||||||
dataSource: MatTableDataSource<any>;
|
|
||||||
/** Row Selection Data */
|
|
||||||
selection: SelectionModel<any>;
|
|
||||||
/** Displayed Columns */
|
/** Displayed Columns */
|
||||||
displayedColumns: string[] = [
|
displayedColumns: string[] = [
|
||||||
'select',
|
'select',
|
||||||
'name',
|
'name',
|
||||||
'accountNumber',
|
'clientId',
|
||||||
'staff'
|
'staff'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
totalRecords = 0;
|
||||||
* Retrieves the grouped client data from `resolve`.
|
pageSize = 20;
|
||||||
* @param {ActivatedRoute} route Activated Route.
|
pageIndex = 0;
|
||||||
* @param {Dialog} dialog MatDialog.
|
pageSizeOptions = [
|
||||||
* @param {Dates} dateUtils Date Utils.
|
10,
|
||||||
* @param {router} router Router.
|
20,
|
||||||
* @param {SettingsService} settingsService Settings Service.
|
50
|
||||||
* @param {TasksService} tasksService Tasks Service.
|
];
|
||||||
*/
|
|
||||||
constructor() {
|
searchTerm = '';
|
||||||
this.route.data.subscribe((data: { groupedClientData: any }) => {
|
activePreset = 'all';
|
||||||
this.groupedClients = _.groupBy(data.groupedClientData.pageItems, 'officeName');
|
readonly datePresets = [
|
||||||
if (Object.keys(this.groupedClients).length) {
|
{ key: 'all', label: 'All requests' },
|
||||||
this.showData = true;
|
{ key: 'today', label: 'Today' },
|
||||||
}
|
{ key: 'yesterday', label: 'Yesterday' }
|
||||||
this.dataSource = new MatTableDataSource(data.groupedClientData.pageItems);
|
];
|
||||||
this.selection = new SelectionModel(true, []);
|
private fromDate: string | undefined;
|
||||||
});
|
private toDate: string | undefined;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
this.loading = true;
|
||||||
|
this.tasksService
|
||||||
|
.getGroupedClientsData(
|
||||||
|
this.searchTerm.trim() || undefined,
|
||||||
|
this.fromDate,
|
||||||
|
this.toDate,
|
||||||
|
this.pageIndex * this.pageSize,
|
||||||
|
this.pageSize
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (res: any) => {
|
||||||
|
const clients = res?.pageItems ?? [];
|
||||||
|
this.groupedClients = _.groupBy(clients, 'officeName');
|
||||||
|
this.totalRecords = Number(res?.totalFilteredRecords ?? 0);
|
||||||
|
this.showData = this.totalRecords > 0 || Object.keys(this.groupedClients).length > 0;
|
||||||
|
this.loading = false;
|
||||||
|
this.loaded = true;
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.groupedClients = {};
|
||||||
|
this.totalRecords = 0;
|
||||||
|
this.loading = false;
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange(event: PageEvent): void {
|
||||||
|
this.pageSize = event.pageSize;
|
||||||
|
this.pageIndex = event.pageIndex;
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter(filterValue: string = ''): void {
|
||||||
|
this.searchTerm = filterValue;
|
||||||
|
this.pageIndex = 0;
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPreset(key: string): void {
|
||||||
|
this.activePreset = key;
|
||||||
|
const today = new Date(this.settingsService.businessDate);
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
if (key === 'today') {
|
||||||
|
this.fromDate = this.isoDate(today);
|
||||||
|
this.toDate = this.isoDate(today);
|
||||||
|
} else if (key === 'yesterday') {
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(today.getDate() - 1);
|
||||||
|
this.fromDate = this.isoDate(yesterday);
|
||||||
|
this.toDate = this.isoDate(yesterday);
|
||||||
|
} else {
|
||||||
|
this.fromDate = undefined;
|
||||||
|
this.toDate = undefined;
|
||||||
|
}
|
||||||
|
this.pageIndex = 0;
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isoDate(d: Date): string {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the number of selected elements matches the total number of rows. */
|
/** Whether the number of selected elements matches the total number of rows. */
|
||||||
@@ -183,6 +257,7 @@ export class ClientApprovalComponent {
|
|||||||
activatedAccounts++;
|
activatedAccounts++;
|
||||||
responseEle.body = JSON.parse(responseEle.body);
|
responseEle.body = JSON.parse(responseEle.body);
|
||||||
if (selectedAccounts === activatedAccounts) {
|
if (selectedAccounts === activatedAccounts) {
|
||||||
|
this.selection.clear();
|
||||||
this.reload();
|
this.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,18 +265,10 @@ export class ClientApprovalComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
applyFilter(filterValue: string = '') {
|
|
||||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refetches data for the component
|
* Refetches the current page after a bulk approval.
|
||||||
* TODO: Replace by a custom reload component instead of hard-coded back-routing.
|
|
||||||
*/
|
*/
|
||||||
reload() {
|
reload() {
|
||||||
const url: string = this.router.url;
|
this.load();
|
||||||
this.router
|
|
||||||
.navigateByUrl(`/checker-inbox-and-tasks`, { skipLocationChange: true })
|
|
||||||
.then(() => this.router.navigate([url]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Angular Imports */
|
|
||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
|
|
||||||
/** rxjs Imports */
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
/** Custom Services */
|
|
||||||
import { TasksService } from '../tasks.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grouped Client Data data resolver.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class GetGroupedClientsData {
|
|
||||||
private tasksService = inject(TasksService);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the grouped client data.
|
|
||||||
* @returns {Observable<any>}
|
|
||||||
*/
|
|
||||||
resolve(): Observable<any> {
|
|
||||||
return this.tasksService.getGroupedClientsData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,6 @@ import { ViewCheckerInboxComponent } from './view-checker-inbox/view-checker-inb
|
|||||||
|
|
||||||
/** Custom Resolvers */
|
/** Custom Resolvers */
|
||||||
import { GetMakerCheckers } from './common-resolvers/getmakercheckers.resolver';
|
import { GetMakerCheckers } from './common-resolvers/getmakercheckers.resolver';
|
||||||
import { GetGroupedClientsData } from './common-resolvers/getGroupedClientsData.resolver';
|
|
||||||
import { GetOffices } from './common-resolvers/getOffices.resolver';
|
import { GetOffices } from './common-resolvers/getOffices.resolver';
|
||||||
import { GetLoansToBeApproved } from './common-resolvers/getLoansToBeApproved.resolver';
|
import { GetLoansToBeApproved } from './common-resolvers/getLoansToBeApproved.resolver';
|
||||||
import { GetLoansToBeDisbursed } from './common-resolvers/getLoansToBeDisbursed.resolver';
|
import { GetLoansToBeDisbursed } from './common-resolvers/getLoansToBeDisbursed.resolver';
|
||||||
@@ -70,10 +69,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'client-approval',
|
path: 'client-approval',
|
||||||
component: ClientApprovalComponent,
|
component: ClientApprovalComponent,
|
||||||
data: { title: 'Client Approval' },
|
data: { title: 'Client Approval' }
|
||||||
resolve: {
|
|
||||||
groupedClientData: GetGroupedClientsData
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'loan-approval',
|
path: 'loan-approval',
|
||||||
@@ -225,7 +221,6 @@ const routes: Routes = [
|
|||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
providers: [
|
providers: [
|
||||||
GetMakerCheckers,
|
GetMakerCheckers,
|
||||||
GetGroupedClientsData,
|
|
||||||
GetOffices,
|
GetOffices,
|
||||||
GetLoansToBeApproved,
|
GetLoansToBeApproved,
|
||||||
GetLoansToBeDisbursed,
|
GetLoansToBeDisbursed,
|
||||||
|
|||||||
@@ -48,10 +48,27 @@ export class TasksService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Grouped Clients Data
|
* Get pending clients awaiting approval — pagination, name search and submitted-date
|
||||||
|
* filtering all happen server-side via the standard /clients list API (offset/limit,
|
||||||
|
* displayName, fromDate/toDate on submittedon_date), mirroring
|
||||||
|
* ClientsService.getAllPendingClientEdits.
|
||||||
*/
|
*/
|
||||||
getGroupedClientsData(): Observable<any> {
|
getGroupedClientsData(
|
||||||
const httpParams = new HttpParams().set('limit', '1000').set('status', 'pending');
|
displayName?: string,
|
||||||
|
fromDate?: string,
|
||||||
|
toDate?: string,
|
||||||
|
offset = 0,
|
||||||
|
limit = 20
|
||||||
|
): Observable<any> {
|
||||||
|
let httpParams = new HttpParams()
|
||||||
|
.set('status', 'pending')
|
||||||
|
.set('orderBy', 'submittedOnDate')
|
||||||
|
.set('sortOrder', 'DESC')
|
||||||
|
.set('offset', offset)
|
||||||
|
.set('limit', limit);
|
||||||
|
if (displayName) httpParams = httpParams.set('displayName', displayName);
|
||||||
|
if (fromDate) httpParams = httpParams.set('fromDate', fromDate);
|
||||||
|
if (toDate) httpParams = httpParams.set('toDate', toDate);
|
||||||
return this.http.get('/clients', { params: httpParams });
|
return this.http.get('/clients', { params: httpParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user