CTR/STR report and Risk Category auto mark on bases of person occupation and anual salary enter
This commit is contained in:
+4
-4
@@ -136,7 +136,7 @@
|
|||||||
| translate
|
| translate
|
||||||
}}
|
}}
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<input matInput required formControlName="fullname" />
|
<input matInput required formControlName="fullname" autocomplete="off" />
|
||||||
@if (createClientForm.controls.fullname.hasError('required')) {
|
@if (createClientForm.controls.fullname.hasError('required')) {
|
||||||
<mat-error>Client name is <strong>required</strong></mat-error>
|
<mat-error>Client name is <strong>required</strong></mat-error>
|
||||||
} @else if (createClientForm.controls.fullname.hasError('pattern')) {
|
} @else if (createClientForm.controls.fullname.hasError('pattern')) {
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
<mat-form-field appearance="outline" class="name-field first-name">
|
<mat-form-field appearance="outline" class="name-field first-name">
|
||||||
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>badge</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>badge</mat-icon></span>
|
||||||
<input matInput required formControlName="firstname" />
|
<input matInput required formControlName="firstname" autocomplete="off" />
|
||||||
@if (createClientForm.controls.firstname.hasError('required')) {
|
@if (createClientForm.controls.firstname.hasError('required')) {
|
||||||
<mat-error>First name is <strong>required</strong></mat-error>
|
<mat-error>First name is <strong>required</strong></mat-error>
|
||||||
} @else if (createClientForm.controls.firstname.hasError('pattern')) {
|
} @else if (createClientForm.controls.firstname.hasError('pattern')) {
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
<mat-form-field appearance="outline" class="name-field middle-name">
|
<mat-form-field appearance="outline" class="name-field middle-name">
|
||||||
<mat-label>{{ 'labels.inputs.Middle Name' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.Middle Name' | translate }}</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>person_outline</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>person_outline</mat-icon></span>
|
||||||
<input matInput formControlName="middlename" />
|
<input matInput formControlName="middlename" autocomplete="off" />
|
||||||
@if (
|
@if (
|
||||||
createClientForm.controls.middlename?.hasError('pattern') &&
|
createClientForm.controls.middlename?.hasError('pattern') &&
|
||||||
createClientForm.controls.middlename?.touched
|
createClientForm.controls.middlename?.touched
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
<mat-form-field appearance="outline" class="name-field last-name">
|
<mat-form-field appearance="outline" class="name-field last-name">
|
||||||
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>person</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>person</mat-icon></span>
|
||||||
<input matInput required formControlName="lastname" />
|
<input matInput required formControlName="lastname" autocomplete="off" />
|
||||||
@if (createClientForm.controls.lastname.hasError('required')) {
|
@if (createClientForm.controls.lastname.hasError('required')) {
|
||||||
<mat-error>Last name is <strong>required</strong></mat-error>
|
<mat-error>Last name is <strong>required</strong></mat-error>
|
||||||
} @else if (createClientForm.controls.lastname.hasError('pattern')) {
|
} @else if (createClientForm.controls.lastname.hasError('pattern')) {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
* m_code_value (code "PwdCategory") via the client template, same as occupationCategoryOptions.
|
* m_code_value (code "PwdCategory") via the client template, same as occupationCategoryOptions.
|
||||||
* Admins can add/reorder/deactivate entries from System -> Manage Codes without a code change. */
|
* Admins can add/reorder/deactivate entries from System -> Manage Codes without a code change. */
|
||||||
pwdCategoryOptions: any[] = [];
|
pwdCategoryOptions: any[] = [];
|
||||||
|
riskRules: any[] = [];
|
||||||
|
|
||||||
/** Set true after a save attempt so the UI can highlight a missing required document */
|
/** Set true after a save attempt so the UI can highlight a missing required document */
|
||||||
seniorPwdDocsTouched = false;
|
seniorPwdDocsTouched = false;
|
||||||
@@ -546,6 +547,10 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.maxDate = this.settingsService.businessDate;
|
this.maxDate = this.settingsService.businessDate;
|
||||||
this.setOptions();
|
this.setOptions();
|
||||||
|
|
||||||
|
this.clientService.getClientRiskRules().subscribe((rules) => {
|
||||||
|
this.riskRules = rules || [];
|
||||||
|
});
|
||||||
// Fetch marital status options from code values
|
// Fetch marital status options from code values
|
||||||
this.clientService.getCodeValuesByName('MaritalStatus').subscribe((options) => {
|
this.clientService.getCodeValuesByName('MaritalStatus').subscribe((options) => {
|
||||||
this.maritalStatusOptions = options || [];
|
this.maritalStatusOptions = options || [];
|
||||||
@@ -2120,6 +2125,74 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
|
|
||||||
return generalDetails;
|
return generalDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get calculatedRisk(): { category: number; reason: string } {
|
||||||
|
const legalFormId = this.createClientForm.get('legalFormId')?.value || 1; // 1 = Person, 2 = Entity
|
||||||
|
const occCatId = this.createClientForm.get('occupationCategoryCvId')?.value;
|
||||||
|
const occId = this.createClientForm.get('occupationCvId')?.value;
|
||||||
|
|
||||||
|
const monthlyIncome = parseFloat(this.createClientForm.get('monthlyIncome')?.value) || 0;
|
||||||
|
const annualIncome = parseFloat(this.createClientForm.get('annualIncome')?.value) || 0;
|
||||||
|
const monthlyTurnover = parseFloat(this.createClientForm.get('monthlyTurnover')?.value) || 0;
|
||||||
|
const annualTurnover = parseFloat(this.createClientForm.get('annualTurnover')?.value) || 0;
|
||||||
|
const agricultureIncome = parseFloat(this.createClientForm.get('agricultureIncome')?.value) || 0;
|
||||||
|
const nonAgricultureIncome = parseFloat(this.createClientForm.get('nonAgricultureIncome')?.value) || 0;
|
||||||
|
|
||||||
|
const totalAnnualIncome = annualIncome + agricultureIncome + nonAgricultureIncome;
|
||||||
|
|
||||||
|
// Evaluate rules from DB (ordered by orderPosition ASC)
|
||||||
|
if (this.riskRules && this.riskRules.length > 0) {
|
||||||
|
for (const rule of this.riskRules) {
|
||||||
|
// Match legal form
|
||||||
|
if (rule.legalFormId != null && rule.legalFormId !== legalFormId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match occupation category
|
||||||
|
if (rule.occupationCategoryCvId != null && rule.occupationCategoryCvId !== occCatId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match occupation
|
||||||
|
if (rule.occupationCvId != null && rule.occupationCvId !== occId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match monthly income ranges
|
||||||
|
if (rule.minMonthlyIncome != null && monthlyIncome < rule.minMonthlyIncome) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule.maxMonthlyIncome != null && monthlyIncome > rule.maxMonthlyIncome) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match annual income ranges (evaluated against totalAnnualIncome)
|
||||||
|
if (rule.minAnnualIncome != null && totalAnnualIncome < rule.minAnnualIncome) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule.maxAnnualIncome != null && totalAnnualIncome > rule.maxAnnualIncome) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match turnover ranges
|
||||||
|
if (rule.minAnnualTurnover != null && annualTurnover < rule.minAnnualTurnover) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule.maxAnnualTurnover != null && annualTurnover > rule.maxAnnualTurnover) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All matched!
|
||||||
|
return { category: rule.riskCategory, reason: rule.reason };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Fallbacks
|
||||||
|
if (legalFormId !== LegalFormId.PERSON) {
|
||||||
|
return { category: 2, reason: 'Non-Individual / Entity profile (Default Medium Risk)' };
|
||||||
|
}
|
||||||
|
return { category: 1, reason: 'Default Low Risk profile (Salary / Service / Agriculture / Others)' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+34
-2
@@ -49,6 +49,10 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
|||||||
/** Minor clients can't hold utility bills, bank statements or an adult-only OVD in their own
|
/** Minor clients can't hold utility bills, bank statements or an adult-only OVD in their own
|
||||||
* name — restricts the address-proof picker to Aadhaar/Passport when true. */
|
* name — restricts the address-proof picker to Aadhaar/Passport when true. */
|
||||||
@Input() isMinor = false;
|
@Input() isMinor = false;
|
||||||
|
/** Non-individual (entity) clients — hides personal OVDs (Aadhaar/Passport/DL/Voter ID/NREGA/
|
||||||
|
* NPR/Birth Certificate/School ID) from the proof-of-address picker; a company's registered
|
||||||
|
* office address is evidenced by utility bills, GST/rent documents etc., not a person's ID. */
|
||||||
|
@Input() isNonIndividual = false;
|
||||||
|
|
||||||
activeTab = 0;
|
activeTab = 0;
|
||||||
sameAsPermanent = false;
|
sameAsPermanent = false;
|
||||||
@@ -282,8 +286,8 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
|||||||
this.clientsService.getKycProofTypes().subscribe({
|
this.clientsService.getKycProofTypes().subscribe({
|
||||||
next: (res: any) => {
|
next: (res: any) => {
|
||||||
const activeFilter = (p: any) => p?.active !== false;
|
const activeFilter = (p: any) => p?.active !== false;
|
||||||
this.addressProofs = this.filterAddressProofsForMinor(
|
this.addressProofs = this.filterAddressProofsForNonIndividual(
|
||||||
(res?.addressProofs || []).filter(activeFilter).map(normalizeProofType)
|
this.filterAddressProofsForMinor((res?.addressProofs || []).filter(activeFilter).map(normalizeProofType))
|
||||||
);
|
);
|
||||||
this.addressGroups = groupProofsByCategory(this.addressProofs);
|
this.addressGroups = groupProofsByCategory(this.addressProofs);
|
||||||
this.loadingProofs = false;
|
this.loadingProofs = false;
|
||||||
@@ -309,6 +313,26 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Personal OVDs — not valid proof of a registered office/entity address. */
|
||||||
|
private static readonly NON_INDIVIDUAL_EXCLUDED_ADDRESS_PROOF_CODES = new Set([
|
||||||
|
'AADHAAR',
|
||||||
|
'PASSPORT',
|
||||||
|
'VOTER_ID',
|
||||||
|
'DRIVING_LICENSE',
|
||||||
|
'NREGA_JOB_CARD',
|
||||||
|
'NPR_LETTER',
|
||||||
|
'BIRTH_CERTIFICATE',
|
||||||
|
'SCHOOL_ID_CARD'
|
||||||
|
]);
|
||||||
|
|
||||||
|
private filterAddressProofsForNonIndividual(proofs: ProofType[]): ProofType[] {
|
||||||
|
if (!this.isNonIndividual) return proofs;
|
||||||
|
return proofs.filter(
|
||||||
|
(p) =>
|
||||||
|
!ClientIndiaAddressStepComponent.NON_INDIVIDUAL_EXCLUDED_ADDRESS_PROOF_CODES.has((p.code || '').toUpperCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
selectAddrProof(proof: ProofType): void {
|
selectAddrProof(proof: ProofType): void {
|
||||||
this.selectedAddrProof = proof;
|
this.selectedAddrProof = proof;
|
||||||
this.permanentForm.get('proofId')?.setValue(proof.id);
|
this.permanentForm.get('proofId')?.setValue(proof.id);
|
||||||
@@ -425,6 +449,14 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Legal form toggled on an already-initialized instance — re-fetch rather than filter
|
||||||
|
// `addressProofs` in place, since the non-individual filter only ever removes entries and
|
||||||
|
// toggling back to individual could never recover what a prior pass had stripped out.
|
||||||
|
if (changes['isNonIndividual'] && !changes['isNonIndividual'].firstChange) {
|
||||||
|
this.selectedAddrProof = null;
|
||||||
|
this.permanentForm.get('proofId')?.setValue('');
|
||||||
|
this.loadProofTypes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1049,6 +1049,12 @@
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
|
<div class="checkbox-field override-checkbox-wrapper full-width">
|
||||||
|
<mat-checkbox formControlName="allowRiskOverride" [disabled]="readOnly">
|
||||||
|
Override Auto-calculated Risk Category
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="form-field">
|
<mat-form-field appearance="outline" class="form-field">
|
||||||
<mat-label>Risk Category</mat-label>
|
<mat-label>Risk Category</mat-label>
|
||||||
<mat-select formControlName="riskCategory">
|
<mat-select formControlName="riskCategory">
|
||||||
@@ -1086,6 +1092,64 @@
|
|||||||
<mat-hint>Auto: Low 10yr, Medium 8yr, High 2yr</mat-hint>
|
<mat-hint>Auto: Low 10yr, Medium 8yr, High 2yr</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (riskCalculationReason && !kycForm.get('allowRiskOverride')?.value && !readOnly) {
|
||||||
|
<div
|
||||||
|
class="risk-auto-calc-info full-width"
|
||||||
|
[class.risk-auto-calc-info--low]="kycForm.get('riskCategory')?.value === 1"
|
||||||
|
[class.risk-auto-calc-info--medium]="kycForm.get('riskCategory')?.value === 2"
|
||||||
|
[class.risk-auto-calc-info--high]="kycForm.get('riskCategory')?.value === 3"
|
||||||
|
>
|
||||||
|
<mat-icon class="info-icon">info</mat-icon>
|
||||||
|
<span class="info-text"
|
||||||
|
>Auto-determined: <strong>{{ riskCalculationReason }}</strong></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="risk-rules-toggle-wrapper full-width">
|
||||||
|
<button mat-button type="button" class="rules-toggle-btn" (click)="showRiskRulesTable = !showRiskRulesTable">
|
||||||
|
<mat-icon class="rules-toggle-icon">{{ showRiskRulesTable ? 'expand_less' : 'expand_more' }}</mat-icon>
|
||||||
|
{{
|
||||||
|
showRiskRulesTable ? 'Hide Risk Classification Criteria Table' : 'View Risk Classification Criteria Table'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (showRiskRulesTable) {
|
||||||
|
<div class="risk-rules-card mat-elevation-z1 full-width">
|
||||||
|
<table class="risk-rules-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Risk Category</th>
|
||||||
|
<th>Primary Criteria / Basis</th>
|
||||||
|
<th>Examples / Thresholds</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="rule-row rule-row--high">
|
||||||
|
<td><span class="risk-badge risk-badge--high">High Risk</span></td>
|
||||||
|
<td>PEPs, High-Risk Industries, High Income / Turnovers</td>
|
||||||
|
<td>
|
||||||
|
Politicians; Real Estate, Import/Export; Commission Agents; Income > ₹20L/yr; Turnover > ₹1 Crore
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="rule-row rule-row--medium">
|
||||||
|
<td><span class="risk-badge risk-badge--medium">Medium Risk</span></td>
|
||||||
|
<td>Self-Employed, Professionals, Cross-Border, Moderate Income</td>
|
||||||
|
<td>
|
||||||
|
Business Owners, Traders; Doctors, Lawyers, CAs; NRI, PIO, OCI; Income > ₹6L/yr; Turnover > ₹20L
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="rule-row rule-row--low">
|
||||||
|
<td><span class="risk-badge risk-badge--low">Low Risk</span></td>
|
||||||
|
<td>Non-Earning, Government Employment, Agriculture / Manual</td>
|
||||||
|
<td>Students, Minors, Homemakers; Govt/PSU Employees; Farmers, Daily Wage; Income <= ₹6L/yr</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="form-field full-width">
|
<mat-form-field appearance="outline" class="form-field full-width">
|
||||||
<mat-label>{{ isKycVerified ? 'Reason for Re-edit *' : 'Remarks' }}</mat-label>
|
<mat-label>{{ isKycVerified ? 'Reason for Re-edit *' : 'Remarks' }}</mat-label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -2257,3 +2257,161 @@
|
|||||||
width: 95px;
|
width: 95px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.override-checkbox-wrapper {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-auto-calc-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: -8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
font-size: 18px !important;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--low {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #065f46;
|
||||||
|
border-color: #a7f3d0;
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--medium {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #92400e;
|
||||||
|
border-color: #fde68a;
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--high {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #991b1b;
|
||||||
|
border-color: #fecaca;
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-rules-toggle-wrapper {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.rules-toggle-btn {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e6bb8;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
background: #f1f5f9;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-rules-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-rules-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
border-bottom: 2px solid #cbd5e1;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-row {
|
||||||
|
transition: background 0.15s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
text-align: center;
|
||||||
|
width: 80px;
|
||||||
|
|
||||||
|
&--high {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--medium {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--low {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #16a34a;
|
||||||
|
border: 1px solid #a7f3d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
@Input() entityName: string = '';
|
@Input() entityName: string = '';
|
||||||
/** Read-only mode — disables form editing until user clicks edit icon */
|
/** Read-only mode — disables form editing until user clicks edit icon */
|
||||||
@Input() readOnly = false;
|
@Input() readOnly = false;
|
||||||
|
@Input() calculatedRisk: { category: number; reason: string } | null = null;
|
||||||
|
|
||||||
today = new Date();
|
today = new Date();
|
||||||
kycId: string = '';
|
kycId: string = '';
|
||||||
@@ -170,6 +171,17 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
{ name: 'Birth Certificate', code: 'BIRTH_CERTIFICATE' },
|
{ name: 'Birth Certificate', code: 'BIRTH_CERTIFICATE' },
|
||||||
{ name: 'School ID Card', code: 'SCHOOL_ID_CARD' }
|
{ name: 'School ID Card', code: 'SCHOOL_ID_CARD' }
|
||||||
];
|
];
|
||||||
|
/** Generic RBI-prescribed KYC documents for legal entities (Master Direction on KYC,
|
||||||
|
* Annexure I) — shown for non-individual clients before a specific constitution type is
|
||||||
|
* picked. Individual OVDs (Aadhaar/Passport/DL/Voter ID/NREGA/NPR) don't apply to entities. */
|
||||||
|
private nonIndividualDefaultDocTypes = [
|
||||||
|
{ name: 'Certificate of Incorporation / Registration Certificate', code: 'CERT_INCORPORATION' },
|
||||||
|
{ name: 'PAN Card of Entity', code: 'ENTITY_PAN' },
|
||||||
|
{ name: 'Memorandum & Articles of Association / Partnership Deed / Trust Deed', code: 'CONSTITUTIVE_DOC' },
|
||||||
|
{ name: 'Board Resolution / Authorization Letter', code: 'BOARD_RESOLUTION' },
|
||||||
|
{ name: 'KYC of Authorized Signatory / Power of Attorney Holder', code: 'AUTH_SIGNATORY_KYC' },
|
||||||
|
{ name: 'GST Registration Certificate', code: 'GST_CERTIFICATE' }
|
||||||
|
];
|
||||||
/** Constitution-based KYC document types for non-individual clients */
|
/** Constitution-based KYC document types for non-individual clients */
|
||||||
private constitutionDocTypes: Record<string, { name: string; mandatory: boolean }[]> = {
|
private constitutionDocTypes: Record<string, { name: string; mandatory: boolean }[]> = {
|
||||||
HUF: [
|
HUF: [
|
||||||
@@ -250,6 +262,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
{ value: 3, label: 'High Risk', years: 2, color: 'red' }
|
{ value: 3, label: 'High Risk', years: 2, color: 'red' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
riskCalculationReason = '';
|
||||||
|
showRiskRulesTable = false;
|
||||||
|
|
||||||
/** PAN verification */
|
/** PAN verification */
|
||||||
panVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
panVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
||||||
panVerifying = false;
|
panVerifying = false;
|
||||||
@@ -441,6 +456,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
kycDocIssueDate: [''],
|
kycDocIssueDate: [''],
|
||||||
kycDocExpiryDate: [''],
|
kycDocExpiryDate: [''],
|
||||||
riskCategory: [1],
|
riskCategory: [1],
|
||||||
|
allowRiskOverride: [false],
|
||||||
remarks: [''],
|
remarks: [''],
|
||||||
amlCheckStatus: [
|
amlCheckStatus: [
|
||||||
'CLEAR',
|
'CLEAR',
|
||||||
@@ -488,6 +504,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
return docs.filter((d) => !ClientKycStepComponent.MINOR_ONLY_DOC_CODES.has((d.code || '').toUpperCase()));
|
return docs.filter((d) => !ClientKycStepComponent.MINOR_ONLY_DOC_CODES.has((d.code || '').toUpperCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Individual-only OVDs vs. entity-only RBI docs — never both, and minor filtering only
|
||||||
|
* ever applies to individuals. */
|
||||||
|
private getDefaultDocTypes(): { name: string; code: string }[] {
|
||||||
|
return this.isNonIndividual
|
||||||
|
? this.nonIndividualDefaultDocTypes
|
||||||
|
: this.filterDocTypesForMinor(this.defaultDocumentTypes);
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seedDefaultDocTypes();
|
this.seedDefaultDocTypes();
|
||||||
this.loadExistingKycIfNeeded();
|
this.loadExistingKycIfNeeded();
|
||||||
@@ -495,20 +519,36 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
if (this.isNonIndividual && this.constitutionMeta) {
|
if (this.isNonIndividual && this.constitutionMeta) {
|
||||||
this.updateDocTypesForConstitution();
|
this.updateDocTypesForConstitution();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.kycForm.get('allowRiskOverride')?.valueChanges.subscribe((allowed: boolean) => {
|
||||||
|
const riskControl = this.kycForm.get('riskCategory');
|
||||||
|
if (allowed) {
|
||||||
|
if (!this.readOnly) {
|
||||||
|
riskControl?.enable();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
riskControl?.disable();
|
||||||
|
if (this.calculatedRisk) {
|
||||||
|
riskControl?.setValue(this.calculatedRisk.category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.kycForm.get('allowRiskOverride')?.value) {
|
||||||
|
this.kycForm.get('riskCategory')?.disable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seed the document-type picker from defaults, filtered by the current isMinor input.
|
/** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs,
|
||||||
* Only seeds when the list is empty — the template API (loadKycTemplate) takes over once
|
* filtered by the current isMinor input for individuals). Only seeds when the list is
|
||||||
* a clientId is available. */
|
* empty — the template API (loadKycTemplate) takes over once a clientId is available. */
|
||||||
private seedDefaultDocTypes(): void {
|
private seedDefaultDocTypes(): void {
|
||||||
if (this.kycDocumentTypes.length) return;
|
if (this.kycDocumentTypes.length) return;
|
||||||
this.kycDocumentTypes = this.filterDocTypesForMinor(
|
this.kycDocumentTypes = this.getDefaultDocTypes().map((d) => ({
|
||||||
this.defaultDocumentTypes.map((d) => ({
|
name: d.name,
|
||||||
name: d.name,
|
code: d.code,
|
||||||
code: d.code,
|
mandatory: false
|
||||||
mandatory: false
|
}));
|
||||||
}))
|
|
||||||
);
|
|
||||||
this.rebuildKycDocPicker();
|
this.rebuildKycDocPicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +566,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.kycForm.reset({
|
this.kycForm.reset({
|
||||||
hasPan: true,
|
hasPan: true,
|
||||||
riskCategory: 1,
|
riskCategory: 1,
|
||||||
|
allowRiskOverride: false,
|
||||||
amlCheckStatus: 'CLEAR',
|
amlCheckStatus: 'CLEAR',
|
||||||
bannedEntityMatch: 'NO',
|
bannedEntityMatch: 'NO',
|
||||||
willfulDefaulter: 'NO',
|
willfulDefaulter: 'NO',
|
||||||
@@ -546,6 +587,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.kycDocMergedFile = null;
|
this.kycDocMergedFile = null;
|
||||||
this.kycDocMergedPreview = null;
|
this.kycDocMergedPreview = null;
|
||||||
this.existingKycIdentifierId = null;
|
this.existingKycIdentifierId = null;
|
||||||
|
this.riskCalculationReason = '';
|
||||||
|
this.showRiskRulesTable = false;
|
||||||
this.existingKycDocuments = [];
|
this.existingKycDocuments = [];
|
||||||
this.photoSigTouched = false;
|
this.photoSigTouched = false;
|
||||||
this.panVerifyStatus = 'NONE';
|
this.panVerifyStatus = 'NONE';
|
||||||
@@ -598,12 +641,41 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.kycForm.disable({ emitEvent: false });
|
this.kycForm.disable({ emitEvent: false });
|
||||||
} else {
|
} else {
|
||||||
this.kycForm.enable({ emitEvent: false });
|
this.kycForm.enable({ emitEvent: false });
|
||||||
|
if (!this.kycForm.get('allowRiskOverride')?.value) {
|
||||||
|
this.kycForm.get('riskCategory')?.disable({ emitEvent: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changes['calculatedRisk']) {
|
||||||
|
const newRisk = changes['calculatedRisk'].currentValue;
|
||||||
|
if (newRisk) {
|
||||||
|
this.riskCalculationReason = newRisk.reason;
|
||||||
|
const allowOverride = this.kycForm.get('allowRiskOverride')?.value;
|
||||||
|
const riskControl = this.kycForm.get('riskCategory');
|
||||||
|
if (!allowOverride && riskControl && riskControl.value !== newRisk.category) {
|
||||||
|
riskControl.setValue(newRisk.category);
|
||||||
|
riskControl.markAsDirty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.riskCalculationReason = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changes['constitutionMeta'] && this.isNonIndividual) {
|
if (changes['constitutionMeta'] && this.isNonIndividual) {
|
||||||
this.updateDocTypesForConstitution();
|
this.updateDocTypesForConstitution();
|
||||||
}
|
}
|
||||||
if (changes['isMinor']) {
|
// Legal form toggled on an already-initialized instance (route-reuse keeps this
|
||||||
|
// component alive across create sessions) — the individual OVD list and the entity RBI
|
||||||
|
// doc list are mutually exclusive, so re-seed from scratch rather than filtering in place.
|
||||||
|
if (changes['isNonIndividual'] && !changes['isNonIndividual'].firstChange && !this.kycTemplateLoaded) {
|
||||||
|
this.kycDocumentTypes = [];
|
||||||
|
this.selectedKycDoc = null;
|
||||||
|
this.kycForm.get('kycDocumentName')?.setValue('');
|
||||||
|
this.seedDefaultDocTypes();
|
||||||
|
if (this.isNonIndividual && this.constitutionMeta) {
|
||||||
|
this.updateDocTypesForConstitution();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changes['isMinor'] && !this.isNonIndividual) {
|
||||||
if (this.isMinor) {
|
if (this.isMinor) {
|
||||||
this.kycForm.get('hasPan')?.setValue(false);
|
this.kycForm.get('hasPan')?.setValue(false);
|
||||||
this.kycForm.get('panNumber')?.setValue('');
|
this.kycForm.get('panNumber')?.setValue('');
|
||||||
@@ -797,6 +869,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
kycDocExpiryDate: parseDate(data.kycDocExpiryDate),
|
kycDocExpiryDate: parseDate(data.kycDocExpiryDate),
|
||||||
// Risk & Remarks
|
// Risk & Remarks
|
||||||
riskCategory: data.riskCategory || 1,
|
riskCategory: data.riskCategory || 1,
|
||||||
|
allowRiskOverride: this.calculatedRisk ? this.calculatedRisk.category !== (data.riskCategory || 1) : false,
|
||||||
remarks: data.remarks || '',
|
remarks: data.remarks || '',
|
||||||
hasPan: !!data.panNumber,
|
hasPan: !!data.panNumber,
|
||||||
// AML & Compliance
|
// AML & Compliance
|
||||||
@@ -1015,13 +1088,11 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
error: () => {
|
error: () => {
|
||||||
// Fallback to defaults if template API unavailable
|
// Fallback to defaults if template API unavailable
|
||||||
this.kycTemplateLoaded = true;
|
this.kycTemplateLoaded = true;
|
||||||
this.kycDocumentTypes = this.filterDocTypesForMinor(
|
this.kycDocumentTypes = this.getDefaultDocTypes().map((d) => ({
|
||||||
this.defaultDocumentTypes.map((d) => ({
|
name: d.name,
|
||||||
name: d.name,
|
code: d.code,
|
||||||
code: d.code,
|
mandatory: false
|
||||||
mandatory: false
|
}));
|
||||||
}))
|
|
||||||
);
|
|
||||||
this.rebuildKycDocPicker();
|
this.rebuildKycDocPicker();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1664,7 +1735,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get kycData(): any {
|
get kycData(): any {
|
||||||
const formVal = this.readOnly ? this.kycForm.getRawValue() : this.kycForm.value;
|
const formVal = this.kycForm.getRawValue();
|
||||||
const aadhaarRaw = formVal.aadhaarNumber || '';
|
const aadhaarRaw = formVal.aadhaarNumber || '';
|
||||||
const aadhaarMasked = aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : null;
|
const aadhaarMasked = aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : null;
|
||||||
const dateFormat = this.settingsService.dateFormat;
|
const dateFormat = this.settingsService.dateFormat;
|
||||||
|
|||||||
@@ -739,6 +739,10 @@ export class ClientsService {
|
|||||||
return this.http.get(`/clients/${clientId}/kyc/template`);
|
return this.http.get(`/clients/${clientId}/kyc/template`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getClientRiskRules(): Observable<any[]> {
|
||||||
|
return this.http.get<any[]>('/kyc/risk-rules');
|
||||||
|
}
|
||||||
|
|
||||||
/** Fetch code values by code ID */
|
/** Fetch code values by code ID */
|
||||||
getCodeValuesById(codeId: number): Observable<any[]> {
|
getCodeValuesById(codeId: number): Observable<any[]> {
|
||||||
return this.http.get<any[]>(`/codes/${codeId}/codevalues`).pipe(catchError(() => of([])));
|
return this.http.get<any[]>(`/codes/${codeId}/codevalues`).pipe(catchError(() => of([])));
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
[constitutionMeta]="clientGeneralStep?.selectedConstitution"
|
[constitutionMeta]="clientGeneralStep?.selectedConstitution"
|
||||||
[entityName]="clientGeneralStep?.createClientForm?.get('fullname')?.value || ''"
|
[entityName]="clientGeneralStep?.createClientForm?.get('fullname')?.value || ''"
|
||||||
[readOnly]="isTabReadOnly(1)"
|
[readOnly]="isTabReadOnly(1)"
|
||||||
|
[calculatedRisk]="clientGeneralStep?.calculatedRisk"
|
||||||
></mifosx-client-kyc-step>
|
></mifosx-client-kyc-step>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -148,6 +149,7 @@
|
|||||||
<mifosx-client-india-address-step
|
<mifosx-client-india-address-step
|
||||||
#clientIndiaAddress
|
#clientIndiaAddress
|
||||||
[isMinor]="clientGeneralStep?.isMinor || false"
|
[isMinor]="clientGeneralStep?.isMinor || false"
|
||||||
|
[isNonIndividual]="legalFormType === 2"
|
||||||
[readOnly]="isTabReadOnly(addressTabIdx)"
|
[readOnly]="isTabReadOnly(addressTabIdx)"
|
||||||
></mifosx-client-india-address-step>
|
></mifosx-client-india-address-step>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
[isMinor]="clientGeneralStep?.isMinor || false"
|
[isMinor]="clientGeneralStep?.isMinor || false"
|
||||||
[clientId]="clientId"
|
[clientId]="clientId"
|
||||||
[readOnly]="isTabReadOnly(1)"
|
[readOnly]="isTabReadOnly(1)"
|
||||||
|
[calculatedRisk]="clientGeneralStep?.calculatedRisk"
|
||||||
></mifosx-client-kyc-step>
|
></mifosx-client-kyc-step>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,6 +209,7 @@
|
|||||||
#clientIndiaAddress
|
#clientIndiaAddress
|
||||||
[clientId]="clientId"
|
[clientId]="clientId"
|
||||||
[isMinor]="clientGeneralStep?.isMinor || false"
|
[isMinor]="clientGeneralStep?.isMinor || false"
|
||||||
|
[isNonIndividual]="legalFormType === 2"
|
||||||
[readOnly]="isTabReadOnly(addressTabIdx)"
|
[readOnly]="isTabReadOnly(addressTabIdx)"
|
||||||
></mifosx-client-india-address-step>
|
></mifosx-client-india-address-step>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -146,6 +146,44 @@ export const PROOF_META: Record<string, Partial<ProofType>> = {
|
|||||||
sides: 1,
|
sides: 1,
|
||||||
category: 'Financial & other',
|
category: 'Financial & other',
|
||||||
info: 'Pension payment order — upload full document issued by pension authority.'
|
info: 'Pension payment order — upload full document issued by pension authority.'
|
||||||
|
},
|
||||||
|
CERT_INCORPORATION: {
|
||||||
|
sides: 1,
|
||||||
|
category: 'Entity KYC documents',
|
||||||
|
documentKeyLabel: 'Registration / incorporation number',
|
||||||
|
documentKeyPlaceholder: 'CIN / registration number',
|
||||||
|
info: 'Certificate of Incorporation (companies/LLPs) or Registration Certificate (firms/trusts/societies) — as prescribed under the RBI Master Direction on KYC, Annexure I.'
|
||||||
|
},
|
||||||
|
ENTITY_PAN: {
|
||||||
|
sides: 1,
|
||||||
|
category: 'Entity KYC documents',
|
||||||
|
documentKeyLabel: 'PAN of entity',
|
||||||
|
documentKeyPlaceholder: 'e.g. ABCDE1234F',
|
||||||
|
info: 'PAN card issued in the name of the entity.'
|
||||||
|
},
|
||||||
|
CONSTITUTIVE_DOC: {
|
||||||
|
sides: 1,
|
||||||
|
category: 'Entity KYC documents',
|
||||||
|
documentKeyLabel: 'Document reference number',
|
||||||
|
documentKeyPlaceholder: 'Reference / registration number',
|
||||||
|
info: 'Constitutive document — Memorandum & Articles of Association (companies), Partnership Deed (firms), Trust Deed (trusts), or By-laws (societies) — as prescribed under the RBI Master Direction on KYC.'
|
||||||
|
},
|
||||||
|
BOARD_RESOLUTION: {
|
||||||
|
sides: 1,
|
||||||
|
category: 'Entity KYC documents',
|
||||||
|
info: 'Board Resolution / Authorization Letter naming the officers, managers or employees authorised to transact on the entity’s behalf.'
|
||||||
|
},
|
||||||
|
AUTH_SIGNATORY_KYC: {
|
||||||
|
sides: 1,
|
||||||
|
category: 'Entity KYC documents',
|
||||||
|
info: 'OVD-based KYC of the person(s) holding Power of Attorney / authorised to transact on the entity’s behalf.'
|
||||||
|
},
|
||||||
|
GST_CERTIFICATE: {
|
||||||
|
sides: 1,
|
||||||
|
category: 'Entity KYC documents',
|
||||||
|
documentKeyLabel: 'GSTIN',
|
||||||
|
documentKeyPlaceholder: 'e.g. 22AAAAA0000A1Z5',
|
||||||
|
info: 'GST Registration Certificate, where the entity is GST-registered.'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1568,8 +1568,12 @@
|
|||||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('Balance Sheet')" (click)="rbiMenuOpen = false">
|
<a class="gl-link" [class.hidden]="!matchesRbiSearch('Balance Sheet')" (click)="rbiMenuOpen = false">
|
||||||
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> Balance Sheet (BR Act & Others)
|
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> Balance Sheet (BR Act & Others)
|
||||||
</a>
|
</a>
|
||||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('CRR / SLR')" (click)="rbiMenuOpen = false">
|
<a
|
||||||
<fa-icon icon="calculator" class="gl-icon"></fa-icon> CRR / SLR
|
class="gl-link"
|
||||||
|
[class.hidden]="!matchesRbiSearch('CTR / STR')"
|
||||||
|
(click)="rbiMenuOpen = false; openCtrStrDialog()"
|
||||||
|
>
|
||||||
|
<fa-icon icon="calculator" class="gl-icon"></fa-icon> CTR / STR
|
||||||
</a>
|
</a>
|
||||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('Annexure')" (click)="rbiMenuOpen = false">
|
<a class="gl-link" [class.hidden]="!matchesRbiSearch('Annexure')" (click)="rbiMenuOpen = false">
|
||||||
<fa-icon icon="paperclip" class="gl-icon"></fa-icon> Annexure
|
<fa-icon icon="paperclip" class="gl-icon"></fa-icon> Annexure
|
||||||
|
|||||||
@@ -280,6 +280,23 @@ export class ShellComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the CTR & STR Compliance Center dialog.
|
||||||
|
*/
|
||||||
|
async openCtrStrDialog(): Promise<void> {
|
||||||
|
const { CtrStrDialogComponent } = await import('../../home/dashboard/ctr-str-dialog/ctr-str-dialog.component');
|
||||||
|
this.dialog.open(CtrStrDialogComponent, {
|
||||||
|
width: '95vw',
|
||||||
|
maxWidth: '1280px',
|
||||||
|
maxHeight: '92vh',
|
||||||
|
panelClass: 'crr-slr-dialog-panel',
|
||||||
|
autoFocus: false,
|
||||||
|
disableClose: false,
|
||||||
|
hasBackdrop: true,
|
||||||
|
backdropClass: 'ckyc-rpt-dlg-backdrop'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the CIMS / XBRL Returns dialog. The initial form (OSS-2, PSA, RAL, RCP-UCB)
|
* Opens the CIMS / XBRL Returns dialog. The initial form (OSS-2, PSA, RAL, RCP-UCB)
|
||||||
* is selected based on the menu link the user clicked; user can switch between
|
* is selected based on the menu link the user clicked; user can switch between
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<!--
|
||||||
|
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="ctr-str-dlg">
|
||||||
|
<!-- Dialog Header -->
|
||||||
|
<div class="dlg-header">
|
||||||
|
<div class="dlg-title-area">
|
||||||
|
<fa-icon icon="shield-alt" class="header-icon"></fa-icon>
|
||||||
|
<div>
|
||||||
|
<h2 class="title">CTR & STR Compliance Control Center</h2>
|
||||||
|
<p class="subtitle">Anti-Money Laundering (AML) & CFT Regulatory Reporting Terminal</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button mat-icon-button [mat-dialog-close]="true" class="close-btn">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigators -->
|
||||||
|
<div class="tab-navs">
|
||||||
|
<button class="tab-btn" [class.active]="activeTab === 'dashboard'" (click)="activeTab = 'dashboard'">
|
||||||
|
<fa-icon icon="chart-bar" class="tab-icon"></fa-icon> Compliance Dashboard
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" [class.active]="activeTab === 'guidelines'" (click)="activeTab = 'guidelines'">
|
||||||
|
<fa-icon icon="university" class="tab-icon"></fa-icon> RBI Regulatory Guidelines
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dialog Body (Tabs) -->
|
||||||
|
<div class="dlg-body">
|
||||||
|
<!-- TAB 1: COMPLIANCE DASHBOARD -->
|
||||||
|
@if (activeTab === 'dashboard') {
|
||||||
|
<div class="tab-content dashboard-tab">
|
||||||
|
<!-- SECTION 1: CTR MANAGMENT -->
|
||||||
|
<div class="dashboard-section card-glass">
|
||||||
|
<div class="section-hdr">
|
||||||
|
<div>
|
||||||
|
<h3>Cash Transaction Reports (CTR - Monthly Cash Registry)</h3>
|
||||||
|
<p class="section-sub">
|
||||||
|
Scans and compiles cash transactions exceeding INR 10 Lakhs limit (Single or monthly cumulative cash
|
||||||
|
deposits/withdrawals).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="action-btn primary" [disabled]="isGeneratingCtr" (click)="generateCtrReport()">
|
||||||
|
@if (isGeneratingCtr) {
|
||||||
|
<span class="spinner"></span> Generating Report...
|
||||||
|
} @else {
|
||||||
|
<fa-icon icon="sync-alt"></fa-icon> Generate Monthly CTR
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-actions">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search CTR files by month or name..."
|
||||||
|
[(ngModel)]="ctrFilter"
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="compliance-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Report Payload Name</th>
|
||||||
|
<th>Reporting Period</th>
|
||||||
|
<th>Record Count</th>
|
||||||
|
<th>Generated By</th>
|
||||||
|
<th>Generated Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (report of filteredCtrReports; track report.id) {
|
||||||
|
<tr>
|
||||||
|
<td class="bold-text"><fa-icon icon="file-code" class="xml-icon"></fa-icon> {{ report.name }}</td>
|
||||||
|
<td>{{ report.month }}</td>
|
||||||
|
<td class="center-text">
|
||||||
|
<span class="cnt-badge">{{ report.recordCount }} txns</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ report.generatedBy }}</td>
|
||||||
|
<td>{{ report.generatedAt | date: 'medium' }}</td>
|
||||||
|
<td><span class="status-badge ready">Ready</span></td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="icon-action-btn"
|
||||||
|
matTooltip="Download FIU-IND XML Payload"
|
||||||
|
(click)="downloadCtrFile(report.name)"
|
||||||
|
>
|
||||||
|
<fa-icon icon="download"></fa-icon> Download XML
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="empty-state">No CTR reports found matching your search.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SECTION 2: STR ALERTS PIPELINE -->
|
||||||
|
<div class="dashboard-section card-glass mt-20">
|
||||||
|
<div class="section-hdr">
|
||||||
|
<div>
|
||||||
|
<h3>Suspicious Transaction Activity Pipeline (STR Management)</h3>
|
||||||
|
<p class="section-sub">
|
||||||
|
Behavioral & structural AML alerts generated by KYC profile rules engine. Investigator actions update
|
||||||
|
state in real-time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="action-btn secondary" (click)="downloadStrReportTemplate()">
|
||||||
|
<fa-icon icon="file-excel"></fa-icon> Export Active Alerts (CSV)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-actions">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search alerts by Client, CIF, or alert type..."
|
||||||
|
[(ngModel)]="strFilter"
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alerts-grid">
|
||||||
|
@for (alert of filteredStrAlerts; track alert.id) {
|
||||||
|
<div class="alert-card" [class]="alert.status.toLowerCase().replace(' ', '-')">
|
||||||
|
<div class="card-hdr">
|
||||||
|
<div class="client-info">
|
||||||
|
<h4>{{ alert.clientName }}</h4>
|
||||||
|
<span class="cif-number">{{ alert.clientCif }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="status-badge" [class]="alert.status.toLowerCase().replace(' ', '-')">{{
|
||||||
|
alert.status
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="amount-line">
|
||||||
|
<span class="label">Amount Highlighted:</span>
|
||||||
|
<span class="amount">₹{{ alert.amount | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="type-line">
|
||||||
|
<span class="label">Alert Flag:</span>
|
||||||
|
<span class="type-value">{{ alert.alertType }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="desc">{{ alert.description }}</p>
|
||||||
|
<span class="date-stamp"
|
||||||
|
><fa-icon icon="clock"></fa-icon> Flagged Date: {{ alert.date | date: 'mediumDate' }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
@if (alert.status === 'Open') {
|
||||||
|
<button class="card-btn investigate" (click)="changeAlertStatus(alert.id, 'Investigating')">
|
||||||
|
<fa-icon icon="search"></fa-icon> Investigate
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (alert.status === 'Open' || alert.status === 'Investigating') {
|
||||||
|
<button class="card-btn file-str" (click)="changeAlertStatus(alert.id, 'Reported STR')">
|
||||||
|
<fa-icon icon="flag"></fa-icon> File STR
|
||||||
|
</button>
|
||||||
|
<button class="card-btn close-alert" (click)="changeAlertStatus(alert.id, 'Closed')">
|
||||||
|
<fa-icon icon="ban"></fa-icon> Dismiss
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (alert.status === 'Closed' || alert.status === 'Reported STR') {
|
||||||
|
<button class="card-btn reset-alert" (click)="changeAlertStatus(alert.id, 'Open')">
|
||||||
|
<fa-icon icon="redo-alt"></fa-icon> Reopen Alert
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @empty {
|
||||||
|
<div class="empty-alerts">No Suspicious Transaction alerts matching your criteria.</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- TAB 2: RBI REGULATORY GUIDELINES -->
|
||||||
|
@if (activeTab === 'guidelines') {
|
||||||
|
<div class="tab-content scroll-y">
|
||||||
|
<div class="info-layout">
|
||||||
|
<!-- CTR Rules Card -->
|
||||||
|
<div class="info-card card-glass">
|
||||||
|
<div class="card-title-badge ctr-badge-color">
|
||||||
|
<fa-icon icon="coins" class="badge-icon"></fa-icon> CTR - Cash Transaction Report
|
||||||
|
</div>
|
||||||
|
<div class="rules-body">
|
||||||
|
<p class="lead-desc">
|
||||||
|
As mandated by Section 12 of the **Prevention of Money Laundering Act (PMLA), 2002** and RBI KYC
|
||||||
|
guidelines, banks must record and report all high-value cash transactions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="rule-point">
|
||||||
|
<h5>1. Transaction Threshold</h5>
|
||||||
|
<p>
|
||||||
|
All cash transactions (either deposits or withdrawals) of **value more than INR 10 Lakhs** (or its
|
||||||
|
equivalent in foreign currency) in a single transaction.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-point">
|
||||||
|
<h5>2. Monthly Aggregation Rule (Structuring check)</h5>
|
||||||
|
<p>
|
||||||
|
A series of cash transactions in a calendar month that are **integrally connected** and accumulate to
|
||||||
|
a total value exceeding **INR 10 Lakhs** must be compiled and reported in the monthly CTR.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-point">
|
||||||
|
<h5>3. Reporting Timeline</h5>
|
||||||
|
<p>
|
||||||
|
CTRs must be generated and uploaded to the FIU-IND gateway **by the 15th day of the succeeding month**
|
||||||
|
(e.g., June transactions must be uploaded by July 15th).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-point text-warning-border">
|
||||||
|
<strong>Important Note:</strong> Transactions where the net cash balance does not change but high volume
|
||||||
|
is rotated (e.g., cash withdrawals immediately deposited back) must be reported under CTR as separate
|
||||||
|
debit and credit records.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STR Rules Card -->
|
||||||
|
<div class="info-card card-glass">
|
||||||
|
<div class="card-title-badge str-badge-color">
|
||||||
|
<fa-icon icon="flag" class="badge-icon"></fa-icon> STR - Suspicious Transaction Report
|
||||||
|
</div>
|
||||||
|
<div class="rules-body">
|
||||||
|
<p class="lead-desc">
|
||||||
|
Every reporting entity must maintain vigilance over account transactions. If there are reasonable
|
||||||
|
grounds to suspect transactions involve proceeds of crime, an STR must be generated.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="rule-point">
|
||||||
|
<h5>1. What Constitutes Suspicion?</h5>
|
||||||
|
<p>
|
||||||
|
Transactions that are complex, show unusual patterns, mismatch the customer’s business/risk profile,
|
||||||
|
have no apparent economic rationale, or involve suspected proceeds of crime or terror funding.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-point">
|
||||||
|
<h5>2. No Minimum Threshold Limit</h5>
|
||||||
|
<p>
|
||||||
|
**There is no minimum monetary limit** for STR. An alert should be generated for any transaction,
|
||||||
|
regardless of size, if suspicious behavior is flagged.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-point">
|
||||||
|
<h5>3. Reporting Timeline</h5>
|
||||||
|
<p>
|
||||||
|
The STR must be filed to FIU-IND **within 7 days** of the Principal Compliance Officer confirming the
|
||||||
|
suspicion.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-point text-danger-border">
|
||||||
|
<strong>Strict Tipping-off Restriction:</strong> It is illegal to disclose or "tip-off" to the client
|
||||||
|
that their account is being monitored, investigated, or reported under an STR. Complete confidentiality
|
||||||
|
must be maintained.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,750 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ctr-str-dlg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 90vh;
|
||||||
|
max-height: 820px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: Inter, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
.dlg-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
.dlg-title-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
color: #64748b;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation Tabs
|
||||||
|
.tab-navs {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 8px 24px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll layout
|
||||||
|
.scroll-y {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialog Body
|
||||||
|
.dlg-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 24px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.scroll-y {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 6px;
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard Tab specific layout
|
||||||
|
.dashboard-tab {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 6px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card Style Container
|
||||||
|
.card-glass {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgb(0 0 0 / 5%),
|
||||||
|
0 2px 4px -1px rgb(0 0 0 / 2%);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.section-hdr {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-sub {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-25 {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search input
|
||||||
|
.table-actions {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: #1e293b;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #4f46e5;
|
||||||
|
box-shadow: 0 0 0 2px rgb(79 70 229 / 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button styles
|
||||||
|
.action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background: #4338ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #94a3b8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: #0ea5e9;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #0284c7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spinner animation
|
||||||
|
.spinner {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid rgb(255 255 255 / 30%);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: #fff;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tables styling
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
color: #334155;
|
||||||
|
|
||||||
|
&.bold-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center-text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cnt-badge {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xml-icon {
|
||||||
|
color: #ea580c;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
padding: 24px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon Action Button
|
||||||
|
.icon-action-btn {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
color: #475569;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert Cards Grid
|
||||||
|
.alerts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left boundary color codes based on status
|
||||||
|
&.open {
|
||||||
|
border-left: 4px solid #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.investigating {
|
||||||
|
border-left: 4px solid #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reported-str {
|
||||||
|
border-left: 4px solid #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
border-left: 4px solid #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hdr {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cif-number {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
.amount-line,
|
||||||
|
.type-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
margin: 8px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px dashed #e2e8f0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-stamp {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 14px;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
padding-top: 12px;
|
||||||
|
|
||||||
|
.card-btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 75px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.investigate {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.file-str {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.close-alert {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reset-alert {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badges
|
||||||
|
.status-badge {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.ready {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.investigating {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reported-str {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-alerts {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 36px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #64748b;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab 2: Guidelines layout
|
||||||
|
.info-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
@media (width >= 900px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.card-title-badge {
|
||||||
|
align-self: flex-start;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctr-badge-color {
|
||||||
|
background: #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.str-badge-color {
|
||||||
|
background: #be185d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-body {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #334155;
|
||||||
|
|
||||||
|
.lead-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-point {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning-border {
|
||||||
|
border-left: 3px solid #eab308;
|
||||||
|
background: #fef9c3;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
color: #854d0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger-border {
|
||||||
|
border-left: 3px solid #ef4444;
|
||||||
|
background: #fee2e2;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab 3: Architecture & Schema
|
||||||
|
.arch-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
margin: 4px 0 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arch-section {
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-sub {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-snippet {
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.plain {
|
||||||
|
background: #1e293b;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Routes
|
||||||
|
.route-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.method {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
min-width: 45px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.get {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.put {
|
||||||
|
background: #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.post {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-desc {
|
||||||
|
color: #475569;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { FaIconComponent, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||||
|
import {
|
||||||
|
faShieldAlt,
|
||||||
|
faChartBar,
|
||||||
|
faUniversity,
|
||||||
|
faCode,
|
||||||
|
faSyncAlt,
|
||||||
|
faFileExcel,
|
||||||
|
faFileCode,
|
||||||
|
faDownload,
|
||||||
|
faClock,
|
||||||
|
faSearch,
|
||||||
|
faFlag,
|
||||||
|
faBan,
|
||||||
|
faRedoAlt,
|
||||||
|
faCoins
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
interface CtrReport {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
month: string;
|
||||||
|
recordCount: number;
|
||||||
|
generatedBy: string;
|
||||||
|
generatedAt: Date;
|
||||||
|
status: 'Ready' | 'Generating';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StrAlert {
|
||||||
|
id: number;
|
||||||
|
clientName: string;
|
||||||
|
clientCif: string;
|
||||||
|
alertType: string;
|
||||||
|
amount: number;
|
||||||
|
status: 'Open' | 'Investigating' | 'Reported STR' | 'Closed';
|
||||||
|
description: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-ctr-str-dialog',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './ctr-str-dialog.component.html',
|
||||||
|
styleUrls: ['./ctr-str-dialog.component.scss'],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatTabsModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatIconModule,
|
||||||
|
FaIconComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CtrStrDialogComponent {
|
||||||
|
dialogRef = inject(MatDialogRef<CtrStrDialogComponent>);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const library = inject(FaIconLibrary);
|
||||||
|
library.addIcons(
|
||||||
|
faShieldAlt,
|
||||||
|
faChartBar,
|
||||||
|
faUniversity,
|
||||||
|
faCode,
|
||||||
|
faSyncAlt,
|
||||||
|
faFileExcel,
|
||||||
|
faFileCode,
|
||||||
|
faDownload,
|
||||||
|
faClock,
|
||||||
|
faSearch,
|
||||||
|
faFlag,
|
||||||
|
faBan,
|
||||||
|
faRedoAlt,
|
||||||
|
faCoins
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTab: 'dashboard' | 'guidelines' = 'dashboard';
|
||||||
|
|
||||||
|
// Mock State
|
||||||
|
isGeneratingCtr = false;
|
||||||
|
ctrFilter = '';
|
||||||
|
strFilter = '';
|
||||||
|
|
||||||
|
ctrReports: CtrReport[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'CTR_REPORT_JUNE_2026.xml',
|
||||||
|
month: 'June 2026',
|
||||||
|
recordCount: 14,
|
||||||
|
generatedBy: 'Compliance_Officer_1',
|
||||||
|
generatedAt: new Date(2026, 6, 5, 10, 30),
|
||||||
|
status: 'Ready'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'CTR_REPORT_MAY_2026.xml',
|
||||||
|
month: 'May 2026',
|
||||||
|
recordCount: 22,
|
||||||
|
generatedBy: 'Compliance_Officer_1',
|
||||||
|
generatedAt: new Date(2026, 5, 4, 14, 15),
|
||||||
|
status: 'Ready'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'CTR_REPORT_APRIL_2026.xml',
|
||||||
|
month: 'April 2026',
|
||||||
|
recordCount: 9,
|
||||||
|
generatedBy: 'Compliance_Officer_2',
|
||||||
|
generatedAt: new Date(2026, 4, 3, 11, 45),
|
||||||
|
status: 'Ready'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
strAlerts: StrAlert[] = [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
clientName: 'Aarav Sharma',
|
||||||
|
clientCif: 'CIF982348',
|
||||||
|
alertType: 'Profile Mismatch',
|
||||||
|
amount: 450000,
|
||||||
|
status: 'Open',
|
||||||
|
description:
|
||||||
|
'Student account received a large inward transfer (INR 4.5L) mismatching declared zero/student income category.',
|
||||||
|
date: new Date(2026, 6, 26)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
clientName: 'Karan Malhotra',
|
||||||
|
clientCif: 'CIF229482',
|
||||||
|
alertType: 'Structuring (Smurfing)',
|
||||||
|
amount: 980000,
|
||||||
|
status: 'Investigating',
|
||||||
|
description:
|
||||||
|
'Multiple cash deposits of INR 2.45L, 2.45L, 2.45L, 2.45L within 10 days, totaling INR 9.8L cash (deliberately staying below the 10L single cash transaction CTR threshold).',
|
||||||
|
date: new Date(2026, 6, 24)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
clientName: 'Nexora Real Estate Ltd',
|
||||||
|
clientCif: 'CIF500481',
|
||||||
|
alertType: 'Turnover Outlier',
|
||||||
|
amount: 15000000,
|
||||||
|
status: 'Open',
|
||||||
|
description:
|
||||||
|
'Entity account registered an outward wire of INR 1.5Cr when maximum monthly turnover limit is set to INR 50L.',
|
||||||
|
date: new Date(2026, 6, 20)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 104,
|
||||||
|
clientName: 'Priyanka Patel',
|
||||||
|
clientCif: 'CIF774920',
|
||||||
|
alertType: 'Dormant Account Reactivation',
|
||||||
|
amount: 600000,
|
||||||
|
status: 'Reported STR',
|
||||||
|
description:
|
||||||
|
'Account inactive for 18 months suddenly received a cash deposit of INR 6L, followed by immediate online transfers.',
|
||||||
|
date: new Date(2026, 6, 15)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
get filteredCtrReports() {
|
||||||
|
return this.ctrReports.filter(
|
||||||
|
(r) =>
|
||||||
|
r.name.toLowerCase().includes(this.ctrFilter.toLowerCase()) ||
|
||||||
|
r.month.toLowerCase().includes(this.ctrFilter.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get filteredStrAlerts() {
|
||||||
|
return this.strAlerts.filter(
|
||||||
|
(a) =>
|
||||||
|
a.clientName.toLowerCase().includes(this.strFilter.toLowerCase()) ||
|
||||||
|
a.clientCif.toLowerCase().includes(this.strFilter.toLowerCase()) ||
|
||||||
|
a.alertType.toLowerCase().includes(this.strFilter.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
generateCtrReport() {
|
||||||
|
if (this.isGeneratingCtr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isGeneratingCtr = true;
|
||||||
|
|
||||||
|
// Simulate API call to Spring Boot batch job
|
||||||
|
setTimeout(() => {
|
||||||
|
const date = new Date();
|
||||||
|
const newReport: CtrReport = {
|
||||||
|
id: this.ctrReports.length + 1,
|
||||||
|
name: `CTR_REPORT_JULY_2026_DRAFT.xml`,
|
||||||
|
month: 'July 2026 (Draft)',
|
||||||
|
recordCount: 8,
|
||||||
|
generatedBy: 'Current_Admin',
|
||||||
|
generatedAt: date,
|
||||||
|
status: 'Ready'
|
||||||
|
};
|
||||||
|
this.ctrReports.unshift(newReport);
|
||||||
|
this.isGeneratingCtr = false;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadCtrFile(reportName: string) {
|
||||||
|
const dummyXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<FIU-IND-CTR-Report xmlns="http://fiuindia.gov.in/ctr/v1">
|
||||||
|
<Header>
|
||||||
|
<ReportType>CTR</ReportType>
|
||||||
|
<ReportingEntityName>CBS Nexora Bank</ReportingEntityName>
|
||||||
|
<ReportingPeriod>2026-06</ReportingPeriod>
|
||||||
|
</Header>
|
||||||
|
<ReportData>
|
||||||
|
<Record>
|
||||||
|
<TransactionID>TXN88924824</TransactionID>
|
||||||
|
<Date>2026-06-15</Date>
|
||||||
|
<Mode>CASH</Mode>
|
||||||
|
<Amount>1250000.00</Amount>
|
||||||
|
<AccountType>SAVINGS</AccountType>
|
||||||
|
<CustomerName>Aarav Sharma</CustomerName>
|
||||||
|
<CifNumber>CIF982348</CifNumber>
|
||||||
|
</Record>
|
||||||
|
</ReportData>
|
||||||
|
</FIU-IND-CTR-Report>`;
|
||||||
|
|
||||||
|
const blob = new Blob([dummyXml], { type: 'application/xml' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = reportName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeAlertStatus(alertId: number, newStatus: 'Open' | 'Investigating' | 'Reported STR' | 'Closed') {
|
||||||
|
const alert = this.strAlerts.find((a) => a.id === alertId);
|
||||||
|
if (alert) {
|
||||||
|
alert.status = newStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadStrReportTemplate() {
|
||||||
|
const dummyCsv =
|
||||||
|
`AlertID,ClientName,CIF,AlertType,Amount,Status,Description\n` +
|
||||||
|
this.strAlerts
|
||||||
|
.map(
|
||||||
|
(a) =>
|
||||||
|
`"${a.id}","${a.clientName}","${a.clientCif}","${a.alertType}","${a.amount}","${a.status}","${a.description.replace(/"/g, '""')}"`
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([dummyCsv], { type: 'text/csv' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `STR_AML_ALERTS_EXPORT.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260728',
|
'version': '260729',
|
||||||
'hash': '2e4187c3'
|
'hash': 'c1fc95d9'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user