feat(digital+toolbar): NEFT/RTGS module, AI menu, polished header icons
- Add /digital/neft-rtgs (entry) and /digital/neft-rtgs-report (analytics): KPIs, tabbed views (Trends, Bank-wise, Date-wise, Pending/Pass/Return, Branch/Mobile, User-wise), filterable transaction table with CSV export, and a non-dismissible Initiate dialog launched from a floating action. - Make BANL verification mandatory before NEFT/RTGS submit; auto-fill and lock beneficiary name from the lookup response, invalidate match on A/C or IFSC edit, demo fallback for unknown accounts. - Add Digital menu links (NEFT/RTGS + Report) in shell top-nav. - Replace toolbar theme toggle with AI / Intelligence Modules mega menu: 4 colour-coded columns covering Lending & Risk, Investment & Profit, Compliance & Advisory, Governance (9 modules total). Wire RBI Notifications Monitoring to /rbi-circulars. - Unify notifications, settings, AI, and profile toolbar buttons into a 26px ring style; colourful gradient bell SVG for notifications, custom gear+petals AI mark, online dot on the avatar. - Trim ISO 27001 chip in the top bar. - Add MPL header to generated .env.ts and its template in version.js. - Bundles other pending working-tree changes (clients family/nominee tab tweaks, auth/2FA fixes, translations) into the same push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
6714897688
commit
595054b0b8
@@ -46,7 +46,11 @@
|
|||||||
"Bash(node -e \"console.log\\(require\\(''./src/assets/translations/en-US.json''\\).labels?.heading?.[''System'']\\)\")",
|
"Bash(node -e \"console.log\\(require\\(''./src/assets/translations/en-US.json''\\).labels?.heading?.[''System'']\\)\")",
|
||||||
"Bash(npm list:*)",
|
"Bash(npm list:*)",
|
||||||
"Bash(find c:UsersadityMusicCBS -type d -name *fineract*)",
|
"Bash(find c:UsersadityMusicCBS -type d -name *fineract*)",
|
||||||
"Bash(xargs -I {} basename {})"
|
"Bash(xargs -I {} basename {})",
|
||||||
|
"Bash(findstr /n \"faExchange faBolt faClock faPaperPlane faFlask faSpinner faUserFriends faCommentDots faListAlt faInfoCircle faSearch faTimes faUndo faCheckCircle faExclamationTriangle\" \"c:\\\\Users\\\\adity\\\\Music\\\\CBS\\\\UI\\\\web-app\\\\src\\\\app\\\\shared\\\\icons.module.ts\")",
|
||||||
|
"Bash(findstr /i \"error neft-rtgs digital\")",
|
||||||
|
"Bash(findstr /I /C:\"error\" /C:\"neft-rtgs\" /C:\"digital\")",
|
||||||
|
"Bash(findstr /V \"warning\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { DeafModule } from './deaf/deaf.module';
|
|||||||
import { DicgcModule } from './dicgc/dicgc.module';
|
import { DicgcModule } from './dicgc/dicgc.module';
|
||||||
import { FiuModule } from './fiu/fiu.module';
|
import { FiuModule } from './fiu/fiu.module';
|
||||||
import { SftModule } from './sft/sft.module';
|
import { SftModule } from './sft/sft.module';
|
||||||
|
import { DigitalModule } from './digital/digital.module';
|
||||||
import { PortalModule } from '@angular/cdk/portal';
|
import { PortalModule } from '@angular/cdk/portal';
|
||||||
|
|
||||||
/** Main Routing Module */
|
/** Main Routing Module */
|
||||||
@@ -158,6 +159,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||||||
DicgcModule,
|
DicgcModule,
|
||||||
FiuModule,
|
FiuModule,
|
||||||
SftModule,
|
SftModule,
|
||||||
|
DigitalModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
NotFoundComponent,
|
NotFoundComponent,
|
||||||
CallbackComponent,
|
CallbackComponent,
|
||||||
|
|||||||
+25
-3
@@ -169,13 +169,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@if (linkedSelectError) {
|
||||||
|
<div class="fm-linked-error">
|
||||||
|
<mat-icon>error_outline</mat-icon>
|
||||||
|
<span>{{ linkedSelectError }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@if (!searching && searchResults.length > 0) {
|
@if (!searching && searchResults.length > 0) {
|
||||||
<div class="fm-search-results">
|
<div class="fm-search-results">
|
||||||
@for (r of searchResults; track r.id) {
|
@for (r of searchResults; track r.id) {
|
||||||
<div class="fm-search-item" (click)="selectClient(r)">
|
<div
|
||||||
|
class="fm-search-item"
|
||||||
|
[class.is-disabled]="isClientAlreadyLinked(r.id)"
|
||||||
|
(click)="selectClient(r)"
|
||||||
|
>
|
||||||
<mat-icon class="fm-search-item__icon">person</mat-icon>
|
<mat-icon class="fm-search-item__icon">person</mat-icon>
|
||||||
<div class="fm-search-item__info">
|
<div class="fm-search-item__info">
|
||||||
<span class="fm-search-item__name">{{ r.displayName }}</span>
|
<span class="fm-search-item__name">
|
||||||
|
{{ r.displayName }}
|
||||||
|
@if (isClientAlreadyLinked(r.id)) {
|
||||||
|
<span class="fm-search-item__taken">· already linked</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
<span class="fm-search-item__meta"
|
<span class="fm-search-item__meta"
|
||||||
>ID: {{ r.id }} · {{ r.officeName }}
|
>ID: {{ r.id }} · {{ r.officeName }}
|
||||||
@if (r.dateOfBirth) {
|
@if (r.dateOfBirth) {
|
||||||
@@ -306,7 +321,14 @@
|
|||||||
</mat-option>
|
</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-hint>Father, Mother and Spouse can only be added once.</mat-hint>
|
<mat-icon
|
||||||
|
matSuffix
|
||||||
|
class="fm-rel-hint-icon"
|
||||||
|
matTooltip="Father, Mother and Spouse can only be added once."
|
||||||
|
matTooltipPosition="left"
|
||||||
|
>
|
||||||
|
info_outline
|
||||||
|
</mat-icon>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@if (entryMode === 'manual') {
|
@if (entryMode === 'manual') {
|
||||||
|
|||||||
+54
@@ -36,6 +36,60 @@
|
|||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Small info icon in the Relationship field's suffix — hover reveals the
|
||||||
|
"only once" hint without consuming layout space below the field. */
|
||||||
|
.fm-rel-hint-icon {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: help;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1074b9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline error shown above the linked-client search when a duplicate is picked. */
|
||||||
|
.fm-linked-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 12.5px;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Greyed-out search result for clients already linked as family members. */
|
||||||
|
.fm-search-item.is-disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fm-search-item__taken {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.fm-mode-btn {
|
.fm-mode-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+36
-1
@@ -24,6 +24,7 @@ import { ClientsService } from 'app/clients/clients.service';
|
|||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
import { MatCheckbox } from '@angular/material/checkbox';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||||
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
|
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
|
||||||
@@ -41,6 +42,7 @@ import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
|
|||||||
MatDialogClose,
|
MatDialogClose,
|
||||||
MatIcon,
|
MatIcon,
|
||||||
MatProgressSpinner,
|
MatProgressSpinner,
|
||||||
|
MatTooltip,
|
||||||
FaIconComponent,
|
FaIconComponent,
|
||||||
DateFormatPipe
|
DateFormatPipe
|
||||||
]
|
]
|
||||||
@@ -245,6 +247,7 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
|||||||
/** Search existing clients */
|
/** Search existing clients */
|
||||||
onSearch(text: string): void {
|
onSearch(text: string): void {
|
||||||
this.searchText = text;
|
this.searchText = text;
|
||||||
|
this.linkedSelectError = '';
|
||||||
clearTimeout(this.searchTimer);
|
clearTimeout(this.searchTimer);
|
||||||
if (!text || text.length < 2) {
|
if (!text || text.length < 2) {
|
||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
@@ -252,7 +255,9 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.searching = true;
|
this.searching = true;
|
||||||
this.searchTimer = setTimeout(() => {
|
this.searchTimer = setTimeout(() => {
|
||||||
this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({
|
// Family-member link picker: only Individual clients can be linked as a relative —
|
||||||
|
// non-individuals (companies / trusts) have no person-to-person relationship to model here.
|
||||||
|
this.clientsService.searchByText(text, 0, 8, '', '', true, 'INDIVIDUAL').subscribe({
|
||||||
next: (data: any) => {
|
next: (data: any) => {
|
||||||
this.searchResults = data?.content || [];
|
this.searchResults = data?.content || [];
|
||||||
this.searching = false;
|
this.searching = false;
|
||||||
@@ -266,8 +271,38 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
|||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline error shown above the linked-client search box. Currently used
|
||||||
|
* to surface "this client is already linked" when the user tries to
|
||||||
|
* select a duplicate.
|
||||||
|
*/
|
||||||
|
linkedSelectError = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the given client id is already linked as a family member of
|
||||||
|
* this client. The member currently being edited is excluded so its own
|
||||||
|
* linked client stays selectable.
|
||||||
|
*/
|
||||||
|
isClientAlreadyLinked(clientId: number | string): boolean {
|
||||||
|
if (clientId == null) return false;
|
||||||
|
const existing = this.data?.existingMembers || [];
|
||||||
|
if (!existing.length) return false;
|
||||||
|
const editingId = this.data?.context === 'Edit' ? this.data?.member?.id : undefined;
|
||||||
|
const target = String(clientId);
|
||||||
|
return existing.some((m: any) => {
|
||||||
|
if (editingId && m.id === editingId) return false;
|
||||||
|
const linked = m.linkedClientId ?? m._linkedClient?.id;
|
||||||
|
return linked != null && String(linked) === target;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Select a client from search results and fetch full details */
|
/** Select a client from search results and fetch full details */
|
||||||
selectClient(client: any): void {
|
selectClient(client: any): void {
|
||||||
|
if (this.isClientAlreadyLinked(client?.id)) {
|
||||||
|
this.linkedSelectError = `${client?.displayName || 'This client'} is already linked as a family member.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.linkedSelectError = '';
|
||||||
this.selectedClient = client;
|
this.selectedClient = client;
|
||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
this.searchText = '';
|
this.searchText = '';
|
||||||
|
|||||||
+5
-2
@@ -255,7 +255,8 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.nomineeSearching = true;
|
this.nomineeSearching = true;
|
||||||
this.nomineeSearchTimer = setTimeout(() => {
|
this.nomineeSearchTimer = setTimeout(() => {
|
||||||
this.clientsService.searchByText(text, 0, 10, '', '', true).subscribe({
|
// Nominee picker: a nominee must be a natural person, never a corporate entity.
|
||||||
|
this.clientsService.searchByText(text, 0, 10, '', '', true, 'INDIVIDUAL').subscribe({
|
||||||
next: (data: any) => {
|
next: (data: any) => {
|
||||||
this.nomineeSearchResults = data?.content || [];
|
this.nomineeSearchResults = data?.content || [];
|
||||||
this.nomineeSearching = false;
|
this.nomineeSearching = false;
|
||||||
@@ -351,7 +352,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.guardianSearching = true;
|
this.guardianSearching = true;
|
||||||
this.guardianSearchTimer = setTimeout(() => {
|
this.guardianSearchTimer = setTimeout(() => {
|
||||||
this.clientsService.searchByText(text, 0, 10, '', '', true).subscribe({
|
// Guardian picker: a guardian is by definition a natural person responsible for a minor /
|
||||||
|
// dependent — non-individuals can never play that role.
|
||||||
|
this.clientsService.searchByText(text, 0, 10, '', '', true, 'INDIVIDUAL').subscribe({
|
||||||
next: (data: any) => {
|
next: (data: any) => {
|
||||||
this.guardianSearchResults = data?.content || [];
|
this.guardianSearchResults = data?.content || [];
|
||||||
this.guardianSearching = false;
|
this.guardianSearching = false;
|
||||||
|
|||||||
@@ -987,7 +987,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
|
|
||||||
this.guardianSearching = true;
|
this.guardianSearching = true;
|
||||||
this.guardianSearchTimer = setTimeout(() => {
|
this.guardianSearchTimer = setTimeout(() => {
|
||||||
this.clientService.searchByText(text, 0, 10, '', '', true).subscribe({
|
// Guardian picker: only natural persons can be a guardian.
|
||||||
|
this.clientService.searchByText(text, 0, 10, '', '', true, 'INDIVIDUAL').subscribe({
|
||||||
next: (data: any) => {
|
next: (data: any) => {
|
||||||
this.guardianSearchResults = data?.content || [];
|
this.guardianSearchResults = data?.content || [];
|
||||||
this.guardianSearching = false;
|
this.guardianSearching = false;
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'edit',
|
path: 'edit',
|
||||||
component: EditFamilyMemberComponent,
|
component: EditFamilyMemberComponent,
|
||||||
data: { title: 'Nominee View', routeParamBreadcrumb: 'familyMemberId' },
|
data: { title: 'Family Member View', routeParamBreadcrumb: 'familyMemberId' },
|
||||||
resolve: {
|
resolve: {
|
||||||
clientTemplate: ClientTemplateResolver,
|
clientTemplate: ClientTemplateResolver,
|
||||||
editFamilyMember: ClientFamilyMemberResolver
|
editFamilyMember: ClientFamilyMemberResolver
|
||||||
|
|||||||
+8
-1
@@ -51,7 +51,14 @@ export class AddFamilyMemberComponent implements OnInit {
|
|||||||
this.route.data.subscribe((data: { clientTemplate: any }) => {
|
this.route.data.subscribe((data: { clientTemplate: any }) => {
|
||||||
this.addFamilyMemberTemplate = data.clientTemplate.familyMemberOptions;
|
this.addFamilyMemberTemplate = data.clientTemplate.familyMemberOptions;
|
||||||
});
|
});
|
||||||
this.clientId = this.route.parent.parent.snapshot.params['clientId'];
|
// Walk up the route tree until we find a clientId param. Hard-coding parent.parent breaks
|
||||||
|
// when Angular flattens componentless intermediate routes (family-members has no component),
|
||||||
|
// which makes the depth unstable — and produces /clients/undefined/familymembers URLs.
|
||||||
|
let r: ActivatedRoute | null = this.route;
|
||||||
|
while (r && !r.snapshot.params['clientId']) {
|
||||||
|
r = r.parent;
|
||||||
|
}
|
||||||
|
this.clientId = r?.snapshot.params['clientId'];
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
/** Custom Components */
|
/** Custom Components */
|
||||||
import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component';
|
import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component';
|
||||||
import { NomineeDialogComponent } from './nominee-dialog/nominee-dialog.component';
|
import { NomineeDialogComponent } from './nominee-dialog/nominee-dialog.component';
|
||||||
|
import { ClientFamilyMemberDialogComponent } from '../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component';
|
||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { ClientsService } from '../../clients.service';
|
import { ClientsService } from '../../clients.service';
|
||||||
@@ -75,7 +76,10 @@ export class FamilyMembersTabComponent {
|
|||||||
this.route.data.subscribe((data: { clientFamilyMembers: any }) => {
|
this.route.data.subscribe((data: { clientFamilyMembers: any }) => {
|
||||||
this.clientFamilyMembers = data.clientFamilyMembers || [];
|
this.clientFamilyMembers = data.clientFamilyMembers || [];
|
||||||
});
|
});
|
||||||
this.clientId = this.route.parent.snapshot.params['clientId'];
|
// Route hierarchy is :clientId → family-members (wrapper) → this component,
|
||||||
|
// so the clientId param lives two levels up, not one.
|
||||||
|
this.clientId =
|
||||||
|
this.route.parent?.parent?.snapshot.params['clientId'] || this.route.parent?.snapshot.params['clientId'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Family members (non-nominee entries) */
|
/** Family members (non-nominee entries) */
|
||||||
@@ -101,49 +105,75 @@ export class FamilyMembersTabComponent {
|
|||||||
|
|
||||||
// ═══ Family Member Actions ═══
|
// ═══ Family Member Actions ═══
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip UI-only helper fields (`_linkedClient`, `_marriageCertFile`, `id`)
|
||||||
|
* from the dialog payload so the backend receives a clean DTO. Mirrors the
|
||||||
|
* pattern used in client-family-members-step.editFamilyMember.
|
||||||
|
*/
|
||||||
|
private toApiPayload(formValue: any): any {
|
||||||
|
const { id, _linkedClient, _marriageCertFile, ...payload } = formValue || {};
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
addFamilyMember() {
|
addFamilyMember() {
|
||||||
this.ensureTemplateLoaded().then(() => {
|
this.ensureTemplateLoaded().then(() => {
|
||||||
const dialogRef = this.dialog.open(NomineeDialogComponent, {
|
const dialogRef = this.dialog.open(ClientFamilyMemberDialogComponent, {
|
||||||
width: '700px',
|
width: '640px',
|
||||||
|
maxWidth: '95vw',
|
||||||
|
panelClass: 'fm-dialog-panel',
|
||||||
data: {
|
data: {
|
||||||
title: 'Add Family Member',
|
context: 'Add',
|
||||||
familyMemberOptions: this.familyMemberOptions,
|
options: this.familyMemberOptions,
|
||||||
isFamilyMember: true
|
existingMembers: this.familyMembers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe((result: any) => {
|
dialogRef.afterClosed().subscribe((response: any) => {
|
||||||
if (result) {
|
if (!response?.member) return;
|
||||||
this.clientsService.addFamilyMember(this.clientId, result).subscribe((res: any) => {
|
const payload = this.toApiPayload(response.member);
|
||||||
this.clientsService.getClientFamilyMember(this.clientId, res.resourceId).subscribe((member: any) => {
|
this.clientsService.addFamilyMember(this.clientId, payload).subscribe({
|
||||||
this.clientFamilyMembers.push(member);
|
next: () => this.refreshFamilyMembers(),
|
||||||
|
error: (err) => console.error('[FamilyMembers] add failed', err)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
editFamilyMember(member: any, index: number) {
|
editFamilyMember(member: any, _index: number) {
|
||||||
const realIndex = this.clientFamilyMembers.indexOf(member);
|
|
||||||
this.ensureTemplateLoaded().then(() => {
|
this.ensureTemplateLoaded().then(() => {
|
||||||
const dialogRef = this.dialog.open(NomineeDialogComponent, {
|
const dialogRef = this.dialog.open(ClientFamilyMemberDialogComponent, {
|
||||||
width: '700px',
|
width: '640px',
|
||||||
|
maxWidth: '95vw',
|
||||||
|
panelClass: 'fm-dialog-panel',
|
||||||
data: {
|
data: {
|
||||||
title: 'Edit Family Member',
|
context: 'Edit',
|
||||||
familyMemberOptions: this.familyMemberOptions,
|
options: this.familyMemberOptions,
|
||||||
member,
|
member,
|
||||||
isFamilyMember: true
|
existingMembers: this.familyMembers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe((result: any) => {
|
dialogRef.afterClosed().subscribe((response: any) => {
|
||||||
if (result) {
|
if (!response?.member) return;
|
||||||
this.clientsService.editFamilyMember(member.clientId, member.id, result).subscribe(() => {
|
const payload = this.toApiPayload(response.member);
|
||||||
this.clientsService.getClientFamilyMember(this.clientId, member.id).subscribe((updated: any) => {
|
this.clientsService.editFamilyMember(member.clientId || this.clientId, member.id, payload).subscribe({
|
||||||
this.clientFamilyMembers[realIndex] = updated;
|
next: () => this.refreshFamilyMembers(),
|
||||||
|
error: (err) => console.error('[FamilyMembers] edit failed', err)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* Re-pull the full family-members list from the backend after any
|
||||||
|
* mutation. More resilient than fetching a single resource by id —
|
||||||
|
* sidesteps cases where the POST response doesn't carry the new id in
|
||||||
|
* the expected `resourceId` field.
|
||||||
|
*/
|
||||||
|
private refreshFamilyMembers(): void {
|
||||||
|
this.clientsService.getClientFamilyMembers(this.clientId).subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
this.clientFamilyMembers = Array.isArray(data) ? data : [];
|
||||||
|
},
|
||||||
|
error: (err) => console.error('[FamilyMembers] refresh failed', err)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -285,7 +285,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (selectedClient) {
|
@if (selectedClient) {
|
||||||
<!-- Linked Card -->
|
<!-- Linked Client Card -->
|
||||||
<div class="nom-linked-card">
|
<div class="nom-linked-card">
|
||||||
<div class="nom-linked-card__left">
|
<div class="nom-linked-card__left">
|
||||||
@if (linkedProfileImage) {
|
@if (linkedProfileImage) {
|
||||||
@@ -295,15 +295,24 @@
|
|||||||
(click)="zoomImage(linkedProfileImage, 'Photo')"
|
(click)="zoomImage(linkedProfileImage, 'Photo')"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<mat-icon class="nom-linked-card__icon">person</mat-icon>
|
<div class="nom-linked-card__photo nom-linked-card__photo--placeholder">
|
||||||
|
<mat-icon>person</mat-icon>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="nom-linked-card__info">
|
<div class="nom-linked-card__info">
|
||||||
<span class="nom-linked-card__name">{{ selectedClient.displayName }}</span>
|
<span class="nom-linked-card__name">{{ selectedClient.displayName }}</span>
|
||||||
<span class="nom-linked-card__meta"
|
<span class="nom-linked-card__meta">
|
||||||
>ID: {{ selectedClient.id }}
|
<span class="nom-linked-card__meta-item"><mat-icon>badge</mat-icon>ID {{ selectedClient.id }}</span>
|
||||||
|
@if (selectedClient.dateOfBirth) {
|
||||||
|
<span class="nom-linked-card__meta-item"
|
||||||
|
><mat-icon>cake</mat-icon>{{ selectedClient.dateOfBirth | dateFormat }}</span
|
||||||
|
>
|
||||||
|
}
|
||||||
@if (selectedClient.mobileNo) {
|
@if (selectedClient.mobileNo) {
|
||||||
· {{ selectedClient.mobileNo }}
|
<span class="nom-linked-card__meta-item">
|
||||||
|
<mat-icon>phone</mat-icon>{{ selectedClient.mobileNo }}
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -315,9 +324,18 @@
|
|||||||
class="zoomable"
|
class="zoomable"
|
||||||
(click)="zoomImage(linkedSignatureImage, 'Signature')"
|
(click)="zoomImage(linkedSignatureImage, 'Signature')"
|
||||||
/>
|
/>
|
||||||
|
<span class="nom-linked-card__sig-label">Signature</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<button mat-icon-button type="button" (click)="removeSelectedClient()"><mat-icon>close</mat-icon></button>
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
class="nom-linked-card__close"
|
||||||
|
matTooltip="Remove linked client"
|
||||||
|
(click)="removeSelectedClient()"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (fetchingDetails) {
|
@if (fetchingDetails) {
|
||||||
@@ -327,34 +345,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (!fetchingDetails) {
|
@if (!fetchingDetails) {
|
||||||
<!-- KYC details -->
|
<!-- KYC chips -->
|
||||||
@if (selectedClient.dateOfBirth || selectedClient.panNumber || selectedClient.aadhaarMasked) {
|
@if (selectedClient.panNumber || selectedClient.aadhaarMasked) {
|
||||||
<div class="nom-readonly-row">
|
<div class="nom-readonly-row">
|
||||||
@if (selectedClient.dateOfBirth) {
|
|
||||||
<span class="nom-ro-item"
|
|
||||||
><span class="nom-ro-label">DOB</span> {{ selectedClient.dateOfBirth | dateFormat }}</span
|
|
||||||
>
|
|
||||||
}
|
|
||||||
@if (selectedClient.panNumber) {
|
@if (selectedClient.panNumber) {
|
||||||
<span class="nom-ro-item"
|
<span class="nom-ro-item">
|
||||||
><span class="nom-ro-label">PAN</span> {{ selectedClient.panNumber }}
|
<span class="nom-ro-label">PAN</span>
|
||||||
|
<span class="nom-ro-value">{{ selectedClient.panNumber }}</span>
|
||||||
@if (linkedKycStatus?.panVerified) {
|
@if (linkedKycStatus?.panVerified) {
|
||||||
<mat-icon class="nom-ro-ok">check_circle</mat-icon>
|
<mat-icon class="nom-ro-ok" matTooltip="PAN verified">check_circle</mat-icon>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@if (selectedClient.aadhaarMasked) {
|
@if (selectedClient.aadhaarMasked) {
|
||||||
<span class="nom-ro-item"
|
<span class="nom-ro-item">
|
||||||
><span class="nom-ro-label">Aadhaar</span> {{ selectedClient.aadhaarMasked }}
|
<span class="nom-ro-label">Aadhaar</span>
|
||||||
|
<span class="nom-ro-value">{{ selectedClient.aadhaarMasked }}</span>
|
||||||
@if (linkedKycStatus?.aadhaarVerified) {
|
@if (linkedKycStatus?.aadhaarVerified) {
|
||||||
<mat-icon class="nom-ro-ok">check_circle</mat-icon>
|
<mat-icon class="nom-ro-ok" matTooltip="Aadhaar verified">check_circle</mat-icon>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Nominee fields inline -->
|
<!-- Section heading + Nominee fields -->
|
||||||
|
<div class="nom-section-heading">
|
||||||
|
<mat-icon>edit_note</mat-icon>
|
||||||
|
<span>Nomination Details</span>
|
||||||
|
</div>
|
||||||
<div class="nom-fields-row">
|
<div class="nom-fields-row">
|
||||||
<mat-form-field appearance="outline" class="nom-field-item">
|
<mat-form-field appearance="outline" class="nom-field-item">
|
||||||
<mat-label>Nomination Type</mat-label>
|
<mat-label>Nomination Type</mat-label>
|
||||||
|
|||||||
@@ -520,12 +520,13 @@ $primary-light: #e8f0fe;
|
|||||||
.nom-linked-card {
|
.nom-linked-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 14px;
|
||||||
padding: 8px 12px;
|
padding: 12px 14px;
|
||||||
background: #f8fafc;
|
background: linear-gradient(135deg, #f0f7ff 0%, #fff 60%);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #c7dbf2;
|
||||||
border-radius: 6px;
|
border-radius: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||||
|
|
||||||
&__left {
|
&__left {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -536,44 +537,127 @@ $primary-light: #e8f0fe;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__photo {
|
&__photo {
|
||||||
width: 36px;
|
width: 48px;
|
||||||
height: 36px;
|
height: 48px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border: 2px solid #e2e8f0;
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 0 1px #c7dbf2;
|
||||||
|
|
||||||
|
&--placeholder {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #4f46e5;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #1e293b;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__meta {
|
&__meta {
|
||||||
font-size: 11px;
|
display: flex;
|
||||||
color: #64748b;
|
flex-wrap: wrap;
|
||||||
font-family: 'Roboto Mono', monospace;
|
gap: 4px 12px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 13px;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__sig {
|
&__sig {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
padding: 4px 6px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 24px;
|
height: 28px;
|
||||||
max-width: 60px;
|
max-width: 72px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border: 1px solid #e2e8f0;
|
cursor: pointer;
|
||||||
border-radius: 3px;
|
|
||||||
background: #fff;
|
|
||||||
padding: 1px 3px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__sig-label {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
color: #64748b;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section heading between linked card and nominee fields */
|
||||||
|
.nom-section-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 6px 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nom-fetching-hint {
|
.nom-fetching-hint {
|
||||||
@@ -585,32 +669,38 @@ $primary-light: #e8f0fe;
|
|||||||
margin: -4px 0 8px;
|
margin: -4px 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Read-only KYC row */
|
/* Read-only KYC chips */
|
||||||
.nom-readonly-row {
|
.nom-readonly-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
padding: 8px 12px;
|
margin-bottom: 12px;
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nom-ro-item {
|
.nom-ro-item {
|
||||||
font-size: 12px;
|
|
||||||
color: #1e293b;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1e293b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nom-ro-label {
|
.nom-ro-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #94a3b8;
|
color: #64748b;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-right: 2px;
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nom-ro-value {
|
||||||
|
font-family: 'SF Mono', 'Roboto Mono', Consolas, monospace;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nom-ro-ok {
|
.nom-ro-ok {
|
||||||
@@ -654,25 +744,34 @@ $primary-light: #e8f0fe;
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Guardian */
|
/* Guardian — amber sub-card to denote the minor-dependency */
|
||||||
.nom-guardian-section {
|
.nom-guardian-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 8px;
|
padding: 10px 12px;
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nom-guardian-label {
|
.nom-guardian-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 5px;
|
||||||
font-size: 12px;
|
font-size: 11.5px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
color: #92400e;
|
color: #92400e;
|
||||||
|
|
||||||
mat-icon {
|
mat-icon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #d97706;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,8 @@ export class NomineeTabComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.searching = true;
|
this.searching = true;
|
||||||
this.searchTimer = setTimeout(() => {
|
this.searchTimer = setTimeout(() => {
|
||||||
this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({
|
// Nominee picker (post-create tab): nominees must be natural persons.
|
||||||
|
this.clientsService.searchByText(text, 0, 8, '', '', true, 'INDIVIDUAL').subscribe({
|
||||||
next: (data: any) => {
|
next: (data: any) => {
|
||||||
this.searchResults = data?.content || [];
|
this.searchResults = data?.content || [];
|
||||||
this.searching = false;
|
this.searching = false;
|
||||||
@@ -412,7 +413,8 @@ export class NomineeTabComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.guardianSearching = true;
|
this.guardianSearching = true;
|
||||||
this.guardianSearchTimer = setTimeout(() => {
|
this.guardianSearchTimer = setTimeout(() => {
|
||||||
this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({
|
// Guardian picker (post-create tab): guardians must be natural persons.
|
||||||
|
this.clientsService.searchByText(text, 0, 8, '', '', true, 'INDIVIDUAL').subscribe({
|
||||||
next: (data: any) => {
|
next: (data: any) => {
|
||||||
this.guardianSearchResults = data?.content || [];
|
this.guardianSearchResults = data?.content || [];
|
||||||
this.guardianSearching = false;
|
this.guardianSearching = false;
|
||||||
|
|||||||
@@ -582,12 +582,20 @@ export class ClientsService {
|
|||||||
pageSize: number,
|
pageSize: number,
|
||||||
sortAttribute: string = '',
|
sortAttribute: string = '',
|
||||||
sortDirection: string = '',
|
sortDirection: string = '',
|
||||||
activeOnly: boolean = false
|
activeOnly: boolean = false,
|
||||||
|
legalFormType: string = ''
|
||||||
) {
|
) {
|
||||||
const innerRequest: any = { text };
|
const innerRequest: any = { text };
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
innerRequest.activeOnly = true;
|
innerRequest.activeOnly = true;
|
||||||
}
|
}
|
||||||
|
// Restrict to one legal form when caller asks for it. Used by guardian/nominee/family-member
|
||||||
|
// pickers — non-individual entities (companies, partnerships) make no sense as a "guardian"
|
||||||
|
// or "spouse", so the SPA passes 'INDIVIDUAL' there. Backend treats this as authoritative;
|
||||||
|
// omitting the param still returns all forms (backwards-compatible for the general client list).
|
||||||
|
if (legalFormType) {
|
||||||
|
innerRequest.legalFormType = legalFormType;
|
||||||
|
}
|
||||||
let request: any = {
|
let request: any = {
|
||||||
request: innerRequest,
|
request: innerRequest,
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -480,6 +480,24 @@ export class AuthenticationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalises login when a TFA access token has already been minted server-side (DSC factor).
|
||||||
|
* The DSC flow ends with /v1/login/dsc/{rn}/claim returning a ready-to-use token — calling the
|
||||||
|
* OTP-shaped /twofactor/validate path here would 4xx because there is no in-memory OTPRequest
|
||||||
|
* to exchange. We just need the post-success bookkeeping: stash the token on the interceptor,
|
||||||
|
* persist the validity window, set credentials, and fire the login alert.
|
||||||
|
*/
|
||||||
|
applyTwoFactorToken(token: string, liveSeconds: number): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const ttlMs = (liveSeconds && liveSeconds > 0 ? liveSeconds : 0) * 1000;
|
||||||
|
const response = {
|
||||||
|
token,
|
||||||
|
validFrom: now,
|
||||||
|
validTo: ttlMs > 0 ? now + ttlMs : now
|
||||||
|
};
|
||||||
|
this.onOTPValidateSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the two factor authorization token followed by one of the following:
|
* Sets the two factor authorization token followed by one of the following:
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -947,7 +947,12 @@
|
|||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
<mat-menu #digitalMenu="matMenu" [overlapTrigger]="false" class="nav-dropdown">
|
<mat-menu #digitalMenu="matMenu" [overlapTrigger]="false" class="nav-dropdown">
|
||||||
<button mat-menu-item tabindex="0"><fa-icon icon="exchange" class="menu-item-icon"></fa-icon> NEFT / RTGS</button>
|
<button mat-menu-item routerLink="/digital/neft-rtgs" tabindex="0">
|
||||||
|
<fa-icon icon="exchange" class="menu-item-icon"></fa-icon> NEFT / RTGS
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item routerLink="/digital/neft-rtgs-report" tabindex="0">
|
||||||
|
<fa-icon icon="chart-line" class="menu-item-icon"></fa-icon> NEFT / RTGS Report
|
||||||
|
</button>
|
||||||
<button mat-menu-item tabindex="0"><fa-icon icon="bolt" class="menu-item-icon"></fa-icon> IMPS / BANL</button>
|
<button mat-menu-item tabindex="0"><fa-icon icon="bolt" class="menu-item-icon"></fa-icon> IMPS / BANL</button>
|
||||||
<button mat-menu-item tabindex="0"><fa-icon icon="mobile-alt" class="menu-item-icon"></fa-icon> UPI</button>
|
<button mat-menu-item tabindex="0"><fa-icon icon="mobile-alt" class="menu-item-icon"></fa-icon> UPI</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<i class="fas fa-lock t-chip-icon t-chip-icon--green"></i>SSL/TLS <strong>256-bit</strong>
|
<i class="fas fa-lock t-chip-icon t-chip-icon--green"></i>SSL/TLS <strong>256-bit</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="t-chip t-chip--secure">
|
<div class="t-chip t-chip--secure">
|
||||||
<i class="fas fa-certificate t-chip-icon t-chip-icon--blue"></i>ISO 27001 <strong>Certified</strong>
|
<i class="fas fa-certificate t-chip-icon t-chip-icon--blue"></i>ISO <strong>27001</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="t-chip t-chip--timer">
|
<div class="t-chip t-chip--timer">
|
||||||
<i class="fas fa-clock t-chip-icon t-chip-icon--amber"></i>Session <strong>{{ sessionTimer }}</strong>
|
<i class="fas fa-clock t-chip-icon t-chip-icon--amber"></i>Session <strong>{{ sessionTimer }}</strong>
|
||||||
@@ -45,26 +45,206 @@
|
|||||||
<mifosx-notifications-tray #notificationsTray class="hide-lt-md" tabindex="0"></mifosx-notifications-tray>
|
<mifosx-notifications-tray #notificationsTray class="hide-lt-md" tabindex="0"></mifosx-notifications-tray>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="t-action-btn" [routerLink]="['/settings']" matTooltip="{{ 'tooltips.Settings' | translate }}">
|
<div
|
||||||
|
class="t-action-btn"
|
||||||
|
[routerLink]="['/settings']"
|
||||||
|
matTooltip="{{ 'tooltips.Settings' | translate }}"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
<fa-icon icon="cog"></fa-icon>
|
<fa-icon icon="cog"></fa-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div #themeToggle class="t-action-btn">
|
<div
|
||||||
<mifosx-theme-toggle tabindex="0"></mifosx-theme-toggle>
|
class="t-action-btn t-action-btn--ai"
|
||||||
|
[matMenuTriggerFor]="aiMenu"
|
||||||
|
matTooltip="AI / Intelligence Modules"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<!-- Custom AI mark: teal petals + blue cog + AI label -->
|
||||||
|
<svg class="t-ai-svg" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<!-- Petals (4 ellipses rotated to form a 4-leaf flower) -->
|
||||||
|
<g class="t-ai-petals" fill="#5eccba" fill-opacity="0.92">
|
||||||
|
<ellipse cx="32" cy="32" rx="22" ry="11" transform="rotate(0 32 32)" />
|
||||||
|
<ellipse cx="32" cy="32" rx="22" ry="11" transform="rotate(45 32 32)" />
|
||||||
|
<ellipse cx="32" cy="32" rx="22" ry="11" transform="rotate(90 32 32)" />
|
||||||
|
<ellipse cx="32" cy="32" rx="22" ry="11" transform="rotate(135 32 32)" />
|
||||||
|
</g>
|
||||||
|
<!-- Cog: 8-tooth gear shape via path -->
|
||||||
|
<path
|
||||||
|
class="t-ai-cog"
|
||||||
|
fill="#1e88e5"
|
||||||
|
d="M32 12.5l3.4 1.6 2.6-2.6 3 2 .2 3.7 3.6 1 1.9 3.2-2 3.1 1.9 3.2-1.9 3.2 2 3.1-1.9 3.2-3.6 1-.2 3.7-3 2-2.6-2.6-3.4 1.6-3.4-1.6-2.6 2.6-3-2-.2-3.7-3.6-1-1.9-3.2 2-3.1-1.9-3.2 1.9-3.2-2-3.1 1.9-3.2 3.6-1 .2-3.7 3-2 2.6 2.6L32 12.5z"
|
||||||
|
/>
|
||||||
|
<!-- Inner circle (AI label background) -->
|
||||||
|
<circle cx="32" cy="32" r="9" fill="#bfe2ff" />
|
||||||
|
<!-- AI text -->
|
||||||
|
<text
|
||||||
|
x="32"
|
||||||
|
y="36"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-family="'Inter', 'DM Sans', system-ui, sans-serif"
|
||||||
|
font-size="11"
|
||||||
|
font-weight="800"
|
||||||
|
fill="#0b1e3d"
|
||||||
|
letter-spacing="0.3"
|
||||||
|
>
|
||||||
|
AI
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
<span class="t-action-dot"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="t-divider"></div>
|
<div class="t-divider"></div>
|
||||||
|
|
||||||
<div #appMenu class="t-action-btn t-user-btn" [matMenuTriggerFor]="applicationMenu" matTooltip="Account">
|
<div
|
||||||
|
#appMenu
|
||||||
|
class="t-action-btn t-user-btn"
|
||||||
|
[matMenuTriggerFor]="applicationMenu"
|
||||||
|
matTooltip="Account"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src="assets/images/user_placeholder.png"
|
src="assets/images/user_placeholder.png"
|
||||||
[alt]="'labels.headings.User Profile' | translate"
|
[alt]="'labels.headings.User Profile' | translate"
|
||||||
class="t-user-img"
|
class="t-user-img"
|
||||||
/>
|
/>
|
||||||
|
<span class="t-user-status"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- AI / Intelligence Modules Menu -->
|
||||||
|
<mat-menu #aiMenu="matMenu" [overlapTrigger]="false" class="ai-menu" xPosition="before">
|
||||||
|
<div class="ai-menu-header">
|
||||||
|
<svg class="ai-menu-header-svg" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<g fill="#5eccba" fill-opacity="0.92">
|
||||||
|
<ellipse cx="32" cy="32" rx="22" ry="11" />
|
||||||
|
<ellipse cx="32" cy="32" rx="22" ry="11" transform="rotate(45 32 32)" />
|
||||||
|
<ellipse cx="32" cy="32" rx="22" ry="11" transform="rotate(90 32 32)" />
|
||||||
|
<ellipse cx="32" cy="32" rx="22" ry="11" transform="rotate(135 32 32)" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
fill="#1e88e5"
|
||||||
|
d="M32 12.5l3.4 1.6 2.6-2.6 3 2 .2 3.7 3.6 1 1.9 3.2-2 3.1 1.9 3.2-1.9 3.2 2 3.1-1.9 3.2-3.6 1-.2 3.7-3 2-2.6-2.6-3.4 1.6-3.4-1.6-2.6 2.6-3-2-.2-3.7-3.6-1-1.9-3.2 2-3.1-1.9-3.2 1.9-3.2-2-3.1 1.9-3.2 3.6-1 .2-3.7 3-2 2.6 2.6L32 12.5z"
|
||||||
|
/>
|
||||||
|
<circle cx="32" cy="32" r="9" fill="#bfe2ff" />
|
||||||
|
<text
|
||||||
|
x="32"
|
||||||
|
y="36"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-family="'Inter', 'DM Sans', system-ui, sans-serif"
|
||||||
|
font-size="11"
|
||||||
|
font-weight="800"
|
||||||
|
fill="#0b1e3d"
|
||||||
|
>
|
||||||
|
AI
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<div class="ai-menu-title">AI / Intelligence Modules</div>
|
||||||
|
<div class="ai-menu-sub">Smart banking · Powered by ML & LLMs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 4-column mega layout -->
|
||||||
|
<div class="ai-menu-cols">
|
||||||
|
<div class="ai-col ai-col--red">
|
||||||
|
<div class="ai-col-h">
|
||||||
|
<fa-icon icon="hand-holding-usd" class="ai-col-h-ic"></fa-icon>
|
||||||
|
<span>Lending & Risk</span>
|
||||||
|
</div>
|
||||||
|
<button mat-menu-item class="ai-card" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="hand-holding-usd"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">Loan Eligibility</span>
|
||||||
|
<span class="ai-card-d">Score applicants & recommend sanction limit</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item class="ai-card" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="exclamation-triangle"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">Probable NPA Prediction</span>
|
||||||
|
<span class="ai-card-d">Flag likely NPA slippages in next 90 days</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item class="ai-card" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="arrow-up"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">Income Increase Analysis</span>
|
||||||
|
<span class="ai-card-d">Detect rising-income segments to up-sell</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ai-col ai-col--teal">
|
||||||
|
<div class="ai-col-h">
|
||||||
|
<fa-icon icon="chart-pie" class="ai-col-h-ic"></fa-icon>
|
||||||
|
<span>Investment & Profit</span>
|
||||||
|
</div>
|
||||||
|
<button mat-menu-item class="ai-card" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="chart-pie"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">Profit Distribution Analysis</span>
|
||||||
|
<span class="ai-card-d">Branch / product / segment profitability split</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item class="ai-card" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="chart-bar"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">Area of Investment Analysis</span>
|
||||||
|
<span class="ai-card-d">Optimal asset / sector deployment mix</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ai-col ai-col--indigo">
|
||||||
|
<div class="ai-col-h">
|
||||||
|
<fa-icon icon="shield-alt" class="ai-col-h-ic"></fa-icon>
|
||||||
|
<span>Compliance & Advisory</span>
|
||||||
|
</div>
|
||||||
|
<button mat-menu-item class="ai-card" routerLink="/rbi-circulars" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="university"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">RBI Notifications Monitoring</span>
|
||||||
|
<span class="ai-card-d">Auto-track circulars relevant to you</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item class="ai-card" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="shield-alt"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">CERT-In Advisory Monitoring</span>
|
||||||
|
<span class="ai-card-d">Cybersecurity advisories mapped to CBS</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item class="ai-card" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="calculator"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">Advance Tax Calculation</span>
|
||||||
|
<span class="ai-card-d">Project quarterly liability per customer</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ai-col ai-col--pink">
|
||||||
|
<div class="ai-col-h">
|
||||||
|
<fa-icon icon="users" class="ai-col-h-ic"></fa-icon>
|
||||||
|
<span>Governance</span>
|
||||||
|
</div>
|
||||||
|
<button mat-menu-item class="ai-card" tabindex="0">
|
||||||
|
<span class="ai-card-ic"><fa-icon icon="users"></fa-icon></span>
|
||||||
|
<span class="ai-card-body">
|
||||||
|
<span class="ai-card-t">Election Management</span>
|
||||||
|
<span class="ai-card-d">Cooperative board elections end-to-end</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ai-menu-footer">
|
||||||
|
<fa-icon icon="info-circle"></fa-icon>
|
||||||
|
<span>Modules are powered by ML models & LLMs. Some are in beta.</span>
|
||||||
|
</div>
|
||||||
|
</mat-menu>
|
||||||
|
|
||||||
<!-- Sub Ledger Mega Menu -->
|
<!-- Sub Ledger Mega Menu -->
|
||||||
@if (subLedgerOpen) {
|
@if (subLedgerOpen) {
|
||||||
<div class="subledger-backdrop" (click)="subLedgerOpen = false"></div>
|
<div class="subledger-backdrop" (click)="subLedgerOpen = false"></div>
|
||||||
|
|||||||
@@ -160,12 +160,15 @@ $top-h: 32px;
|
|||||||
box-shadow: 0 0 0 2.5px rgb(16 185 129 / 25%);
|
box-shadow: 0 0 0 2.5px rgb(16 185 129 / 25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Right Actions — Unified =====
|
// ===== Right Actions — Unified, polished =====
|
||||||
$btn-size: 30px;
|
$btn-size: 26px;
|
||||||
$icon-size: 14px;
|
$icon-size: 13px;
|
||||||
$icon-color: rgb(255 255 255 / 55%);
|
$icon-color: rgb(255 255 255 / 70%);
|
||||||
$icon-hover: #fff;
|
$icon-hover: #fff;
|
||||||
$icon-hover-bg: rgb(255 255 255 / 10%);
|
$icon-bg: rgb(255 255 255 / 6%);
|
||||||
|
$icon-border: rgb(255 255 255 / 10%);
|
||||||
|
$icon-hover-bg: rgb(255 255 255 / 14%);
|
||||||
|
$icon-hover-border: rgb(255 255 255 / 28%);
|
||||||
|
|
||||||
.t-actions {
|
.t-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -173,41 +176,58 @@ $icon-hover-bg: rgb(255 255 255 / 10%);
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
height: $top-h;
|
height: $top-h;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: 2px;
|
gap: 6px;
|
||||||
padding-right: 8px;
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.t-divider {
|
.t-divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
background: rgb(255 255 255 / 12%);
|
background: rgb(255 255 255 / 14%);
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.t-action-btn {
|
.t-action-btn {
|
||||||
|
position: relative;
|
||||||
width: $btn-size;
|
width: $btn-size;
|
||||||
height: $btn-size;
|
height: $btn-size;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $icon-color;
|
color: $icon-color;
|
||||||
border-radius: 6px;
|
background: $icon-bg;
|
||||||
transition: all 0.15s;
|
border: 1px solid $icon-border;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition:
|
||||||
|
color 0.16s ease,
|
||||||
|
background 0.16s ease,
|
||||||
|
border-color 0.16s ease,
|
||||||
|
transform 0.16s ease;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
color: $icon-hover;
|
color: $icon-hover;
|
||||||
background: $icon-hover-bg;
|
background: $icon-hover-bg;
|
||||||
|
border-color: $icon-hover-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct fa-icon (Settings)
|
&:active {
|
||||||
fa-icon {
|
transform: scale(0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct fa-icon (Settings, AI)
|
||||||
|
> fa-icon {
|
||||||
font-size: $icon-size;
|
font-size: $icon-size;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifications tray — force same size
|
// Notifications tray — kill its own button chrome; outer pill is the visual
|
||||||
::ng-deep mifosx-notifications-tray {
|
::ng-deep mifosx-notifications-tray {
|
||||||
|
width: $btn-size;
|
||||||
|
height: $btn-size;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -216,67 +236,103 @@ $icon-hover-bg: rgb(255 255 255 / 10%);
|
|||||||
width: $btn-size;
|
width: $btn-size;
|
||||||
height: $btn-size;
|
height: $btn-size;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: $icon-color;
|
color: inherit;
|
||||||
|
background: transparent !important;
|
||||||
|
border-radius: 50%;
|
||||||
line-height: $btn-size;
|
line-height: $btn-size;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
// Suppress Material's ripple/state layer so we don't get a double hover
|
||||||
|
.mat-mdc-button-persistent-ripple,
|
||||||
|
.mat-mdc-button-ripple,
|
||||||
|
.mat-mdc-focus-indicator {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $icon-hover;
|
color: inherit;
|
||||||
background: transparent; // parent handles hover bg
|
background: transparent !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-icon,
|
// Colorful bell SVG — same 20px box as the AI icon
|
||||||
mat-icon {
|
.t-notif-svg {
|
||||||
font-size: $icon-size;
|
width: 20px;
|
||||||
width: $icon-size;
|
height: 20px;
|
||||||
height: $icon-size;
|
display: block;
|
||||||
line-height: $icon-size;
|
filter: drop-shadow(0 1px 2px rgb(11 30 61 / 35%));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-badge-content {
|
.mat-badge-content {
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
width: 14px;
|
width: 13px;
|
||||||
height: 14px;
|
height: 13px;
|
||||||
line-height: 14px;
|
line-height: 13px;
|
||||||
top: 2px !important;
|
top: -2px !important;
|
||||||
right: 2px !important;
|
right: -2px !important;
|
||||||
|
background: #ef4444 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1.5px solid $brand-dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme toggle — force same size
|
// ===== AI button — full-color SVG mark, ringed pill =====
|
||||||
::ng-deep mifosx-theme-toggle {
|
.t-action-btn--ai {
|
||||||
display: flex;
|
background: rgb(255 255 255 / 9%);
|
||||||
align-items: center;
|
border-color: rgb(94 204 186 / 50%);
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: $btn-size;
|
|
||||||
height: $btn-size;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: $icon-color;
|
overflow: visible;
|
||||||
line-height: $btn-size;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
color: $icon-hover;
|
&:focus-visible {
|
||||||
background: transparent;
|
background: rgb(255 255 255 / 14%);
|
||||||
}
|
border-color: rgb(94 204 186 / 85%);
|
||||||
|
box-shadow: 0 0 0 3px rgb(94 204 186 / 22%);
|
||||||
mat-icon,
|
|
||||||
.mat-icon {
|
|
||||||
font-size: $icon-size;
|
|
||||||
width: $icon-size;
|
|
||||||
height: $icon-size;
|
|
||||||
line-height: $icon-size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== User Button — Same size, round avatar =====
|
.t-ai-svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
filter: drop-shadow(0 1px 2px rgb(11 30 61 / 35%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-action-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: 1px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #34d399;
|
||||||
|
border: 1.5px solid $brand-dark;
|
||||||
|
box-shadow: 0 0 0 0 rgb(52 211 153 / 50%);
|
||||||
|
animation: t-ai-pulse 2.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes t-ai-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgb(52 211 153 / 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 5px rgb(52 211 153 / 0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== User Button — Same pill, round avatar inside =====
|
||||||
.t-user-btn {
|
.t-user-btn {
|
||||||
width: $btn-size;
|
width: $btn-size;
|
||||||
height: $btn-size;
|
height: $btn-size;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.t-user-img {
|
.t-user-img {
|
||||||
@@ -284,7 +340,252 @@ $icon-hover-bg: rgb(255 255 255 / 10%);
|
|||||||
height: 22px;
|
height: 22px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border: 1.5px solid rgb(255 255 255 / 20%);
|
border: 1.5px solid rgb(255 255 255 / 22%);
|
||||||
|
transition: border-color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-user-btn:hover .t-user-img,
|
||||||
|
.t-user-btn:focus-visible .t-user-img {
|
||||||
|
border-color: rgb(255 255 255 / 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-user-status {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #10b981;
|
||||||
|
border: 1.5px solid $brand-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== AI Mega Menu Panel =====
|
||||||
|
::ng-deep .ai-menu {
|
||||||
|
&.mat-mdc-menu-panel {
|
||||||
|
min-width: 880px !important;
|
||||||
|
max-width: 96vw !important;
|
||||||
|
max-height: 80vh;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
box-shadow:
|
||||||
|
0 18px 36px rgb(11 30 61 / 22%),
|
||||||
|
0 6px 14px rgb(11 30 61 / 14%) !important;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-menu-content {
|
||||||
|
padding: 0 !important;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-menu-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
background: linear-gradient(135deg, #0b1e3d 0%, #1d4ed8 100%);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.ai-menu-header-svg {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: drop-shadow(0 1px 3px rgb(0 0 0 / 35%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-menu-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-menu-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgb(255 255 255 / 80%);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Mega Layout — 4 colour-coded columns =====
|
||||||
|
.ai-menu-cols {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
padding: 14px 14px 6px;
|
||||||
|
background: #f8fafc;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 8px 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 0 6px;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 23 42 / 5%);
|
||||||
|
border-top: 3px solid var(--c-color);
|
||||||
|
|
||||||
|
--c-color: #4f46e5;
|
||||||
|
--c-bg: #eef2ff;
|
||||||
|
--c-hover: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-col--red {
|
||||||
|
--c-color: #c0392b;
|
||||||
|
--c-bg: #fee2e2;
|
||||||
|
--c-hover: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-col--teal {
|
||||||
|
--c-color: #0e7e6b;
|
||||||
|
--c-bg: #ccfbf1;
|
||||||
|
--c-hover: #0e7e6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-col--indigo {
|
||||||
|
--c-color: #3730a3;
|
||||||
|
--c-bg: #e0e7ff;
|
||||||
|
--c-hover: #3730a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-col--pink {
|
||||||
|
--c-color: #be185d;
|
||||||
|
--c-bg: #fce7f3;
|
||||||
|
--c-hover: #be185d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-col-h {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 6px 8px 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--c-color);
|
||||||
|
border-bottom: 1px dashed rgb(15 23 42 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-col-h-ic {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact card per AI module
|
||||||
|
.ai-card.mat-mdc-menu-item {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
min-height: 0;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 9px;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.3;
|
||||||
|
white-space: normal;
|
||||||
|
transition: background 0.16s ease;
|
||||||
|
|
||||||
|
.ai-card-ic {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--c-bg);
|
||||||
|
color: var(--c-color);
|
||||||
|
transition:
|
||||||
|
background 0.16s ease,
|
||||||
|
color 0.16s ease,
|
||||||
|
transform 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-card-t {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-card-d {
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 2px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--c-bg);
|
||||||
|
|
||||||
|
.ai-card-ic {
|
||||||
|
background: var(--c-hover);
|
||||||
|
color: #fff;
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-card-t {
|
||||||
|
color: var(--c-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-menu-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 18px 11px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-top: 1px solid #eef2f7;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive collapse
|
||||||
|
@media (width <= 1100px) {
|
||||||
|
::ng-deep .ai-menu.mat-mdc-menu-panel {
|
||||||
|
min-width: 680px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-menu-cols {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 720px) {
|
||||||
|
::ng-deep .ai-menu.mat-mdc-menu-panel {
|
||||||
|
min-width: 90vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-menu-cols {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-col {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Sub Ledger Mega Menu =====
|
// ===== Sub Ledger Mega Menu =====
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import { SearchToolComponent } from '../../../shared/search-tool/search-tool.com
|
|||||||
import { LanguageSelectorComponent } from '../../../shared/language-selector/language-selector.component';
|
import { LanguageSelectorComponent } from '../../../shared/language-selector/language-selector.component';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { NotificationsTrayComponent as NotificationsTrayComponent_1 } from '../../../shared/notifications-tray/notifications-tray.component';
|
import { NotificationsTrayComponent as NotificationsTrayComponent_1 } from '../../../shared/notifications-tray/notifications-tray.component';
|
||||||
import { ThemeToggleComponent } from '../../../shared/theme-toggle/theme-toggle.component';
|
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
import { DocumentationLinksService } from 'app/shared/services/documentation-links.service';
|
import { DocumentationLinksService } from 'app/shared/services/documentation-links.service';
|
||||||
|
|
||||||
@@ -69,7 +68,6 @@ import { DocumentationLinksService } from 'app/shared/services/documentation-lin
|
|||||||
LanguageSelectorComponent,
|
LanguageSelectorComponent,
|
||||||
MatIcon,
|
MatIcon,
|
||||||
NotificationsTrayComponent_1,
|
NotificationsTrayComponent_1,
|
||||||
ThemeToggleComponent,
|
|
||||||
MatMenu,
|
MatMenu,
|
||||||
MatMenuItem
|
MatMenuItem
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 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 { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { Route } from '../core/route/route.service';
|
||||||
|
import { NeftRtgsEntryComponent } from './neft-rtgs-entry/neft-rtgs-entry.component';
|
||||||
|
import { NeftRtgsReportComponent } from './neft-rtgs-report/neft-rtgs-report.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
Route.withShell([
|
||||||
|
{
|
||||||
|
path: 'digital/neft-rtgs',
|
||||||
|
component: NeftRtgsEntryComponent,
|
||||||
|
data: { title: 'NEFT / RTGS Entry', breadcrumb: 'NEFT / RTGS' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'digital/neft-rtgs-report',
|
||||||
|
component: NeftRtgsReportComponent,
|
||||||
|
data: { title: 'NEFT / RTGS Report', breadcrumb: 'NEFT / RTGS Report' }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class DigitalRoutingModule {}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 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 { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { DigitalRoutingModule } from './digital-routing.module';
|
||||||
|
import { NeftRtgsEntryComponent } from './neft-rtgs-entry/neft-rtgs-entry.component';
|
||||||
|
import { NeftRtgsReportComponent } from './neft-rtgs-report/neft-rtgs-report.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
SharedModule,
|
||||||
|
DigitalRoutingModule,
|
||||||
|
NeftRtgsEntryComponent,
|
||||||
|
NeftRtgsReportComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class DigitalModule {}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
<!--
|
||||||
|
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="nr-root" [class.nr-in-dialog]="isDialog">
|
||||||
|
<!-- HEADER -->
|
||||||
|
<div class="nr-head">
|
||||||
|
<div class="nr-head-left">
|
||||||
|
<fa-icon icon="exchange" class="nr-head-ic"></fa-icon>
|
||||||
|
<div>
|
||||||
|
<h2 class="nr-head-t">NEFT / RTGS — Outward Remittance</h2>
|
||||||
|
<div class="nr-head-s">
|
||||||
|
<span [class.is-rtgs]="isRtgs" [class.is-neft]="!isRtgs">{{ entryForm?.get('transactionType')?.value }}</span>
|
||||||
|
window ·
|
||||||
|
<fa-icon [icon]="isRtgs ? 'bolt' : 'clock'" class="nr-head-s-ic"></fa-icon>
|
||||||
|
{{ isRtgs ? 'Real-time gross settlement' : 'Half-hourly NEFT batches · 24×7' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nr-head-meta">
|
||||||
|
<div class="nr-head-pill" [class.is-rtgs]="isRtgs">
|
||||||
|
<span class="nr-head-pill-l">RTGS Floor</span>
|
||||||
|
<span class="nr-head-pill-v mono">₹{{ RTGS_MIN | number: '1.0-0' }}</span>
|
||||||
|
</div>
|
||||||
|
@if (isDialog) {
|
||||||
|
<button type="button" class="nr-dialog-x" (click)="back()" matTooltip="Close">
|
||||||
|
<fa-icon icon="times"></fa-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (entryForm) {
|
||||||
|
<form [formGroup]="entryForm" class="nr-form" (ngSubmit)="submit()">
|
||||||
|
<!-- SECTION 1: Transaction Setup -->
|
||||||
|
<section class="nr-section">
|
||||||
|
<div class="nr-section-h">
|
||||||
|
<fa-icon icon="list-alt" class="nr-section-ic"></fa-icon>
|
||||||
|
<span>Transaction Setup</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nr-grid">
|
||||||
|
<mat-form-field appearance="outline" class="nr-field">
|
||||||
|
<mat-label>Transaction Type</mat-label>
|
||||||
|
<mat-select formControlName="transactionType" required>
|
||||||
|
<mat-option value="NEFT">NEFT — National EFT</mat-option>
|
||||||
|
<mat-option value="RTGS">RTGS — Real-Time Gross</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<fa-icon
|
||||||
|
matSuffix
|
||||||
|
icon="info-circle"
|
||||||
|
class="nr-hint-ic"
|
||||||
|
matTooltip="Determines clearing window. RTGS settles in real time; NEFT settles in half-hourly batches."
|
||||||
|
matTooltipPosition="left"
|
||||||
|
></fa-icon>
|
||||||
|
@if (entryForm.get('transactionType')?.hasError('required')) {
|
||||||
|
<mat-error>Transaction Type is <strong>required</strong></mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="nr-field" (click)="valueDatePicker.open()">
|
||||||
|
<mat-label>Value Date</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[min]="minDate"
|
||||||
|
[max]="maxDate"
|
||||||
|
[matDatepicker]="valueDatePicker"
|
||||||
|
required
|
||||||
|
formControlName="valueDate"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="valueDatePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #valueDatePicker></mat-datepicker>
|
||||||
|
@if (entryForm.get('valueDate')?.hasError('required')) {
|
||||||
|
<mat-error>Value Date is <strong>required</strong></mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="nr-field">
|
||||||
|
<mat-label>Amount (₹)</mat-label>
|
||||||
|
<span matPrefix class="nr-prefix">₹ </span>
|
||||||
|
<input matInput type="number" min="1" step="0.01" required formControlName="amount" placeholder="0.00" />
|
||||||
|
@if (entryForm.get('amount')?.hasError('required')) {
|
||||||
|
<mat-error>Amount is <strong>required</strong></mat-error>
|
||||||
|
}
|
||||||
|
@if (entryForm.get('amount')?.hasError('min') && !entryForm.get('amount')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
@if (rtgsUnderMin) {
|
||||||
|
RTGS min ₹{{ RTGS_MIN | number: '1.0-0' }}
|
||||||
|
} @else {
|
||||||
|
Amount must be > 0
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="nr-field">
|
||||||
|
<mat-label>Sender Account No.</mat-label>
|
||||||
|
<input matInput required formControlName="senderAccount" placeholder="e.g. SB10029384" autocomplete="off" />
|
||||||
|
@if (entryForm.get('senderAccount')?.hasError('required')) {
|
||||||
|
<mat-error>Sender Account is <strong>required</strong></mat-error>
|
||||||
|
}
|
||||||
|
@if (
|
||||||
|
entryForm.get('senderAccount')?.hasError('pattern') &&
|
||||||
|
!entryForm.get('senderAccount')?.hasError('required')
|
||||||
|
) {
|
||||||
|
<mat-error>Use 6–20 alphanumeric chars</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SECTION 2: Beneficiary -->
|
||||||
|
<section class="nr-section">
|
||||||
|
<div class="nr-section-h">
|
||||||
|
<fa-icon icon="users" class="nr-section-ic"></fa-icon>
|
||||||
|
<span>Beneficiary Details</span>
|
||||||
|
@if (banlVerified) {
|
||||||
|
<span class="nr-banl-status-pill verified" matTooltip="Resolved via BANL / IMPS Switch">
|
||||||
|
<fa-icon icon="check-circle"></fa-icon> Verified
|
||||||
|
</span>
|
||||||
|
} @else {
|
||||||
|
<span class="nr-banl-status-pill pending" matTooltip="BANL Look-up is mandatory before initiating">
|
||||||
|
<fa-icon icon="exclamation-circle"></fa-icon> Verification Required
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="nr-banl-btn"
|
||||||
|
[class.is-pending]="!banlVerified && canPerformBanl"
|
||||||
|
(click)="performBanl()"
|
||||||
|
[disabled]="banlLooking || !canPerformBanl"
|
||||||
|
matTooltip="Look up the beneficiary via account number + IFSC (mocks the IMPS Switch API for the demo)"
|
||||||
|
>
|
||||||
|
@if (banlLooking) {
|
||||||
|
<fa-icon icon="spinner" class="fa-spin"></fa-icon> Looking up…
|
||||||
|
} @else if (banlVerified) {
|
||||||
|
<fa-icon icon="sync-alt"></fa-icon> Re-verify
|
||||||
|
} @else {
|
||||||
|
<fa-icon icon="search"></fa-icon> BANL Look-up
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nr-grid">
|
||||||
|
<mat-form-field appearance="outline" class="nr-field">
|
||||||
|
<mat-label>Beneficiary Account</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
required
|
||||||
|
formControlName="beneficiaryAccount"
|
||||||
|
placeholder="Credit A/C number"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
@if (entryForm.get('beneficiaryAccount')?.hasError('required')) {
|
||||||
|
<mat-error>Beneficiary Account is <strong>required</strong></mat-error>
|
||||||
|
}
|
||||||
|
@if (
|
||||||
|
entryForm.get('beneficiaryAccount')?.hasError('pattern') &&
|
||||||
|
!entryForm.get('beneficiaryAccount')?.hasError('required')
|
||||||
|
) {
|
||||||
|
<mat-error>Use 6–20 alphanumeric chars</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="nr-field">
|
||||||
|
<mat-label>IFSC Code</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
required
|
||||||
|
maxlength="11"
|
||||||
|
formControlName="ifscCode"
|
||||||
|
placeholder="e.g. HDFC0001234"
|
||||||
|
style="text-transform: uppercase"
|
||||||
|
(input)="
|
||||||
|
entryForm.get('ifscCode')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
@if (entryForm.get('ifscCode')?.hasError('required')) {
|
||||||
|
<mat-error>IFSC Code is <strong>required</strong></mat-error>
|
||||||
|
}
|
||||||
|
@if (entryForm.get('ifscCode')?.hasError('pattern') && !entryForm.get('ifscCode')?.hasError('required')) {
|
||||||
|
<mat-error>Invalid IFSC — 11 chars (e.g. HDFC0001234)</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="nr-field nr-span-2 nr-name-field">
|
||||||
|
<mat-label>Beneficiary Name (auto-filled by BANL)</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
required
|
||||||
|
readonly
|
||||||
|
formControlName="beneficiaryName"
|
||||||
|
placeholder="Run BANL Look-up to fetch the registered name"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
@if (banlVerified) {
|
||||||
|
<fa-icon matSuffix icon="check-circle" class="nr-name-ok"></fa-icon>
|
||||||
|
} @else {
|
||||||
|
<fa-icon matSuffix icon="lock" class="nr-name-lock"></fa-icon>
|
||||||
|
}
|
||||||
|
<mat-hint>Fetched from the IMPS Switch via account no. + IFSC — cannot be typed manually.</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BANL result card (compact) -->
|
||||||
|
@if (banlMatch) {
|
||||||
|
<div class="nr-banl-card success">
|
||||||
|
<fa-icon icon="check-circle" class="nr-banl-card-ic"></fa-icon>
|
||||||
|
<div class="nr-banl-card-body">
|
||||||
|
<strong>{{ banlMatch.beneficiaryName }}</strong> ·
|
||||||
|
<span class="mono">{{ banlMatch.beneficiaryAccount }}</span> ·
|
||||||
|
<span class="mono">{{ banlMatch.ifscCode }}</span> · {{ banlMatch.bankName }},
|
||||||
|
{{ banlMatch.branchName }}
|
||||||
|
<span class="nr-banl-status" [class.dormant]="banlMatch.status === 'DORMANT'"
|
||||||
|
>· {{ banlMatch.status }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="nr-banl-card-x" (click)="clearBanl()" matTooltip="Dismiss">
|
||||||
|
<fa-icon icon="times"></fa-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (banlError) {
|
||||||
|
<div class="nr-banl-card error">
|
||||||
|
<fa-icon icon="exclamation-triangle" class="nr-banl-card-ic"></fa-icon>
|
||||||
|
<div class="nr-banl-card-body">{{ banlError }}</div>
|
||||||
|
<button type="button" class="nr-banl-card-x" (click)="clearBanl()" matTooltip="Dismiss">
|
||||||
|
<fa-icon icon="times"></fa-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SECTION 3: Purpose & Remarks -->
|
||||||
|
<section class="nr-section">
|
||||||
|
<div class="nr-section-h">
|
||||||
|
<fa-icon icon="comment-alt" class="nr-section-ic"></fa-icon>
|
||||||
|
<span>Purpose & Notes</span>
|
||||||
|
@if (purposeMandatory) {
|
||||||
|
<span class="nr-required-pill">Mandatory · {{ isRtgs ? 'RTGS' : 'High-Value NEFT' }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nr-grid">
|
||||||
|
<mat-form-field appearance="outline" class="nr-field nr-span-2">
|
||||||
|
<mat-label>
|
||||||
|
Purpose of Remittance
|
||||||
|
@if (purposeMandatory) {
|
||||||
|
<span class="nr-req-star"> *</span>
|
||||||
|
}
|
||||||
|
</mat-label>
|
||||||
|
<mat-select formControlName="purposeOfRemittance" [required]="purposeMandatory">
|
||||||
|
@for (p of purposeOptions; track p.code) {
|
||||||
|
<mat-option [value]="p.code">{{ p.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
@if (entryForm.get('purposeOfRemittance')?.hasError('required')) {
|
||||||
|
<mat-error>Purpose required for {{ isRtgs ? 'RTGS' : 'high-value NEFT' }}</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="nr-field nr-span-2">
|
||||||
|
<mat-label>Remarks (optional)</mat-label>
|
||||||
|
<input matInput formControlName="remarks" maxlength="140" placeholder="Internal note · max 140 chars" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ACTIONS -->
|
||||||
|
<div class="nr-actions">
|
||||||
|
<button type="button" mat-stroked-button class="nr-btn-ghost" (click)="loadSample()">
|
||||||
|
<fa-icon icon="lightbulb"></fa-icon> Load Sample
|
||||||
|
</button>
|
||||||
|
<button type="button" mat-stroked-button class="nr-btn-ghost" (click)="resetForm()">
|
||||||
|
<fa-icon icon="undo"></fa-icon> Reset
|
||||||
|
</button>
|
||||||
|
<span class="nr-actions-spacer"></span>
|
||||||
|
@if (!banlVerified) {
|
||||||
|
<span class="nr-submit-hint">
|
||||||
|
<fa-icon icon="exclamation-circle"></fa-icon>
|
||||||
|
BANL Look-up required to enable Initiate
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<button type="button" mat-stroked-button (click)="back()">Cancel</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
[disabled]="!canSubmit"
|
||||||
|
class="nr-btn-submit"
|
||||||
|
[matTooltip]="!banlVerified ? 'Run BANL Look-up first to verify the beneficiary' : ''"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
>
|
||||||
|
<fa-icon icon="paper-plane"></fa-icon>
|
||||||
|
{{ isRtgs ? 'Initiate RTGS' : 'Initiate NEFT' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,595 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:host {
|
||||||
|
--nr-navy: #0b1e3d;
|
||||||
|
--nr-blue: #1d4ed8;
|
||||||
|
--nr-blue-d: #1e40af;
|
||||||
|
--nr-blue-l: #eff6ff;
|
||||||
|
--nr-teal: #0e7e6b;
|
||||||
|
--nr-teal-l: #e0f4f0;
|
||||||
|
--nr-green: #15803d;
|
||||||
|
--nr-green-l: #dcfce7;
|
||||||
|
--nr-amber: #d97706;
|
||||||
|
--nr-amber-l: #fef3c7;
|
||||||
|
--nr-red: #c0392b;
|
||||||
|
--nr-red-l: #fdecea;
|
||||||
|
--nr-purple: #6d28d9;
|
||||||
|
--nr-purple-l: #ede9fe;
|
||||||
|
--nr-g50: #f8fafc;
|
||||||
|
--nr-g100: #f1f5f9;
|
||||||
|
--nr-g200: #e2e8f0;
|
||||||
|
--nr-g300: #cbd5e1;
|
||||||
|
--nr-g500: #64748b;
|
||||||
|
--nr-g600: #475569;
|
||||||
|
--nr-g700: #334155;
|
||||||
|
--nr-g900: #0f172a;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
font-family:
|
||||||
|
'DM Sans',
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
background: var(--nr-g50);
|
||||||
|
color: var(--nr-g900);
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── ROUTED PAGE LAYOUT ───────── */
|
||||||
|
.nr-root {
|
||||||
|
padding: 18px 22px;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-ic {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--nr-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-t {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--nr-g900);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-s {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--nr-g500);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.is-rtgs {
|
||||||
|
color: var(--nr-purple);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-neft {
|
||||||
|
color: var(--nr-blue);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-s-ic {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--nr-g500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--nr-blue-l);
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 5px 12px 5px 14px;
|
||||||
|
color: var(--nr-blue-d);
|
||||||
|
|
||||||
|
&.is-rtgs {
|
||||||
|
background: var(--nr-purple-l);
|
||||||
|
border-color: #ddd6fe;
|
||||||
|
color: #4c1d95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-pill-l {
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-pill-v {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-dialog-x {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--nr-g200);
|
||||||
|
color: var(--nr-g600);
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--nr-red-l);
|
||||||
|
color: var(--nr-red);
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── SECTION ───────── */
|
||||||
|
.nr-section {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--nr-g200);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-section-h {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px dashed var(--nr-g200);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--nr-g700);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-section-ic {
|
||||||
|
color: var(--nr-blue);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-required-pill {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 3px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--nr-amber-l);
|
||||||
|
color: var(--nr-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-banl-status-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.verified {
|
||||||
|
background: var(--nr-green-l);
|
||||||
|
color: var(--nr-green);
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background: var(--nr-amber-l);
|
||||||
|
color: var(--nr-amber);
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-banl-btn {
|
||||||
|
margin-left: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: var(--nr-teal-l);
|
||||||
|
color: var(--nr-teal);
|
||||||
|
border: 1px solid #b8e5dc;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background: var(--nr-teal);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--nr-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When verification is pending but the operator has entered a valid A/C — nudge them */
|
||||||
|
&.is-pending {
|
||||||
|
background: var(--nr-amber);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--nr-amber);
|
||||||
|
animation: nr-banl-pulse 2s ease-in-out infinite;
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background: #b45309;
|
||||||
|
border-color: #b45309;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nr-banl-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgb(217 119 6 / 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 6px rgb(217 119 6 / 0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-name-field .nr-name-ok {
|
||||||
|
color: var(--nr-green);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-name-field .nr-name-lock {
|
||||||
|
color: var(--nr-g500);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read-only beneficiary name: full-strength text, no caret, subtle tint when verified */
|
||||||
|
/* stylelint-disable selector-no-qualifying-type */
|
||||||
|
:host ::ng-deep .nr-name-field input[readonly] {
|
||||||
|
cursor: default;
|
||||||
|
color: var(--nr-g900);
|
||||||
|
-webkit-text-fill-color: var(--nr-g900);
|
||||||
|
caret-color: transparent;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
/* stylelint-enable selector-no-qualifying-type */
|
||||||
|
|
||||||
|
.nr-submit-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--nr-amber);
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: var(--nr-amber-l);
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-right: 6px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── GRID (auto-fit) ───────── */
|
||||||
|
.nr-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-span-2 {
|
||||||
|
grid-column: span 2;
|
||||||
|
|
||||||
|
@media (width <= 560px) {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-prefix {
|
||||||
|
color: var(--nr-g500);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-hint-ic {
|
||||||
|
color: var(--nr-g500);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-req-star {
|
||||||
|
color: var(--nr-red);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── BANL RESULT (compact, single-line) ───────── */
|
||||||
|
.nr-banl-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: var(--nr-green-l);
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
color: var(--nr-g900);
|
||||||
|
|
||||||
|
.nr-banl-card-ic {
|
||||||
|
color: var(--nr-green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: var(--nr-red-l);
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: var(--nr-red);
|
||||||
|
|
||||||
|
.nr-banl-card-ic {
|
||||||
|
color: var(--nr-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-banl-card-ic {
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-banl-card-body {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--nr-g700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-banl-status {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--nr-green);
|
||||||
|
|
||||||
|
&.dormant {
|
||||||
|
color: var(--nr-amber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-banl-card-x {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: var(--nr-g500);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgb(0 0 0 / 5%);
|
||||||
|
color: var(--nr-g700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── ACTIONS ───────── */
|
||||||
|
.nr-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 4px 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-actions-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-btn-ghost {
|
||||||
|
color: var(--nr-g600) !important;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-btn-submit fa-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
font-size: 12.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
DIALOG MODE — sticky header/footer, compact body
|
||||||
|
═══════════════════════════════════════════════════ */
|
||||||
|
.nr-root.nr-in-dialog {
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
max-width: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: min(720px, 88vh);
|
||||||
|
|
||||||
|
/* compact sticky header */
|
||||||
|
.nr-head {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 5;
|
||||||
|
background: linear-gradient(180deg, #fff 0%, var(--nr-g50) 100%);
|
||||||
|
border-bottom: 1px solid var(--nr-g200);
|
||||||
|
padding: 12px 18px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-ic {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-t {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-s {
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-pill {
|
||||||
|
padding: 4px 10px 4px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-pill-l {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-head-pill-v {
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* scrollable body */
|
||||||
|
.nr-form {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 14px 18px 12px;
|
||||||
|
background: var(--nr-g50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sections become subtle blocks with smaller padding */
|
||||||
|
.nr-section {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--nr-g200);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-section-h {
|
||||||
|
font-size: 11px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-section-ic {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-banl-btn {
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-required-pill {
|
||||||
|
font-size: 9.5px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tighter grid in dialog — packs 3+ columns at full width */
|
||||||
|
.nr-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||||
|
gap: 4px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sticky footer actions */
|
||||||
|
.nr-actions {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid var(--nr-g200);
|
||||||
|
padding: 10px 18px;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 5;
|
||||||
|
box-shadow: 0 -2px 6px rgb(15 23 42 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nr-banl-card {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 7px 11px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Density: shrink Material outlined form-field internals in dialog mode */
|
||||||
|
:host ::ng-deep .nr-root.nr-in-dialog .mat-mdc-form-field {
|
||||||
|
.mat-mdc-text-field-wrapper {
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-form-field-infix {
|
||||||
|
min-height: 42px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-form-field-subscript-wrapper {
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
/**
|
||||||
|
* 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, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
type TxnType = 'NEFT' | 'RTGS';
|
||||||
|
|
||||||
|
interface PurposeOption {
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
highValueOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BanlMatch {
|
||||||
|
beneficiaryName: string;
|
||||||
|
beneficiaryAccount: string;
|
||||||
|
ifscCode: string;
|
||||||
|
bankName: string;
|
||||||
|
branchName: string;
|
||||||
|
status: 'ACTIVE' | 'DORMANT';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-neft-rtgs-entry',
|
||||||
|
templateUrl: './neft-rtgs-entry.component.html',
|
||||||
|
styleUrls: ['./neft-rtgs-entry.component.scss'],
|
||||||
|
imports: [
|
||||||
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
|
FaIconComponent,
|
||||||
|
MatTooltip
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class NeftRtgsEntryComponent implements OnInit {
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
private snack = inject(MatSnackBar);
|
||||||
|
private router = inject(Router);
|
||||||
|
/** Present when this component is opened inside a dialog. */
|
||||||
|
private dialogRef = inject(MatDialogRef<NeftRtgsEntryComponent>, { optional: true });
|
||||||
|
|
||||||
|
get isDialog(): boolean {
|
||||||
|
return this.dialogRef !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RTGS minimum threshold (₹2 Lakhs). */
|
||||||
|
readonly RTGS_MIN = 200000;
|
||||||
|
/** Threshold above which Purpose of Remittance is mandatory for NEFT (cross-border-style high value). */
|
||||||
|
readonly HIGH_VALUE_THRESHOLD = 200000;
|
||||||
|
|
||||||
|
minDate = new Date(2000, 0, 1);
|
||||||
|
maxDate = new Date(2100, 11, 31);
|
||||||
|
|
||||||
|
entryForm: FormGroup;
|
||||||
|
|
||||||
|
/** BANL lookup state */
|
||||||
|
banlLooking = false;
|
||||||
|
banlMatch: BanlMatch | null = null;
|
||||||
|
banlError: string | null = null;
|
||||||
|
|
||||||
|
/** Mock BANL directory — replace with API call later. */
|
||||||
|
private banlDirectory: BanlMatch[] = [
|
||||||
|
{
|
||||||
|
beneficiaryName: 'Rohan S. Mehta',
|
||||||
|
beneficiaryAccount: '50100123456789',
|
||||||
|
ifscCode: 'HDFC0001234',
|
||||||
|
bankName: 'HDFC Bank',
|
||||||
|
branchName: 'Mumbai — Andheri (E)',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
beneficiaryName: 'Bluestone Trading Co.',
|
||||||
|
beneficiaryAccount: '345678901234',
|
||||||
|
ifscCode: 'ICIC0000567',
|
||||||
|
bankName: 'ICICI Bank',
|
||||||
|
branchName: 'Bengaluru — MG Road',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
beneficiaryName: 'Anita Verma',
|
||||||
|
beneficiaryAccount: '912345678901',
|
||||||
|
ifscCode: 'SBIN0011223',
|
||||||
|
bankName: 'State Bank of India',
|
||||||
|
branchName: 'New Delhi — Connaught Place',
|
||||||
|
status: 'DORMANT'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/** RBI standard purpose-of-remittance categories (subset). */
|
||||||
|
purposeOptions: PurposeOption[] = [
|
||||||
|
{ code: 'CAPITAL_TRANSFER', label: 'Capital Transfer' },
|
||||||
|
{ code: 'FAMILY_MAINTENANCE', label: 'Family Maintenance' },
|
||||||
|
{ code: 'TRADE_SETTLEMENT', label: 'Trade Settlement' },
|
||||||
|
{ code: 'SALARY_PAYMENT', label: 'Salary Payment' },
|
||||||
|
{ code: 'LOAN_REPAYMENT', label: 'Loan Repayment' },
|
||||||
|
{ code: 'TAX_PAYMENT', label: 'Tax Payment' },
|
||||||
|
{ code: 'VENDOR_PAYMENT', label: 'Vendor / Supplier Payment' },
|
||||||
|
{ code: 'INVESTMENT', label: 'Investment / Securities' },
|
||||||
|
{ code: 'GIFT', label: 'Gift / Personal Remittance' },
|
||||||
|
{ code: 'OTHER', label: 'Other' }
|
||||||
|
];
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.entryForm = this.fb.group({
|
||||||
|
transactionType: [
|
||||||
|
'NEFT' as TxnType,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
valueDate: [
|
||||||
|
new Date(),
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
amount: [
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
senderAccount: [
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(/^[A-Za-z0-9]{6,20}$/)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
beneficiaryName: [
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.maxLength(120)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
beneficiaryAccount: [
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(/^[A-Za-z0-9]{6,20}$/)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
ifscCode: [
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(/^[A-Z]{4}0[A-Z0-9]{6}$/)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
purposeOfRemittance: [
|
||||||
|
'',
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
remarks: ['']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-evaluate purpose requirement whenever amount or type changes.
|
||||||
|
this.entryForm.get('amount')?.valueChanges.subscribe(() => this.refreshPurposeValidator());
|
||||||
|
this.entryForm.get('transactionType')?.valueChanges.subscribe((t) => {
|
||||||
|
this.refreshPurposeValidator();
|
||||||
|
this.refreshAmountValidator(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Any change to A/C or IFSC invalidates the prior BANL match — operator must re-verify.
|
||||||
|
this.entryForm.get('beneficiaryAccount')?.valueChanges.subscribe(() => this.invalidateBanl());
|
||||||
|
this.entryForm.get('ifscCode')?.valueChanges.subscribe(() => this.invalidateBanl());
|
||||||
|
|
||||||
|
this.refreshAmountValidator(this.entryForm.get('transactionType')?.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drop the prior BANL match + clear the auto-filled beneficiary name. */
|
||||||
|
private invalidateBanl(): void {
|
||||||
|
if (!this.banlMatch) return;
|
||||||
|
this.banlMatch = null;
|
||||||
|
this.banlError = null;
|
||||||
|
this.entryForm.get('beneficiaryName')?.setValue('', { emitEvent: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────── Validators ─────────
|
||||||
|
private refreshAmountValidator(type: TxnType): void {
|
||||||
|
const amt = this.entryForm.get('amount');
|
||||||
|
if (!amt) return;
|
||||||
|
const validators = [
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1)
|
||||||
|
];
|
||||||
|
if (type === 'RTGS') {
|
||||||
|
validators.push(Validators.min(this.RTGS_MIN));
|
||||||
|
}
|
||||||
|
amt.setValidators(validators);
|
||||||
|
amt.updateValueAndValidity({ emitEvent: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshPurposeValidator(): void {
|
||||||
|
const purpose = this.entryForm.get('purposeOfRemittance');
|
||||||
|
if (!purpose) return;
|
||||||
|
if (this.purposeMandatory) {
|
||||||
|
purpose.setValidators([
|
||||||
|
Validators.required
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
purpose.clearValidators();
|
||||||
|
}
|
||||||
|
purpose.updateValueAndValidity({ emitEvent: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────── Derived state ─────────
|
||||||
|
get isRtgs(): boolean {
|
||||||
|
return this.entryForm?.get('transactionType')?.value === 'RTGS';
|
||||||
|
}
|
||||||
|
|
||||||
|
get amount(): number {
|
||||||
|
return Number(this.entryForm?.get('amount')?.value || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Purpose is mandatory for RTGS or any high-value (≥ ₹2 L) txn per spec. */
|
||||||
|
get purposeMandatory(): boolean {
|
||||||
|
return this.isRtgs || this.amount >= this.HIGH_VALUE_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rtgsUnderMin(): boolean {
|
||||||
|
return this.isRtgs && this.amount > 0 && this.amount < this.RTGS_MIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True once a BANL lookup has resolved a valid beneficiary record. */
|
||||||
|
get banlVerified(): boolean {
|
||||||
|
return this.banlMatch !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Master gate — Initiate is only enabled when both form is valid AND BANL is verified. */
|
||||||
|
get canSubmit(): boolean {
|
||||||
|
return this.entryForm?.valid === true && this.banlVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pre-requisites for kicking off BANL (need A/C number that's structurally valid). */
|
||||||
|
get canPerformBanl(): boolean {
|
||||||
|
const acct = this.entryForm?.get('beneficiaryAccount');
|
||||||
|
return !!acct && acct.value && !acct.hasError('pattern');
|
||||||
|
}
|
||||||
|
|
||||||
|
get clearingWindow(): string {
|
||||||
|
if (this.isRtgs) {
|
||||||
|
return 'RTGS — settled in real time during RBI hours (Mon–Sat, 24×7 supported since Dec 2020).';
|
||||||
|
}
|
||||||
|
return 'NEFT — half-hourly batches, 24×7 settlement.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────── BANL lookup ─────────
|
||||||
|
/**
|
||||||
|
* Beneficiary Account Number Look-up — stand-in for the IMPS Switch API
|
||||||
|
* (account-no + IFSC → beneficiary name). Mandatory before Initiate.
|
||||||
|
*/
|
||||||
|
performBanl(): void {
|
||||||
|
const acct = (this.entryForm.get('beneficiaryAccount')?.value || '').trim();
|
||||||
|
const ifsc = (this.entryForm.get('ifscCode')?.value || '').trim().toUpperCase();
|
||||||
|
this.banlMatch = null;
|
||||||
|
this.banlError = null;
|
||||||
|
|
||||||
|
if (!acct) {
|
||||||
|
this.banlError = 'Enter the Beneficiary Account Number to look up.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.entryForm.get('beneficiaryAccount')?.hasError('pattern')) {
|
||||||
|
this.banlError = 'Beneficiary Account format is invalid — fix it before look-up.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ifsc && this.entryForm.get('ifscCode')?.hasError('pattern')) {
|
||||||
|
this.banlError = 'IFSC format is invalid — fix it before look-up.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.banlLooking = true;
|
||||||
|
// Simulated lookup; swap for the IMPS Switch API call later.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.banlLooking = false;
|
||||||
|
const match = this.banlDirectory.find((b) => b.beneficiaryAccount === acct && (!ifsc || b.ifscCode === ifsc));
|
||||||
|
if (!match) {
|
||||||
|
// Demo fallback: synthesise a name for any account that passes structural checks.
|
||||||
|
// Real implementation will route through the IMPS Switch.
|
||||||
|
const demo = this.synthesiseBanlForDemo(acct, ifsc);
|
||||||
|
this.banlMatch = demo;
|
||||||
|
this.applyBanlMatch(demo, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.applyBanlMatch(match, false);
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyBanlMatch(match: BanlMatch, isDemo: boolean): void {
|
||||||
|
this.banlMatch = match;
|
||||||
|
// The input is `readonly` in the template, so only BANL writes this value.
|
||||||
|
this.entryForm.get('beneficiaryName')?.setValue(match.beneficiaryName, { emitEvent: false });
|
||||||
|
this.entryForm.patchValue({ ifscCode: match.ifscCode }, { emitEvent: false });
|
||||||
|
const tag = isDemo ? ' (demo)' : '';
|
||||||
|
this.snack.open(`BANL verified${tag} — ${match.beneficiaryName}`, 'OK', { duration: 2500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demo-only stand-in for the IMPS Switch response.
|
||||||
|
* Produces a plausible-looking beneficiary record from the entered A/C + IFSC.
|
||||||
|
*/
|
||||||
|
private synthesiseBanlForDemo(acct: string, ifsc: string): BanlMatch {
|
||||||
|
const bankFromIfsc = (code: string): string => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
HDFC: 'HDFC Bank',
|
||||||
|
ICIC: 'ICICI Bank',
|
||||||
|
SBIN: 'State Bank of India',
|
||||||
|
KKBK: 'Kotak Mahindra Bank',
|
||||||
|
AXIS: 'Axis Bank',
|
||||||
|
UTIB: 'Axis Bank',
|
||||||
|
PUNB: 'Punjab National Bank',
|
||||||
|
BARB: 'Bank of Baroda',
|
||||||
|
CNRB: 'Canara Bank',
|
||||||
|
YESB: 'YES Bank',
|
||||||
|
IDFB: 'IDFC FIRST Bank'
|
||||||
|
};
|
||||||
|
return map[(code || '').slice(0, 4).toUpperCase()] || 'Partner Bank';
|
||||||
|
};
|
||||||
|
const seed = acct.slice(-2);
|
||||||
|
const sampleNames = [
|
||||||
|
'Rohan S. Mehta',
|
||||||
|
'Anita Verma',
|
||||||
|
'Bluestone Trading Co.',
|
||||||
|
'Madhavi Singh',
|
||||||
|
'Aarav Enterprises',
|
||||||
|
'Neha Kapoor',
|
||||||
|
'Spectra Components',
|
||||||
|
'Indrani Sen',
|
||||||
|
'Vijaya Traders',
|
||||||
|
'Pinnacle Realty Pvt Ltd'
|
||||||
|
];
|
||||||
|
const name = sampleNames[parseInt(seed, 10) % sampleNames.length] || 'Verified Beneficiary';
|
||||||
|
return {
|
||||||
|
beneficiaryName: name,
|
||||||
|
beneficiaryAccount: acct,
|
||||||
|
ifscCode: ifsc || 'HDFC0001234',
|
||||||
|
bankName: bankFromIfsc(ifsc),
|
||||||
|
branchName: 'Verified via IMPS Switch',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBanl(): void {
|
||||||
|
this.banlMatch = null;
|
||||||
|
this.banlError = null;
|
||||||
|
this.entryForm.get('beneficiaryName')?.setValue('', { emitEvent: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────── Actions ─────────
|
||||||
|
submit(): void {
|
||||||
|
if (!this.banlVerified) {
|
||||||
|
this.banlError = 'BANL verification is mandatory before initiating a transfer.';
|
||||||
|
this.snack.open('Run BANL Look-up to verify the beneficiary before submitting.', 'OK', {
|
||||||
|
duration: 3500
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.entryForm.invalid) {
|
||||||
|
this.entryForm.markAllAsTouched();
|
||||||
|
this.snack.open('Please fix the highlighted fields.', 'OK', { duration: 2800 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const v = this.entryForm.getRawValue(); // include disabled (BANL-filled) beneficiaryName
|
||||||
|
const txnId = 'TX' + Date.now().toString().slice(-10);
|
||||||
|
this.snack.open(
|
||||||
|
`${v.transactionType} initiated · ₹${Number(v.amount).toLocaleString('en-IN')} · Ref ${txnId}`,
|
||||||
|
'OK',
|
||||||
|
{ duration: 4500 }
|
||||||
|
);
|
||||||
|
if (this.isDialog) {
|
||||||
|
this.dialogRef!.close({ submitted: true, payload: { ...v, txnId } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm(): void {
|
||||||
|
this.entryForm.reset({
|
||||||
|
transactionType: 'NEFT',
|
||||||
|
valueDate: new Date(),
|
||||||
|
amount: null,
|
||||||
|
senderAccount: '',
|
||||||
|
beneficiaryName: '',
|
||||||
|
beneficiaryAccount: '',
|
||||||
|
ifscCode: '',
|
||||||
|
purposeOfRemittance: '',
|
||||||
|
remarks: ''
|
||||||
|
});
|
||||||
|
this.clearBanl();
|
||||||
|
this.refreshAmountValidator('NEFT');
|
||||||
|
this.refreshPurposeValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sample / demo entry to make the form discoverable. Auto-runs BANL after prefill. */
|
||||||
|
loadSample(): void {
|
||||||
|
this.entryForm.patchValue(
|
||||||
|
{
|
||||||
|
transactionType: 'RTGS',
|
||||||
|
valueDate: new Date(),
|
||||||
|
amount: 250000,
|
||||||
|
senderAccount: 'SB10029384',
|
||||||
|
beneficiaryAccount: '50100123456789',
|
||||||
|
ifscCode: 'HDFC0001234',
|
||||||
|
purposeOfRemittance: 'TRADE_SETTLEMENT',
|
||||||
|
remarks: 'Q1 vendor settlement'
|
||||||
|
},
|
||||||
|
{ emitEvent: false }
|
||||||
|
);
|
||||||
|
this.refreshAmountValidator('RTGS');
|
||||||
|
this.refreshPurposeValidator();
|
||||||
|
this.banlMatch = null;
|
||||||
|
this.banlError = null;
|
||||||
|
// Kick BANL so sample is immediately submittable.
|
||||||
|
this.performBanl();
|
||||||
|
}
|
||||||
|
|
||||||
|
back(): void {
|
||||||
|
if (this.isDialog) {
|
||||||
|
this.dialogRef!.close({ submitted: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.router.navigate([
|
||||||
|
'/home'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
<!--
|
||||||
|
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="nrr-root">
|
||||||
|
<!-- HEADER -->
|
||||||
|
<div class="nrr-head">
|
||||||
|
<div class="nrr-head-left">
|
||||||
|
<fa-icon icon="chart-line" class="nrr-head-ic"></fa-icon>
|
||||||
|
<div>
|
||||||
|
<h2 class="nrr-head-t">NEFT / RTGS — Report & Reconciliation</h2>
|
||||||
|
<div class="nrr-head-s">
|
||||||
|
Digital · Outward Remittance Analytics · Showing
|
||||||
|
<strong>{{ totalCount | number }}</strong> transactions worth
|
||||||
|
<strong>₹{{ totalAmount | number: '1.0-0' }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-head-actions">
|
||||||
|
<button mat-stroked-button (click)="exportCsv()" matTooltip="Download filtered transactions as CSV">
|
||||||
|
<fa-icon icon="download"></fa-icon> Export CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI STRIP -->
|
||||||
|
<div class="nrr-kpis">
|
||||||
|
<div class="nrr-kpi nrr-kpi--blue">
|
||||||
|
<div class="nrr-kpi-ic"><fa-icon icon="exchange"></fa-icon></div>
|
||||||
|
<div>
|
||||||
|
<div class="nrr-kpi-n">{{ totalCount | number }}</div>
|
||||||
|
<div class="nrr-kpi-l">Total Transactions</div>
|
||||||
|
<div class="nrr-kpi-sub">{{ neftCount }} NEFT · {{ rtgsCount }} RTGS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-kpi nrr-kpi--teal">
|
||||||
|
<div class="nrr-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
|
||||||
|
<div>
|
||||||
|
<div class="nrr-kpi-n">₹{{ totalAmount | number: '1.0-0' }}</div>
|
||||||
|
<div class="nrr-kpi-l">Total Value</div>
|
||||||
|
<div class="nrr-kpi-sub">avg ₹{{ avgTicket | number: '1.0-0' }} / txn</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-kpi nrr-kpi--green">
|
||||||
|
<div class="nrr-kpi-ic"><fa-icon icon="check-circle"></fa-icon></div>
|
||||||
|
<div>
|
||||||
|
<div class="nrr-kpi-n">{{ settledCount | number }}</div>
|
||||||
|
<div class="nrr-kpi-l">Settled</div>
|
||||||
|
<div class="nrr-kpi-sub">success {{ successRate | number: '1.1-1' }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-kpi nrr-kpi--amber">
|
||||||
|
<div class="nrr-kpi-ic"><fa-icon icon="hourglass-half"></fa-icon></div>
|
||||||
|
<div>
|
||||||
|
<div class="nrr-kpi-n">{{ pendingCount | number }}</div>
|
||||||
|
<div class="nrr-kpi-l">In-Flight</div>
|
||||||
|
<div class="nrr-kpi-sub">uploaded / validated / sent</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-kpi nrr-kpi--red">
|
||||||
|
<div class="nrr-kpi-ic"><fa-icon icon="times-circle"></fa-icon></div>
|
||||||
|
<div>
|
||||||
|
<div class="nrr-kpi-n">{{ returnedCount | number }}</div>
|
||||||
|
<div class="nrr-kpi-l">Returned</div>
|
||||||
|
<div class="nrr-kpi-sub">verify return codes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABS -->
|
||||||
|
<div class="nrr-tabs">
|
||||||
|
@for (t of tabs; track t.id) {
|
||||||
|
<button class="nrr-tab" [class.active]="activeTab === t.id" (click)="showTab(t.id)">
|
||||||
|
<fa-icon [icon]="t.icon"></fa-icon> {{ t.label }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TRENDS -->
|
||||||
|
@if (activeTab === 'trends') {
|
||||||
|
<div class="nrr-card">
|
||||||
|
<div class="nrr-card-h">
|
||||||
|
<span><fa-icon icon="chart-line" class="nrr-card-h-ic"></fa-icon> Daily Throughput (Last 7 Value Dates)</span>
|
||||||
|
<span class="nrr-pill nrr-pill--blue">NEFT</span>
|
||||||
|
<span class="nrr-pill nrr-pill--purple">RTGS</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-trend">
|
||||||
|
@for (b of trendBuckets; track b.date) {
|
||||||
|
<div class="nrr-trend-col">
|
||||||
|
<div class="nrr-trend-bar-wrap">
|
||||||
|
<div class="nrr-trend-bar" [style.height.%]="trendBarHeight(b.amount)">
|
||||||
|
<span class="nrr-trend-amt">₹{{ b.amount | number: '1.0-0' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-trend-meta">
|
||||||
|
<div class="nrr-trend-date">{{ b.date | date: 'd MMM' }}</div>
|
||||||
|
<div class="nrr-trend-split">
|
||||||
|
<span class="nrr-pill nrr-pill--blue mini">N {{ b.neft }}</span>
|
||||||
|
<span class="nrr-pill nrr-pill--purple mini">R {{ b.rtgs }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- BANK-WISE -->
|
||||||
|
@if (activeTab === 'bank') {
|
||||||
|
<div class="nrr-card">
|
||||||
|
<div class="nrr-card-h">
|
||||||
|
<span><fa-icon icon="university" class="nrr-card-h-ic"></fa-icon> Beneficiary Bank Distribution</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-table-wrap">
|
||||||
|
<table class="nrr-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bank</th>
|
||||||
|
<th class="num">Count</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
<th class="num">Settled</th>
|
||||||
|
<th class="num">Pending</th>
|
||||||
|
<th class="num">Returned</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of bankRows; track r.bank) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ r.bank }}</strong>
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ r.count | number }}</td>
|
||||||
|
<td class="num">
|
||||||
|
<strong>₹{{ r.amount | number: '1.0-0' }}</strong>
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
<span class="nrr-pill nrr-pill--green mini">{{ r.settled }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
<span class="nrr-pill nrr-pill--amber mini">{{ r.pending }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
<span class="nrr-pill nrr-pill--red mini">{{ r.returned }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- DATE-WISE -->
|
||||||
|
@if (activeTab === 'date') {
|
||||||
|
<div class="nrr-card">
|
||||||
|
<div class="nrr-card-h">
|
||||||
|
<span><fa-icon icon="calendar-alt" class="nrr-card-h-ic"></fa-icon> Value-Date Aggregation</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-table-wrap">
|
||||||
|
<table class="nrr-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Value Date</th>
|
||||||
|
<th class="num">Count</th>
|
||||||
|
<th class="num">NEFT</th>
|
||||||
|
<th class="num">RTGS</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of dateRows; track r.date) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ r.date | date: 'EEE, d MMM y' }}</td>
|
||||||
|
<td class="num">{{ r.count | number }}</td>
|
||||||
|
<td class="num">
|
||||||
|
<span class="nrr-pill nrr-pill--blue mini">{{ r.neft }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
<span class="nrr-pill nrr-pill--purple mini">{{ r.rtgs }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
<strong>₹{{ r.amount | number: '1.0-0' }}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- PENDING / PASS / RETURN -->
|
||||||
|
@if (activeTab === 'pending') {
|
||||||
|
<div class="nrr-grid-2">
|
||||||
|
<div class="nrr-card">
|
||||||
|
<div class="nrr-card-h">
|
||||||
|
<span><fa-icon icon="tasks" class="nrr-card-h-ic"></fa-icon> Processing-State Funnel</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-funnel">
|
||||||
|
<div class="nrr-funnel-row">
|
||||||
|
<span class="nrr-funnel-l"> <span class="nrr-dot nrr-dot--amber"></span> Uploaded / Validated / Sent </span>
|
||||||
|
<span class="nrr-funnel-v">{{ pendingCount | number }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-funnel-row">
|
||||||
|
<span class="nrr-funnel-l"> <span class="nrr-dot nrr-dot--green"></span> Settled (Pass) </span>
|
||||||
|
<span class="nrr-funnel-v">{{ settledCount | number }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-funnel-row">
|
||||||
|
<span class="nrr-funnel-l"> <span class="nrr-dot nrr-dot--red"></span> Returned </span>
|
||||||
|
<span class="nrr-funnel-v">{{ returnedCount | number }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-funnel-meter">
|
||||||
|
<span class="seg green" [style.flex]="settledCount || 0"></span>
|
||||||
|
<span class="seg amber" [style.flex]="pendingCount || 0"></span>
|
||||||
|
<span class="seg red" [style.flex]="returnedCount || 0"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-card">
|
||||||
|
<div class="nrr-card-h">
|
||||||
|
<span><fa-icon icon="exclamation-triangle" class="nrr-card-h-ic"></fa-icon> Return-Code Breakdown</span>
|
||||||
|
</div>
|
||||||
|
@if (returnCodeRows.length === 0) {
|
||||||
|
<div class="nrr-empty">No returned transactions in current window.</div>
|
||||||
|
} @else {
|
||||||
|
<table class="nrr-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<th>Reason</th>
|
||||||
|
<th class="num">Count</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of returnCodeRows; track r.code) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="nrr-pill nrr-pill--red mini">{{ r.code }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ r.reason }}</td>
|
||||||
|
<td class="num">{{ r.count }}</td>
|
||||||
|
<td class="num">₹{{ r.amount | number: '1.0-0' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- CHANNEL (BRANCH / MOBILE) -->
|
||||||
|
@if (activeTab === 'channel') {
|
||||||
|
<div class="nrr-card">
|
||||||
|
<div class="nrr-card-h">
|
||||||
|
<span><fa-icon icon="mobile-alt" class="nrr-card-h-ic"></fa-icon> Channel Mix — Branch vs Mobile</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-channel-grid">
|
||||||
|
@for (r of channelRows; track r.channel) {
|
||||||
|
<div class="nrr-channel-card" [class.is-mobile]="r.channel === 'MOBILE'">
|
||||||
|
<div class="nrr-channel-ic">
|
||||||
|
<fa-icon [icon]="r.channel === 'MOBILE' ? 'mobile-alt' : 'building'"></fa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-channel-body">
|
||||||
|
<div class="nrr-channel-t">
|
||||||
|
{{ r.channel === 'MOBILE' ? 'Mobile / Self-Service' : 'Branch / Counter' }}
|
||||||
|
</div>
|
||||||
|
<div class="nrr-channel-n">{{ r.count | number }} <span>txns</span></div>
|
||||||
|
<div class="nrr-channel-amt">₹{{ r.amount | number: '1.0-0' }}</div>
|
||||||
|
<div class="nrr-channel-bar">
|
||||||
|
<span [style.width.%]="r.share"></span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-channel-share">{{ r.share | number: '1.1-1' }}% of value</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- USER-WISE -->
|
||||||
|
@if (activeTab === 'user') {
|
||||||
|
<div class="nrr-card">
|
||||||
|
<div class="nrr-card-h">
|
||||||
|
<span><fa-icon icon="user-tie" class="nrr-card-h-ic"></fa-icon> Maker / Operator Leaderboard</span>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-table-wrap">
|
||||||
|
<table class="nrr-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Branch</th>
|
||||||
|
<th class="num">Txns</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of userRows; track r.user + r.branch) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ r.user }}</strong>
|
||||||
|
</td>
|
||||||
|
<td>{{ r.branch }}</td>
|
||||||
|
<td class="num">{{ r.count | number }}</td>
|
||||||
|
<td class="num">
|
||||||
|
<strong>₹{{ r.amount | number: '1.0-0' }}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- TRANSACTION DETAIL TABLE -->
|
||||||
|
<div class="nrr-card">
|
||||||
|
<div class="nrr-card-h">
|
||||||
|
<span><fa-icon icon="list-alt" class="nrr-card-h-ic"></fa-icon> Transactions</span>
|
||||||
|
<div class="nrr-filters">
|
||||||
|
<div class="nrr-search">
|
||||||
|
<fa-icon icon="search" class="nrr-search-ic"></fa-icon>
|
||||||
|
<input type="text" [(ngModel)]="search" placeholder="Search UTR, Batch, Instruction, A/C, IFSC, Bene Name…" />
|
||||||
|
</div>
|
||||||
|
<select [(ngModel)]="typeFilter" class="nrr-select" matTooltip="Filter by payment type">
|
||||||
|
<option value="">All Types</option>
|
||||||
|
<option value="NEFT">NEFT</option>
|
||||||
|
<option value="RTGS">RTGS</option>
|
||||||
|
</select>
|
||||||
|
<select [(ngModel)]="stateFilter" class="nrr-select" matTooltip="Filter by processing state">
|
||||||
|
<option value="">All States</option>
|
||||||
|
<option value="UPLOADED">Uploaded</option>
|
||||||
|
<option value="VALIDATED">Validated</option>
|
||||||
|
<option value="SENT">Sent</option>
|
||||||
|
<option value="SETTLED">Settled</option>
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="RETURNED">Returned</option>
|
||||||
|
</select>
|
||||||
|
@if (search || typeFilter || stateFilter) {
|
||||||
|
<button type="button" class="nrr-clear-btn" (click)="clearFilters()">
|
||||||
|
<fa-icon icon="times"></fa-icon> Clear
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nrr-table-wrap">
|
||||||
|
<table class="nrr-table nrr-table-detail">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Batch / Instruction</th>
|
||||||
|
<th>UTR</th>
|
||||||
|
<th>Remitter A/C</th>
|
||||||
|
<th>Beneficiary</th>
|
||||||
|
<th>IFSC</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
<th>Value Date</th>
|
||||||
|
<th>Purpose</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Sent</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (t of filteredTxns; track t.utrNumber) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="mono small">{{ t.batchId }}</div>
|
||||||
|
<div class="mono small muted">{{ t.instructionId }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="mono small">{{ t.utrNumber }}</td>
|
||||||
|
<td class="mono small">{{ t.remitterAcc }}</td>
|
||||||
|
<td>
|
||||||
|
<div>{{ t.beneName }}</div>
|
||||||
|
<div class="mono small muted">{{ t.beneAcc }} · {{ t.beneBank }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="mono small">{{ t.beneIfsc }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="nrr-pill mini"
|
||||||
|
[class.nrr-pill--purple]="t.paymentType === 'RTGS'"
|
||||||
|
[class.nrr-pill--blue]="t.paymentType === 'NEFT'"
|
||||||
|
>
|
||||||
|
{{ t.paymentType }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
<strong>₹{{ t.amount | number: '1.2-2' }}</strong>
|
||||||
|
</td>
|
||||||
|
<td>{{ t.valueDate | date: 'd MMM y' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="mono small">{{ t.purposeCode }}</span>
|
||||||
|
@if (t.leiCode) {
|
||||||
|
<span class="nrr-pill nrr-pill--teal mini" matTooltip="LEI: {{ t.leiCode }}">LEI</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="nrr-pill mini" [ngClass]="'nrr-pill--' + stateColor(t.processingState)">
|
||||||
|
{{ t.processingState }}
|
||||||
|
</span>
|
||||||
|
@if (t.returnCode) {
|
||||||
|
<div class="small muted">{{ t.returnCode }} · {{ t.returnReason }}</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="mono small">{{ t.sentTimestamp }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (filteredTxns.length === 0) {
|
||||||
|
<tr>
|
||||||
|
<td colspan="11" class="nrr-empty">No transactions match the current filters.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FLOATING ACTION BUTTON -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="nrr-fab"
|
||||||
|
(click)="openInitiateDialog()"
|
||||||
|
matTooltip="Initiate a new NEFT / RTGS transfer"
|
||||||
|
matTooltipPosition="left"
|
||||||
|
>
|
||||||
|
<fa-icon icon="paper-plane"></fa-icon>
|
||||||
|
<span class="nrr-fab-label">Initiate NEFT / RTGS</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,795 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:host {
|
||||||
|
--r-navy: #0b1e3d;
|
||||||
|
--r-blue: #1d4ed8;
|
||||||
|
--r-blue-l: #eff6ff;
|
||||||
|
--r-teal: #0e7e6b;
|
||||||
|
--r-teal-l: #e0f4f0;
|
||||||
|
--r-green: #15803d;
|
||||||
|
--r-green-l: #dcfce7;
|
||||||
|
--r-amber: #d97706;
|
||||||
|
--r-amber-l: #fef3c7;
|
||||||
|
--r-red: #c0392b;
|
||||||
|
--r-red-l: #fdecea;
|
||||||
|
--r-purple: #6d28d9;
|
||||||
|
--r-purple-l: #ede9fe;
|
||||||
|
--r-pink: #db2777;
|
||||||
|
--r-pink-l: #fce7f3;
|
||||||
|
--r-g50: #f8fafc;
|
||||||
|
--r-g100: #f1f5f9;
|
||||||
|
--r-g200: #e2e8f0;
|
||||||
|
--r-g300: #cbd5e1;
|
||||||
|
--r-g500: #64748b;
|
||||||
|
--r-g600: #475569;
|
||||||
|
--r-g700: #334155;
|
||||||
|
--r-g900: #0f172a;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
font-family:
|
||||||
|
'DM Sans',
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
background: var(--r-g50);
|
||||||
|
color: var(--r-g900);
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-root {
|
||||||
|
padding: 18px 22px 90px;
|
||||||
|
max-width: 1500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── HEADER ───────── */
|
||||||
|
.nrr-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-head-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-head-ic {
|
||||||
|
font-size: 26px;
|
||||||
|
color: var(--r-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-head-t {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--r-g900);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-head-s {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--r-g500);
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--r-g700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-head-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── KPIs ───────── */
|
||||||
|
.nrr-kpis {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1100px) {
|
||||||
|
.nrr-kpis {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 720px) {
|
||||||
|
.nrr-kpis {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--r-g200);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi-ic {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi-n {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--r-g900);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi-l {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--r-g700);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--r-g500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi--blue {
|
||||||
|
border-left-color: var(--r-blue);
|
||||||
|
|
||||||
|
.nrr-kpi-ic {
|
||||||
|
background: var(--r-blue-l);
|
||||||
|
color: var(--r-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi--teal {
|
||||||
|
border-left-color: var(--r-teal);
|
||||||
|
|
||||||
|
.nrr-kpi-ic {
|
||||||
|
background: var(--r-teal-l);
|
||||||
|
color: var(--r-teal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi--green {
|
||||||
|
border-left-color: var(--r-green);
|
||||||
|
|
||||||
|
.nrr-kpi-ic {
|
||||||
|
background: var(--r-green-l);
|
||||||
|
color: var(--r-green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi--amber {
|
||||||
|
border-left-color: var(--r-amber);
|
||||||
|
|
||||||
|
.nrr-kpi-ic {
|
||||||
|
background: var(--r-amber-l);
|
||||||
|
color: var(--r-amber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-kpi--red {
|
||||||
|
border-left-color: var(--r-red);
|
||||||
|
|
||||||
|
.nrr-kpi-ic {
|
||||||
|
background: var(--r-red-l);
|
||||||
|
color: var(--r-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── TABS ───────── */
|
||||||
|
.nrr-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--r-g200);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 4px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--r-g600);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--r-g100);
|
||||||
|
color: var(--r-g900);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--r-blue);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── CARDS ───────── */
|
||||||
|
.nrr-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--r-g200);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-card-h {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--r-g700);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px dashed var(--r-g200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-card-h-ic {
|
||||||
|
color: var(--r-blue);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── PILLS ───────── */
|
||||||
|
.nrr-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 3px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
|
||||||
|
&.mini {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nrr-pill--blue {
|
||||||
|
background: var(--r-blue-l);
|
||||||
|
color: var(--r-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nrr-pill--teal {
|
||||||
|
background: var(--r-teal-l);
|
||||||
|
color: var(--r-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nrr-pill--green {
|
||||||
|
background: var(--r-green-l);
|
||||||
|
color: var(--r-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nrr-pill--amber {
|
||||||
|
background: var(--r-amber-l);
|
||||||
|
color: var(--r-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nrr-pill--red {
|
||||||
|
background: var(--r-red-l);
|
||||||
|
color: var(--r-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nrr-pill--purple {
|
||||||
|
background: var(--r-purple-l);
|
||||||
|
color: var(--r-purple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── TREND CHART ───────── */
|
||||||
|
.nrr-trend {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 10px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-trend-col {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-trend-bar-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-trend-bar {
|
||||||
|
width: 75%;
|
||||||
|
min-height: 6px;
|
||||||
|
background: linear-gradient(180deg, var(--r-blue) 0%, var(--r-purple) 100%);
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
position: relative;
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-trend-amt {
|
||||||
|
position: absolute;
|
||||||
|
top: -22px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--r-g700);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-trend-meta {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-trend-date {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--r-g700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-trend-split {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── TABLE ───────── */
|
||||||
|
.nrr-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12.5px;
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
background: var(--r-g100);
|
||||||
|
color: var(--r-g700);
|
||||||
|
text-align: left;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--r-g200);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.num {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--r-g100);
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&.num {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background: var(--r-blue-l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-table-detail {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--r-g500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── FUNNEL ───────── */
|
||||||
|
.nrr-funnel {
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-funnel-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px dashed var(--r-g200);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-funnel-l {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--r-g700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-funnel-v {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--r-g900);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.nrr-dot--green {
|
||||||
|
background: var(--r-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nrr-dot--amber {
|
||||||
|
background: var(--r-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nrr-dot--red {
|
||||||
|
background: var(--r-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-funnel-meter {
|
||||||
|
display: flex;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 12px;
|
||||||
|
background: var(--r-g100);
|
||||||
|
|
||||||
|
.seg {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&.green {
|
||||||
|
background: var(--r-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.amber {
|
||||||
|
background: var(--r-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
background: var(--r-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── CHANNEL CARDS ───────── */
|
||||||
|
.nrr-channel-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media (width <= 720px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-channel-card {
|
||||||
|
background: linear-gradient(135deg, var(--r-blue-l) 0%, #fff 100%);
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
&.is-mobile {
|
||||||
|
background: linear-gradient(135deg, var(--r-purple-l) 0%, #fff 100%);
|
||||||
|
border-color: #ddd6fe;
|
||||||
|
|
||||||
|
.nrr-channel-ic {
|
||||||
|
background: var(--r-purple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-channel-ic {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--r-blue);
|
||||||
|
color: #fff;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-channel-body {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-channel-t {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--r-g700);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-channel-n {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--r-g900);
|
||||||
|
line-height: 1.1;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--r-g500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-channel-amt {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--r-g700);
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-channel-bar {
|
||||||
|
background: rgb(0 0 0 / 6%);
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--r-blue);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-mobile .nrr-channel-bar span {
|
||||||
|
background: var(--r-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-channel-share {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--r-g500);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── FILTER ROW ───────── */
|
||||||
|
.nrr-filters {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-search {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 1px solid var(--r-g200);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 10px 6px 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 240px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--r-g900);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--r-blue);
|
||||||
|
box-shadow: 0 0 0 2px rgb(29 78 216 / 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-search-ic {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--r-g500);
|
||||||
|
font-size: 11px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-select {
|
||||||
|
border: 1px solid var(--r-g200);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--r-g900);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--r-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-clear-btn {
|
||||||
|
background: var(--r-red-l);
|
||||||
|
color: var(--r-red);
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--r-red);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-empty {
|
||||||
|
padding: 22px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--r-g500);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── ENTRY DIALOG PANEL OVERRIDE ───────── */
|
||||||
|
:host ::ng-deep .nr-entry-dialog {
|
||||||
|
.mat-mdc-dialog-surface {
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
box-shadow:
|
||||||
|
0 24px 48px rgb(15 23 42 / 28%),
|
||||||
|
0 8px 16px rgb(15 23 42 / 18%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-dialog-container .mdc-dialog__container,
|
||||||
|
.mat-mdc-dialog-container .mdc-dialog__surface {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-dialog-content {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .nr-entry-dialog-backdrop {
|
||||||
|
background: rgb(11 30 61 / 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── FAB ───────── */
|
||||||
|
.nrr-fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 28px;
|
||||||
|
bottom: 28px;
|
||||||
|
background: linear-gradient(135deg, var(--r-blue) 0%, var(--r-purple) 100%);
|
||||||
|
color: #fff;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 14px 22px;
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 24px rgb(29 78 216 / 35%),
|
||||||
|
0 4px 8px rgb(15 23 42 / 18%);
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
z-index: 50;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nrr-fab-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow:
|
||||||
|
0 14px 30px rgb(29 78 216 / 45%),
|
||||||
|
0 6px 12px rgb(15 23 42 / 22%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,747 @@
|
|||||||
|
/**
|
||||||
|
* 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, OnInit } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
import { NeftRtgsEntryComponent } from '../neft-rtgs-entry/neft-rtgs-entry.component';
|
||||||
|
|
||||||
|
export type PaymentType = 'NEFT' | 'RTGS';
|
||||||
|
export type ProcessingState = 'UPLOADED' | 'VALIDATED' | 'SENT' | 'SETTLED' | 'PENDING' | 'RETURNED';
|
||||||
|
|
||||||
|
export interface NeftRtgsTxn {
|
||||||
|
batchId: string;
|
||||||
|
instructionId: string;
|
||||||
|
utrNumber: string;
|
||||||
|
remitterAcc: string;
|
||||||
|
remitterBranch: string;
|
||||||
|
remitterUser: string;
|
||||||
|
remitterChannel: 'BRANCH' | 'MOBILE';
|
||||||
|
beneAcc: string;
|
||||||
|
beneIfsc: string;
|
||||||
|
beneName: string;
|
||||||
|
beneBank: string;
|
||||||
|
paymentType: PaymentType;
|
||||||
|
amount: number;
|
||||||
|
valueDate: string; // YYYY-MM-DD
|
||||||
|
purposeCode: string;
|
||||||
|
leiCode?: string;
|
||||||
|
processingState: ProcessingState;
|
||||||
|
returnCode?: string;
|
||||||
|
returnReason?: string;
|
||||||
|
sentTimestamp: string; // YYYY-MM-DD HH:mm:ss
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportTab = 'trends' | 'bank' | 'date' | 'pending' | 'channel' | 'user';
|
||||||
|
|
||||||
|
interface BankRow {
|
||||||
|
bank: string;
|
||||||
|
count: number;
|
||||||
|
amount: number;
|
||||||
|
settled: number;
|
||||||
|
pending: number;
|
||||||
|
returned: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DateRow {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
amount: number;
|
||||||
|
neft: number;
|
||||||
|
rtgs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserRow {
|
||||||
|
user: string;
|
||||||
|
count: number;
|
||||||
|
amount: number;
|
||||||
|
branch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChannelRow {
|
||||||
|
channel: 'BRANCH' | 'MOBILE';
|
||||||
|
count: number;
|
||||||
|
amount: number;
|
||||||
|
share: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-neft-rtgs-report',
|
||||||
|
templateUrl: './neft-rtgs-report.component.html',
|
||||||
|
styleUrls: ['./neft-rtgs-report.component.scss'],
|
||||||
|
imports: [
|
||||||
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
|
FormsModule,
|
||||||
|
FaIconComponent,
|
||||||
|
MatTooltip
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class NeftRtgsReportComponent implements OnInit {
|
||||||
|
private dialog = inject(MatDialog);
|
||||||
|
|
||||||
|
/** Active tab */
|
||||||
|
activeTab: ReportTab = 'trends';
|
||||||
|
|
||||||
|
/** Search / filter */
|
||||||
|
search = '';
|
||||||
|
typeFilter: '' | PaymentType = '';
|
||||||
|
stateFilter: '' | ProcessingState = '';
|
||||||
|
|
||||||
|
/** All transactions (mock data — replace with API). */
|
||||||
|
txns: NeftRtgsTxn[] = [];
|
||||||
|
|
||||||
|
/** Tab labels */
|
||||||
|
readonly tabs: { id: ReportTab; label: string; icon: string }[] = [
|
||||||
|
{ id: 'trends', label: 'Trends', icon: 'chart-line' },
|
||||||
|
{ id: 'bank', label: 'Bank-wise', icon: 'university' },
|
||||||
|
{ id: 'date', label: 'Date-wise', icon: 'calendar-alt' },
|
||||||
|
{ id: 'pending', label: 'Pending / Pass / Return', icon: 'tasks' },
|
||||||
|
{ id: 'channel', label: 'Branch / Mobile', icon: 'mobile-alt' },
|
||||||
|
{ id: 'user', label: 'User-wise', icon: 'user-tie' }
|
||||||
|
];
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.txns = this.seedTransactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────── KPIs ─────────────────────
|
||||||
|
get totalCount(): number {
|
||||||
|
return this.txns.length;
|
||||||
|
}
|
||||||
|
get totalAmount(): number {
|
||||||
|
return this.txns.reduce((s, t) => s + t.amount, 0);
|
||||||
|
}
|
||||||
|
get settledCount(): number {
|
||||||
|
return this.txns.filter((t) => t.processingState === 'SETTLED').length;
|
||||||
|
}
|
||||||
|
get pendingCount(): number {
|
||||||
|
return this.txns.filter((t) => [
|
||||||
|
'UPLOADED',
|
||||||
|
'VALIDATED',
|
||||||
|
'SENT',
|
||||||
|
'PENDING'
|
||||||
|
].includes(t.processingState)).length;
|
||||||
|
}
|
||||||
|
get returnedCount(): number {
|
||||||
|
return this.txns.filter((t) => t.processingState === 'RETURNED').length;
|
||||||
|
}
|
||||||
|
get neftCount(): number {
|
||||||
|
return this.txns.filter((t) => t.paymentType === 'NEFT').length;
|
||||||
|
}
|
||||||
|
get rtgsCount(): number {
|
||||||
|
return this.txns.filter((t) => t.paymentType === 'RTGS').length;
|
||||||
|
}
|
||||||
|
get neftAmount(): number {
|
||||||
|
return this.txns.filter((t) => t.paymentType === 'NEFT').reduce((s, t) => s + t.amount, 0);
|
||||||
|
}
|
||||||
|
get rtgsAmount(): number {
|
||||||
|
return this.txns.filter((t) => t.paymentType === 'RTGS').reduce((s, t) => s + t.amount, 0);
|
||||||
|
}
|
||||||
|
get successRate(): number {
|
||||||
|
if (this.totalCount === 0) return 0;
|
||||||
|
return (this.settledCount / this.totalCount) * 100;
|
||||||
|
}
|
||||||
|
get avgTicket(): number {
|
||||||
|
if (this.totalCount === 0) return 0;
|
||||||
|
return this.totalAmount / this.totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────── Filtered list ─────────────────────
|
||||||
|
get filteredTxns(): NeftRtgsTxn[] {
|
||||||
|
const q = this.search.trim().toLowerCase();
|
||||||
|
return this.txns.filter((t) => {
|
||||||
|
if (this.typeFilter && t.paymentType !== this.typeFilter) return false;
|
||||||
|
if (this.stateFilter && t.processingState !== this.stateFilter) return false;
|
||||||
|
if (!q) return true;
|
||||||
|
return (
|
||||||
|
t.utrNumber.toLowerCase().includes(q) ||
|
||||||
|
t.batchId.toLowerCase().includes(q) ||
|
||||||
|
t.instructionId.toLowerCase().includes(q) ||
|
||||||
|
t.beneAcc.toLowerCase().includes(q) ||
|
||||||
|
t.beneIfsc.toLowerCase().includes(q) ||
|
||||||
|
t.beneName.toLowerCase().includes(q) ||
|
||||||
|
t.remitterAcc.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────── Tab-specific aggregations ─────────────────────
|
||||||
|
get bankRows(): BankRow[] {
|
||||||
|
const map = new Map<string, BankRow>();
|
||||||
|
for (const t of this.txns) {
|
||||||
|
const key = t.beneBank;
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { bank: key, count: 0, amount: 0, settled: 0, pending: 0, returned: 0 });
|
||||||
|
}
|
||||||
|
const r = map.get(key)!;
|
||||||
|
r.count++;
|
||||||
|
r.amount += t.amount;
|
||||||
|
if (t.processingState === 'SETTLED') r.settled++;
|
||||||
|
else if (t.processingState === 'RETURNED') r.returned++;
|
||||||
|
else r.pending++;
|
||||||
|
}
|
||||||
|
return Array.from(map.values()).sort((a, b) => b.amount - a.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
get dateRows(): DateRow[] {
|
||||||
|
const map = new Map<string, DateRow>();
|
||||||
|
for (const t of this.txns) {
|
||||||
|
if (!map.has(t.valueDate)) {
|
||||||
|
map.set(t.valueDate, { date: t.valueDate, count: 0, amount: 0, neft: 0, rtgs: 0 });
|
||||||
|
}
|
||||||
|
const r = map.get(t.valueDate)!;
|
||||||
|
r.count++;
|
||||||
|
r.amount += t.amount;
|
||||||
|
if (t.paymentType === 'NEFT') r.neft++;
|
||||||
|
else r.rtgs++;
|
||||||
|
}
|
||||||
|
return Array.from(map.values()).sort((a, b) => (a.date < b.date ? 1 : -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
get userRows(): UserRow[] {
|
||||||
|
const map = new Map<string, UserRow>();
|
||||||
|
for (const t of this.txns) {
|
||||||
|
const key = `${t.remitterUser}|${t.remitterBranch}`;
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { user: t.remitterUser, branch: t.remitterBranch, count: 0, amount: 0 });
|
||||||
|
}
|
||||||
|
const r = map.get(key)!;
|
||||||
|
r.count++;
|
||||||
|
r.amount += t.amount;
|
||||||
|
}
|
||||||
|
return Array.from(map.values()).sort((a, b) => b.amount - a.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
get channelRows(): ChannelRow[] {
|
||||||
|
const total = this.totalAmount || 1;
|
||||||
|
const map = new Map<'BRANCH' | 'MOBILE', ChannelRow>();
|
||||||
|
for (const t of this.txns) {
|
||||||
|
if (!map.has(t.remitterChannel)) {
|
||||||
|
map.set(t.remitterChannel, {
|
||||||
|
channel: t.remitterChannel,
|
||||||
|
count: 0,
|
||||||
|
amount: 0,
|
||||||
|
share: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const r = map.get(t.remitterChannel)!;
|
||||||
|
r.count++;
|
||||||
|
r.amount += t.amount;
|
||||||
|
}
|
||||||
|
map.forEach((r) => (r.share = (r.amount / total) * 100));
|
||||||
|
return Array.from(map.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Trend buckets — last 7 value-dates */
|
||||||
|
get trendBuckets(): DateRow[] {
|
||||||
|
return this.dateRows.slice(0, 7).reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Max amount across trend buckets (for bar chart scaling). */
|
||||||
|
get trendMaxAmount(): number {
|
||||||
|
const buckets = this.trendBuckets;
|
||||||
|
if (!buckets.length) return 1;
|
||||||
|
return Math.max(...buckets.map((b) => b.amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return-code breakdown for Pending/Pass/Return tab */
|
||||||
|
get returnCodeRows(): { code: string; reason: string; count: number; amount: number }[] {
|
||||||
|
const map = new Map<string, { code: string; reason: string; count: number; amount: number }>();
|
||||||
|
for (const t of this.txns) {
|
||||||
|
if (t.processingState !== 'RETURNED' || !t.returnCode) continue;
|
||||||
|
if (!map.has(t.returnCode)) {
|
||||||
|
map.set(t.returnCode, {
|
||||||
|
code: t.returnCode,
|
||||||
|
reason: t.returnReason || '',
|
||||||
|
count: 0,
|
||||||
|
amount: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const r = map.get(t.returnCode)!;
|
||||||
|
r.count++;
|
||||||
|
r.amount += t.amount;
|
||||||
|
}
|
||||||
|
return Array.from(map.values()).sort((a, b) => b.count - a.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────── Actions ─────────────────────
|
||||||
|
showTab(tab: ReportTab): void {
|
||||||
|
this.activeTab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Floating action — open NEFT/RTGS entry in a non-dismissible dialog. */
|
||||||
|
openInitiateDialog(): void {
|
||||||
|
const ref = this.dialog.open(NeftRtgsEntryComponent, {
|
||||||
|
width: '880px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
maxHeight: '88vh',
|
||||||
|
disableClose: true,
|
||||||
|
autoFocus: false,
|
||||||
|
restoreFocus: true,
|
||||||
|
panelClass: 'nr-entry-dialog',
|
||||||
|
backdropClass: 'nr-entry-backdrop'
|
||||||
|
});
|
||||||
|
ref.afterClosed().subscribe((result) => {
|
||||||
|
if (result && result.submitted && result.payload) {
|
||||||
|
this.txns = [
|
||||||
|
this.toTxn(result.payload),
|
||||||
|
...this.txns
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exportCsv(): void {
|
||||||
|
const headers = [
|
||||||
|
'Batch ID',
|
||||||
|
'Instruction ID',
|
||||||
|
'UTR Number',
|
||||||
|
'Remitter Acc',
|
||||||
|
'Bene Acc No',
|
||||||
|
'Bene IFSC',
|
||||||
|
'Bene Name',
|
||||||
|
'Payment Type',
|
||||||
|
'Amount',
|
||||||
|
'Value Date',
|
||||||
|
'Purpose Code',
|
||||||
|
'LEI Code',
|
||||||
|
'Processing State',
|
||||||
|
'Return Code',
|
||||||
|
'Sent Timestamp'
|
||||||
|
];
|
||||||
|
const rows: string[][] = [headers];
|
||||||
|
for (const t of this.filteredTxns) {
|
||||||
|
rows.push([
|
||||||
|
t.batchId,
|
||||||
|
t.instructionId,
|
||||||
|
t.utrNumber,
|
||||||
|
t.remitterAcc,
|
||||||
|
t.beneAcc,
|
||||||
|
t.beneIfsc,
|
||||||
|
t.beneName,
|
||||||
|
t.paymentType,
|
||||||
|
t.amount.toFixed(2),
|
||||||
|
t.valueDate,
|
||||||
|
t.purposeCode,
|
||||||
|
t.leiCode || '',
|
||||||
|
t.processingState,
|
||||||
|
t.returnCode || '',
|
||||||
|
t.sentTimestamp
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
const escape = (v: string): string => {
|
||||||
|
const s = (v || '').replace(/"/g, '""');
|
||||||
|
return /[,"\n]/.test(s) ? `"${s}"` : s;
|
||||||
|
};
|
||||||
|
const csv = rows.map((r) => r.map(escape).join(',')).join('\n') + '\n';
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `NEFT_RTGS_Report_${new Date().toISOString().slice(0, 10)}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFilters(): void {
|
||||||
|
this.search = '';
|
||||||
|
this.typeFilter = '';
|
||||||
|
this.stateFilter = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────── Helpers ─────────────────────
|
||||||
|
stateColor(state: ProcessingState): string {
|
||||||
|
switch (state) {
|
||||||
|
case 'SETTLED':
|
||||||
|
return 'green';
|
||||||
|
case 'RETURNED':
|
||||||
|
return 'red';
|
||||||
|
case 'SENT':
|
||||||
|
return 'blue';
|
||||||
|
case 'VALIDATED':
|
||||||
|
return 'teal';
|
||||||
|
case 'UPLOADED':
|
||||||
|
return 'amber';
|
||||||
|
case 'PENDING':
|
||||||
|
return 'amber';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trendBarHeight(amount: number): number {
|
||||||
|
return Math.max(6, Math.round((amount / this.trendMaxAmount) * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a submitted entry-form payload into a synthetic transaction row. */
|
||||||
|
private toTxn(p: any): NeftRtgsTxn {
|
||||||
|
const valueDate = p.valueDate
|
||||||
|
? new Date(p.valueDate).toISOString().slice(0, 10)
|
||||||
|
: new Date().toISOString().slice(0, 10);
|
||||||
|
const ts = new Date();
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return {
|
||||||
|
batchId: `H2H-${valueDate.replace(/-/g, '')}-${pad(Math.floor(Math.random() * 99) + 1)}`,
|
||||||
|
instructionId: 'ERP' + Date.now().toString().slice(-8),
|
||||||
|
utrNumber: 'KKBK' + Date.now().toString().slice(-11),
|
||||||
|
remitterAcc: p.senderAccount || '',
|
||||||
|
remitterBranch: 'Main Branch',
|
||||||
|
remitterUser: 'Current User',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: p.beneficiaryAccount || '',
|
||||||
|
beneIfsc: (p.ifscCode || '').toUpperCase(),
|
||||||
|
beneName: p.beneficiaryName || '',
|
||||||
|
beneBank: this.bankFromIfsc(p.ifscCode || ''),
|
||||||
|
paymentType: p.transactionType,
|
||||||
|
amount: Number(p.amount) || 0,
|
||||||
|
valueDate,
|
||||||
|
purposeCode: this.purposeCodeFor(p.purposeOfRemittance),
|
||||||
|
leiCode: Number(p.amount) >= 500000000 ? '549300ABCDEF1234' : undefined,
|
||||||
|
processingState: 'UPLOADED',
|
||||||
|
sentTimestamp:
|
||||||
|
ts.getFullYear() +
|
||||||
|
'-' +
|
||||||
|
pad(ts.getMonth() + 1) +
|
||||||
|
'-' +
|
||||||
|
pad(ts.getDate()) +
|
||||||
|
' ' +
|
||||||
|
pad(ts.getHours()) +
|
||||||
|
':' +
|
||||||
|
pad(ts.getMinutes()) +
|
||||||
|
':' +
|
||||||
|
pad(ts.getSeconds())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private bankFromIfsc(ifsc: string): string {
|
||||||
|
const code = ifsc.slice(0, 4).toUpperCase();
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
HDFC: 'HDFC Bank',
|
||||||
|
ICIC: 'ICICI Bank',
|
||||||
|
SBIN: 'State Bank of India',
|
||||||
|
KKBK: 'Kotak Mahindra Bank',
|
||||||
|
AXIS: 'Axis Bank',
|
||||||
|
UTIB: 'Axis Bank',
|
||||||
|
PUNB: 'Punjab National Bank',
|
||||||
|
BARB: 'Bank of Baroda',
|
||||||
|
CNRB: 'Canara Bank',
|
||||||
|
YESB: 'YES Bank',
|
||||||
|
IDFB: 'IDFC FIRST Bank'
|
||||||
|
};
|
||||||
|
return map[code] || code + ' Bank';
|
||||||
|
}
|
||||||
|
|
||||||
|
private purposeCodeFor(p: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
CAPITAL_TRANSFER: 'P0102',
|
||||||
|
FAMILY_MAINTENANCE: 'P1301',
|
||||||
|
TRADE_SETTLEMENT: 'P0801',
|
||||||
|
SALARY_PAYMENT: 'P1006',
|
||||||
|
LOAN_REPAYMENT: 'P0103',
|
||||||
|
TAX_PAYMENT: 'P1099',
|
||||||
|
VENDOR_PAYMENT: 'P0802',
|
||||||
|
INVESTMENT: 'P0202',
|
||||||
|
GIFT: 'P1302',
|
||||||
|
OTHER: 'P0099'
|
||||||
|
};
|
||||||
|
return map[p] || 'P0099';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────── Mock seed data ─────────────────────
|
||||||
|
private seedTransactions(): NeftRtgsTxn[] {
|
||||||
|
const today = new Date();
|
||||||
|
const dt = (offset: number, h = 10, m = 0): string => {
|
||||||
|
const d = new Date(today);
|
||||||
|
d.setDate(d.getDate() - offset);
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return (
|
||||||
|
d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(h) + ':' + pad(m) + ':00'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const vd = (offset: number): string => dt(offset).slice(0, 10);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260326-01',
|
||||||
|
instructionId: 'ERP77889922',
|
||||||
|
utrNumber: 'KKBKR5202608511',
|
||||||
|
remitterAcc: '50200012345678',
|
||||||
|
remitterBranch: 'Mumbai — BKC',
|
||||||
|
remitterUser: 'Anita Desai',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '91000222333444',
|
||||||
|
beneIfsc: 'HDFC0001234',
|
||||||
|
beneName: 'Global Logistics Ltd',
|
||||||
|
beneBank: 'HDFC Bank',
|
||||||
|
paymentType: 'RTGS',
|
||||||
|
amount: 550000,
|
||||||
|
valueDate: vd(0),
|
||||||
|
purposeCode: 'P0102',
|
||||||
|
leiCode: '549300ABCDEF1234',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(0, 14, 5)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260326-01',
|
||||||
|
instructionId: 'ERP77889923',
|
||||||
|
utrNumber: 'HDFCR5202608712',
|
||||||
|
remitterAcc: '50200012345678',
|
||||||
|
remitterBranch: 'Mumbai — BKC',
|
||||||
|
remitterUser: 'Anita Desai',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '345678901234',
|
||||||
|
beneIfsc: 'ICIC0000567',
|
||||||
|
beneName: 'Bluestone Trading Co.',
|
||||||
|
beneBank: 'ICICI Bank',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 78500,
|
||||||
|
valueDate: vd(0),
|
||||||
|
purposeCode: 'P0802',
|
||||||
|
processingState: 'SENT',
|
||||||
|
sentTimestamp: dt(0, 14, 6)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260325-04',
|
||||||
|
instructionId: 'ERP77889811',
|
||||||
|
utrNumber: 'SBINR5202608902',
|
||||||
|
remitterAcc: '50200099887766',
|
||||||
|
remitterBranch: 'Bengaluru — MG Road',
|
||||||
|
remitterUser: 'Karthik Rao',
|
||||||
|
remitterChannel: 'MOBILE',
|
||||||
|
beneAcc: '912345678901',
|
||||||
|
beneIfsc: 'SBIN0011223',
|
||||||
|
beneName: 'Anita Verma',
|
||||||
|
beneBank: 'State Bank of India',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 15000,
|
||||||
|
valueDate: vd(1),
|
||||||
|
purposeCode: 'P1301',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(1, 11, 22)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260325-04',
|
||||||
|
instructionId: 'ERP77889812',
|
||||||
|
utrNumber: 'AXISR5202608811',
|
||||||
|
remitterAcc: '50200099887766',
|
||||||
|
remitterBranch: 'Bengaluru — MG Road',
|
||||||
|
remitterUser: 'Karthik Rao',
|
||||||
|
remitterChannel: 'MOBILE',
|
||||||
|
beneAcc: '6789012345',
|
||||||
|
beneIfsc: 'UTIB0000456',
|
||||||
|
beneName: 'Rakesh Industries',
|
||||||
|
beneBank: 'Axis Bank',
|
||||||
|
paymentType: 'RTGS',
|
||||||
|
amount: 1250000,
|
||||||
|
valueDate: vd(1),
|
||||||
|
purposeCode: 'P0801',
|
||||||
|
leiCode: '549300XYZAB7890',
|
||||||
|
processingState: 'RETURNED',
|
||||||
|
returnCode: 'R01',
|
||||||
|
returnReason: 'Insufficient Funds',
|
||||||
|
sentTimestamp: dt(1, 11, 25)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260324-02',
|
||||||
|
instructionId: 'ERP77889701',
|
||||||
|
utrNumber: 'PUNBR5202608619',
|
||||||
|
remitterAcc: '50100221122334',
|
||||||
|
remitterBranch: 'Delhi — Connaught',
|
||||||
|
remitterUser: 'Sushma Iyer',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '50100998877665',
|
||||||
|
beneIfsc: 'PUNB0123400',
|
||||||
|
beneName: 'Madhavi Singh',
|
||||||
|
beneBank: 'Punjab National Bank',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 42000,
|
||||||
|
valueDate: vd(2),
|
||||||
|
purposeCode: 'P1006',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(2, 9, 35)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260324-02',
|
||||||
|
instructionId: 'ERP77889702',
|
||||||
|
utrNumber: 'YESBR5202608520',
|
||||||
|
remitterAcc: '50100221122334',
|
||||||
|
remitterBranch: 'Delhi — Connaught',
|
||||||
|
remitterUser: 'Sushma Iyer',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '11223344556677',
|
||||||
|
beneIfsc: 'YESB0000123',
|
||||||
|
beneName: 'Sigma Constructions',
|
||||||
|
beneBank: 'YES Bank',
|
||||||
|
paymentType: 'RTGS',
|
||||||
|
amount: 825000,
|
||||||
|
valueDate: vd(2),
|
||||||
|
purposeCode: 'P0102',
|
||||||
|
processingState: 'PENDING',
|
||||||
|
sentTimestamp: dt(2, 9, 38)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260323-07',
|
||||||
|
instructionId: 'ERP77889601',
|
||||||
|
utrNumber: 'CNRBR5202608417',
|
||||||
|
remitterAcc: '50100887766554',
|
||||||
|
remitterBranch: 'Chennai — T. Nagar',
|
||||||
|
remitterUser: 'Ramesh Krishnan',
|
||||||
|
remitterChannel: 'MOBILE',
|
||||||
|
beneAcc: '99887766554433',
|
||||||
|
beneIfsc: 'CNRB0001890',
|
||||||
|
beneName: 'Padma Rao',
|
||||||
|
beneBank: 'Canara Bank',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 9800,
|
||||||
|
valueDate: vd(3),
|
||||||
|
purposeCode: 'P1302',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(3, 16, 15)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260323-07',
|
||||||
|
instructionId: 'ERP77889602',
|
||||||
|
utrNumber: 'BARBR5202608318',
|
||||||
|
remitterAcc: '50100887766554',
|
||||||
|
remitterBranch: 'Chennai — T. Nagar',
|
||||||
|
remitterUser: 'Ramesh Krishnan',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '50100123456987',
|
||||||
|
beneIfsc: 'BARB0VJVATA',
|
||||||
|
beneName: 'Vijaya Traders',
|
||||||
|
beneBank: 'Bank of Baroda',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 125000,
|
||||||
|
valueDate: vd(3),
|
||||||
|
purposeCode: 'P0802',
|
||||||
|
processingState: 'VALIDATED',
|
||||||
|
sentTimestamp: dt(3, 16, 20)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260322-03',
|
||||||
|
instructionId: 'ERP77889501',
|
||||||
|
utrNumber: 'IDFBR5202608215',
|
||||||
|
remitterAcc: '50200445566778',
|
||||||
|
remitterBranch: 'Pune — Koregaon Park',
|
||||||
|
remitterUser: 'Vikram Joshi',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '70010023456789',
|
||||||
|
beneIfsc: 'IDFB0040101',
|
||||||
|
beneName: 'Pinnacle Realty Pvt Ltd',
|
||||||
|
beneBank: 'IDFC FIRST Bank',
|
||||||
|
paymentType: 'RTGS',
|
||||||
|
amount: 3450000,
|
||||||
|
valueDate: vd(4),
|
||||||
|
purposeCode: 'P0202',
|
||||||
|
leiCode: '549300QRSTUV5678',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(4, 12, 45)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260322-03',
|
||||||
|
instructionId: 'ERP77889502',
|
||||||
|
utrNumber: 'HDFCR5202608116',
|
||||||
|
remitterAcc: '50200445566778',
|
||||||
|
remitterBranch: 'Pune — Koregaon Park',
|
||||||
|
remitterUser: 'Vikram Joshi',
|
||||||
|
remitterChannel: 'MOBILE',
|
||||||
|
beneAcc: '50100654321789',
|
||||||
|
beneIfsc: 'HDFC0009876',
|
||||||
|
beneName: 'Neha Kapoor',
|
||||||
|
beneBank: 'HDFC Bank',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 25000,
|
||||||
|
valueDate: vd(4),
|
||||||
|
purposeCode: 'P1301',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(4, 12, 50)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260321-05',
|
||||||
|
instructionId: 'ERP77889411',
|
||||||
|
utrNumber: 'KKBKR5202608013',
|
||||||
|
remitterAcc: '50200778899001',
|
||||||
|
remitterBranch: 'Hyderabad — Banjara',
|
||||||
|
remitterUser: 'Lakshmi Naidu',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '12345678900011',
|
||||||
|
beneIfsc: 'KKBK0001899',
|
||||||
|
beneName: 'Aarav Enterprises',
|
||||||
|
beneBank: 'Kotak Mahindra Bank',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 67000,
|
||||||
|
valueDate: vd(5),
|
||||||
|
purposeCode: 'P0802',
|
||||||
|
processingState: 'RETURNED',
|
||||||
|
returnCode: 'R03',
|
||||||
|
returnReason: 'Account Closed',
|
||||||
|
sentTimestamp: dt(5, 10, 5)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260321-05',
|
||||||
|
instructionId: 'ERP77889412',
|
||||||
|
utrNumber: 'SBINR5202607914',
|
||||||
|
remitterAcc: '50200778899001',
|
||||||
|
remitterBranch: 'Hyderabad — Banjara',
|
||||||
|
remitterUser: 'Lakshmi Naidu',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '50100445566778',
|
||||||
|
beneIfsc: 'SBIN0008765',
|
||||||
|
beneName: 'Spectra Components',
|
||||||
|
beneBank: 'State Bank of India',
|
||||||
|
paymentType: 'RTGS',
|
||||||
|
amount: 980000,
|
||||||
|
valueDate: vd(5),
|
||||||
|
purposeCode: 'P0801',
|
||||||
|
leiCode: '549300LMNOPQ2468',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(5, 10, 10)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260320-06',
|
||||||
|
instructionId: 'ERP77889301',
|
||||||
|
utrNumber: 'ICICR5202607812',
|
||||||
|
remitterAcc: '50100334455667',
|
||||||
|
remitterBranch: 'Kolkata — Park Street',
|
||||||
|
remitterUser: 'Arjun Banerjee',
|
||||||
|
remitterChannel: 'MOBILE',
|
||||||
|
beneAcc: '88990011223344',
|
||||||
|
beneIfsc: 'ICIC0001234',
|
||||||
|
beneName: 'Eastern Logistics',
|
||||||
|
beneBank: 'ICICI Bank',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 188000,
|
||||||
|
valueDate: vd(6),
|
||||||
|
purposeCode: 'P0802',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(6, 14, 30)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchId: 'H2H-20260320-06',
|
||||||
|
instructionId: 'ERP77889302',
|
||||||
|
utrNumber: 'AXISR5202607711',
|
||||||
|
remitterAcc: '50100334455667',
|
||||||
|
remitterBranch: 'Kolkata — Park Street',
|
||||||
|
remitterUser: 'Arjun Banerjee',
|
||||||
|
remitterChannel: 'BRANCH',
|
||||||
|
beneAcc: '50200998877665',
|
||||||
|
beneIfsc: 'UTIB0001234',
|
||||||
|
beneName: 'Indrani Sen',
|
||||||
|
beneBank: 'Axis Bank',
|
||||||
|
paymentType: 'NEFT',
|
||||||
|
amount: 5500,
|
||||||
|
valueDate: vd(6),
|
||||||
|
purposeCode: 'P1302',
|
||||||
|
processingState: 'SETTLED',
|
||||||
|
sentTimestamp: dt(6, 14, 32)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,15 +317,11 @@ export class TwoFactorAuthenticationComponent implements OnInit, OnDestroy {
|
|||||||
res?.errorMessage || 'DSC verified, but the verification token could not be claimed.';
|
res?.errorMessage || 'DSC verified, but the verification token could not be claimed.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Reuse the existing /twofactor/validate pipeline; backend treats the
|
// The token returned by /claim is already a valid TFAccessToken (DscLoginChallengeService
|
||||||
// DSC-issued bearer token the same as an OTP code.
|
// mints it directly into tf_access_token). The OTP-shaped /twofactor/validate path would
|
||||||
this.authenticationService.validateOTP(token).subscribe({
|
// 4xx here because there's no in-memory OTPRequest to exchange — just run the finalise-
|
||||||
error: (err) => {
|
// login bookkeeping with the token we already have.
|
||||||
this.dscFailed = true;
|
this.authenticationService.applyTwoFactorToken(token, res?.tfaTokenLiveSeconds || 0);
|
||||||
this.dscErrorMessage =
|
|
||||||
err?.error?.defaultUserMessage || err?.error?.message || err?.message || 'Token rejected by server.';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.dscFailed = true;
|
this.dscFailed = true;
|
||||||
|
|||||||
@@ -188,7 +188,11 @@ import {
|
|||||||
faCube,
|
faCube,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faStream,
|
faStream,
|
||||||
faThLarge
|
faThLarge,
|
||||||
|
faBrain,
|
||||||
|
faRobot,
|
||||||
|
faMicrochip,
|
||||||
|
faMagic
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -383,7 +387,11 @@ export class IconsModule {
|
|||||||
faCube,
|
faCube,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faStream,
|
faStream,
|
||||||
faThLarge
|
faThLarge,
|
||||||
|
faBrain,
|
||||||
|
faRobot,
|
||||||
|
faMicrochip,
|
||||||
|
faMagic
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,36 @@
|
|||||||
matBadgeSize="medium"
|
matBadgeSize="medium"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{ 'tooltips.Notifications' | translate }}</span>
|
<span class="sr-only">{{ 'tooltips.Notifications' | translate }}</span>
|
||||||
<fa-icon icon="bell" size="lg"></fa-icon>
|
<!-- Custom colorful bell mark — same size language as the AI icon -->
|
||||||
|
<svg class="t-notif-svg" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="cbsNotifBell" x1="32" y1="14" x2="32" y2="50" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#fde68a" />
|
||||||
|
<stop offset="55%" stop-color="#fbbf24" />
|
||||||
|
<stop offset="100%" stop-color="#f59e0b" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<!-- top loop / handle -->
|
||||||
|
<circle cx="32" cy="12" r="3.2" fill="#b45309" />
|
||||||
|
<!-- bell body -->
|
||||||
|
<path
|
||||||
|
d="M32 15 c-10 0 -15 7 -15 17 v8 c0 1 -1 2 -2 2 h-2 v3 h38 v-3 h-2 c-1 0 -2 -1 -2 -2 v-8 c0 -10 -5 -17 -15 -17 z"
|
||||||
|
fill="url(#cbsNotifBell)"
|
||||||
|
stroke="#b45309"
|
||||||
|
stroke-width="1.4"
|
||||||
|
/>
|
||||||
|
<!-- inside highlight -->
|
||||||
|
<path
|
||||||
|
d="M22 23 c-2 4 -2 9 -2 12"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="2.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.55"
|
||||||
|
/>
|
||||||
|
<!-- clapper -->
|
||||||
|
<circle cx="32" cy="52" r="3.6" fill="#b45309" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<mat-menu
|
<mat-menu
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { MatIconButton } from '@angular/material/button';
|
|||||||
import { MatTooltip } from '@angular/material/tooltip';
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
|
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
|
||||||
import { MatBadge } from '@angular/material/badge';
|
import { MatBadge } from '@angular/material/badge';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
@@ -38,7 +37,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatMenuTrigger,
|
MatMenuTrigger,
|
||||||
MatBadge,
|
MatBadge,
|
||||||
FaIconComponent,
|
|
||||||
MatMenu,
|
MatMenu,
|
||||||
MatIcon,
|
MatIcon,
|
||||||
MatMenuItem
|
MatMenuItem
|
||||||
|
|||||||
@@ -1136,8 +1136,8 @@
|
|||||||
"Extend Repayment Period": "Extend Repayment Period",
|
"Extend Repayment Period": "Extend Repayment Period",
|
||||||
"External Asset Owner": "External Asset Owner",
|
"External Asset Owner": "External Asset Owner",
|
||||||
"External Services": "External Services",
|
"External Services": "External Services",
|
||||||
"Family Member": "Nominee",
|
"Family Member": "Family Member",
|
||||||
"Family Members": "Nominee",
|
"Family Members": "Family Members",
|
||||||
"Fee Amount": "Fee Amount",
|
"Fee Amount": "Fee Amount",
|
||||||
"Fees to Specific Income Accounts": "Fees to Specific Income Accounts",
|
"Fees to Specific Income Accounts": "Fees to Specific Income Accounts",
|
||||||
"Filter holidays": "Search bar to filter holidays based on different offices.",
|
"Filter holidays": "Search bar to filter holidays based on different offices.",
|
||||||
@@ -1974,7 +1974,7 @@
|
|||||||
"FCM End Point": "FCM End Point",
|
"FCM End Point": "FCM End Point",
|
||||||
"Failed to load vendors": "Failed to load vendors",
|
"Failed to load vendors": "Failed to load vendors",
|
||||||
"Failure Count": "Failure Count",
|
"Failure Count": "Failure Count",
|
||||||
"Family Members": "Nominee",
|
"Family Members": "Family Members",
|
||||||
"Father": "Father",
|
"Father": "Father",
|
||||||
"Father's Name": "Father's Name",
|
"Father's Name": "Father's Name",
|
||||||
"Favicon": "Favicon",
|
"Favicon": "Favicon",
|
||||||
@@ -3583,8 +3583,8 @@
|
|||||||
"Failed to load complaints": "Failed to load complaints",
|
"Failed to load complaints": "Failed to load complaints",
|
||||||
"Failed to search clients. Please try again.": "Failed to search clients. Please try again.",
|
"Failed to search clients. Please try again.": "Failed to search clients. Please try again.",
|
||||||
"Failed to submit action": "Failed to submit action",
|
"Failed to submit action": "Failed to submit action",
|
||||||
"Family Member View": "Nominee View",
|
"Family Member View": "Family Member View",
|
||||||
"Family Members": "Nominee",
|
"Family Members": "Family Members",
|
||||||
"Filling Details": "Click to start filling the details. * mark fields are necessary. For more details click:",
|
"Filling Details": "Click to start filling the details. * mark fields are necessary. For more details click:",
|
||||||
"Filter by name": "Filter by name",
|
"Filter by name": "Filter by name",
|
||||||
"FilterByLoanIdOrError": "Filter by loan Id or error",
|
"FilterByLoanIdOrError": "Filter by loan Id or error",
|
||||||
|
|||||||
@@ -5,13 +5,12 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
|
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260511',
|
'version': '260514',
|
||||||
'hash': 'g3b752dc6a'
|
'hash': 'g671489768'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -994,6 +994,29 @@ mat-card {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── NEFT / RTGS Entry Dialog Panel ─── */
|
||||||
|
.nr-entry-dialog {
|
||||||
|
.mat-mdc-dialog-container {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-dialog-surface {
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
box-shadow:
|
||||||
|
0 24px 48px rgb(15 23 42 / 25%),
|
||||||
|
0 12px 24px rgb(15 23 42 / 15%) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NEFT / RTGS Entry Dialog Backdrop — darker so the report behind dims out */
|
||||||
|
.nr-entry-backdrop {
|
||||||
|
background: rgb(15 23 42 / 55%);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Add Beneficiary Dialog Panel ─── */
|
/* ─── Add Beneficiary Dialog Panel ─── */
|
||||||
.abd-dialog-panel {
|
.abd-dialog-panel {
|
||||||
.mat-mdc-dialog-container {
|
.mat-mdc-dialog-container {
|
||||||
|
|||||||
+8
-1
@@ -21,7 +21,14 @@ gitInfo.version = moment().format('YYMMDD');
|
|||||||
const file = resolve(__dirname, '.', 'src', 'environments', '.env.ts');
|
const file = resolve(__dirname, '.', 'src', 'environments', '.env.ts');
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
file,
|
file,
|
||||||
`// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
|
`/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
|
|||||||
Reference in New Issue
Block a user