Merge branch 'feature/account-charges' of https://git.zeptic.in/CBSNexora/Angular-UI-CBS-Main into feature/account-charges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,7 +66,14 @@
|
||||
"Bash(node node_modules/prettier/bin/prettier.cjs --write \"src/**/*.{ts,html,scss}\")",
|
||||
"Bash(node node_modules/stylelint/bin/stylelint.mjs --fix \"src/**/*.scss\")",
|
||||
"Bash(node node_modules/stylelint/bin/stylelint.mjs \"src/**/*.scss\")",
|
||||
"Bash(node node_modules/eslint/bin/eslint.js --cache \"src/app/users/users.component.ts\")"
|
||||
"Bash(node node_modules/eslint/bin/eslint.js --cache \"src/app/users/users.component.ts\")",
|
||||
"Bash(git stash *)",
|
||||
"Bash(npx sass *)",
|
||||
"Bash(awk '/library\\\\.addIcons/{p=1} p' src/app/shared/icons.module.ts)",
|
||||
"Bash(echo \"EXIT=$?\")",
|
||||
"Bash(npx lint-staged *)",
|
||||
"Bash(npx --no-install tsc --noEmit -p tsconfig.json)",
|
||||
"Bash(npx --no-install prettier --write src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html src/app/clients/edit-client/edit-client.component.html src/app/clients/utils/family-nominee.guard.ts)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/* Shared Nexora skin for CFCFRMS form pages (escalate, report-suspect,
|
||||
report-mule, report-fraud-txn, fmr-submission). Each component @use's
|
||||
this partial and @include's `cfc-form-skin($accent)`.
|
||||
|
||||
$accent: 'blue' | 'red' — controls the title-bar gradient + submit
|
||||
button colour. */
|
||||
|
||||
@mixin cfc-form-skin($accent: 'blue') {
|
||||
$accent-from: #3b82f6;
|
||||
$accent-to: #1e40af;
|
||||
$accent-shadow: rgb(30 64 175 / 30%);
|
||||
$submit-from: #1e88e5;
|
||||
$submit-to: #1e40af;
|
||||
|
||||
@if $accent == 'red' {
|
||||
$accent-from: #ef4444;
|
||||
$accent-to: #991b1b;
|
||||
$accent-shadow: rgb(153 27 27 / 35%);
|
||||
$submit-from: #dc2626;
|
||||
$submit-to: #991b1b;
|
||||
}
|
||||
|
||||
/* ─── Card chrome (overrides Material's mat-card) ─────────────────── */
|
||||
.report-card,
|
||||
.escalate-card,
|
||||
.instructions-card {
|
||||
border-radius: 14px !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%) !important;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
::ng-deep .mat-mdc-card-header {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
display: block;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-card-title {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0 !important;
|
||||
padding: 14px 18px;
|
||||
font-size: 14.5px !important;
|
||||
font-weight: 700 !important;
|
||||
color: #0d1b2a;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1;
|
||||
|
||||
/* Promote the FA icon into a small gradient tile. */
|
||||
.fa-icon,
|
||||
fa-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, $accent-from 0%, $accent-to 100%);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 2px 6px -2px $accent-shadow;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-card-content {
|
||||
padding: 22px !important;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Form fields (consistent compact density) ────────────────────── */
|
||||
.full-width,
|
||||
.half-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Tighten the default Material outlined field — the dashboard hero +
|
||||
pill tabs are compact, so the form fields should match. */
|
||||
::ng-deep .mat-mdc-form-field {
|
||||
margin-bottom: 2px;
|
||||
|
||||
.mat-mdc-text-field-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.mdc-notched-outline__leading,
|
||||
.mdc-notched-outline__notch,
|
||||
.mdc-notched-outline__trailing {
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
&.mat-focused {
|
||||
.mdc-notched-outline__leading,
|
||||
.mdc-notched-outline__notch,
|
||||
.mdc-notched-outline__trailing {
|
||||
border-color: $accent-to !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-infix {
|
||||
min-height: 48px;
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-icon-suffix {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Datepicker icon size + tone */
|
||||
::ng-deep .mat-datepicker-toggle .mat-mdc-icon-button {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* ─── Form layout helpers ────────────────────────────────────────── */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
|
||||
@media (width <= 720px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-lookup-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
|
||||
.ticket-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lookup-btn {
|
||||
height: 48px !important;
|
||||
margin-top: 0;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Submit button ───────────────────────────────────────────────── */
|
||||
.submit-btn {
|
||||
margin-top: 10px;
|
||||
height: 46px !important;
|
||||
border-radius: 12px !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 0.02em !important;
|
||||
background: linear-gradient(135deg, $submit-from 0%, $submit-to 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px $accent-shadow !important;
|
||||
transition:
|
||||
filter 0.15s ease,
|
||||
transform 0.1s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ─── Success result panel ───────────────────────────────────────── */
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 14px 0 6px;
|
||||
|
||||
.success-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #34d399 0%, #047857 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 22px -8px rgb(4 120 87 / 50%);
|
||||
|
||||
fa-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 18px;
|
||||
color: #065f46;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
.result-details {
|
||||
text-align: left;
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed #bbf7d0;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
text-align: right;
|
||||
|
||||
&.mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
color: #1e40af;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-flagged {
|
||||
color: #b91c1c !important;
|
||||
font-weight: 800 !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Two-column "form + side panel" layout ─────────────────────────── */
|
||||
@mixin cfc-side-layout {
|
||||
.report-layout,
|
||||
.escalate-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
|
||||
/* Promote to two-column once the right-hand panel exists (escalate). */
|
||||
&:has(.instructions-card) {
|
||||
grid-template-columns: minmax(0, 1fr) 360px;
|
||||
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { Route } from '../core/route/route.service';
|
||||
import { CfcfrmsShellComponent } from './cfcfrms-shell/cfcfrms-shell.component';
|
||||
import { CfcfrmsDashboardComponent } from './dashboard/cfcfrms-dashboard.component';
|
||||
import { ComplaintListComponent } from './complaint-list/complaint-list.component';
|
||||
import { ComplaintDetailComponent } from './complaint-detail/complaint-detail.component';
|
||||
@@ -23,6 +24,7 @@ const routes: Routes = [
|
||||
Route.withShell([
|
||||
{
|
||||
path: 'cfcfrms',
|
||||
component: CfcfrmsShellComponent,
|
||||
data: { title: 'CFCFRMS', breadcrumb: 'CFCFRMS' },
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<!--
|
||||
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="cfc-container">
|
||||
<!-- Hero header -->
|
||||
<div class="cfc-header">
|
||||
<div class="cfc-header__left">
|
||||
<fa-icon icon="user-shield" class="cfc-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cfc-header__title">CFCFRMS</h2>
|
||||
<div class="cfc-header__sub">
|
||||
Cyber Fraud Complaint Management & Reporting — track complaints, escalate, and report suspect / mule
|
||||
/ fraud transactions.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pill-style tab navigation -->
|
||||
<nav mat-tab-nav-bar class="cfc-tabs" [tabPanel]="tabPanel">
|
||||
<a
|
||||
mat-tab-link
|
||||
class="cfc-tab"
|
||||
[routerLink]="['./dashboard']"
|
||||
routerLinkActive
|
||||
#dashboard="routerLinkActive"
|
||||
[active]="dashboard.isActive"
|
||||
>
|
||||
<fa-icon icon="chart-line" class="cfc-tab__ic"></fa-icon>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="cfc-tab"
|
||||
[routerLink]="['./complaints']"
|
||||
routerLinkActive
|
||||
#complaints="routerLinkActive"
|
||||
[active]="complaints.isActive"
|
||||
>
|
||||
<fa-icon icon="list-alt" class="cfc-tab__ic"></fa-icon>
|
||||
<span>Complaints</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="cfc-tab"
|
||||
[routerLink]="['./escalate']"
|
||||
routerLinkActive
|
||||
#escalate="routerLinkActive"
|
||||
[active]="escalate.isActive"
|
||||
>
|
||||
<fa-icon icon="arrow-up" class="cfc-tab__ic"></fa-icon>
|
||||
<span>Escalate</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="cfc-tab cfc-tab--danger"
|
||||
[routerLink]="['./report-suspect']"
|
||||
routerLinkActive
|
||||
#suspect="routerLinkActive"
|
||||
[active]="suspect.isActive"
|
||||
>
|
||||
<fa-icon icon="user-slash" class="cfc-tab__ic"></fa-icon>
|
||||
<span>Report Suspect</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="cfc-tab cfc-tab--danger"
|
||||
[routerLink]="['./report-mule']"
|
||||
routerLinkActive
|
||||
#mule="routerLinkActive"
|
||||
[active]="mule.isActive"
|
||||
>
|
||||
<fa-icon icon="ban" class="cfc-tab__ic"></fa-icon>
|
||||
<span>Report Mule</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="cfc-tab cfc-tab--danger"
|
||||
[routerLink]="['./report-fraud-txn']"
|
||||
routerLinkActive
|
||||
#fraud="routerLinkActive"
|
||||
[active]="fraud.isActive"
|
||||
>
|
||||
<fa-icon icon="exclamation-triangle" class="cfc-tab__ic"></fa-icon>
|
||||
<span>Report Fraud Txn</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<mat-tab-nav-panel #tabPanel>
|
||||
<div class="cfc-body">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-tab-nav-panel>
|
||||
</div>
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$indigo: #4f46e5;
|
||||
$red: #dc2626;
|
||||
$red-d: #991b1b;
|
||||
|
||||
.cfc-container {
|
||||
padding: 20px 24px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cfc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, $blue 0%, $indigo 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pill-style tab navigation */
|
||||
.cfc-tabs {
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-link-container {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-links {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mdc-tab-indicator__content--underline,
|
||||
.mat-mdc-tab-header-pagination-controls-enabled .mdc-tab-indicator__content--underline {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cfc-tab {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 38px !important;
|
||||
min-width: 0 !important;
|
||||
padding: 0 18px !important;
|
||||
border-radius: 10px !important;
|
||||
font-family: Inter, 'DM Sans', Roboto, sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em !important;
|
||||
color: #64748b !important;
|
||||
background: transparent !important;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent !important;
|
||||
opacity: 1 !important;
|
||||
|
||||
&__ic {
|
||||
font-size: 13px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover:not(.mdc-tab--active) {
|
||||
background: #f1f5f9 !important;
|
||||
color: $navy !important;
|
||||
}
|
||||
|
||||
&.mdc-tab--active,
|
||||
&.mat-mdc-tab-link-active {
|
||||
background: linear-gradient(135deg, $blue 0%, $blue-d 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 2px 6px -1px rgb(30 64 175 / 35%);
|
||||
border-color: $blue-d !important;
|
||||
|
||||
::ng-deep .mdc-tab__text-label,
|
||||
::ng-deep .mat-mdc-tab-link-content,
|
||||
::ng-deep span,
|
||||
.cfc-tab__ic {
|
||||
color: #fff !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Danger-flavour tabs (the three reporting tabs) glow red when active so
|
||||
users can see at a glance that they're on a high-impact submission flow. */
|
||||
&--danger {
|
||||
&.mdc-tab--active,
|
||||
&.mat-mdc-tab-link-active {
|
||||
background: linear-gradient(135deg, $red 0%, $red-d 100%) !important;
|
||||
box-shadow: 0 2px 6px -1px rgb(153 27 27 / 40%);
|
||||
border-color: $red-d !important;
|
||||
}
|
||||
|
||||
&:hover:not(.mdc-tab--active) {
|
||||
background: #fef2f2 !important;
|
||||
color: $red-d !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cfc-body {
|
||||
/* Sub-pages no longer carry their own page-header (the hero above already
|
||||
does that). Add a little breathing room and let their content cards
|
||||
stand on their own. */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.cfc-tabs {
|
||||
overflow-x: auto;
|
||||
|
||||
::ng-deep .mat-mdc-tab-links {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.cfc-tab {
|
||||
padding: 0 14px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
|
||||
import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* CFCFRMS Shell — single landing page that exposes the full Cyber Fraud
|
||||
* Complaint Management workflow (Dashboard, Complaints, Escalate, Suspect /
|
||||
* Mule / Fraud reporting) as pill-style tabs above a router-outlet.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-cfcfrms-shell',
|
||||
templateUrl: './cfcfrms-shell.component.html',
|
||||
styleUrls: ['./cfcfrms-shell.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatTabNav,
|
||||
MatTabLink,
|
||||
RouterLinkActive,
|
||||
MatTabNavPanel,
|
||||
RouterOutlet,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class CfcfrmsShellComponent {}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CfcfrmsRoutingModule } from './cfcfrms-routing.module';
|
||||
import { CfcfrmsShellComponent } from './cfcfrms-shell/cfcfrms-shell.component';
|
||||
import { CfcfrmsDashboardComponent } from './dashboard/cfcfrms-dashboard.component';
|
||||
import { ComplaintListComponent } from './complaint-list/complaint-list.component';
|
||||
import { ComplaintDetailComponent } from './complaint-detail/complaint-detail.component';
|
||||
@@ -21,6 +22,7 @@ import { CfrLookupComponent } from './cfr-lookup/cfr-lookup.component';
|
||||
@NgModule({
|
||||
imports: [
|
||||
CfcfrmsRoutingModule,
|
||||
CfcfrmsShellComponent,
|
||||
CfcfrmsDashboardComponent,
|
||||
ComplaintListComponent,
|
||||
ComplaintDetailComponent,
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="cfr-lookup-page">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.text.CFR Lookup' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Query RBI Central Fraud Registry before customer onboarding' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Search Card -->
|
||||
<mat-card class="search-card">
|
||||
<mat-card-content>
|
||||
|
||||
@@ -7,27 +7,12 @@
|
||||
*/
|
||||
|
||||
.cfr-lookup-page {
|
||||
padding: 24px;
|
||||
/* Wrapper hero already handles outer padding. */
|
||||
padding: 0;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -7,14 +7,22 @@
|
||||
-->
|
||||
|
||||
<div class="complaint-detail-page">
|
||||
<div class="page-header">
|
||||
<a mat-icon-button routerLink="/cfcfrms/complaints" matTooltip="{{ 'labels.text.Back to Complaints' | translate }}">
|
||||
<fa-icon icon="arrow-left"></fa-icon>
|
||||
<!-- Sub-toolbar: back-link + ticket id chip. The wrapper hero owns the
|
||||
page title so we just need contextual navigation here. -->
|
||||
<div class="cfc-subtoolbar">
|
||||
<a
|
||||
mat-stroked-button
|
||||
class="cfc-back-btn"
|
||||
routerLink="/cfcfrms/complaints"
|
||||
matTooltip="{{ 'labels.text.Back to Complaints' | translate }}"
|
||||
>
|
||||
<fa-icon icon="arrow-left" class="mr-05"></fa-icon>
|
||||
{{ 'labels.text.Back' | translate }}
|
||||
</a>
|
||||
<div>
|
||||
<h2>{{ 'labels.menus.Complaint Detail' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Ticket ID' | translate }}: {{ ticketId }}</p>
|
||||
</div>
|
||||
<span class="cfc-subtoolbar__ticket">
|
||||
<fa-icon icon="hashtag"></fa-icon>
|
||||
{{ ticketId }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (isLoading) {
|
||||
|
||||
@@ -7,29 +7,38 @@
|
||||
*/
|
||||
|
||||
.complaint-detail-page {
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
/* Sub-toolbar (back-button + ticket chip). Lives under the wrapper hero. */
|
||||
.cfc-subtoolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
&__ticket {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
background: #eff6ff;
|
||||
color: #1e40af;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 2px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
.cfc-back-btn {
|
||||
height: 36px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
-->
|
||||
|
||||
<div class="complaint-list-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{ 'labels.menus.Complaint Management' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.I4C/MHA CFCFRMS Complaint Tickets' | translate }}</p>
|
||||
</div>
|
||||
<!-- Toolbar (export sits at the right; the wrapper's hero header
|
||||
already conveys the page title) -->
|
||||
<div class="cfc-subtoolbar">
|
||||
<span class="cfc-subtoolbar__hint">{{ 'labels.text.I4C/MHA CFCFRMS Complaint Tickets' | translate }}</span>
|
||||
<span class="cfc-subtoolbar__spacer"></span>
|
||||
<button mat-raised-button color="accent" (click)="exportCsv()" [disabled]="complaints.length === 0">
|
||||
<fa-icon icon="file-csv" class="mr-05"></fa-icon> {{ 'labels.text.Export CSV' | translate }}
|
||||
</button>
|
||||
|
||||
@@ -6,46 +6,73 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
|
||||
.complaint-list-page {
|
||||
padding: 24px;
|
||||
/* Wrapper hero owns the page chrome — sub-page just needs to breathe. */
|
||||
padding: 0;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
/* ─── Sub-toolbar (hint + export) ───────────────────────────────────── */
|
||||
.cfc-subtoolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
&__hint {
|
||||
font-size: 13px;
|
||||
color: $muted;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
&__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-raised-button {
|
||||
border-radius: 10px !important;
|
||||
height: 38px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Status pill-chip bar ──────────────────────────────────────────── */
|
||||
.filter-chips {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
border-radius: 20px !important;
|
||||
font-size: 13px;
|
||||
height: 34px !important;
|
||||
padding: 0 14px !important;
|
||||
border-radius: 999px !important;
|
||||
font-size: 12.5px !important;
|
||||
font-weight: 600 !important;
|
||||
border: 1px solid $border !important;
|
||||
background: #fff !important;
|
||||
color: $slate !important;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover:not(.active-chip) {
|
||||
background: #f1f5f9 !important;
|
||||
color: $navy !important;
|
||||
}
|
||||
}
|
||||
|
||||
.active-chip {
|
||||
background: #1e88e5 !important;
|
||||
background: linear-gradient(135deg, #1e88e5 0%, #1e40af 100%) !important;
|
||||
color: #fff !important;
|
||||
border-color: #1e88e5 !important;
|
||||
border-color: #1e40af !important;
|
||||
box-shadow: 0 2px 6px -1px rgb(30 64 175 / 35%);
|
||||
}
|
||||
|
||||
.chip-dot {
|
||||
@@ -69,12 +96,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Filter bar (Nexora card chrome) ───────────────────────────────── */
|
||||
.filter-bar {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px !important;
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid $border !important;
|
||||
background: #fff !important;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%) !important;
|
||||
|
||||
mat-card-content {
|
||||
padding: 12px 16px !important;
|
||||
::ng-deep .mat-mdc-card-content {
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,13 +118,48 @@
|
||||
.filter-field {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border-radius: 8px !important;
|
||||
/* Compact form-field density inside the filter bar */
|
||||
::ng-deep .filter-bar .mat-mdc-form-field {
|
||||
.mat-mdc-text-field-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
.mat-mdc-form-field-infix {
|
||||
min-height: 42px;
|
||||
padding-top: 9px !important;
|
||||
padding-bottom: 9px !important;
|
||||
}
|
||||
|
||||
.mdc-notched-outline__leading,
|
||||
.mdc-notched-outline__notch,
|
||||
.mdc-notched-outline__trailing {
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
&.mat-focused {
|
||||
.mdc-notched-outline__leading,
|
||||
.mdc-notched-outline__notch,
|
||||
.mdc-notched-outline__trailing {
|
||||
border-color: #1e40af !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Table card ────────────────────────────────────────────────────── */
|
||||
.table-card {
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid $border !important;
|
||||
background: #fff !important;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%) !important;
|
||||
overflow: hidden;
|
||||
|
||||
::ng-deep .mat-mdc-card-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -106,10 +172,41 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::ng-deep .table-card {
|
||||
.mat-mdc-header-cell {
|
||||
background: $soft;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: $slate;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.mat-mdc-cell {
|
||||
font-size: 13.5px;
|
||||
color: $navy;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.mat-mdc-row:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.mat-mdc-paginator {
|
||||
border-top: 1px solid $border;
|
||||
background: $soft;
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-link {
|
||||
color: #1e88e5;
|
||||
color: #1e40af;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
@@ -117,31 +214,46 @@
|
||||
}
|
||||
|
||||
.amount-cell {
|
||||
font-weight: 600;
|
||||
color: #0d1b2a;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
color: $navy;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
padding: 56px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 64px;
|
||||
color: #94a3b8;
|
||||
padding: 56px 24px;
|
||||
color: $muted;
|
||||
|
||||
fa-icon {
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $slate;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 4px;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,133 +7,137 @@
|
||||
-->
|
||||
|
||||
<div class="cfcfrms-dashboard">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.menus.CFCFRMS Dashboard' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Cyber Fraud Complaint Management Overview' | translate }}</p>
|
||||
<!-- KPI strip — gradient icon block + value/label -->
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi-card kpi-card--blue">
|
||||
<div class="kpi-card__ico">
|
||||
<fa-icon icon="file-alt"></fa-icon>
|
||||
</div>
|
||||
<div class="kpi-card__body">
|
||||
<div class="kpi-card__value">{{ stats?.totalComplaints || 0 }}</div>
|
||||
<div class="kpi-card__label">{{ 'labels.text.Total Complaints' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-card--amber">
|
||||
<div class="kpi-card__ico">
|
||||
<fa-icon icon="clock"></fa-icon>
|
||||
</div>
|
||||
<div class="kpi-card__body">
|
||||
<div class="kpi-card__value">{{ stats?.pendingReview || 0 }}</div>
|
||||
<div class="kpi-card__label">{{ 'labels.text.Pending Review' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-card--purple">
|
||||
<div class="kpi-card__ico">
|
||||
<fa-icon icon="lock"></fa-icon>
|
||||
</div>
|
||||
<div class="kpi-card__body">
|
||||
<div class="kpi-card__value">{{ stats?.lienMarked || 0 }}</div>
|
||||
<div class="kpi-card__label">{{ 'labels.text.Lien Marked' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-card--green">
|
||||
<div class="kpi-card__ico">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
</div>
|
||||
<div class="kpi-card__body">
|
||||
<div class="kpi-card__value">{{ formatCurrency(stats?.fundsRecovered || 0) }}</div>
|
||||
<div class="kpi-card__label">{{ 'labels.text.Funds Recovered' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stat Cards -->
|
||||
<div class="stat-cards">
|
||||
<mat-card class="stat-card stat-total">
|
||||
<mat-card-content>
|
||||
<div class="stat-icon"><fa-icon icon="file-alt" size="2x"></fa-icon></div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats?.totalComplaints || 0 }}</span>
|
||||
<span class="stat-label">{{ 'labels.text.Total Complaints' | translate }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="stat-card stat-pending">
|
||||
<mat-card-content>
|
||||
<div class="stat-icon"><fa-icon icon="clock" size="2x"></fa-icon></div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats?.pendingReview || 0 }}</span>
|
||||
<span class="stat-label">{{ 'labels.text.Pending Review' | translate }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="stat-card stat-lien">
|
||||
<mat-card-content>
|
||||
<div class="stat-icon"><fa-icon icon="lock" size="2x"></fa-icon></div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats?.lienMarked || 0 }}</span>
|
||||
<span class="stat-label">{{ 'labels.text.Lien Marked' | translate }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="stat-card stat-recovered">
|
||||
<mat-card-content>
|
||||
<div class="stat-icon"><fa-icon icon="check-circle" size="2x"></fa-icon></div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ formatCurrency(stats?.fundsRecovered || 0) }}</span>
|
||||
<span class="stat-label">{{ 'labels.text.Funds Recovered' | translate }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<!-- Charts row — two side-by-side analytics cards -->
|
||||
<div class="charts-row">
|
||||
<mat-card class="chart-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'labels.text.Status Distribution' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<section class="nx-card">
|
||||
<header class="nx-card__head">
|
||||
<fa-icon icon="chart-pie" class="nx-card__ico"></fa-icon>
|
||||
<h3 class="nx-card__title">{{ 'labels.text.Status Distribution' | translate }}</h3>
|
||||
</header>
|
||||
<div class="nx-card__body">
|
||||
<div class="chart-container">
|
||||
<canvas #donutCanvas></canvas>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="chart-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'labels.text.Fraud Amount by Status' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
</div>
|
||||
</section>
|
||||
<section class="nx-card">
|
||||
<header class="nx-card__head">
|
||||
<fa-icon icon="chart-bar" class="nx-card__ico"></fa-icon>
|
||||
<h3 class="nx-card__title">{{ 'labels.text.Fraud Amount by Status' | translate }}</h3>
|
||||
</header>
|
||||
<div class="nx-card__body">
|
||||
<div class="chart-container">
|
||||
<canvas #barCanvas></canvas>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Recent Complaints Table -->
|
||||
<mat-card class="recent-complaints-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'labels.text.Recent Complaints' | translate }}</mat-card-title>
|
||||
<span class="spacer"></span>
|
||||
<a mat-stroked-button color="primary" routerLink="/cfcfrms/complaints">{{
|
||||
'labels.text.View All' | translate
|
||||
}}</a>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<!-- Recent complaints -->
|
||||
<section class="nx-card">
|
||||
<header class="nx-card__head">
|
||||
<fa-icon icon="inbox" class="nx-card__ico"></fa-icon>
|
||||
<h3 class="nx-card__title">{{ 'labels.text.Recent Complaints' | translate }}</h3>
|
||||
<span class="nx-card__spacer"></span>
|
||||
<a mat-stroked-button color="primary" routerLink="/cfcfrms/complaints" class="nx-card__action">
|
||||
{{ 'labels.text.View All' | translate }}
|
||||
<fa-icon icon="arrow-right" class="ml-04"></fa-icon>
|
||||
</a>
|
||||
</header>
|
||||
<div class="nx-card__body nx-card__body--flush">
|
||||
@if (isLoading) {
|
||||
<div class="loading-container">
|
||||
<div class="nx-state">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
</div>
|
||||
} @else if (recentComplaints.length === 0) {
|
||||
<div class="empty-state">
|
||||
<fa-icon icon="inbox" size="3x"></fa-icon>
|
||||
<p>{{ 'labels.text.No complaints found' | translate }}</p>
|
||||
<div class="nx-state nx-state--empty">
|
||||
<fa-icon icon="inbox" class="nx-state__ico"></fa-icon>
|
||||
<div class="nx-state__title">{{ 'labels.text.No complaints found' | translate }}</div>
|
||||
<div class="nx-state__sub">Recent activity will appear here.</div>
|
||||
</div>
|
||||
} @else {
|
||||
<table mat-table [dataSource]="recentComplaints" class="full-width">
|
||||
<ng-container matColumnDef="ticketId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Ticket ID' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<a [routerLink]="['/cfcfrms/complaints', row.ticketId]" class="ticket-link">{{ row.ticketId }}</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="complainantName">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Complainant' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.complainantName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fraudAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Fraud Amount' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ formatCurrency(row.fraudAmount) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Status' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mifosx-cfcfrms-status-badge [status]="row.status"></mifosx-cfcfrms-status-badge>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="createdAt">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Created' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ formatDate(row.createdAt) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<a mat-icon-button color="primary" [routerLink]="['/cfcfrms/complaints', row.ticketId]">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
<div class="nx-table-wrap">
|
||||
<table mat-table [dataSource]="recentComplaints" class="nx-table">
|
||||
<ng-container matColumnDef="ticketId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Ticket ID' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<a [routerLink]="['/cfcfrms/complaints', row.ticketId]" class="ticket-link">{{ row.ticketId }}</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="complainantName">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Complainant' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.complainantName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fraudAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Fraud Amount' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row" class="amount-cell">{{ formatCurrency(row.fraudAmount) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Status' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mifosx-cfcfrms-status-badge [status]="row.status"></mifosx-cfcfrms-status-badge>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="createdAt">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Created' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ formatDate(row.createdAt) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-right">
|
||||
{{ 'labels.text.Actions' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row" class="text-right">
|
||||
<a mat-icon-button color="primary" [routerLink]="['/cfcfrms/complaints', row.ticketId]">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -6,147 +6,193 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
|
||||
.cfcfrms-dashboard {
|
||||
padding: 24px;
|
||||
/* Wrapper hero handles outer padding/centering. */
|
||||
padding: 0;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
/* ─── KPI strip ─────────────────────────────────────────────────────── */
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media (width <= 1280px) {
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-left: 4px solid;
|
||||
border-radius: 8px !important;
|
||||
|
||||
mat-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&.stat-total {
|
||||
border-color: #1e88e5;
|
||||
|
||||
.stat-icon {
|
||||
background: #1e88e5;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #1e88e5;
|
||||
}
|
||||
}
|
||||
|
||||
&.stat-pending {
|
||||
border-color: #f59e0b;
|
||||
|
||||
.stat-icon {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #f59e0b;
|
||||
}
|
||||
}
|
||||
|
||||
&.stat-lien {
|
||||
border-color: #eab308;
|
||||
|
||||
.stat-icon {
|
||||
background: #eab308;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #92400e;
|
||||
}
|
||||
}
|
||||
|
||||
&.stat-recovered {
|
||||
border-color: #10b981;
|
||||
|
||||
.stat-icon {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media (width <= 1280px) {
|
||||
@media (width <= 640px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border-radius: 8px !important;
|
||||
.kpi-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
padding: 16px 18px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
|
||||
mat-card-header {
|
||||
padding: 16px 16px 0;
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px -6px rgb(15 23 42 / 12%);
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
padding: 16px !important;
|
||||
&__ico {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: $muted;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Variant tint — left accent stripe + gradient icon. */
|
||||
&--blue {
|
||||
border-left: 3px solid #1e88e5;
|
||||
|
||||
.kpi-card__ico {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
|
||||
box-shadow: 0 4px 10px -4px rgb(30 64 175 / 50%);
|
||||
}
|
||||
}
|
||||
|
||||
&--amber {
|
||||
border-left: 3px solid #f59e0b;
|
||||
|
||||
.kpi-card__ico {
|
||||
background: linear-gradient(135deg, #fbbf24 0%, #d97706 100%);
|
||||
box-shadow: 0 4px 10px -4px rgb(217 119 6 / 50%);
|
||||
}
|
||||
}
|
||||
|
||||
&--purple {
|
||||
border-left: 3px solid #8b5cf6;
|
||||
|
||||
.kpi-card__ico {
|
||||
background: linear-gradient(135deg, #a78bfa 0%, #6d28d9 100%);
|
||||
box-shadow: 0 4px 10px -4px rgb(109 40 217 / 50%);
|
||||
}
|
||||
}
|
||||
|
||||
&--green {
|
||||
border-left: 3px solid #10b981;
|
||||
|
||||
.kpi-card__ico {
|
||||
background: linear-gradient(135deg, #34d399 0%, #047857 100%);
|
||||
box-shadow: 0 4px 10px -4px rgb(4 120 87 / 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Charts row ────────────────────────────────────────────────────── */
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Generic Nexora card ───────────────────────────────────────────── */
|
||||
.nx-card {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid $border;
|
||||
background: $soft;
|
||||
}
|
||||
|
||||
&__ico {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
&__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__action {
|
||||
height: 32px !important;
|
||||
border-radius: 8px !important;
|
||||
font-size: 12.5px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 18px;
|
||||
|
||||
&--flush {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,53 +201,87 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.recent-complaints-card {
|
||||
border-radius: 8px !important;
|
||||
|
||||
mat-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
/* ─── Table ─────────────────────────────────────────────────────────── */
|
||||
.nx-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
.nx-table {
|
||||
width: 100%;
|
||||
|
||||
::ng-deep .mat-mdc-header-cell {
|
||||
background: $soft;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: $slate;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-cell {
|
||||
font-size: 13.5px;
|
||||
color: $navy;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-row:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-link {
|
||||
color: #1e88e5;
|
||||
color: #1e40af;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
.amount-cell {
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ml-04 {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* ─── States (loading / empty) ──────────────────────────────────────── */
|
||||
.nx-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48px;
|
||||
color: #94a3b8;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 56px 24px;
|
||||
color: $muted;
|
||||
|
||||
p {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
&--empty {
|
||||
.nx-state__ico {
|
||||
font-size: 38px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.nx-state__title {
|
||||
font-size: 14.5px;
|
||||
font-weight: 700;
|
||||
color: $slate;
|
||||
}
|
||||
|
||||
.nx-state__sub {
|
||||
font-size: 12.5px;
|
||||
color: $muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,15 @@
|
||||
import { Component, OnInit, inject, ViewChild, ElementRef } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CfcfrmsService } from '../cfcfrms.service';
|
||||
import { StatusBadgeComponent } from '../components/status-badge/status-badge.component';
|
||||
import { Complaint, DashboardStats, ComplaintStatus } from '../models/cfcfrms.models';
|
||||
import { Complaint, DashboardStats } from '../models/cfcfrms.models';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
@@ -29,11 +27,9 @@ Chart.register(...registerables);
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatCardModule,
|
||||
MatTableModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatIconModule,
|
||||
FaIconComponent,
|
||||
TranslateModule,
|
||||
StatusBadgeComponent
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="escalate-page">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.menus.Escalate Complaint' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Forward complaint to the next bank in the transaction chain' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="escalate-layout">
|
||||
<!-- Escalation Form -->
|
||||
<mat-card class="escalate-card">
|
||||
|
||||
@@ -6,206 +6,138 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use '../cfc-form' as *;
|
||||
|
||||
.escalate-page {
|
||||
padding: 24px;
|
||||
/* Wrapper hero already handles outer padding. */
|
||||
padding: 0;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.escalate-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 24px;
|
||||
grid-template-columns: minmax(0, 1fr) 360px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
|
||||
@media (width <= 1280px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.escalate-card,
|
||||
.instructions-card {
|
||||
border-radius: 8px !important;
|
||||
|
||||
mat-card-header {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
padding: 16px 20px 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-lookup-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
|
||||
.ticket-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.lookup-btn {
|
||||
margin-top: 4px;
|
||||
height: 56px;
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.lookup-result {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
.lookup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
color: #64748b;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #0d1b2a;
|
||||
font-weight: 700;
|
||||
|
||||
&.mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
color: #1e40af;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 8px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
// Success result
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px;
|
||||
color: #065f46;
|
||||
font-size: 20px;
|
||||
/* ─── Instructions side panel ───────────────────────────────────────── */
|
||||
.instructions-card {
|
||||
::ng-deep .mat-mdc-card-content {
|
||||
padding: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.result-details {
|
||||
text-align: left;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
|
||||
.result-label {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instructions card
|
||||
.guidelines {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.guideline-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
padding: 7px 0;
|
||||
|
||||
.guideline-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
background: #ecfdf5;
|
||||
color: #10b981;
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
|
||||
&.warn {
|
||||
color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
font-size: 12.5px;
|
||||
color: #334155;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.bank-info {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 800;
|
||||
color: #0d1b2a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.bank-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #0d1b2a;
|
||||
font-weight: 700;
|
||||
|
||||
&.mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
color: #1e40af;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include cfc-form-skin('blue');
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="report-page">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.text.FMR Submission' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Submit Fraud Monitoring Return to RBI FUA System' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="report-layout">
|
||||
<mat-card class="report-card wide-card">
|
||||
<mat-card-header>
|
||||
|
||||
@@ -7,27 +7,12 @@
|
||||
*/
|
||||
|
||||
.report-page {
|
||||
padding: 24px;
|
||||
/* Wrapper hero already handles outer padding. */
|
||||
padding: 0;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-layout {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="report-page">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.text.Report Fraud Transaction' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Report a detected fraud transaction to I4C' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="report-layout">
|
||||
<mat-card class="report-card">
|
||||
<mat-card-header>
|
||||
|
||||
@@ -6,114 +6,23 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use '../cfc-form' as *;
|
||||
|
||||
.report-page {
|
||||
padding: 24px;
|
||||
/* Wrapper hero already handles outer padding. */
|
||||
padding: 0;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-layout {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
flex: 1;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 8px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #10b981;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-details {
|
||||
text-align: left;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.result-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
flex: 1;
|
||||
}
|
||||
/* Layered Nexora skin: red-accented for the high-impact fraud report. */
|
||||
@include cfc-form-skin('red');
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="report-page">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.text.Report Mule Account' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Flag a mule account proactively to I4C/CFCFRMS' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="report-layout">
|
||||
<mat-card class="report-card">
|
||||
<mat-card-header>
|
||||
|
||||
@@ -6,110 +6,21 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use '../cfc-form' as *;
|
||||
|
||||
.report-page {
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-layout {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
flex: 1;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 8px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #10b981;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-details {
|
||||
text-align: left;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.result-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.status-flagged {
|
||||
color: #ef4444;
|
||||
font-weight: 700;
|
||||
}
|
||||
@include cfc-form-skin('red');
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="report-page">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.text.Report Suspect Account' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Report internally detected suspect account to I4C' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="report-layout">
|
||||
<mat-card class="report-card">
|
||||
<mat-card-header>
|
||||
|
||||
@@ -6,105 +6,21 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use '../cfc-form' as *;
|
||||
|
||||
.report-page {
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-layout {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
flex: 1;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 8px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #10b981;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-details {
|
||||
text-align: left;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.result-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
@include cfc-form-skin('red');
|
||||
|
||||
+33
-17
@@ -16,29 +16,45 @@
|
||||
<div class="step-header__left">
|
||||
<fa-icon icon="users" class="step-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h3 class="step-header__title">Family Members</h3>
|
||||
<p class="step-header__sub">Add family members and dependents of the client.</p>
|
||||
@if (nomineeOnly) {
|
||||
<h3 class="step-header__title">Nominees</h3>
|
||||
<p class="step-header__sub">
|
||||
Nominees for this client. Family members are not applicable for this entity type.
|
||||
</p>
|
||||
} @else {
|
||||
<h3 class="step-header__title">Family Members</h3>
|
||||
<p class="step-header__sub">Add family members and dependents of the client.</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (nomineeOnly) {
|
||||
<div class="fm-step__empty">
|
||||
<mat-icon class="fm-step__empty-icon">shield</mat-icon>
|
||||
<span>Nominees are managed per account from the <strong>Nominees</strong> tab once accounts are opened.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ FAMILY MEMBERS ═══ -->
|
||||
<div class="fm-step__header">
|
||||
<div class="fm-step__title">
|
||||
<fa-icon icon="users" class="fm-step__icon"></fa-icon>
|
||||
<span>Family Members</span>
|
||||
@if (clientFamilyMembers.length > 0) {
|
||||
<span class="fm-step__count">{{ clientFamilyMembers.length }}</span>
|
||||
@if (!nomineeOnly) {
|
||||
<div class="fm-step__header">
|
||||
<div class="fm-step__title">
|
||||
<fa-icon icon="users" class="fm-step__icon"></fa-icon>
|
||||
<span>Family Members</span>
|
||||
@if (clientFamilyMembers.length > 0) {
|
||||
<span class="fm-step__count">{{ clientFamilyMembers.length }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (!readOnly) {
|
||||
<button mat-flat-button color="primary" type="button" class="fm-step__add-btn" (click)="addFamilyMember()">
|
||||
<mat-icon>add</mat-icon> Add Member
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (!readOnly) {
|
||||
<button mat-flat-button color="primary" type="button" class="fm-step__add-btn" (click)="addFamilyMember()">
|
||||
<mat-icon>add</mat-icon> Add Member
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (clientFamilyMembers.length === 0) {
|
||||
@if (!nomineeOnly && clientFamilyMembers.length === 0) {
|
||||
<div class="fm-step__empty fm-step__empty--required">
|
||||
<mat-icon class="fm-step__empty-icon">people_outline</mat-icon>
|
||||
<span>No family members added yet.</span>
|
||||
@@ -47,14 +63,14 @@
|
||||
}
|
||||
|
||||
<!-- Spouse hint for married clients -->
|
||||
@if (isMarried && !hasSpouse) {
|
||||
@if (!nomineeOnly && isMarried && !hasSpouse) {
|
||||
<div class="fm-spouse-hint">
|
||||
<mat-icon class="fm-spouse-hint__icon">favorite</mat-icon>
|
||||
<span>Marital status is <strong>Married</strong>. Consider adding spouse as a family member.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (clientFamilyMembers.length > 0) {
|
||||
@if (!nomineeOnly && clientFamilyMembers.length > 0) {
|
||||
<div class="fm-table">
|
||||
<div class="fm-table__header">
|
||||
<span class="fm-table__col fm-table__col--name">Name</span>
|
||||
|
||||
+7
@@ -52,6 +52,12 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
@Input() maritalStatusOptions: any[] = [];
|
||||
/** Read-only mode — hides add/edit/delete buttons */
|
||||
@Input() readOnly = false;
|
||||
/**
|
||||
* When true, hide the family-member section entirely. Used for clients that
|
||||
* are eligible for nominees but not for family members (e.g. proprietorship
|
||||
* entities — see {@link nomineeAllowed} / {@link familyAllowed}).
|
||||
*/
|
||||
@Input() nomineeOnly = false;
|
||||
|
||||
/** Family Members */
|
||||
clientFamilyMembers: any[] = [];
|
||||
@@ -131,6 +137,7 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
addFamilyMember() {
|
||||
if (this.nomineeOnly || this.readOnly) return;
|
||||
const ref = this.dialog.open(ClientFamilyMemberDialogComponent, {
|
||||
data: {
|
||||
context: 'Add',
|
||||
|
||||
+2
-2
@@ -1024,8 +1024,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Family Members (Individual only) -->
|
||||
@if (client?.legalFormId !== 2) {
|
||||
<!-- Family Members — hidden when client isn't eligible (entities other than proprietorship). -->
|
||||
@if (showFamilyPreview) {
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<fa-icon icon="users" class="card-icon"></fa-icon>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { YesnoPipe } from '../../../pipes/yesno.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { LegalFormId } from 'app/clients/models/legal-form.enum';
|
||||
import { familyAllowed, nomineeAllowed } from 'app/clients/utils/client-rules';
|
||||
|
||||
/**
|
||||
* Client Preview Step Component
|
||||
@@ -69,6 +70,11 @@ export class ClientPreviewStepComponent {
|
||||
@Input() nominees: any[] = [];
|
||||
/** Family members from family step */
|
||||
@Input() familyMembers: any[] = [];
|
||||
/**
|
||||
* Constitution display name for entity clients — drives the proprietorship-only
|
||||
* nominee rule so the preview matches what's eligible elsewhere.
|
||||
*/
|
||||
@Input() constitutionLabel: string = '';
|
||||
/** Marital Status ID from general step */
|
||||
@Input() maritalStatusId: number | null = null;
|
||||
/** Marital Status Options */
|
||||
@@ -99,6 +105,14 @@ export class ClientPreviewStepComponent {
|
||||
/** Form submission event */
|
||||
@Output() submitEvent = new EventEmitter();
|
||||
|
||||
get showFamilyPreview(): boolean {
|
||||
return familyAllowed(this.client?.legalFormId, this.constitutionLabel);
|
||||
}
|
||||
|
||||
get showNomineePreview(): boolean {
|
||||
return nomineeAllowed(this.client?.legalFormId, this.constitutionLabel);
|
||||
}
|
||||
|
||||
/** Expose enum to template */
|
||||
readonly LegalFormId = LegalFormId;
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ import { KycTabComponent } from './clients-view/kyc-tab/kyc-tab.component';
|
||||
import { CkycTabComponent } from './clients-view/ckyc-tab/ckyc-tab.component';
|
||||
import { Form60TabComponent } from './clients-view/form60-tab/form60-tab.component';
|
||||
import { EntityClassificationTabComponent } from './clients-view/entity-classification-tab/entity-classification-tab.component';
|
||||
import { familyAllowedGuard, nomineeAllowedGuard } from './utils/family-nominee.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
@@ -123,10 +124,12 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'nominees',
|
||||
component: NomineeTabComponent,
|
||||
canActivate: [nomineeAllowedGuard],
|
||||
data: { title: 'Nominees', breadcrumb: 'Nominees', routeParamBreadcrumb: false }
|
||||
},
|
||||
{
|
||||
path: 'family-members',
|
||||
canActivate: [familyAllowedGuard],
|
||||
data: { title: 'Family Members', breadcrumb: 'Family Members', routeParamBreadcrumb: false },
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -481,7 +481,7 @@
|
||||
>
|
||||
<i class="fa fa-map-marker cv-tab-icon"></i> {{ 'labels.inputs.Address' | translate }}
|
||||
</a>
|
||||
@if (clientViewData?.legalForm?.id !== 2) {
|
||||
@if (showFamilyTab) {
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./family-members']"
|
||||
@@ -492,15 +492,17 @@
|
||||
<i class="fa fa-users cv-tab-icon"></i> Family Members
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nomineesTab="routerLinkActive"
|
||||
[active]="nomineesTab.isActive"
|
||||
>
|
||||
<i class="fa fa-shield cv-tab-icon"></i> Nominee
|
||||
</a>
|
||||
@if (showNomineeTab) {
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nomineesTab="routerLinkActive"
|
||||
[active]="nomineesTab.isActive"
|
||||
>
|
||||
<i class="fa fa-shield cv-tab-icon"></i> Nominee
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./identities']"
|
||||
|
||||
@@ -26,6 +26,7 @@ import { CaptureImageDialogComponent } from './custom-dialogs/capture-image-dial
|
||||
/** Custom Services */
|
||||
import { ClientsService } from '../clients.service';
|
||||
import { LegalFormId } from '../models/legal-form.enum';
|
||||
import { familyAllowed, nomineeAllowed } from '../utils/client-rules';
|
||||
import {
|
||||
MatCard,
|
||||
MatCardHeader,
|
||||
@@ -143,6 +144,21 @@ export class ClientsViewComponent implements OnInit {
|
||||
clientImage: any;
|
||||
clientTemplateData: any;
|
||||
|
||||
/** Constitution display name (entity clients only). Empty for individuals. */
|
||||
get constitutionLabel(): string {
|
||||
return this.clientViewData?.clientNonPersonDetails?.constitution?.name || '';
|
||||
}
|
||||
|
||||
/** Whether the "Family Members" tab should appear. */
|
||||
get showFamilyTab(): boolean {
|
||||
return familyAllowed(this.clientViewData?.legalForm?.id, this.constitutionLabel);
|
||||
}
|
||||
|
||||
/** Whether the "Nominees" tab should appear. */
|
||||
get showNomineeTab(): boolean {
|
||||
return nomineeAllowed(this.clientViewData?.legalForm?.id, this.constitutionLabel);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { clientViewData: any; clientTemplateData: any; clientDatatables: any }) => {
|
||||
this.clientViewData = data.clientViewData;
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
<h3 class="nom-header__title">Family Members</h3>
|
||||
<span class="nom-header__badge">{{ familyMembers.length }}</span>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" class="nom-header__btn" (click)="addFamilyMember()">
|
||||
<mat-icon>add</mat-icon> Add Member
|
||||
</button>
|
||||
@if (canAdd) {
|
||||
<button mat-flat-button color="primary" class="nom-header__btn" (click)="addFamilyMember()">
|
||||
<mat-icon>add</mat-icon> Add Member
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (familyMembers.length === 0) {
|
||||
@@ -44,21 +46,28 @@
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="nom-actions">
|
||||
<div class="nom-actions__buttons">
|
||||
<button mat-stroked-button color="primary" class="nom-actions__edit" (click)="editFamilyMember(member, i)">
|
||||
<fa-icon icon="edit" class="m-r-10"></fa-icon> Edit
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
class="nom-actions__delete"
|
||||
(click)="deleteMember(member, 'Family Member')"
|
||||
>
|
||||
<fa-icon icon="trash" class="m-r-10"></fa-icon> Delete
|
||||
</button>
|
||||
@if (canAdd) {
|
||||
<div class="nom-actions">
|
||||
<div class="nom-actions__buttons">
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="nom-actions__edit"
|
||||
(click)="editFamilyMember(member, i)"
|
||||
>
|
||||
<fa-icon icon="edit" class="m-r-10"></fa-icon> Edit
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
class="nom-actions__delete"
|
||||
(click)="deleteMember(member, 'Family Member')"
|
||||
>
|
||||
<fa-icon icon="trash" class="m-r-10"></fa-icon> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="nom-grid">
|
||||
@if (member.firstName) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ClientsService } from '../../clients.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { familyAllowed } from '../../utils/client-rules';
|
||||
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
@@ -71,6 +72,12 @@ export class FamilyMembersTabComponent {
|
||||
clientId: string;
|
||||
/** Family member template options */
|
||||
familyMemberOptions: any;
|
||||
/**
|
||||
* Defensive flag — the route guard already blocks unauthorised clients, but
|
||||
* we re-check here so that if the component is somehow reachable via direct
|
||||
* link or stale state, the Add / Edit buttons stay hidden.
|
||||
*/
|
||||
canAdd = false;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { clientFamilyMembers: any }) => {
|
||||
@@ -80,6 +87,17 @@ export class FamilyMembersTabComponent {
|
||||
// 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'];
|
||||
if (this.clientId) {
|
||||
this.clientsService.getClientData(this.clientId).subscribe({
|
||||
next: (client: any) => {
|
||||
const constitutionLabel = client?.clientNonPersonDetails?.constitution?.name || '';
|
||||
this.canAdd = familyAllowed(client?.legalForm?.id, constitutionLabel);
|
||||
},
|
||||
error: () => {
|
||||
this.canAdd = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Family members (non-nominee entries) */
|
||||
@@ -116,6 +134,7 @@ export class FamilyMembersTabComponent {
|
||||
}
|
||||
|
||||
addFamilyMember() {
|
||||
if (!this.canAdd) return;
|
||||
this.ensureTemplateLoaded().then(() => {
|
||||
const dialogRef = this.dialog.open(ClientFamilyMemberDialogComponent, {
|
||||
width: '640px',
|
||||
@@ -124,7 +143,10 @@ export class FamilyMembersTabComponent {
|
||||
data: {
|
||||
context: 'Add',
|
||||
options: this.familyMemberOptions,
|
||||
existingMembers: this.familyMembers
|
||||
existingMembers: this.familyMembers,
|
||||
// Pass the host client's id so the dialog's self-add guard can
|
||||
// filter the search results and refuse self-link attempts.
|
||||
hostClientId: this.clientId
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
@@ -139,6 +161,7 @@ export class FamilyMembersTabComponent {
|
||||
}
|
||||
|
||||
editFamilyMember(member: any, _index: number) {
|
||||
if (!this.canAdd) return;
|
||||
this.ensureTemplateLoaded().then(() => {
|
||||
const dialogRef = this.dialog.open(ClientFamilyMemberDialogComponent, {
|
||||
width: '640px',
|
||||
@@ -148,7 +171,8 @@ export class FamilyMembersTabComponent {
|
||||
context: 'Edit',
|
||||
options: this.familyMemberOptions,
|
||||
member,
|
||||
existingMembers: this.familyMembers
|
||||
existingMembers: this.familyMembers,
|
||||
hostClientId: this.clientId
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
@@ -226,6 +250,7 @@ export class FamilyMembersTabComponent {
|
||||
// ═══ Common ═══
|
||||
|
||||
deleteMember(member: any, label: string) {
|
||||
if (!this.canAdd) return;
|
||||
const realIndex = this.clientFamilyMembers.indexOf(member);
|
||||
const deleteFamilyMemberDialogRef = this.dialog.open(DeleteDialogComponent, {
|
||||
data: { deleteContext: `${label} ${member.firstName || ''}` }
|
||||
|
||||
@@ -560,9 +560,14 @@
|
||||
matInput
|
||||
formControlName="aadhaarNumber"
|
||||
maxlength="12"
|
||||
type="password"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
placeholder="Enter 12-digit Aadhaar"
|
||||
[readonly]="aadhaarOtpSent"
|
||||
/>
|
||||
@if (kycData?.aadhaarNumberMasked) {
|
||||
<mat-hint>On file: {{ kycData.aadhaarNumberMasked }}</mat-hint>
|
||||
}
|
||||
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-error>
|
||||
}
|
||||
|
||||
@@ -147,12 +147,15 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
this.loading = false;
|
||||
this.kycData = data;
|
||||
if (data) {
|
||||
this.kycId = data.id?.toString() || null;
|
||||
const rawKycId = data.id ?? data.kycId ?? data.resourceId;
|
||||
this.kycId = rawKycId != null ? rawKycId.toString() : null;
|
||||
this.kycForm.patchValue({
|
||||
kycType: data.kycType || 1,
|
||||
riskCategory: data.riskCategory || 1,
|
||||
panNumber: data.panNumber || '',
|
||||
aadhaarNumber: data.aadhaarNumberMasked || '',
|
||||
// Aadhaar is stored masked on the server; user must re-enter
|
||||
// the full 12-digit number to re-verify, so don't pre-fill.
|
||||
aadhaarNumber: '',
|
||||
gstinNumber: data.gstinNumber || '',
|
||||
remarks: data.remarks || ''
|
||||
});
|
||||
@@ -181,6 +184,15 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
toggleEditMode(): void {
|
||||
this.isEditMode = !this.isEditMode;
|
||||
this.resetOtpState();
|
||||
if (this.isEditMode) {
|
||||
// Clear Aadhaar so the user types a fresh 12-digit number for re-verify;
|
||||
// server only returns the masked form which would fail validation.
|
||||
this.kycForm.get('aadhaarNumber')?.reset('');
|
||||
this.verificationStatus.aadhaar = null;
|
||||
this.verificationStatus.pan = null;
|
||||
this.verificationError.aadhaar = null;
|
||||
this.verificationError.pan = null;
|
||||
}
|
||||
}
|
||||
|
||||
saveKyc(): void {
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (selectedAccountId && !showAddForm) {
|
||||
@if (canAdd && selectedAccountId && !showAddForm) {
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
@@ -153,18 +153,20 @@
|
||||
<button mat-icon-button type="button" matTooltip="View" (click)="viewNominee(nominee)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button type="button" matTooltip="Edit" (click)="editNominee(nominee, i)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
matTooltip="Delete"
|
||||
class="nom-card__delete"
|
||||
(click)="deleteNominee(nominee, i)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
@if (canAdd) {
|
||||
<button mat-icon-button type="button" matTooltip="Edit" (click)="editNominee(nominee, i)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
matTooltip="Delete"
|
||||
class="nom-card__delete"
|
||||
(click)="deleteNominee(nominee, i)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (nominee.nomineeIsMinor && (nominee.guardianDisplayName || nominee.guardianName)) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { ClientsService } from '../../clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { nomineeAllowed } from '../../utils/client-rules';
|
||||
import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component';
|
||||
import { ImageZoomDialogComponent } from '../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
@@ -55,6 +56,12 @@ export class NomineeTabComponent implements OnInit {
|
||||
clientId: string;
|
||||
loading = true;
|
||||
accountGroups: AccountGroup[] = [];
|
||||
/**
|
||||
* Defensive flag — route guard already blocks unauthorised clients, but
|
||||
* we re-check here so Add / Edit / Delete stay hidden if the component is
|
||||
* reached via direct link or stale state.
|
||||
*/
|
||||
canAdd = false;
|
||||
selectedType: string = '';
|
||||
selectedAccountId: number | null = null;
|
||||
|
||||
@@ -133,6 +140,23 @@ export class NomineeTabComponent implements OnInit {
|
||||
this.clientId = this.route.parent.snapshot.params['clientId'];
|
||||
this.loadAccounts();
|
||||
this.loadTemplate();
|
||||
this.loadClientEligibility();
|
||||
}
|
||||
|
||||
private loadClientEligibility(): void {
|
||||
if (!this.clientId) {
|
||||
this.canAdd = false;
|
||||
return;
|
||||
}
|
||||
this.clientsService.getClientData(this.clientId).subscribe({
|
||||
next: (client: any) => {
|
||||
const constitutionLabel = client?.clientNonPersonDetails?.constitution?.name || '';
|
||||
this.canAdd = nomineeAllowed(client?.legalForm?.id, constitutionLabel);
|
||||
},
|
||||
error: () => {
|
||||
this.canAdd = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadTemplate(): void {
|
||||
@@ -505,6 +529,7 @@ export class NomineeTabComponent implements OnInit {
|
||||
// ═══ Save Nominee ═══
|
||||
|
||||
saveNominee(): void {
|
||||
if (!this.canAdd) return;
|
||||
if (!this.selectedClient || !this.nomineeRelationshipId || !this.nomineeShare) return;
|
||||
if (this.totalShare + this.nomineeShare > 100) return;
|
||||
|
||||
@@ -574,6 +599,7 @@ export class NomineeTabComponent implements OnInit {
|
||||
}
|
||||
|
||||
editNominee(nominee: any, index: number): void {
|
||||
if (!this.canAdd) return;
|
||||
this.editingIndex = index;
|
||||
this.editRelId = nominee.relationshipId || null;
|
||||
this.editShare = nominee.percentageShare;
|
||||
@@ -609,6 +635,7 @@ export class NomineeTabComponent implements OnInit {
|
||||
}
|
||||
|
||||
deleteNominee(nominee: any, index: number): void {
|
||||
if (!this.canAdd) return;
|
||||
const ref = this.dialog.open(DeleteDialogComponent, {
|
||||
data: { deleteContext: `nominee ${nominee.nomineeDisplayName || nominee.firstName || ''}` }
|
||||
});
|
||||
|
||||
@@ -150,12 +150,12 @@
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab: Family Members / Nominees (Individual only) -->
|
||||
@if (legalFormType !== 2) {
|
||||
<!-- Tab: Family / Nominees — hidden for entity clients that aren't proprietorships -->
|
||||
@if (showFamilyTab) {
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="users" class="tab-icon"></fa-icon>
|
||||
FAMILY
|
||||
{{ familyTabNomineeOnly ? 'NOMINEES' : 'FAMILY' }}
|
||||
</ng-template>
|
||||
@let familyTabIdx = legalFormType === 2 ? 4 : 3;
|
||||
<div class="tab-content" [class.tab-content--readonly]="isTabReadOnly(familyTabIdx)">
|
||||
@@ -183,6 +183,7 @@
|
||||
[maritalStatusId]="clientGeneralStep?.createClientForm?.get('maritalStatusId')?.value"
|
||||
[maritalStatusOptions]="clientGeneralStep?.maritalStatusOptions || []"
|
||||
[readOnly]="isTabReadOnly(familyTabIdx)"
|
||||
[nomineeOnly]="familyTabNomineeOnly"
|
||||
></mifosx-client-family-members-step>
|
||||
</div>
|
||||
</mat-tab>
|
||||
@@ -205,6 +206,7 @@
|
||||
[guardianRelation]="clientGeneralStep?.createClientForm?.get('guardianDetails.guardianRelation')?.value || ''"
|
||||
[entityClassificationData]="clientEntityKycStep?.classificationData"
|
||||
[familyMembers]="clientFamilyMembersStep?.clientFamilyMembers || []"
|
||||
[constitutionLabel]="constitutionLabel"
|
||||
[maritalStatusId]="clientGeneralStep?.createClientForm?.get('maritalStatusId')?.value"
|
||||
[maritalStatusOptions]="clientGeneralStep?.maritalStatusOptions || []"
|
||||
[educationLevelOptions]="clientGeneralStep?.educationLevelOptions || []"
|
||||
|
||||
@@ -14,6 +14,7 @@ import { map } from 'rxjs/operators';
|
||||
|
||||
/** Custom Services */
|
||||
import { ClientsService } from '../clients.service';
|
||||
import { familyAllowed, nomineeAllowed, isProprietorship } from '../utils/client-rules';
|
||||
|
||||
/** Custom Components */
|
||||
import { ClientGeneralStepComponent } from '../client-stepper/client-general-step/client-general-step.component';
|
||||
@@ -130,9 +131,37 @@ export class CreateClientComponent {
|
||||
return this.clientGeneralStep?.createClientForm;
|
||||
}
|
||||
|
||||
/** Total tab count: Individual has Family tab, Non-Individual has Scheme & Sector tab — both 5 */
|
||||
/**
|
||||
* Constitution display name for the selected entity constitution. Drives the
|
||||
* proprietorship-only nominee rule below. Empty / null for individual clients.
|
||||
*/
|
||||
get constitutionLabel(): string {
|
||||
return this.clientGeneralStep?.selectedConstitution?.name || '';
|
||||
}
|
||||
|
||||
/** Whether the Family/Nominee tab should appear at all. */
|
||||
get showFamilyTab(): boolean {
|
||||
return nomineeAllowed(this.legalFormType, this.constitutionLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Family/Nominee tab should render in nominee-only mode (family
|
||||
* member rows hidden). True for entity proprietorship, false for individuals.
|
||||
*/
|
||||
get familyTabNomineeOnly(): boolean {
|
||||
return !familyAllowed(this.legalFormType, this.constitutionLabel) && this.showFamilyTab;
|
||||
}
|
||||
|
||||
/** Total tab count varies by legal form + constitution.
|
||||
* Individual: General + KYC + Address + Family + Preview = 5
|
||||
* Entity (proprietorship): General + KYC + S&S + Address + Nominees + Preview = 6
|
||||
* Entity (non-proprietor): General + KYC + S&S + Address + Preview = 5
|
||||
*/
|
||||
get totalTabs(): number {
|
||||
return 5;
|
||||
let count = 4; // General + KYC + Address + Preview
|
||||
if (this.legalFormType === 2) count++; // S&S only for entities
|
||||
if (this.showFamilyTab) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,9 +387,11 @@ export class CreateClientComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// Family tab (index 3 for Individual, 4 for Non-Individual)
|
||||
// Family tab (index 3 for Individual, 4 for Non-Individual proprietorship).
|
||||
// Skip entirely when the tab is hidden (entity non-proprietor) so we don't
|
||||
// confuse Preview at the same index slot with the Family step.
|
||||
const familyTabIndex = this.legalFormType === 2 ? 4 : 3;
|
||||
if (tabIndex === familyTabIndex) {
|
||||
if (this.showFamilyTab && tabIndex === familyTabIndex) {
|
||||
if (this.clientFamilyMembersStep?.hasFamilyMembers) {
|
||||
this.saveClientFamilyMembers(clientId);
|
||||
}
|
||||
|
||||
@@ -138,33 +138,40 @@
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab: Family Members / Nominees -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="users" class="tab-icon"></fa-icon>
|
||||
FAMILY
|
||||
</ng-template>
|
||||
@let familyTabIdx = legalFormType === 2 ? 4 : 3;
|
||||
<div class="tab-content" [class.tab-content--readonly]="isTabReadOnly(familyTabIdx)">
|
||||
<div class="tab-mode-bar">
|
||||
@if (isTabReadOnly(familyTabIdx)) {
|
||||
<button mat-flat-button class="tab-mode-btn tab-mode-btn--edit" (click)="enableTabEdit(familyTabIdx)">
|
||||
<mat-icon>edit</mat-icon> Edit
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-stroked-button class="tab-mode-btn tab-mode-btn--cancel" (click)="cancelTabEdit(familyTabIdx)">
|
||||
<mat-icon>close</mat-icon> Cancel
|
||||
</button>
|
||||
}
|
||||
<!-- Tab: Family / Nominees — hidden for entity clients that aren't proprietorships -->
|
||||
@if (showFamilyTab) {
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="users" class="tab-icon"></fa-icon>
|
||||
{{ familyTabNomineeOnly ? 'NOMINEES' : 'FAMILY' }}
|
||||
</ng-template>
|
||||
@let familyTabIdx = legalFormType === 2 ? 4 : 3;
|
||||
<div class="tab-content" [class.tab-content--readonly]="isTabReadOnly(familyTabIdx)">
|
||||
<div class="tab-mode-bar">
|
||||
@if (isTabReadOnly(familyTabIdx)) {
|
||||
<button mat-flat-button class="tab-mode-btn tab-mode-btn--edit" (click)="enableTabEdit(familyTabIdx)">
|
||||
<mat-icon>edit</mat-icon> Edit
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="tab-mode-btn tab-mode-btn--cancel"
|
||||
(click)="cancelTabEdit(familyTabIdx)"
|
||||
>
|
||||
<mat-icon>close</mat-icon> Cancel
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<mifosx-client-family-members-step
|
||||
#clientFamilyMembers
|
||||
[clientTemplate]="clientDataAndTemplate"
|
||||
[clientId]="clientId"
|
||||
[readOnly]="isTabReadOnly(familyTabIdx)"
|
||||
[nomineeOnly]="familyTabNomineeOnly"
|
||||
></mifosx-client-family-members-step>
|
||||
</div>
|
||||
<mifosx-client-family-members-step
|
||||
#clientFamilyMembers
|
||||
[clientTemplate]="clientDataAndTemplate"
|
||||
[clientId]="clientId"
|
||||
[readOnly]="isTabReadOnly(familyTabIdx)"
|
||||
></mifosx-client-family-members-step>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab>
|
||||
}
|
||||
|
||||
<!-- Tab: Application Summary & Submit -->
|
||||
<mat-tab>
|
||||
@@ -182,6 +189,7 @@
|
||||
[guardianRelation]="clientGeneralStep?.createClientForm?.get('guardianDetails.guardianRelation')?.value || ''"
|
||||
[entityClassificationData]="clientEntityKycStep?.classificationData"
|
||||
[familyMembers]="clientFamilyMembersStep?.clientFamilyMembers || []"
|
||||
[constitutionLabel]="constitutionLabel"
|
||||
[maritalStatusId]="clientGeneralStep?.createClientForm?.get('maritalStatusId')?.value"
|
||||
[maritalStatusOptions]="clientGeneralStep?.maritalStatusOptions || []"
|
||||
[educationLevelOptions]="clientGeneralStep?.educationLevelOptions || []"
|
||||
|
||||
@@ -17,6 +17,7 @@ import { map } from 'rxjs/operators';
|
||||
import { ClientsService } from '../clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { familyAllowed, nomineeAllowed } from '../utils/client-rules';
|
||||
|
||||
/** Custom Components */
|
||||
import { ClientGeneralStepComponent } from '../client-stepper/client-general-step/client-general-step.component';
|
||||
@@ -121,9 +122,40 @@ export class EditClientComponent {
|
||||
return this.clientGeneralStep?.createClientForm;
|
||||
}
|
||||
|
||||
/** Total tab count (+1 for Nominees, +1 for Non-Individual Entity KYC) */
|
||||
/**
|
||||
* Constitution display name pulled from the live general step (preferred —
|
||||
* stays in sync if the user changes constitution mid-edit) with a fallback
|
||||
* to the resolver-provided snapshot. Drives the proprietorship-only nominee
|
||||
* rule below.
|
||||
*/
|
||||
get constitutionLabel(): string {
|
||||
return (
|
||||
this.clientGeneralStep?.selectedConstitution?.name ||
|
||||
this.clientDataAndTemplate?.clientNonPersonDetails?.constitution?.name ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether the Family/Nominee tab should appear at all. */
|
||||
get showFamilyTab(): boolean {
|
||||
return nomineeAllowed(this.legalFormType, this.constitutionLabel);
|
||||
}
|
||||
|
||||
/** True when the tab should render in nominee-only mode (family rows hidden). */
|
||||
get familyTabNomineeOnly(): boolean {
|
||||
return !familyAllowed(this.legalFormType, this.constitutionLabel) && this.showFamilyTab;
|
||||
}
|
||||
|
||||
/** Total tab count varies by legal form + constitution.
|
||||
* Individual: General + KYC + Address + Family + Preview = 5
|
||||
* Entity (proprietorship): General + KYC + S&S + Address + Nominees + Preview = 6
|
||||
* Entity (non-proprietor): General + KYC + S&S + Address + Preview = 5
|
||||
*/
|
||||
get totalTabs(): number {
|
||||
return this.legalFormType === 2 ? 6 : 5;
|
||||
let count = 4; // General + KYC + Address + Preview
|
||||
if (this.legalFormType === 2) count++; // S&S only for entities
|
||||
if (this.showFamilyTab) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Get constitution type from general step for entity KYC */
|
||||
@@ -411,9 +443,10 @@ export class EditClientComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// Family tab
|
||||
// Family tab — only present for individuals and proprietorship entities.
|
||||
// Skip when hidden so Preview at the same index isn't mistaken for Family.
|
||||
const familyTabIdx = this.legalFormType === 2 ? 4 : 3;
|
||||
if (tabIndex === familyTabIdx) {
|
||||
if (this.showFamilyTab && tabIndex === familyTabIdx) {
|
||||
this.savingClient = true;
|
||||
if (this.clientFamilyMembersStep?.hasFamilyMembers) {
|
||||
this.saveClientFamilyMembers(this.clientId);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Predicates that decide which optional family/nominee UI a client is
|
||||
* eligible for. Mirrors the backend authorization checks so the UI stays
|
||||
* in lockstep with what the server will accept.
|
||||
*
|
||||
* Individual (legalForm = 1) → family + nominees
|
||||
* Non-individual + Proprietorship → nominees only
|
||||
* Non-individual + anything else → neither
|
||||
*/
|
||||
|
||||
const normalize = (s: string | null | undefined): string => (s || '').toUpperCase().replace(/[\s_-]+/g, '');
|
||||
|
||||
export function isProprietorship(constitutionLabel: string | null | undefined): boolean {
|
||||
const n = normalize(constitutionLabel);
|
||||
return n === 'PROPRIETORSHIP' || n === 'PROPRIETOR' || n === 'SOLEPROPRIETORSHIP' || n === 'SOLEPROPRIETOR';
|
||||
}
|
||||
|
||||
export function isIndividualClient(legalFormId: number | null | undefined): boolean {
|
||||
return legalFormId === 1; // 1 = PERSON, 2 = ENTITY
|
||||
}
|
||||
|
||||
export function familyAllowed(legalFormId: number | null | undefined, _constitutionLabel?: string | null): boolean {
|
||||
return isIndividualClient(legalFormId);
|
||||
}
|
||||
|
||||
export function nomineeAllowed(legalFormId: number | null | undefined, constitutionLabel?: string | null): boolean {
|
||||
if (isIndividualClient(legalFormId)) return true;
|
||||
return isProprietorship(constitutionLabel);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
import { ClientsService } from '../clients.service';
|
||||
import { familyAllowed, nomineeAllowed } from './client-rules';
|
||||
|
||||
/**
|
||||
* Walk up the route snapshot tree until we find a `clientId` path param. The
|
||||
* family-members / nominees routes are nested two levels under the client
|
||||
* shell, so simple `route.params['clientId']` returns undefined.
|
||||
*/
|
||||
function findClientId(route: ActivatedRouteSnapshot): string | null {
|
||||
let cur: ActivatedRouteSnapshot | null = route;
|
||||
while (cur) {
|
||||
const id = cur.params?.['clientId'];
|
||||
if (id) return id;
|
||||
cur = cur.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a guard that redirects to the client's General tab when the client
|
||||
* isn't eligible for the section. Resolves the client lazily so we don't
|
||||
* couple the guard to any specific parent resolver.
|
||||
*/
|
||||
function makeEligibilityGuard(
|
||||
predicate: (legalFormId: number | null | undefined, constitutionLabel?: string | null) => boolean
|
||||
): CanActivateFn {
|
||||
return (route) => {
|
||||
const router = inject(Router);
|
||||
const clientsService = inject(ClientsService);
|
||||
const clientId = findClientId(route);
|
||||
// No clientId in the URL — let the framework's own missing-param handling kick in.
|
||||
if (!clientId) return true;
|
||||
|
||||
return clientsService.getClientData(clientId).pipe(
|
||||
map((client: any) => {
|
||||
const legalFormId = client?.legalForm?.id;
|
||||
const constitutionLabel = client?.clientNonPersonDetails?.constitution?.name || '';
|
||||
if (predicate(legalFormId, constitutionLabel)) return true;
|
||||
return router.createUrlTree([
|
||||
'/clients',
|
||||
clientId,
|
||||
'general'
|
||||
]);
|
||||
}),
|
||||
// If the client lookup fails, fail open and let downstream handle it
|
||||
// rather than redirecting users into an infinite loop on a stale URL.
|
||||
catchError(() => of(true))
|
||||
) as Observable<boolean | ReturnType<Router['createUrlTree']>>;
|
||||
};
|
||||
}
|
||||
|
||||
export const familyAllowedGuard: CanActivateFn = makeEligibilityGuard(familyAllowed);
|
||||
export const nomineeAllowedGuard: CanActivateFn = makeEligibilityGuard(nomineeAllowed);
|
||||
@@ -89,20 +89,36 @@
|
||||
<span class="ti-d">│</span>
|
||||
</ng-template>
|
||||
|
||||
<!-- Main Header — 2-Row Navigation -->
|
||||
<!-- ============================================================
|
||||
Main Header — 2-Row Navigation
|
||||
============================================================
|
||||
CURRENT ORDER (per design SS, 20 May 2026):
|
||||
Row 1: Client Master · CFCFRMS · Approvals · Cashier · Clearing ·
|
||||
Deposits · Advances · Share · NPA · General Ledger ·
|
||||
Sub Ledger
|
||||
Row 2: Accounting · CKYC · Insurance · Digital · HRM ·
|
||||
Admin · RBI · Reports · Others · Reconciliation ·
|
||||
Documents Hub
|
||||
|
||||
TO REVERT: delete the new <div class="main-header">…</div>
|
||||
block below and uncomment the OLD-ORDER block kept as an
|
||||
HTML comment for easy rollback.
|
||||
============================================================ -->
|
||||
|
||||
<!--
|
||||
════════ OLD ORDER — KEPT FOR EASY REVERT ════════
|
||||
Row 1: Institution · Tasks · Accounting · Reports · Admin · Sub Ledger ·
|
||||
General Ledger · Cashier · Officer · Deposits · Clearing
|
||||
Row 2: HRM · Share · Others · NPA · Advances · Insurance · RBI ·
|
||||
Digital · Reconciliation · CKYC Mgmt · Letter Mgmt
|
||||
|
||||
<div class="main-header">
|
||||
<!-- Row 1 — 11 items (permission-gated) -->
|
||||
<div class="nav-row r1">
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="institutionMenu" *mifosxHasPermission="'MENU_INSTITUTION'">
|
||||
<fa-icon icon="university" class="ni-ico"></fa-icon>Institution
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
routerLink="/checker-inbox-and-tasks/checker-inbox"
|
||||
*mifosxHasPermission="'MENU_TASKS'"
|
||||
>
|
||||
<a class="ni" tabindex="0" routerLink="/checker-inbox-and-tasks/checker-inbox" *mifosxHasPermission="'MENU_TASKS'">
|
||||
<fa-icon icon="check" class="ni-ico"></fa-icon>Tasks
|
||||
</a>
|
||||
<a class="ni" tabindex="0" routerLink="/accounting" *mifosxHasPermission="'MENU_ACCOUNTING'">
|
||||
@@ -116,21 +132,11 @@
|
||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Admin
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="subLedgerOpen = !subLedgerOpen; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_SUB_LEDGER'"
|
||||
>
|
||||
<a class="ni" tabindex="0" (click)="subLedgerOpen = !subLedgerOpen; glMenuOpen = false" *mifosxHasPermission="'MENU_SUB_LEDGER'">
|
||||
<fa-icon icon="book" class="ni-ico"></fa-icon>Sub Ledger
|
||||
<fa-icon [icon]="subLedgerOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="glMenuOpen = !glMenuOpen; subLedgerOpen = false"
|
||||
*mifosxHasPermission="'MENU_GENERAL_LEDGER'"
|
||||
>
|
||||
<a class="ni" tabindex="0" (click)="glMenuOpen = !glMenuOpen; subLedgerOpen = false" *mifosxHasPermission="'MENU_GENERAL_LEDGER'">
|
||||
<fa-icon icon="book-open" class="ni-ico"></fa-icon>General Ledger
|
||||
<fa-icon [icon]="glMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
@@ -150,8 +156,6 @@
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Row 2 — 11 items (permission-gated) -->
|
||||
<div class="nav-row r2">
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="hrmMenu" *mifosxHasPermission="'MENU_HRM'">
|
||||
<fa-icon icon="money-check" class="ni-ico"></fa-icon>HRM
|
||||
@@ -177,12 +181,7 @@
|
||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Insurance
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="rbiMenuOpen = !rbiMenuOpen; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_RBI'"
|
||||
>
|
||||
<a class="ni" tabindex="0" (click)="rbiMenuOpen = !rbiMenuOpen; glMenuOpen = false" *mifosxHasPermission="'MENU_RBI'">
|
||||
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
|
||||
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
@@ -204,6 +203,123 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
════════ END OLD ORDER ════════
|
||||
-->
|
||||
|
||||
<div class="main-header">
|
||||
<!-- Row 1 — Client Master · CFCFRMS · Approvals · Cashier · Clearing · Deposits · Advances · Share · NPA · General Ledger · Sub Ledger -->
|
||||
<div class="nav-row r1">
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="institutionMenu" *mifosxHasPermission="'MENU_INSTITUTION'">
|
||||
<fa-icon icon="users-cog" class="ni-ico"></fa-icon>Client Master
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" routerLink="/cfcfrms" *mifosxHasPermission="'SIDEBAR_CFCFRMS'">
|
||||
<fa-icon icon="user-shield" class="ni-ico"></fa-icon>CFCFRMS
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
routerLink="/checker-inbox-and-tasks/checker-inbox"
|
||||
*mifosxHasPermission="'MENU_TASKS'"
|
||||
>
|
||||
<fa-icon icon="check" class="ni-ico"></fa-icon>Approvals
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="cashierMenu" *mifosxHasPermission="'MENU_CASHIER'">
|
||||
<fa-icon icon="money-bill" class="ni-ico"></fa-icon>Cashier
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="clearingMenu" *mifosxHasPermission="'MENU_CLEARING'">
|
||||
<fa-icon icon="exchange" class="ni-ico"></fa-icon>Clearing
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="depositsMenu" *mifosxHasPermission="'MENU_DEPOSITS'">
|
||||
<fa-icon icon="piggy-bank" class="ni-ico"></fa-icon>Deposits
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu" *mifosxHasPermission="'MENU_ADVANCES'">
|
||||
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="shareMenu" *mifosxHasPermission="'MENU_SHARE'">
|
||||
<fa-icon icon="coins" class="ni-ico"></fa-icon>Share
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="npaMenu" *mifosxHasPermission="'MENU_NPA'">
|
||||
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="glMenuOpen = !glMenuOpen; subLedgerOpen = false"
|
||||
*mifosxHasPermission="'MENU_GENERAL_LEDGER'"
|
||||
>
|
||||
<fa-icon icon="book-open" class="ni-ico"></fa-icon>General Ledger
|
||||
<fa-icon [icon]="glMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="subLedgerOpen = !subLedgerOpen; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_SUB_LEDGER'"
|
||||
>
|
||||
<fa-icon icon="book" class="ni-ico"></fa-icon>Sub Ledger
|
||||
<fa-icon [icon]="subLedgerOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Row 2 — Accounting · CKYC · Insurance · Digital · HRM · Admin · RBI · Reports · Others · Reconciliation · Documents Hub -->
|
||||
<div class="nav-row r2">
|
||||
<a class="ni" tabindex="0" routerLink="/accounting" *mifosxHasPermission="'MENU_ACCOUNTING'">
|
||||
<fa-icon icon="money-bill-alt" class="ni-ico"></fa-icon>Accounting
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="ckycMenu" *mifosxHasPermission="'MENU_CKYC_MGMT'">
|
||||
<fa-icon icon="fingerprint" class="ni-ico"></fa-icon>CKYC
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="insuranceMenu" *mifosxHasPermission="'MENU_INSURANCE'">
|
||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Insurance
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="digitalMenu" *mifosxHasPermission="'MENU_DIGITAL'">
|
||||
<fa-icon icon="mobile-alt" class="ni-ico"></fa-icon>Digital
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="hrmMenu" *mifosxHasPermission="'MENU_HRM'">
|
||||
<fa-icon icon="money-check" class="ni-ico"></fa-icon>HRM
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="adminMenu" *mifosxHasPermission="'MENU_ADMIN'">
|
||||
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Admin
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="rbiMenuOpen = !rbiMenuOpen; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_RBI'"
|
||||
>
|
||||
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
|
||||
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="reportsMenu" *mifosxHasPermission="'MENU_REPORTS'">
|
||||
<fa-icon icon="chart-bar" class="ni-ico"></fa-icon>Reports
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="othersMenu" *mifosxHasPermission="'MENU_OTHERS'">
|
||||
<fa-icon icon="folder-open" class="ni-ico"></fa-icon>Others
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="reconciliationMenu" *mifosxHasPermission="'MENU_CONFIG_WIZARD'">
|
||||
<fa-icon icon="sync-alt" class="ni-ico"></fa-icon>Reconciliation
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="letterMgmtMenu" *mifosxHasPermission="'MENU_LETTER_MGMT'">
|
||||
<fa-icon icon="envelope" class="ni-ico"></fa-icon>Documents Hub
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LIVE KPI TICKER (below the menu — pauses on hover) -->
|
||||
<div class="kpi-ticker" role="marquee" aria-label="Live key-performance ticker">
|
||||
@@ -252,35 +368,65 @@
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #adminMenu="matMenu" [overlapTrigger]="false" class="nav-dropdown">
|
||||
<button mat-menu-item [routerLink]="['/appusers']" tabindex="0">
|
||||
<i class="fa fa-user-cog menu-item-icon"></i> Users
|
||||
</button>
|
||||
<button mat-menu-item [routerLink]="['/organization']" tabindex="0">
|
||||
<i class="fa fa-sitemap menu-item-icon"></i> Organization
|
||||
</button>
|
||||
<button mat-menu-item [routerLink]="['/system']" tabindex="0">
|
||||
<i class="fa fa-server menu-item-icon"></i> System
|
||||
</button>
|
||||
<button mat-menu-item [routerLink]="['/products']" tabindex="0">
|
||||
<i class="fa fa-box menu-item-icon"></i> Products
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/templates" tabindex="0">
|
||||
<i class="fa fa-file-alt menu-item-icon"></i> Templates
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/otp-audit-logs" tabindex="0">
|
||||
<i class="fa fa-key menu-item-icon"></i> OTP Audit Logs
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<fa-icon icon="building" class="menu-item-icon" style="color: #7c3aed"></fa-icon> Bank Profile
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<fa-icon icon="user-tie" class="menu-item-icon" style="color: #7c3aed"></fa-icon> Statutory Auditor Appointment
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<fa-icon icon="clipboard-check" class="menu-item-icon" style="color: #7c3aed"></fa-icon> Internal Audit Report
|
||||
</button>
|
||||
<!-- Admin Mega Menu — 3-column rectangular panel grouping Setup,
|
||||
Catalog and Audit & Compliance entries. -->
|
||||
<mat-menu #adminMenu="matMenu" class="admin-mega-panel" [overlapTrigger]="false">
|
||||
<div class="deposit-menu-grid admin-grid" (click)="$event.stopPropagation()">
|
||||
<!-- Setup -->
|
||||
<div class="deposit-menu-group">
|
||||
<div class="deposit-group-title">Setup</div>
|
||||
<button mat-menu-item [routerLink]="['/appusers']" tabindex="0">
|
||||
<fa-icon icon="user-cog" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
Users
|
||||
</button>
|
||||
<button mat-menu-item [routerLink]="['/organization']" tabindex="0">
|
||||
<fa-icon icon="sitemap" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
Organization
|
||||
</button>
|
||||
<button mat-menu-item [routerLink]="['/system']" tabindex="0">
|
||||
<fa-icon icon="server" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
System
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Catalog -->
|
||||
<div class="deposit-menu-group">
|
||||
<div class="deposit-group-title">Catalog</div>
|
||||
<button mat-menu-item [routerLink]="['/products']" tabindex="0">
|
||||
<fa-icon icon="box" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
Products
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/templates" tabindex="0">
|
||||
<fa-icon icon="file-alt" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
Templates
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/otp-audit-logs" tabindex="0">
|
||||
<fa-icon icon="key" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
OTP Audit Logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Audit & Compliance -->
|
||||
<div class="deposit-menu-group">
|
||||
<div class="deposit-group-title">Audit & Compliance</div>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<fa-icon icon="building" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
Bank Profile
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<fa-icon icon="user-tie" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
Statutory Auditor Appointment
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<fa-icon icon="clipboard-check" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
Internal Audit Report
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/rbi-circulars" tabindex="0">
|
||||
<fa-icon icon="newspaper" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
RBI Circulars & Notifications
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
||||
<!-- Sub Ledger Mega Menu -->
|
||||
@@ -877,6 +1023,9 @@
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<!-- CFCFRMS — single landing page (CfcfrmsShell) hosts the full
|
||||
workflow as pill-tabs, so no header dropdown is needed. -->
|
||||
|
||||
<!-- Reconciliation Mega Menu -->
|
||||
<mat-menu #reconciliationMenu="matMenu" class="reconciliation-mega-panel" [overlapTrigger]="false">
|
||||
<div class="deposit-menu-grid reconciliation-grid" (click)="$event.stopPropagation()">
|
||||
|
||||
@@ -130,25 +130,32 @@ $gray-700: #334155;
|
||||
border-bottom: 2.5px solid $brand;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 7%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
/* Sit directly under the sticky toolbar (32px tall) so both stay visible. */
|
||||
top: 32px;
|
||||
z-index: 900;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.nav-row {
|
||||
height: 44px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(11, minmax(0, 1fr));
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
|
||||
/* Grid container background shows through the 1 px gap as crisp 1 px borders
|
||||
on every side of every cell — no doubled-up 2 px lines between adjacent
|
||||
items, perfect 1 px outer frame too. */
|
||||
gap: 1px;
|
||||
background: $gray-200;
|
||||
/* Outer frame; vertical dividers between cells are drawn as `border-right`
|
||||
on each `.ni` below. Switched away from the equal-column grid so each
|
||||
cell can size to its own label — long names (General Ledger, Documents
|
||||
Hub) no longer overflow and short ones (Approvals, HRM) no longer waste
|
||||
space. */
|
||||
border: 1px solid $gray-200;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.r2.nav-row {
|
||||
/* Row 2 shares the row-1 bottom edge — collapse the duplicate border. */
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.r1 .ni {
|
||||
@@ -168,7 +175,17 @@ $gray-700: #334155;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
|
||||
/* Each cell sizes to its own label (min-width: max-content) and grows
|
||||
to share any leftover row width. Short labels stay compact, long ones
|
||||
get the room they need — no overlap, no wasted gap. */
|
||||
flex: 1 0 auto;
|
||||
min-width: max-content;
|
||||
padding: 0 14px;
|
||||
|
||||
/* Vertical divider between adjacent cells; outer frame is the row's
|
||||
own border, and the last cell drops its right border below. */
|
||||
border-right: 1px solid $gray-200;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-family:
|
||||
@@ -193,8 +210,8 @@ $gray-700: #334155;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
top: 0;
|
||||
height: 3px;
|
||||
background: transparent;
|
||||
@@ -224,6 +241,12 @@ $gray-700: #334155;
|
||||
outline: 2px solid $brand;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Last cell in a row sits flush against the row's own right edge, so
|
||||
drop its divider to avoid a double 2 px line. */
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ni-ico {
|
||||
|
||||
@@ -159,83 +159,25 @@
|
||||
<a matLine>{{ 'labels.menus.Analytics' | translate }}</a>
|
||||
</mat-list-item>
|
||||
</ng-container>
|
||||
<!-- CFCFRMS Section -->
|
||||
<!-- CFCFRMS Section — single entry points to the unified shell page
|
||||
(CfcfrmsShell, /cfcfrms) which exposes Dashboard, Complaints,
|
||||
Escalate and the three Report flows as in-page pill tabs. -->
|
||||
<ng-container *mifosxHasPermission="'SIDEBAR_CFCFRMS'">
|
||||
<div class="nav-item-sep mt-1">
|
||||
<mat-divider></mat-divider>
|
||||
<span class="text-muted">{{ 'labels.menus.CFCFRMS' | translate }}</span>
|
||||
</div>
|
||||
<mat-list-item
|
||||
[routerLink]="['/cfcfrms/dashboard']"
|
||||
[routerLink]="['/cfcfrms']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="{{ 'tooltips.CFCFRMS Dashboard' | translate }}"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="shield-alt" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<a matLine>{{ 'labels.menus.CFCFRMS Dashboard' | translate }}</a>
|
||||
</mat-list-item>
|
||||
<mat-list-item
|
||||
[routerLink]="['/cfcfrms/complaints']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="{{ 'tooltips.Complaints' | translate }}"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="list-alt" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<a matLine>{{ 'labels.menus.Complaints' | translate }}</a>
|
||||
</mat-list-item>
|
||||
<mat-list-item
|
||||
[routerLink]="['/cfcfrms/escalate']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="{{ 'tooltips.Escalate Complaint' | translate }}"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="arrow-up" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<a matLine>{{ 'labels.menus.Escalate' | translate }}</a>
|
||||
</mat-list-item>
|
||||
<mat-list-item
|
||||
[routerLink]="['/cfcfrms/report-suspect']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="{{ 'tooltips.Report Suspect' | translate }}"
|
||||
matTooltip="{{ 'tooltips.CFCFRMS' | translate }}"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="user-shield" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<a matLine>{{ 'labels.menus.Report Suspect' | translate }}</a>
|
||||
</mat-list-item>
|
||||
<mat-list-item
|
||||
[routerLink]="['/cfcfrms/report-mule']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="{{ 'tooltips.Report Mule Account' | translate }}"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="user-slash" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<a matLine>{{ 'labels.menus.Report Mule' | translate }}</a>
|
||||
</mat-list-item>
|
||||
<mat-list-item
|
||||
[routerLink]="['/cfcfrms/report-fraud-txn']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="{{ 'tooltips.Report Fraud Transaction' | translate }}"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="exclamation-triangle" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<a matLine>{{ 'labels.menus.Report Fraud Txn' | translate }}</a>
|
||||
<a matLine>{{ 'labels.menus.CFCFRMS' | translate }}</a>
|
||||
</mat-list-item>
|
||||
</ng-container>
|
||||
|
||||
|
||||
@@ -11,6 +11,16 @@ $accent: #0ea5e9;
|
||||
$accent-2: #10b981;
|
||||
$top-h: 32px;
|
||||
|
||||
// Pin the toolbar to the top of the viewport so it stays visible while
|
||||
// scrolling, just like the .main-header menu below it. z-index sits one tier
|
||||
// above the menu so the toolbar's dropdowns / chips never get clipped.
|
||||
:host {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 950;
|
||||
}
|
||||
|
||||
// ===== Top Identity Bar =====
|
||||
.top-bar {
|
||||
height: $top-h;
|
||||
|
||||
@@ -6,100 +6,129 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-stepper
|
||||
orientation="vertical"
|
||||
[linear]="false"
|
||||
class="mat-elevation-z8"
|
||||
labelPosition="bottom"
|
||||
#savingProductStepper
|
||||
<div class="csa-container">
|
||||
<!-- Hero -->
|
||||
<div class="csa-header">
|
||||
<fa-icon icon="piggy-bank" class="csa-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="csa-header__title">Create Savings Account</h2>
|
||||
<div class="csa-header__sub">
|
||||
Pick a product, confirm currency, set digital channels and review before submit.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard tabs -->
|
||||
<mat-tab-group
|
||||
class="csa-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<ng-template matStepperIcon="number">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="edit">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon icon="check" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="error">
|
||||
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="preview">
|
||||
<fa-icon icon="eye" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<!-- Step 1: Product Classification + Details -->
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Product Classification</ng-template>
|
||||
|
||||
<mifosx-savings-account-details-step
|
||||
[savingsAccountTemplate]="savingsAccountTemplate"
|
||||
(savingsAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-savings-account-details-step>
|
||||
</mat-step>
|
||||
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<!-- Step 2: Currency -->
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Currency</ng-template>
|
||||
|
||||
<mifosx-savings-account-currency-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[primaryClientId]="savingsAccountTemplate?.clientId ?? null"
|
||||
></mifosx-savings-account-currency-step>
|
||||
|
||||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||||
<button mat-raised-button matStepperPrevious>
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button matStepperNext>
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 3: Digital -->
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Digital</ng-template>
|
||||
|
||||
<mifosx-savings-account-digital-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
|
||||
</mifosx-savings-account-digital-step>
|
||||
|
||||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||||
<button mat-raised-button matStepperPrevious>
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button matStepperNext>
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 4: Preview -->
|
||||
<mat-step completed>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
|
||||
<mifosx-savings-account-preview-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
<!-- Tab 1: Product Classification + Details -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="check-square" class="csa-tab__icon"></fa-icon>
|
||||
Product
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
<mifosx-savings-account-details-step
|
||||
[savingsAccountTemplate]="savingsAccountTemplate"
|
||||
[savingsAccountTermsForm]="savingsAccountTermsForm"
|
||||
[savingsAccount]="savingsAccount"
|
||||
[savingsAccountDigital]="savingsAccountDigital"
|
||||
[savingsAccountNominees]="savingsAccountCurrencyStep?.nominees ?? []"
|
||||
(submitEvent)="submit()"
|
||||
(savingsAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-savings-account-preview-step>
|
||||
</mat-step>
|
||||
}
|
||||
</mat-stepper>
|
||||
</mifosx-savings-account-details-step>
|
||||
|
||||
<div class="csa-actions">
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" [disabled]="!savingsAccountProductTemplate" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 2: Currency -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="dollar-sign" class="csa-tab__icon"></fa-icon>
|
||||
Currency
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-currency-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[primaryClientId]="savingsAccountTemplate?.clientId ?? null"
|
||||
></mifosx-savings-account-currency-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 3: Digital -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="mobile-alt" class="csa-tab__icon"></fa-icon>
|
||||
Digital
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-digital-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
|
||||
</mifosx-savings-account-digital-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 4: Preview / Submit -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="eye" class="csa-tab__icon"></fa-icon>
|
||||
{{ 'labels.inputs.PREVIEW' | translate }}
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-preview-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[savingsAccountTemplate]="savingsAccountTemplate"
|
||||
[savingsAccountTermsForm]="savingsAccountTermsForm"
|
||||
[savingsAccount]="savingsAccount"
|
||||
[savingsAccountDigital]="savingsAccountDigital"
|
||||
[savingsAccountNominees]="savingsAccountCurrencyStep?.nominees ?? []"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-savings-account-preview-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
@@ -6,75 +6,153 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// Container styling with proper spacing and margins
|
||||
.container {
|
||||
margin: 1.5rem auto;
|
||||
max-width: 75rem;
|
||||
width: 100%;
|
||||
padding: 0 1.5rem;
|
||||
box-sizing: border-box;
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$indigo: #4f46e5;
|
||||
$border: #e2e8f0;
|
||||
|
||||
.csa-container {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
// Material Stepper styling with proper spacing
|
||||
:host ::ng-deep mat-stepper {
|
||||
background: transparent;
|
||||
/* ─── Hero header (matches Nexora pages) ────────────────────────────── */
|
||||
.csa-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.mat-step-header {
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, $blue 0%, $indigo 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mat-step-content {
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
overflow: visible !important;
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
// Form field spacing within steps
|
||||
:host ::ng-deep mat-step-content {
|
||||
overflow: visible !important;
|
||||
/* ─── Wizard tab strip ──────────────────────────────────────────────── */
|
||||
.csa-tabs {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-row-wrap {
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (width <= 768px) {
|
||||
.container {
|
||||
padding: 0 1rem;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
:host ::ng-deep mat-stepper {
|
||||
.mat-step-content {
|
||||
padding: 1rem;
|
||||
margin: 0.75rem 0;
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.mat-step-header {
|
||||
padding: 0.75rem 1rem;
|
||||
.mat-mdc-tab {
|
||||
min-width: 140px;
|
||||
height: 50px;
|
||||
padding: 0 22px;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em;
|
||||
color: #475569 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) {
|
||||
background: rgb(30 64 175 / 5%);
|
||||
color: $navy !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mdc-tab--active {
|
||||
.mdc-tab__text-label {
|
||||
color: $blue-d !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Active-tab underline indicator — slightly bolder than Material's default. */
|
||||
.mdc-tab-indicator__content--underline {
|
||||
border-color: $blue-d !important;
|
||||
border-top-width: 3px !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mat-mdc-tab-disabled {
|
||||
opacity: 0.45 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body-wrapper {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body,
|
||||
.mat-mdc-tab-body-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 480px) {
|
||||
.container {
|
||||
padding: 0 0.75rem;
|
||||
margin: 0.75rem auto;
|
||||
}
|
||||
.csa-tab__icon {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:host ::ng-deep mat-stepper {
|
||||
.mat-step-content {
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
/* ─── Tab body + action bar ─────────────────────────────────────────── */
|
||||
.csa-tab-body {
|
||||
padding: 24px;
|
||||
|
||||
@media (width <= 768px) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.csa-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
&__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 40px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility helpers preserved from the prior stepper template. */
|
||||
.m-l-10 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.m-r-10 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@@ -15,27 +15,24 @@ import { FormGroup } from '@angular/forms';
|
||||
import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component';
|
||||
import { SavingsAccountCurrencyStepComponent } from '../savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component';
|
||||
import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component';
|
||||
import { SavingsAccountSettingsStepComponent } from '../savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component';
|
||||
|
||||
/** Custom Services */
|
||||
import { SavingsService } from '../savings.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import {
|
||||
MatStepper,
|
||||
MatStepperIcon,
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
MatStepperNext,
|
||||
MatStepperPrevious
|
||||
} from '@angular/material/stepper';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { SavingsAccountPreviewStepComponent } from '../savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Create Savings Account Component
|
||||
*
|
||||
* Wizard-tab UI: Product → Currency → Digital → Preview. Tabs 2-4 are
|
||||
* disabled until a product is picked on Tab 1 (matches the prior stepper
|
||||
* gate that only rendered later steps once `savingsAccountProductTemplate`
|
||||
* was set).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-create-savings-account',
|
||||
@@ -43,13 +40,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./create-savings-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatStepper,
|
||||
MatStepperIcon,
|
||||
MatStepperNext,
|
||||
MatStepperPrevious,
|
||||
MatTabsModule,
|
||||
FaIconComponent,
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
SavingsAccountDetailsStepComponent,
|
||||
SavingsAccountCurrencyStepComponent,
|
||||
SavingsAccountDigitalStepComponent,
|
||||
@@ -68,6 +60,10 @@ export class CreateSavingsAccountComponent {
|
||||
savingsAccountTemplate: any;
|
||||
/** Savings Account Product Template */
|
||||
savingsAccountProductTemplate: any;
|
||||
/** Currently visible wizard tab (0-indexed). */
|
||||
selectedTabIndex = 0;
|
||||
/** Total wizard tabs — Product, Currency, Digital, Preview. */
|
||||
private readonly totalTabs = 4;
|
||||
|
||||
@ViewChild(SavingsAccountDetailsStepComponent, { static: true })
|
||||
savingsAccountDetailsStep: SavingsAccountDetailsStepComponent;
|
||||
@@ -88,6 +84,18 @@ export class CreateSavingsAccountComponent {
|
||||
this.savingsAccountProductTemplate = $event;
|
||||
}
|
||||
|
||||
next(): void {
|
||||
if (this.selectedTabIndex < this.totalTabs - 1) {
|
||||
this.selectedTabIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev(): void {
|
||||
if (this.selectedTabIndex > 0) {
|
||||
this.selectedTabIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
get savingsAccountDetailsForm() {
|
||||
return this.savingsAccountDetailsStep.savingsAccountDetailsForm;
|
||||
}
|
||||
|
||||
+67
-47
@@ -6,53 +6,73 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container mat-elevation-z8">
|
||||
<mat-card>
|
||||
<form [formGroup]="activateSavingsAccountForm" (ngSubmit)="submit()">
|
||||
<mat-card-content>
|
||||
<div class="layout-column">
|
||||
<mat-form-field class="flex-fill" (click)="activatedOnDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Activated On Date' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="activatedOnDatePicker"
|
||||
required
|
||||
formControlName="activatedOnDate"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="activatedOnDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #activatedOnDatePicker></mat-datepicker>
|
||||
@if (activateSavingsAccountForm.controls.activatedOnDate.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Activated On Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="act-savings">
|
||||
<!-- Hero -->
|
||||
<div class="act-savings__hero">
|
||||
<div class="act-savings__hero-icon">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="act-savings__title">Activate Savings Account</h2>
|
||||
<p class="act-savings__sub">Set the activation date and opening balance to bring this account live.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="flex-fill">
|
||||
<mat-label>Opening Balance</mat-label>
|
||||
<input matInput type="number" [min]="minRequiredOpeningBalance ?? 0" formControlName="openingBalance" />
|
||||
<span matPrefix>₹ </span>
|
||||
@if (minRequiredOpeningBalance) {
|
||||
<mat-hint>Minimum required: ₹{{ minRequiredOpeningBalance }}</mat-hint>
|
||||
}
|
||||
@if (activateSavingsAccountForm.controls.openingBalance.hasError('min')) {
|
||||
<mat-error>Opening balance must be at least ₹{{ minRequiredOpeningBalance ?? 0 }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<!-- Form card -->
|
||||
<form class="act-savings__card" [formGroup]="activateSavingsAccountForm" (ngSubmit)="submit()">
|
||||
<div class="act-savings__card-head">
|
||||
<fa-icon icon="info-circle" class="act-savings__card-ico"></fa-icon>
|
||||
<h3 class="act-savings__card-title">Activation Details</h3>
|
||||
</div>
|
||||
|
||||
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
|
||||
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!activateSavingsAccountForm.valid">
|
||||
{{ 'labels.buttons.Confirm' | translate }}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</form>
|
||||
</mat-card>
|
||||
<div class="act-savings__card-body">
|
||||
<mat-form-field appearance="outline" class="act-savings__field" (click)="activatedOnDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Activated On Date' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="activatedOnDatePicker"
|
||||
required
|
||||
formControlName="activatedOnDate"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="activatedOnDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #activatedOnDatePicker></mat-datepicker>
|
||||
@if (activateSavingsAccountForm.controls.activatedOnDate.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Activated On Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="act-savings__field">
|
||||
<mat-label>Opening Balance</mat-label>
|
||||
<input matInput type="number" [min]="minRequiredOpeningBalance ?? 0" formControlName="openingBalance" />
|
||||
<span matPrefix class="act-savings__prefix">₹ </span>
|
||||
@if (minRequiredOpeningBalance) {
|
||||
<mat-hint>Minimum required: ₹{{ minRequiredOpeningBalance }}</mat-hint>
|
||||
}
|
||||
@if (activateSavingsAccountForm.controls.openingBalance.hasError('min')) {
|
||||
<mat-error>Opening balance must be at least ₹{{ minRequiredOpeningBalance ?? 0 }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="act-savings__actions">
|
||||
<button type="button" mat-stroked-button class="act-savings__cancel" [routerLink]="['../../transactions']">
|
||||
<fa-icon icon="times" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
type="submit"
|
||||
class="act-savings__confirm"
|
||||
[disabled]="!activateSavingsAccountForm.valid"
|
||||
>
|
||||
<fa-icon icon="check" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Confirm' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+186
-2
@@ -6,6 +6,190 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.container {
|
||||
max-width: 37rem;
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$green: #10b981;
|
||||
$green-d: #047857;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
|
||||
.act-savings {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 24px;
|
||||
|
||||
/* ─── Hero ───────────────────────────────────────────────────────── */
|
||||
&__hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
&__hero-icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, $green 0%, $green-d 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 4px 14px -4px rgb(4 120 87 / 45%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.15;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
max-width: 540px;
|
||||
}
|
||||
|
||||
/* ─── Form card ─────────────────────────────────────────────────── */
|
||||
&__card {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 13px 20px;
|
||||
background: $soft;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
&__card-ico {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, $blue 0%, $blue-d 100%);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__card-title {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
&__card-body {
|
||||
padding: 22px 22px 4px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Form field tweaks — slightly more compact density. */
|
||||
&__field {
|
||||
width: 100%;
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
min-height: 48px;
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-notched-outline__leading,
|
||||
::ng-deep .mdc-notched-outline__notch,
|
||||
::ng-deep .mdc-notched-outline__trailing {
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
::ng-deep &.mat-focused {
|
||||
.mdc-notched-outline__leading,
|
||||
.mdc-notched-outline__notch,
|
||||
.mdc-notched-outline__trailing {
|
||||
border-color: $blue-d !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__prefix {
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* ─── Action row ────────────────────────────────────────────────── */
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 14px 20px 18px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
background: #fff;
|
||||
|
||||
button {
|
||||
height: 42px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 22px !important;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
color: #475569 !important;
|
||||
border-color: $border !important;
|
||||
}
|
||||
|
||||
&__confirm {
|
||||
background: linear-gradient(135deg, $green 0%, $green-d 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
filter: grayscale(0.3);
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.act-savings {
|
||||
padding: 14px;
|
||||
|
||||
&__actions {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -11,6 +11,7 @@ import { Component, OnInit, Input, inject } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
/** Custom Services */
|
||||
import { SavingsService } from 'app/savings/savings.service';
|
||||
@@ -19,13 +20,17 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Activate Savings Account Component
|
||||
*
|
||||
* Nexora redesign: gradient green hero + section card with form fields and
|
||||
* a primary/secondary action row.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-activate-savings-account',
|
||||
templateUrl: './activate-savings-account.component.html',
|
||||
styleUrls: ['./activate-savings-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class ActivateSavingsAccountComponent implements OnInit {
|
||||
|
||||
+137
-105
@@ -6,121 +6,153 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container mat-elevation-z8">
|
||||
<mat-card>
|
||||
<form [formGroup]="savingsChargeForm" (ngSubmit)="submit()">
|
||||
<mat-card-content>
|
||||
<div class="layout-column">
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
|
||||
<mat-select required formControlName="chargeId">
|
||||
@for (savingsCharge of savingsChargeOptions; track savingsCharge) {
|
||||
<mat-option [value]="savingsCharge.id">
|
||||
{{ savingsCharge.name + ' (' + savingsCharge.currency.name + ')' }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (savingsChargeForm.controls.chargeId.hasError('required')) {
|
||||
<div class="acs">
|
||||
<!-- Hero -->
|
||||
<div class="acs__hero">
|
||||
<button type="button" mat-stroked-button class="acs__back" [routerLink]="['../../transactions']">
|
||||
<fa-icon icon="arrow-left"></fa-icon>
|
||||
</button>
|
||||
|
||||
<div class="acs__hero-ico">
|
||||
<fa-icon icon="plus-circle"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div class="acs__hero-text">
|
||||
<h2 class="acs__title">Add Charge to Savings Account</h2>
|
||||
<p class="acs__sub">
|
||||
Attach a fee or penalty from the product's catalogue. Amount and timing are pre-filled from the selected charge
|
||||
— override only when needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="acs__hero-pill">
|
||||
<fa-icon icon="receipt"></fa-icon>
|
||||
Charge
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Form card -->
|
||||
<form class="acs__card" [formGroup]="savingsChargeForm" (ngSubmit)="submit()">
|
||||
<div class="acs__card-head">
|
||||
<fa-icon icon="info-circle" class="acs__card-ico"></fa-icon>
|
||||
<h3 class="acs__card-title">Charge Details</h3>
|
||||
</div>
|
||||
|
||||
<div class="acs__card-body">
|
||||
<mat-form-field appearance="outline" class="acs__field">
|
||||
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
|
||||
<mat-select required formControlName="chargeId">
|
||||
@for (savingsCharge of savingsChargeOptions; track savingsCharge.id) {
|
||||
<mat-option [value]="savingsCharge.id">
|
||||
{{ savingsCharge.name + ' (' + savingsCharge.currency.name + ')' }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (savingsChargeForm.controls.chargeId.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Charge' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (chargeDetails) {
|
||||
<div class="acs__field-grid">
|
||||
<mat-form-field appearance="outline" class="acs__field">
|
||||
<mat-label>{{ 'labels.inputs.Amount' | translate }}</mat-label>
|
||||
<input type="number" required matInput formControlName="amount" />
|
||||
@if (savingsChargeForm.controls.amount.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Charge' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
{{ 'labels.inputs.Amount' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (chargeDetails) {
|
||||
<div class="layout-column">
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Amount' | translate }}</mat-label>
|
||||
<input type="number" required matInput formControlName="amount" />
|
||||
@if (savingsChargeForm.controls.amount.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Amount' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Charge Calculation' | translate }}</mat-label>
|
||||
<mat-select formControlName="chargeCalculationType">
|
||||
@for (chargeCalculation of chargeDetails.chargeCalculationTypeOptions; track chargeCalculation) {
|
||||
<mat-option [value]="chargeCalculation.id">
|
||||
{{ chargeCalculation.value }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Charge time type' | translate }}</mat-label>
|
||||
<mat-select formControlName="chargeTimeType">
|
||||
@for (chargeTime of chargeDetails.chargeTimeTypeOptions; track chargeTime) {
|
||||
<mat-option [value]="chargeTime.id">
|
||||
{{ chargeTime.value }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (savingsChargeForm.contains('dueDate')) {
|
||||
<mat-form-field (click)="dueDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Due for collection on' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="dueDatePicker"
|
||||
required
|
||||
formControlName="dueDate"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dueDatePicker></mat-datepicker>
|
||||
@if (savingsChargeForm.controls.dueDate.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Due for collection on' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="acs__field">
|
||||
<mat-label>{{ 'labels.inputs.Charge Calculation' | translate }}</mat-label>
|
||||
<mat-select formControlName="chargeCalculationType">
|
||||
@for (chargeCalculation of chargeDetails.chargeCalculationTypeOptions; track chargeCalculation.id) {
|
||||
<mat-option [value]="chargeCalculation.id">{{ chargeCalculation.value }}</mat-option>
|
||||
}
|
||||
@if (savingsChargeForm.contains('feeOnMonthDay')) {
|
||||
<mat-form-field (click)="feeOnMonthDayPicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Due On' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="feeOnMonthDayPicker"
|
||||
required
|
||||
formControlName="feeOnMonthDay"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="feeOnMonthDayPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #feeOnMonthDayPicker></mat-datepicker>
|
||||
@if (savingsChargeForm.controls.feeOnMonthDay.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Due Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="acs__field">
|
||||
<mat-label>{{ 'labels.inputs.Charge time type' | translate }}</mat-label>
|
||||
<mat-select formControlName="chargeTimeType">
|
||||
@for (chargeTime of chargeDetails.chargeTimeTypeOptions; track chargeTime.id) {
|
||||
<mat-option [value]="chargeTime.id">{{ chargeTime.value }}</mat-option>
|
||||
}
|
||||
@if (savingsChargeForm.contains('feeInterval')) {
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Repeats Every' | translate }}</mat-label>
|
||||
<input matInput formControlName="feeInterval" />
|
||||
</mat-form-field>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
@if (savingsChargeForm.contains('dueDate')) {
|
||||
<mat-form-field appearance="outline" class="acs__field" (click)="dueDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Due for collection on' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="dueDatePicker"
|
||||
required
|
||||
formControlName="dueDate"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dueDatePicker></mat-datepicker>
|
||||
@if (savingsChargeForm.controls.dueDate.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Due for collection on' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</div>
|
||||
</mat-form-field>
|
||||
}
|
||||
@if (savingsChargeForm.contains('feeOnMonthDay')) {
|
||||
<mat-form-field appearance="outline" class="acs__field" (click)="feeOnMonthDayPicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Due On' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="feeOnMonthDayPicker"
|
||||
required
|
||||
formControlName="feeOnMonthDay"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="feeOnMonthDayPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #feeOnMonthDayPicker></mat-datepicker>
|
||||
@if (savingsChargeForm.controls.feeOnMonthDay.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Due Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
@if (savingsChargeForm.contains('feeInterval')) {
|
||||
<mat-form-field appearance="outline" class="acs__field">
|
||||
<mat-label>{{ 'labels.inputs.Repeats Every' | translate }}</mat-label>
|
||||
<input matInput formControlName="feeInterval" />
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="acs__hint">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
Pick a charge above to load its amount, calculation type and timing.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-card-actions class="layout-row layout-xs-column layout-align-center gap-5px">
|
||||
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!savingsChargeForm.valid">
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card-content>
|
||||
</form>
|
||||
</mat-card>
|
||||
<div class="acs__actions">
|
||||
<button type="button" mat-stroked-button class="acs__cancel" [routerLink]="['../../transactions']">
|
||||
<fa-icon icon="times" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button type="submit" class="acs__submit" [disabled]="!savingsChargeForm.valid">
|
||||
<fa-icon icon="check" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+269
-2
@@ -6,6 +6,273 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.container {
|
||||
width: 37rem;
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
$violet: #6d28d9;
|
||||
$violet-d: #4c1d95;
|
||||
|
||||
.acs {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 14px 18px 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ─── Hero ─────────────────────────────────────────────────────────── */
|
||||
.acs__hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1e1b4b 0%, $violet-d 45%, $violet 100%);
|
||||
box-shadow: 0 6px 18px -10px rgb(76 29 149 / 50%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top right, rgb(255 255 255 / 12%), transparent 55%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.acs__back {
|
||||
height: 30px !important;
|
||||
min-width: 30px !important;
|
||||
width: 30px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
background: rgb(255 255 255 / 14%) !important;
|
||||
color: #fff !important;
|
||||
border: 1px solid rgb(255 255 255 / 28%) !important;
|
||||
font-size: 12px !important;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 26%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.acs__hero-ico {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 9px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 28%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.acs__hero-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.acs__title {
|
||||
margin: 0;
|
||||
font-size: 14.5px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.acs__sub {
|
||||
margin: 2px 0 0;
|
||||
font-size: 11.5px;
|
||||
color: rgb(255 255 255 / 82%);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.acs__hero-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 22px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 16%);
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
font-size: 10.5px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
fa-icon {
|
||||
font-size: 10px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Form card ────────────────────────────────────────────────────── */
|
||||
.acs__card {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.acs__card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 14px;
|
||||
background: $soft;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.acs__card-ico {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(135deg, $violet 0%, $violet-d 100%);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.acs__card-title {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.acs__card-body {
|
||||
padding: 16px 18px 6px;
|
||||
}
|
||||
|
||||
.acs__field {
|
||||
width: 100%;
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
min-height: 46px;
|
||||
padding-top: 11px !important;
|
||||
padding-bottom: 11px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-notched-outline__leading,
|
||||
::ng-deep .mdc-notched-outline__notch,
|
||||
::ng-deep .mdc-notched-outline__trailing {
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Two-column grid for the charge-detail fields when a charge is selected. */
|
||||
.acs__field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hint shown before a charge is picked. */
|
||||
.acs__hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
margin-top: 4px;
|
||||
border-radius: 10px;
|
||||
background: #eef2ff;
|
||||
border: 1px dashed #c7d2fe;
|
||||
color: #3730a3;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
|
||||
fa-icon {
|
||||
color: $violet;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Actions ─────────────────────────────────────────────────────── */
|
||||
.acs__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 12px 18px 14px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
button {
|
||||
height: 38px !important;
|
||||
border-radius: 9px !important;
|
||||
font-weight: 700 !important;
|
||||
padding: 0 18px !important;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
.acs__cancel {
|
||||
color: $slate !important;
|
||||
border-color: $border !important;
|
||||
}
|
||||
|
||||
.acs__submit {
|
||||
background: linear-gradient(135deg, $violet 0%, $violet-d 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 3px 10px -2px rgb(76 29 149 / 35%) !important;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
filter: grayscale(0.3);
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.acs {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.acs__hero {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.acs__actions {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -21,6 +21,7 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { SavingsService } from '../../savings.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
@@ -31,7 +32,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
templateUrl: './add-charge-savings-account.component.html',
|
||||
styleUrls: ['./add-charge-savings-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class AddChargeSavingsAccountComponent implements OnInit {
|
||||
|
||||
+98
-42
@@ -6,36 +6,92 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container mat-elevation-z8">
|
||||
<mat-card>
|
||||
@if (transactionType.holdamount) {
|
||||
<mat-card-title>{{ 'labels.heading.Hold Amount' | translate }}</mat-card-title>
|
||||
}
|
||||
@if (transactionType.blockaccount) {
|
||||
<mat-card-title>{{ 'labels.heading.Block Savings Account' | translate }}</mat-card-title>
|
||||
}
|
||||
<div
|
||||
class="msa"
|
||||
[class.msa--hold]="transactionType.holdamount"
|
||||
[class.msa--block-account]="transactionType.blockaccount"
|
||||
[class.msa--block-deposit]="transactionType.blockdeposit"
|
||||
[class.msa--block-withdrawal]="transactionType.blockwithdrawal"
|
||||
>
|
||||
<!-- Hero -->
|
||||
<div class="msa__hero">
|
||||
<button type="button" mat-stroked-button class="msa__back" [routerLink]="['../../transactions']">
|
||||
<fa-icon icon="arrow-left"></fa-icon>
|
||||
</button>
|
||||
|
||||
<form [formGroup]="manageSavingsAccountForm" (ngSubmit)="submit()">
|
||||
<mat-card-content class="layout-row">
|
||||
<mat-form-field class="flex-100">
|
||||
<mat-label>{{ 'labels.inputs.Reason' | translate }}</mat-label>
|
||||
<mat-select required formControlName="reasonForBlock">
|
||||
@for (codeValue of reasonOptions; track codeValue) {
|
||||
<mat-option [value]="codeValue.id">
|
||||
{{ codeValue.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (manageSavingsAccountForm.controls.reasonForBlock.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Reason' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="msa__hero-ico">
|
||||
<fa-icon
|
||||
[icon]="
|
||||
transactionType.holdamount
|
||||
? 'pause'
|
||||
: transactionType.blockaccount
|
||||
? 'lock'
|
||||
: transactionType.blockdeposit
|
||||
? 'lock'
|
||||
: transactionType.blockwithdrawal
|
||||
? 'lock'
|
||||
: 'lock'
|
||||
"
|
||||
></fa-icon>
|
||||
</div>
|
||||
|
||||
<div class="msa__hero-text">
|
||||
<h2 class="msa__title">
|
||||
@if (transactionType.holdamount) {
|
||||
<mat-form-field class="flex-100" (click)="transactionDatePicker.open()">
|
||||
{{ 'labels.heading.Hold Amount' | translate }}
|
||||
} @else if (transactionType.blockaccount) {
|
||||
Block Savings Account
|
||||
} @else if (transactionType.blockdeposit) {
|
||||
Block Deposits
|
||||
} @else if (transactionType.blockwithdrawal) {
|
||||
Block Withdrawals
|
||||
}
|
||||
</h2>
|
||||
<p class="msa__sub">
|
||||
@if (transactionType.holdamount) {
|
||||
Place a hold on a portion of the available balance with a reason and amount.
|
||||
} @else if (transactionType.blockaccount) {
|
||||
Freezes all account activity. Select a reason to record the action in the audit log.
|
||||
} @else if (transactionType.blockdeposit) {
|
||||
Stops incoming credits to this account. Existing withdrawals continue to work.
|
||||
} @else if (transactionType.blockwithdrawal) {
|
||||
Stops outgoing debits from this account. Deposits continue to be accepted.
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="msa__hero-pill">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
Action recorded
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Form card -->
|
||||
<form class="msa__card" [formGroup]="manageSavingsAccountForm" (ngSubmit)="submit()">
|
||||
<div class="msa__card-head">
|
||||
<fa-icon icon="info-circle" class="msa__card-ico"></fa-icon>
|
||||
<h3 class="msa__card-title">Action Details</h3>
|
||||
</div>
|
||||
|
||||
<div class="msa__card-body">
|
||||
<mat-form-field appearance="outline" class="msa__field">
|
||||
<mat-label>{{ 'labels.inputs.Reason' | translate }}</mat-label>
|
||||
<mat-select required formControlName="reasonForBlock">
|
||||
@for (codeValue of reasonOptions; track codeValue.id) {
|
||||
<mat-option [value]="codeValue.id">{{ codeValue.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (manageSavingsAccountForm.controls.reasonForBlock.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Reason' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (transactionType.holdamount) {
|
||||
<div class="msa__field-row">
|
||||
<mat-form-field appearance="outline" class="msa__field" (click)="transactionDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Transaction Date' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
@@ -54,28 +110,28 @@
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (transactionType.holdamount) {
|
||||
<mifosx-input-amount
|
||||
class="flex-100"
|
||||
class="msa__field"
|
||||
[currency]="currency"
|
||||
[isRequired]="true"
|
||||
[inputFormControl]="manageSavingsAccountForm.controls.transactionAmount"
|
||||
[inputLabel]="'Transaction Amount'"
|
||||
>
|
||||
</mifosx-input-amount>
|
||||
}
|
||||
</mat-card-content>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
|
||||
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!manageSavingsAccountForm.valid">
|
||||
{{ 'labels.buttons.Confirm' | translate }}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</form>
|
||||
</mat-card>
|
||||
<div class="msa__actions">
|
||||
<button type="button" mat-stroked-button class="msa__cancel" [routerLink]="['../../transactions']">
|
||||
<fa-icon icon="times" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button type="submit" class="msa__confirm" [disabled]="!manageSavingsAccountForm.valid">
|
||||
<fa-icon icon="check" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Confirm' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+301
-2
@@ -6,6 +6,305 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.container {
|
||||
width: 37rem;
|
||||
/* Manage-Savings-Account screen — drives four flows: Hold Amount (blue),
|
||||
Block Deposit (orange), Block Withdrawal (orange), Block Account (red).
|
||||
Hero gradient + Confirm button tint shift per modifier class. */
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$amber: #d97706;
|
||||
$amber-d: #b45309;
|
||||
$red: #b91c1c;
|
||||
$red-d: #7f1d1d;
|
||||
|
||||
.msa {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 14px 18px 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ─── Hero ─────────────────────────────────────────────────────────── */
|
||||
.msa__hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
/* Default (Hold Amount) — calm blue. Modifier classes override below. */
|
||||
background: linear-gradient(135deg, #0f172a 0%, $blue-d 45%, $blue 100%);
|
||||
box-shadow: 0 6px 18px -10px rgb(30 64 175 / 50%);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top right, rgb(255 255 255 / 12%), transparent 55%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero gradient variants */
|
||||
.msa--block-deposit .msa__hero,
|
||||
.msa--block-withdrawal .msa__hero {
|
||||
background: linear-gradient(135deg, #7c2d12 0%, $amber-d 45%, $amber 100%);
|
||||
box-shadow: 0 6px 18px -10px rgb(180 83 9 / 50%);
|
||||
}
|
||||
|
||||
.msa--block-account .msa__hero {
|
||||
background: linear-gradient(135deg, $red-d 0%, $red 45%, #dc2626 100%);
|
||||
box-shadow: 0 6px 18px -10px rgb(153 27 27 / 55%);
|
||||
}
|
||||
|
||||
.msa__back {
|
||||
height: 30px !important;
|
||||
min-width: 30px !important;
|
||||
width: 30px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
background: rgb(255 255 255 / 14%) !important;
|
||||
color: #fff !important;
|
||||
border: 1px solid rgb(255 255 255 / 28%) !important;
|
||||
font-size: 12px !important;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 26%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.msa__hero-ico {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 9px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 28%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.msa__hero-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.msa__title {
|
||||
margin: 0;
|
||||
font-size: 14.5px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.msa__sub {
|
||||
margin: 2px 0 0;
|
||||
font-size: 11.5px;
|
||||
color: rgb(255 255 255 / 82%);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.msa__hero-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 22px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 16%);
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
font-size: 10.5px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
fa-icon {
|
||||
font-size: 10px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Form card ────────────────────────────────────────────────────── */
|
||||
.msa__card {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.msa__card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 14px;
|
||||
background: $soft;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.msa__card-ico {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(135deg, $blue 0%, $blue-d 100%);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.msa--block-deposit .msa__card-ico,
|
||||
.msa--block-withdrawal .msa__card-ico {
|
||||
background: linear-gradient(135deg, $amber 0%, $amber-d 100%);
|
||||
}
|
||||
|
||||
.msa--block-account .msa__card-ico {
|
||||
background: linear-gradient(135deg, $red 0%, $red-d 100%);
|
||||
}
|
||||
|
||||
.msa__card-title {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.msa__card-body {
|
||||
padding: 16px 18px 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.msa__field {
|
||||
width: 100%;
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
min-height: 46px;
|
||||
padding-top: 11px !important;
|
||||
padding-bottom: 11px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-notched-outline__leading,
|
||||
::ng-deep .mdc-notched-outline__notch,
|
||||
::ng-deep .mdc-notched-outline__trailing {
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.msa__field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Actions ─────────────────────────────────────────────────────── */
|
||||
.msa__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 12px 18px 14px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
button {
|
||||
height: 38px !important;
|
||||
border-radius: 9px !important;
|
||||
font-weight: 700 !important;
|
||||
padding: 0 18px !important;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
.msa__cancel {
|
||||
color: $slate !important;
|
||||
border-color: $border !important;
|
||||
}
|
||||
|
||||
.msa__confirm {
|
||||
/* Default — blue (Hold Amount) */
|
||||
background: linear-gradient(135deg, $blue 0%, $blue-d 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 3px 10px -2px rgb(30 64 175 / 35%) !important;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
filter: grayscale(0.3);
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.msa--block-deposit .msa__confirm,
|
||||
.msa--block-withdrawal .msa__confirm {
|
||||
background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important;
|
||||
box-shadow: 0 3px 10px -2px rgb(180 83 9 / 35%) !important;
|
||||
|
||||
&:disabled {
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.msa--block-account .msa__confirm {
|
||||
background: linear-gradient(135deg, $red 0%, $red-d 100%) !important;
|
||||
box-shadow: 0 3px 10px -2px rgb(153 27 27 / 40%) !important;
|
||||
|
||||
&:disabled {
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.msa {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.msa__hero {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.msa__actions {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-8
@@ -14,8 +14,10 @@ import { SavingsService } from 'app/savings/savings.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Currency } from 'app/shared/models/general.model';
|
||||
import { SystemService } from 'app/system/system.service';
|
||||
import { MatCard, MatCardTitle, MatCardContent, MatCardActions } from '@angular/material/card';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component';
|
||||
import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
type TransactionCommandType = 'holdamount' | 'blockaccount' | 'blockdeposit' | 'blockwithdrawal';
|
||||
@@ -33,7 +35,7 @@ interface TransactionType {
|
||||
styleUrls: ['./manage-savings-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatCardTitle,
|
||||
FaIconComponent,
|
||||
InputAmountComponent
|
||||
]
|
||||
})
|
||||
@@ -45,6 +47,7 @@ export class ManageSavingsAccountComponent implements OnInit {
|
||||
private router = inject(Router);
|
||||
private systemService = inject(SystemService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
@Input() currency: Currency;
|
||||
/** Minimum date allowed. */
|
||||
@@ -170,9 +173,7 @@ export class ManageSavingsAccountComponent implements OnInit {
|
||||
|
||||
this.savingsService
|
||||
.executeSavingsAccountTransactionsCommand(this.savingAccountId, command, payload)
|
||||
.subscribe((response: any) => {
|
||||
this.router.navigate(['../../transactions'], { relativeTo: this.route });
|
||||
});
|
||||
.subscribe(() => this.showSuccessAndExit());
|
||||
} else {
|
||||
payload = {
|
||||
...this.manageSavingsAccountForm.value
|
||||
@@ -186,9 +187,39 @@ export class ManageSavingsAccountComponent implements OnInit {
|
||||
|
||||
this.savingsService
|
||||
.executeSavingsAccountCommand(this.savingAccountId, command, payload)
|
||||
.subscribe((response: any) => {
|
||||
this.router.navigate(['../../transactions'], { relativeTo: this.route });
|
||||
});
|
||||
.subscribe(() => this.showSuccessAndExit());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the shared success pop-up with copy tuned to the current
|
||||
* transactionType, then navigates back to the transactions tab when
|
||||
* the cashier dismisses it.
|
||||
*/
|
||||
private showSuccessAndExit(): void {
|
||||
let title = 'Action Completed';
|
||||
let message = 'The action has been recorded on the account.';
|
||||
if (this.transactionType.holdamount) {
|
||||
title = 'Amount Held Successfully';
|
||||
message = 'The selected amount has been placed on hold on this savings account.';
|
||||
} else if (this.transactionType.blockaccount) {
|
||||
title = 'Account Blocked Successfully';
|
||||
message = 'All activity on this savings account has been frozen.';
|
||||
} else if (this.transactionType.blockdeposit) {
|
||||
title = 'Deposits Blocked Successfully';
|
||||
message = 'Incoming credits to this savings account have been blocked.';
|
||||
} else if (this.transactionType.blockwithdrawal) {
|
||||
title = 'Withdrawals Blocked Successfully';
|
||||
message = 'Outgoing debits from this savings account have been blocked.';
|
||||
}
|
||||
const ref = this.dialog.open(SuccessDialogComponent, {
|
||||
width: '420px',
|
||||
maxWidth: '92vw',
|
||||
autoFocus: false,
|
||||
data: { title, message, buttonLabel: 'Done' }
|
||||
});
|
||||
ref.afterClosed().subscribe(() => {
|
||||
this.router.navigate(['../../transactions'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="idv">
|
||||
<!-- Header — single-line teal strip matching the withdrawal hero. -->
|
||||
<div class="idv__head">
|
||||
<fa-icon icon="id-card" class="idv__head-ico"></fa-icon>
|
||||
<h2 class="idv__title">Verify Customer Identity</h2>
|
||||
<span class="idv__head-spacer"></span>
|
||||
<span class="idv__head-tag">
|
||||
<fa-icon icon="shield-alt"></fa-icon>
|
||||
KYC Check
|
||||
</span>
|
||||
<button type="button" class="idv__head-close" (click)="close()" aria-label="Close">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-dialog-content class="idv__content">
|
||||
<div class="idv__row">
|
||||
<!-- Profile photo (click to zoom) -->
|
||||
<div
|
||||
class="idv__photo"
|
||||
[class.idv__photo--zoomable]="!!profileImage"
|
||||
(click)="zoomImage(profileImage, 'Profile Photo')"
|
||||
[attr.role]="profileImage ? 'button' : null"
|
||||
[attr.tabindex]="profileImage ? 0 : null"
|
||||
>
|
||||
@if (profileImage) {
|
||||
<img [src]="profileImage" alt="Profile" />
|
||||
<span class="idv__zoom-hint" aria-hidden="true">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
</span>
|
||||
} @else {
|
||||
<fa-icon icon="user-circle"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="idv__meta">
|
||||
<div class="idv__meta-row">
|
||||
<span class="idv__meta-label">Client</span>
|
||||
<span class="idv__meta-value">{{ displayName || data.clientName || '—' }}</span>
|
||||
</div>
|
||||
<div class="idv__meta-row">
|
||||
<span class="idv__meta-label">CIF Number</span>
|
||||
<span class="idv__meta-value idv__meta-value--mono">{{ cifNumber || '—' }}</span>
|
||||
</div>
|
||||
<div class="idv__meta-row">
|
||||
<span class="idv__meta-label">Account No</span>
|
||||
<span class="idv__meta-value idv__meta-value--mono">{{ data.accountNo || '—' }}</span>
|
||||
</div>
|
||||
@if (mobileNo) {
|
||||
<div class="idv__meta-row">
|
||||
<span class="idv__meta-label">Mobile</span>
|
||||
<span class="idv__meta-value idv__meta-value--mono">{{ mobileNo }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (emailAddress) {
|
||||
<div class="idv__meta-row">
|
||||
<span class="idv__meta-label">Email</span>
|
||||
<span class="idv__meta-value">{{ emailAddress }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KYC -->
|
||||
<div class="idv__section">
|
||||
<span class="idv__section-title">KYC Identifiers</span>
|
||||
<div class="idv__kyc">
|
||||
<div class="idv__kyc-row">
|
||||
<span class="idv__kyc-label">PAN</span>
|
||||
<span class="idv__kyc-value idv__meta-value--mono">{{ panNumber || '—' }}</span>
|
||||
@if (panNumber) {
|
||||
<span class="idv__pill" [class.idv__pill--ok]="panVerified" [class.idv__pill--warn]="!panVerified">
|
||||
<fa-icon [icon]="panVerified ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
|
||||
{{ panVerified ? 'Verified' : 'Unverified' }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="idv__kyc-row">
|
||||
<span class="idv__kyc-label">Aadhaar</span>
|
||||
<span class="idv__kyc-value idv__meta-value--mono">{{ aadhaarMasked || '—' }}</span>
|
||||
@if (aadhaarMasked) {
|
||||
<span class="idv__pill" [class.idv__pill--ok]="aadhaarVerified" [class.idv__pill--warn]="!aadhaarVerified">
|
||||
<fa-icon [icon]="aadhaarVerified ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
|
||||
{{ aadhaarVerified ? 'Verified' : 'Unverified' }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature (click to zoom) -->
|
||||
<div class="idv__section">
|
||||
<span class="idv__section-title">Specimen Signature</span>
|
||||
<div
|
||||
class="idv__sig"
|
||||
[class.idv__sig--zoomable]="!!signatureImage"
|
||||
(click)="zoomImage(signatureImage, 'Specimen Signature')"
|
||||
[attr.role]="signatureImage ? 'button' : null"
|
||||
[attr.tabindex]="signatureImage ? 0 : null"
|
||||
>
|
||||
@if (signatureImage) {
|
||||
<img [src]="signatureImage" alt="Signature" />
|
||||
<span class="idv__zoom-hint idv__zoom-hint--sig" aria-hidden="true">
|
||||
<fa-icon icon="search"></fa-icon> Zoom
|
||||
</span>
|
||||
} @else if (loading) {
|
||||
<span class="idv__sig-empty">Loading…</span>
|
||||
} @else {
|
||||
<span class="idv__sig-empty">No signature on file</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="idv__actions">
|
||||
<button mat-flat-button type="button" class="idv__close" (click)="close()">
|
||||
<fa-icon icon="check" class="m-r-6"></fa-icon> Done
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
$teal: #0f766e;
|
||||
$teal-d: #134e4a;
|
||||
$green: #047857;
|
||||
$amber: #b45309;
|
||||
|
||||
.idv {
|
||||
width: 480px;
|
||||
max-width: 92vw;
|
||||
}
|
||||
|
||||
/* Single-line teal strip — icon · title · KYC badge · close. Compact,
|
||||
reaches edge-to-edge, no subtitle clutter. */
|
||||
.idv__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 14px;
|
||||
height: 44px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, $teal-d 0%, #115e59 45%, $teal 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top right, rgb(255 255 255 / 14%), transparent 55%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.idv__head-ico {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.idv__title {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.idv__head-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* KYC badge — communicates the purpose of the dialog without needing
|
||||
subtitle copy. */
|
||||
.idv__head-tag {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 22px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 16%);
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
font-size: 10.5px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
|
||||
fa-icon {
|
||||
font-size: 10px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
/* Close (×) button — clean white circle, right-anchored. */
|
||||
.idv__head-close {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
background: rgb(255 255 255 / 12%);
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
transform 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 26%);
|
||||
border-color: rgb(255 255 255 / 46%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
}
|
||||
|
||||
.idv__content {
|
||||
padding: 16px 18px 8px !important;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Profile photo + identity meta */
|
||||
.idv__row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px dashed $border;
|
||||
}
|
||||
|
||||
.idv__photo {
|
||||
position: relative;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: $soft;
|
||||
border: 1px solid $border;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #cbd5e1;
|
||||
font-size: 42px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
&--zoomable {
|
||||
cursor: zoom-in;
|
||||
|
||||
&:hover img {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
&:hover .idv__zoom-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $teal;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tiny magnifier badge in the corner of zoomable surfaces. */
|
||||
.idv__zoom-hint {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: rgb(15 23 42 / 78%);
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.15s ease;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 2px 6px -2px rgb(0 0 0 / 35%);
|
||||
|
||||
&--sig {
|
||||
width: auto;
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
|
||||
fa-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.idv__meta {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.idv__meta-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.idv__meta-label {
|
||||
color: $slate;
|
||||
font-size: 10.5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.idv__meta-value {
|
||||
color: $navy;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
|
||||
&--mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 11.5px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sections (KYC / Signature) */
|
||||
.idv__section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.idv__section-title {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: $slate;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.idv__kyc {
|
||||
background: $soft;
|
||||
border: 1px solid $border;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.idv__kyc-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
font-size: 12.5px;
|
||||
|
||||
& + & {
|
||||
border-top: 1px dashed #eef2f7;
|
||||
}
|
||||
}
|
||||
|
||||
.idv__kyc-label {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: $slate;
|
||||
min-width: 64px;
|
||||
}
|
||||
|
||||
.idv__kyc-value {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.idv__pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
fa-icon {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
&--ok {
|
||||
background: #ecfdf5;
|
||||
color: $green;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&--warn {
|
||||
background: #fffbeb;
|
||||
color: $amber;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
}
|
||||
|
||||
/* Signature */
|
||||
.idv__sig {
|
||||
position: relative;
|
||||
background: $soft;
|
||||
border: 1px solid $border;
|
||||
border-radius: 10px;
|
||||
height: 110px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
img {
|
||||
max-width: 92%;
|
||||
max-height: 92%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
&--zoomable {
|
||||
cursor: zoom-in;
|
||||
|
||||
&:hover {
|
||||
border-color: $teal;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
&:hover .idv__zoom-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $teal;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.idv__sig-empty {
|
||||
font-size: 12px;
|
||||
color: $muted;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.idv__actions {
|
||||
padding: 12px 18px 16px !important;
|
||||
justify-content: flex-end;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.idv__close {
|
||||
height: 36px !important;
|
||||
border-radius: 9px !important;
|
||||
font-weight: 700 !important;
|
||||
padding: 0 22px !important;
|
||||
background: linear-gradient(135deg, $teal 0%, $teal-d 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 3px 10px -2px rgb(15 118 110 / 35%) !important;
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import {
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
} from '@angular/material/dialog';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { ImageZoomDialogComponent } from 'app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
export interface IdentityVerifyData {
|
||||
clientId: number;
|
||||
/** Optional — we already know these from the parent screen, used as fallback
|
||||
* while the API calls are still in flight. */
|
||||
clientName: string | null;
|
||||
accountNo: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity-verify pop-up shown on the withdrawal screen. Loads the
|
||||
* client's CIF, profile picture, signature, PAN and Aadhaar so the
|
||||
* cashier can match the customer against the system before paying out
|
||||
* cash.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-identity-verify-dialog',
|
||||
standalone: true,
|
||||
templateUrl: './identity-verify-dialog.component.html',
|
||||
styleUrls: ['./identity-verify-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class IdentityVerifyDialogComponent implements OnInit {
|
||||
dialogRef = inject<MatDialogRef<IdentityVerifyDialogComponent>>(MatDialogRef);
|
||||
data = inject<IdentityVerifyData>(MAT_DIALOG_DATA);
|
||||
private clientsService = inject(ClientsService);
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
loading = true;
|
||||
/** Best-effort population — any failing fetch leaves the corresponding
|
||||
* field null and the template falls back to "—". */
|
||||
cifNumber: string | null = null;
|
||||
displayName: string | null = null;
|
||||
mobileNo: string | null = null;
|
||||
emailAddress: string | null = null;
|
||||
panNumber: string | null = null;
|
||||
panVerified = false;
|
||||
aadhaarMasked: string | null = null;
|
||||
aadhaarVerified = false;
|
||||
profileImage: SafeUrl | null = null;
|
||||
signatureImage: SafeUrl | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.data?.clientId) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
const id = String(this.data.clientId);
|
||||
let pending = 3;
|
||||
const done = () => {
|
||||
pending--;
|
||||
if (pending <= 0) this.loading = false;
|
||||
};
|
||||
|
||||
// Client profile (CIF, name, mobile, email)
|
||||
this.clientsService.getClientData(id).subscribe({
|
||||
next: (client: any) => {
|
||||
this.cifNumber = client?.cifNumber ?? client?.cifNo ?? null;
|
||||
this.displayName = client?.displayName ?? client?.fullname ?? this.data.clientName;
|
||||
this.mobileNo = client?.mobileNo ?? null;
|
||||
this.emailAddress = client?.emailAddress ?? client?.email ?? null;
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
|
||||
// KYC (PAN, Aadhaar)
|
||||
this.clientsService.getClientKyc(id).subscribe({
|
||||
next: (kyc: any) => {
|
||||
this.panNumber = kyc?.panNumber ?? null;
|
||||
this.panVerified = !!kyc?.panVerified;
|
||||
this.aadhaarMasked = kyc?.aadhaarNumberMasked ?? kyc?.aadhaarNumber ?? null;
|
||||
this.aadhaarVerified = !!kyc?.aadhaarVerified;
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
|
||||
// Profile picture
|
||||
this.clientsService.getClientProfileImage(id).subscribe({
|
||||
next: (img: any) => {
|
||||
if (img) this.profileImage = this.sanitizer.bypassSecurityTrustResourceUrl(img);
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
|
||||
// Signature — finds the doc named/described 'signature' then pulls
|
||||
// the blob. Counted separately because not all clients have one.
|
||||
this.clientsService.getClientDocuments(id).subscribe({
|
||||
next: (docs: any[]) => {
|
||||
const sigDoc = docs?.find(
|
||||
(d: any) =>
|
||||
d?.name?.toLowerCase()?.includes('signature') || d?.description?.toLowerCase()?.includes('signature')
|
||||
);
|
||||
if (sigDoc?.id != null) {
|
||||
this.clientsService.getClientSignatureImage(id, String(sigDoc.id)).subscribe({
|
||||
next: (blob: Blob) => {
|
||||
this.signatureImage = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the image up in the shared full-screen zoom viewer so the
|
||||
* cashier can match minute details (signature loops, photo features)
|
||||
* against the customer at the counter.
|
||||
*/
|
||||
zoomImage(imageSrc: SafeUrl | null, title: string): void {
|
||||
if (!imageSrc) return;
|
||||
this.dialog.open(ImageZoomDialogComponent, {
|
||||
data: { imageSrc, title },
|
||||
panelClass: 'image-zoom-panel',
|
||||
maxWidth: '90vw',
|
||||
maxHeight: '90vh'
|
||||
});
|
||||
}
|
||||
}
|
||||
+334
-97
@@ -6,127 +6,364 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container mat-elevation-z8">
|
||||
<mat-card>
|
||||
@if (transactionType.withdrawal) {
|
||||
<mat-card-title class="form-heading">
|
||||
{{ 'labels.heading.Withdraw Money From Saving Account' | translate }}
|
||||
</mat-card-title>
|
||||
}
|
||||
@if (transactionType.deposit) {
|
||||
<mat-card-title class="form-heading">
|
||||
{{ 'labels.heading.Deposit Money To Saving Account' | translate }}
|
||||
</mat-card-title>
|
||||
}
|
||||
<div class="sat" [class.sat--deposit]="transactionType.deposit" [class.sat--withdrawal]="transactionType.withdrawal">
|
||||
<!-- Hero header -->
|
||||
<div class="sat__hero">
|
||||
<div class="sat__hero-top">
|
||||
<button type="button" mat-stroked-button class="sat__hero-back" [routerLink]="['../../transactions']">
|
||||
<fa-icon icon="arrow-left" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
</button>
|
||||
<div class="sat__hero-ico">
|
||||
<fa-icon [icon]="transactionType.deposit ? 'arrow-down' : 'arrow-up'"></fa-icon>
|
||||
</div>
|
||||
<div class="sat__hero-title-wrap">
|
||||
<h2 class="sat__title">
|
||||
{{
|
||||
transactionType.withdrawal
|
||||
? ('labels.heading.Withdraw Money From Saving Account' | translate)
|
||||
: ('labels.heading.Deposit Money To Saving Account' | translate)
|
||||
}}
|
||||
</h2>
|
||||
<p class="sat__sub">
|
||||
{{
|
||||
transactionType.deposit
|
||||
? 'Count the cash by denomination, then confirm the payment details.'
|
||||
: 'Disburse cash by denomination and confirm the payment details.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
@if (transactionType.withdrawal && clientId) {
|
||||
<button
|
||||
type="button"
|
||||
mat-flat-button
|
||||
class="sat__hero-verify"
|
||||
[class.sat__hero-verify--pulse]="!identityVerified"
|
||||
(click)="openIdentityVerify()"
|
||||
aria-label="Verify customer identity"
|
||||
>
|
||||
<fa-icon icon="id-card" class="m-r-6"></fa-icon>
|
||||
Verify Identity
|
||||
</button>
|
||||
}
|
||||
<div class="sat__hero-meta">
|
||||
<span class="sat__hero-meta-label">Counted Total</span>
|
||||
<span class="sat__hero-meta-value"> ₹{{ denominationTotal | number: '1.2-2' }} </span>
|
||||
@if (totalPieces > 0) {
|
||||
<span class="sat__hero-meta-sub">{{ totalPieces }} piece(s)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="savingAccountTransactionForm" (ngSubmit)="submit()">
|
||||
<mat-card-content>
|
||||
<div class="layout-column">
|
||||
<mat-form-field (click)="dueDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Transaction Date' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="dueDatePicker"
|
||||
formControlName="transactionDate"
|
||||
required
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dueDatePicker></mat-datepicker>
|
||||
@if (savingAccountTransactionForm.controls.transactionDate.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<!-- Context strip: client / account / KYC / risk / same-day NEFT-RTGS -->
|
||||
<div class="sat__ctx">
|
||||
<div class="sat__ctx-item">
|
||||
<fa-icon icon="user" class="sat__ctx-ico"></fa-icon>
|
||||
<div class="sat__ctx-text">
|
||||
<span class="sat__ctx-label">Client</span>
|
||||
<span class="sat__ctx-value">{{ clientName || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mifosx-input-amount
|
||||
[currency]="currency"
|
||||
[isRequired]="true"
|
||||
[inputFormControl]="savingAccountTransactionForm.controls.transactionAmount"
|
||||
[inputLabel]="'Transaction Amount'"
|
||||
<div class="sat__ctx-divider"></div>
|
||||
|
||||
<div class="sat__ctx-item">
|
||||
<fa-icon icon="hashtag" class="sat__ctx-ico"></fa-icon>
|
||||
<div class="sat__ctx-text">
|
||||
<span class="sat__ctx-label">Account No</span>
|
||||
<span class="sat__ctx-value sat__ctx-value--mono">{{ accountNo || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sat__ctx-divider"></div>
|
||||
|
||||
<div class="sat__ctx-item sat__ctx-item--balance">
|
||||
<fa-icon icon="wallet" class="sat__ctx-ico"></fa-icon>
|
||||
<div class="sat__ctx-text">
|
||||
<span class="sat__ctx-label">Available Balance</span>
|
||||
<span class="sat__ctx-value sat__ctx-value--amount sat__ctx-value--lg">
|
||||
₹{{ availableBalance | number: '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sat__ctx-divider"></div>
|
||||
|
||||
<div class="sat__ctx-item">
|
||||
<fa-icon icon="id-card" class="sat__ctx-ico"></fa-icon>
|
||||
<div class="sat__ctx-text">
|
||||
<span class="sat__ctx-label">KYC</span>
|
||||
<span
|
||||
class="sat__ctx-badge"
|
||||
[class.sat__ctx-badge--ok]="kycStatus === 'Verified'"
|
||||
[class.sat__ctx-badge--warn]="kycStatus === 'Partial'"
|
||||
[class.sat__ctx-badge--danger]="kycStatus === 'Pending'"
|
||||
>
|
||||
</mifosx-input-amount>
|
||||
<fa-icon
|
||||
[icon]="
|
||||
kycStatus === 'Verified'
|
||||
? 'check-circle'
|
||||
: kycStatus === 'Partial'
|
||||
? 'exclamation-triangle'
|
||||
: 'times-circle'
|
||||
"
|
||||
></fa-icon>
|
||||
{{ kycStatus }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Payment Type' | translate }}</mat-label>
|
||||
<mat-select required formControlName="paymentTypeId">
|
||||
@for (paymentType of paymentTypeOptions; track paymentType) {
|
||||
<mat-option [value]="paymentType.id">
|
||||
{{ paymentType.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (savingAccountTransactionForm.controls.paymentTypeId.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Payment Type' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="sat__ctx-divider"></div>
|
||||
|
||||
<div class="flex-fill">
|
||||
<span class="expandcollapsebutton m-l-10 m-t-40 flex-75" (click)="addPaymentDetails()">
|
||||
<mat-slide-toggle>
|
||||
<div [className]="addPaymentDetailsFlag ? 'enabled' : 'disabled'">
|
||||
<span class="m-l-10">{{ 'labels.inputs.Show Payment Details' | translate }}</span>
|
||||
</div>
|
||||
</mat-slide-toggle>
|
||||
<div class="sat__ctx-item">
|
||||
<fa-icon icon="exchange-alt" class="sat__ctx-ico"></fa-icon>
|
||||
<div class="sat__ctx-text">
|
||||
<span class="sat__ctx-label">Today's NEFT/RTGS</span>
|
||||
<span class="sat__ctx-value sat__ctx-value--amount"> ₹{{ sameDayNeftRtgs | number: '1.2-2' }} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sat__ctx-divider"></div>
|
||||
|
||||
<div class="sat__ctx-item">
|
||||
<fa-icon icon="money-check-alt" class="sat__ctx-ico"></fa-icon>
|
||||
<div class="sat__ctx-text">
|
||||
<span class="sat__ctx-label">Cheque In Clearing</span>
|
||||
<span class="sat__ctx-value sat__ctx-value--amount"> ₹{{ sameDayChequeClearing | number: '1.2-2' }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="savingAccountTransactionForm" (ngSubmit)="submit()" class="sat__form">
|
||||
<!-- ─── LEFT COLUMN: denomination breakdown ────────────────────── -->
|
||||
<section class="sat__panel sat__panel--denom">
|
||||
<header class="sat__panel-head">
|
||||
<fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon>
|
||||
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
|
||||
<span class="sat__panel-spacer"></span>
|
||||
<button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()">
|
||||
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Banknotes -->
|
||||
<div class="sat__denom-group">
|
||||
<div class="sat__denom-group-head">
|
||||
<span class="sat__denom-group-title">Banknotes</span>
|
||||
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="sat__denom-grid">
|
||||
@for (d of notes; track d.value) {
|
||||
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0">
|
||||
<div class="sat__denom-top">
|
||||
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
|
||||
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="sat__denom-stepper">
|
||||
<button
|
||||
type="button"
|
||||
class="sat__denom-step"
|
||||
(click)="bumpDenom(d, -1)"
|
||||
[disabled]="countFor(d) === 0"
|
||||
aria-label="Decrease"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
[value]="countFor(d)"
|
||||
(input)="onDenomChange(d, $any($event.target).value)"
|
||||
inputmode="numeric"
|
||||
class="sat__denom-count"
|
||||
/>
|
||||
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coins -->
|
||||
<div class="sat__denom-group">
|
||||
<div class="sat__denom-group-head">
|
||||
<span class="sat__denom-group-title">Coins</span>
|
||||
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="sat__denom-grid">
|
||||
@for (d of coins; track d.value) {
|
||||
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0">
|
||||
<div class="sat__denom-top">
|
||||
<span class="sat__denom-chip sat__denom-chip--coin">{{ d.label }}</span>
|
||||
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="sat__denom-stepper">
|
||||
<button
|
||||
type="button"
|
||||
class="sat__denom-step"
|
||||
(click)="bumpDenom(d, -1)"
|
||||
[disabled]="countFor(d) === 0"
|
||||
aria-label="Decrease"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
[value]="countFor(d)"
|
||||
(input)="onDenomChange(d, $any($event.target).value)"
|
||||
inputmode="numeric"
|
||||
class="sat__denom-count"
|
||||
/>
|
||||
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tally + apply -->
|
||||
<div class="sat__tally">
|
||||
<div class="sat__tally-row">
|
||||
<span class="sat__tally-label">Banknotes</span>
|
||||
<span class="sat__tally-value">₹{{ notesTotal | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="sat__tally-row">
|
||||
<span class="sat__tally-label">Coins</span>
|
||||
<span class="sat__tally-value">₹{{ coinsTotal | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="sat__tally-row sat__tally-row--grand">
|
||||
<span class="sat__tally-label">Grand Total</span>
|
||||
<span class="sat__tally-value">₹{{ denominationTotal | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
mat-flat-button
|
||||
class="sat__apply"
|
||||
[disabled]="denominationTotal === 0"
|
||||
(click)="applyDenominationTotal()"
|
||||
>
|
||||
<fa-icon icon="arrow-right" class="m-r-6"></fa-icon>
|
||||
Use ₹{{ denominationTotal | number: '1.2-2' }} as Transaction Amount
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── RIGHT COLUMN: transaction + payment details ────────────── -->
|
||||
<section class="sat__panel sat__panel--side">
|
||||
<header class="sat__panel-head">
|
||||
<fa-icon icon="info-circle" class="sat__panel-ico"></fa-icon>
|
||||
<h3 class="sat__panel-title">Transaction Details</h3>
|
||||
</header>
|
||||
|
||||
<div class="sat__panel-body">
|
||||
<mat-form-field appearance="outline" class="sat__field" (click)="dueDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Transaction Date' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="dueDatePicker"
|
||||
formControlName="transactionDate"
|
||||
required
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dueDatePicker></mat-datepicker>
|
||||
@if (savingAccountTransactionForm.controls.transactionDate.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mifosx-input-amount
|
||||
[currency]="currency"
|
||||
[isRequired]="true"
|
||||
[inputFormControl]="savingAccountTransactionForm.controls.transactionAmount"
|
||||
[inputLabel]="'Transaction Amount'"
|
||||
>
|
||||
</mifosx-input-amount>
|
||||
|
||||
@if (denominationTotal > 0 && !denominationMatchesForm) {
|
||||
<div class="sat__mismatch">
|
||||
<fa-icon icon="exclamation-triangle" class="sat__mismatch-ico"></fa-icon>
|
||||
<span>
|
||||
Counted cash <strong>₹{{ denominationTotal | number: '1.2-2' }}</strong> doesn't match the transaction
|
||||
amount. Click <strong>Use ₹{{ denominationTotal | number: '1.2-2' }}</strong> to sync.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (denominationMatchesForm) {
|
||||
<div class="sat__match">
|
||||
<fa-icon icon="check-circle" class="sat__match-ico"></fa-icon>
|
||||
Cash count matches transaction amount.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (addPaymentDetailsFlag) {
|
||||
<mat-form-field>
|
||||
<mat-form-field appearance="outline" class="sat__field">
|
||||
<mat-label>{{ 'labels.inputs.Payment Type' | translate }}</mat-label>
|
||||
<mat-select required formControlName="paymentTypeId">
|
||||
@for (paymentType of paymentTypeOptions; track paymentType) {
|
||||
<mat-option [value]="paymentType.id">{{ paymentType.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (savingAccountTransactionForm.controls.paymentTypeId.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Payment Type' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div class="sat__toggle-row" (click)="addPaymentDetails()">
|
||||
<mat-slide-toggle [checked]="!!addPaymentDetailsFlag">
|
||||
<span class="sat__toggle-label">{{ 'labels.inputs.Show Payment Details' | translate }}</span>
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
@if (addPaymentDetailsFlag) {
|
||||
<div class="sat__sub-grid">
|
||||
<mat-form-field appearance="outline" class="sat__field">
|
||||
<mat-label>{{ 'labels.inputs.Account Number' | translate }}</mat-label>
|
||||
<input type="number" formControlName="accountNumber" matInput />
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (addPaymentDetailsFlag) {
|
||||
<mat-form-field>
|
||||
<mat-form-field appearance="outline" class="sat__field">
|
||||
<mat-label>{{ 'labels.inputs.Cheque' | translate }}</mat-label>
|
||||
<input type="number" formControlName="checkNumber" matInput />
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (addPaymentDetailsFlag) {
|
||||
<mat-form-field>
|
||||
<mat-form-field appearance="outline" class="sat__field">
|
||||
<mat-label>{{ 'labels.inputs.Routing Code' | translate }}</mat-label>
|
||||
<input formControlName="routingCode" matInput />
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (addPaymentDetailsFlag) {
|
||||
<mat-form-field>
|
||||
<mat-form-field appearance="outline" class="sat__field">
|
||||
<mat-label>{{ 'labels.inputs.Receipt Number' | translate }}</mat-label>
|
||||
<input formControlName="receiptNumber" matInput />
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (addPaymentDetailsFlag) {
|
||||
<mat-form-field>
|
||||
<mat-form-field appearance="outline" class="sat__field sat__field--wide">
|
||||
<mat-label>{{ 'labels.inputs.Bank' | translate }}</mat-label>
|
||||
<input formControlName="bankNumber" matInput />
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Notes' | translate }}</mat-label>
|
||||
<textarea formControlName="note" matInput cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="sat__field">
|
||||
<mat-label>{{ 'labels.inputs.Notes' | translate }}</mat-label>
|
||||
<textarea formControlName="note" matInput cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-card-actions class="layout-row layout-xs-column layout-align-center gap-5px">
|
||||
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!savingAccountTransactionForm.valid">
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
<div class="sat__actions">
|
||||
<button type="button" mat-stroked-button class="sat__cancel" [routerLink]="['../../transactions']">
|
||||
<fa-icon icon="times" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button type="submit" class="sat__submit" [disabled]="!savingAccountTransactionForm.valid">
|
||||
<fa-icon icon="check" class="m-r-6"></fa-icon>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</form>
|
||||
</mat-card>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+858
-10
@@ -6,24 +6,872 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.expandcollapsebutton {
|
||||
margin-top: -7px;
|
||||
/* Banking-grade transaction-entry screen. Two-column layout: cash
|
||||
denomination breakdown on the left (the cashier's primary workspace),
|
||||
transaction & payment details on the right. Hero header is green for
|
||||
deposits / amber for withdrawals. */
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
$blue: #1e40af;
|
||||
$green: #059669;
|
||||
$green-d: #047857;
|
||||
$amber: #d97706;
|
||||
$amber-d: #b45309;
|
||||
$red: #b91c1c;
|
||||
|
||||
.sat {
|
||||
width: 100%;
|
||||
padding: 10px 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 37rem;
|
||||
/* ─── Hero ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Deep navy-blue banking gradient — formal, high-contrast, sits well
|
||||
with KYC / Risk / NEFT-RTGS / Cheque badges that follow. */
|
||||
.sat__hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px 16px 12px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 45%, #1e40af 100%);
|
||||
box-shadow: 0 6px 18px -10px rgb(15 23 42 / 50%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top right, rgb(255 255 255 / 12%), transparent 55%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.right-input {
|
||||
.sat__hero-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Back button — sits at the top-left of the hero on a translucent
|
||||
surface so it reads clearly against the dark gradient. */
|
||||
.sat__hero-back {
|
||||
height: 30px !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 0 11px !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 12px !important;
|
||||
letter-spacing: 0.01em;
|
||||
background: rgb(255 255 255 / 14%) !important;
|
||||
color: #fff !important;
|
||||
border: 1px solid rgb(255 255 255 / 28%) !important;
|
||||
flex-shrink: 0;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 26%) !important;
|
||||
border-color: rgb(255 255 255 / 42%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__hero-title-wrap {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Deep teal banking gradient for withdrawals — clearly distinct from the
|
||||
navy-blue deposit hero without resorting to a loud red. */
|
||||
.sat--withdrawal .sat__hero {
|
||||
background: linear-gradient(135deg, #134e4a 0%, #115e59 45%, #0f766e 100%);
|
||||
box-shadow: 0 6px 18px -10px rgb(15 118 110 / 50%);
|
||||
}
|
||||
|
||||
.sat__hero-ico {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 9px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 28%);
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sat__title {
|
||||
margin: 0;
|
||||
font-size: 14.5px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.sat__sub {
|
||||
margin: 1px 0 0;
|
||||
font-size: 11.5px;
|
||||
color: rgb(255 255 255 / 80%);
|
||||
}
|
||||
|
||||
/* Withdrawal-only "Verify Identity" pill — sits next to the Counted
|
||||
Total tile and opens the identity-verify dialog.
|
||||
|
||||
Adds a subtle outer pulse-ring (via ::before) until the cashier has
|
||||
opened the dialog once — drives attention to the compliance step
|
||||
without changing the button's resting colour. */
|
||||
.sat__hero-verify {
|
||||
position: relative;
|
||||
height: 30px !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 0 12px !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 12px !important;
|
||||
letter-spacing: 0.01em;
|
||||
color: #fff !important;
|
||||
background: rgb(255 255 255 / 16%) !important;
|
||||
border: 1px solid rgb(255 255 255 / 30%) !important;
|
||||
flex-shrink: 0;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
overflow: visible !important;
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 28%) !important;
|
||||
border-color: rgb(255 255 255 / 42%) !important;
|
||||
}
|
||||
|
||||
/* Pulse ring lives just outside the pill so it doesn't clip; sits on
|
||||
a separate paint layer so it doesn't trigger layout. */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid rgb(255 255 255 / 70%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
&--pulse::before {
|
||||
animation: sat-verify-pulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Pause the pulse on hover so the hover state isn't fighting the
|
||||
animation — gives the cashier a calm target to click. */
|
||||
&--pulse:hover::before {
|
||||
animation-play-state: paused;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sat-verify-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.18);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1.18);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sat__hero-verify--pulse::before {
|
||||
animation: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__hero-meta {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
padding: 5px 12px;
|
||||
border-radius: 8px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
backdrop-filter: blur(8px);
|
||||
min-width: 130px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sat__hero-meta-label {
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: rgb(255 255 255 / 78%);
|
||||
}
|
||||
|
||||
.sat__hero-meta-value {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.sat__hero-meta-sub {
|
||||
font-size: 10px;
|
||||
color: rgb(255 255 255 / 75%);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* ─── Context strip (client / account / KYC / risk / NEFT-RTGS) ──── */
|
||||
.sat__ctx {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
padding: 5px 8px;
|
||||
border-radius: 8px;
|
||||
background: rgb(255 255 255 / 12%);
|
||||
border: 1px solid rgb(255 255 255 / 22%);
|
||||
backdrop-filter: blur(8px);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sat__ctx-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
/* Tight flex-basis (110px) so all chips pack on a single row at typical
|
||||
desktop widths — items shrink down to their content when space is
|
||||
constrained instead of wrapping. */
|
||||
flex: 1 1 110px;
|
||||
padding: 2px 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sat__ctx-ico {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 25%);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sat__ctx-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.sat__ctx-label {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: rgb(255 255 255 / 70%);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sat__ctx-value {
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&--mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
&--amount {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlight the live balance — the cashier's anchor figure. */
|
||||
.sat__ctx-item--balance {
|
||||
background: rgb(255 255 255 / 12%);
|
||||
border: 1px solid rgb(255 255 255 / 22%);
|
||||
border-radius: 7px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.sat__ctx-divider {
|
||||
width: 1px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
margin: 4px 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* Vertical divider — hidden when items wrap to multiple rows */
|
||||
@media (width <= 720px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__ctx-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 22px;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
color: #fff;
|
||||
align-self: flex-start;
|
||||
width: fit-content;
|
||||
|
||||
fa-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&--ok {
|
||||
background: rgb(16 185 129 / 36%);
|
||||
border-color: rgb(110 231 183 / 60%);
|
||||
color: #d1fae5;
|
||||
}
|
||||
|
||||
&--warn {
|
||||
background: rgb(245 158 11 / 36%);
|
||||
border-color: rgb(252 211 77 / 60%);
|
||||
color: #fef3c7;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: rgb(239 68 68 / 36%);
|
||||
border-color: rgb(252 165 165 / 60%);
|
||||
color: #fee2e2;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Two-column layout ───────────────────────────────────────────── */
|
||||
.sat__form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__panel {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sat__panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
background: $soft;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.sat__panel-ico {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(135deg, $green 0%, $green-d 100%);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sat--withdrawal .sat__panel-ico {
|
||||
background: linear-gradient(135deg, $amber 0%, $amber-d 100%);
|
||||
}
|
||||
|
||||
.sat__panel-title {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.sat__panel-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sat__panel-action {
|
||||
height: 26px !important;
|
||||
border-radius: 7px !important;
|
||||
font-size: 11.5px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
.sat__panel-body {
|
||||
padding: 12px 14px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
/* ─── Denomination groups ─────────────────────────────────────────── */
|
||||
.sat__denom-group {
|
||||
padding: 8px 14px 4px;
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__denom-group-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.sat__denom-group-title {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: $slate;
|
||||
}
|
||||
|
||||
.sat__denom-group-total {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.sat__denom-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ── Single denomination tile ──
|
||||
Two-row layout: top has the denomination chip + running subtotal,
|
||||
bottom has a full-width stepper (− / count / +). Easier to read,
|
||||
gives the cashier obvious tap targets, and never clips on narrow
|
||||
columns. */
|
||||
.sat__denom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: 6px 9px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
background 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
min-width: 0;
|
||||
|
||||
&--active {
|
||||
border-color: #86efac;
|
||||
background: #f0fdf4;
|
||||
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
|
||||
}
|
||||
}
|
||||
|
||||
.sat__denom-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sat__denom-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 48px;
|
||||
height: 22px;
|
||||
padding: 0 9px;
|
||||
border-radius: 5px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--note {
|
||||
background: linear-gradient(135deg, #ecfeff 0%, #cffafe 100%);
|
||||
color: #155e75;
|
||||
border: 1px solid #a5f3fc;
|
||||
}
|
||||
|
||||
&--coin {
|
||||
background: linear-gradient(135deg, #fef9c3 0%, #fde68a 100%);
|
||||
color: #854d0e;
|
||||
border: 1px solid #fcd34d;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__denom-amount {
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.right-label {
|
||||
padding-right: 25px !important;
|
||||
/* Stepper — full-width row beneath the chip. */
|
||||
.sat__denom-stepper {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid $border;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-heading {
|
||||
font-size: 1.25rem;
|
||||
.sat__denom-count {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding: 0 4px;
|
||||
|
||||
/* Hide the native number spinner — the explicit + / − buttons replace it. */
|
||||
appearance: textfield;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__denom-step {
|
||||
flex: 0 0 auto;
|
||||
width: 28px;
|
||||
background: $soft;
|
||||
border: none;
|
||||
border-right: 1px solid $border;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: $slate;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.12s ease,
|
||||
color 0.12s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
border-left: 1px solid $border;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #e2e8f0;
|
||||
color: $navy;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Tally / apply ───────────────────────────────────────────────── */
|
||||
.sat__tally {
|
||||
margin: 8px 14px 14px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border: 1px solid $border;
|
||||
}
|
||||
|
||||
.sat__tally-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 3px 0;
|
||||
font-size: 12px;
|
||||
color: $slate;
|
||||
|
||||
&--grand {
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #cbd5e1;
|
||||
|
||||
.sat__tally-label {
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sat__tally-value {
|
||||
font-size: 15.5px;
|
||||
font-weight: 800;
|
||||
color: $green-d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sat--withdrawal .sat__tally-row--grand .sat__tally-value {
|
||||
color: $amber-d;
|
||||
}
|
||||
|
||||
.sat__tally-value {
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.sat__apply {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
height: 34px !important;
|
||||
border-radius: 8px !important;
|
||||
background: linear-gradient(135deg, $green 0%, $green-d 100%) !important;
|
||||
color: #fff !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 12.5px !important;
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: 0 3px 10px -2px rgb(4 120 87 / 35%) !important;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.4);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sat--withdrawal .sat__apply {
|
||||
background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(180 83 9 / 35%) !important;
|
||||
}
|
||||
|
||||
/* ─── Form fields (right panel) ───────────────────────────────────── */
|
||||
.sat__field {
|
||||
width: 100%;
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
min-height: 48px;
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-notched-outline__leading,
|
||||
::ng-deep .mdc-notched-outline__notch,
|
||||
::ng-deep .mdc-notched-outline__trailing {
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__sub-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 12px;
|
||||
|
||||
.sat__field--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 4px 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
.sat__toggle-label {
|
||||
margin-left: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $slate;
|
||||
}
|
||||
}
|
||||
|
||||
/* Match / mismatch banners */
|
||||
.sat__mismatch,
|
||||
.sat__match {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
margin: 2px 0 8px;
|
||||
}
|
||||
|
||||
.sat__mismatch {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
|
||||
&-ico {
|
||||
color: $amber;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__match {
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
|
||||
&-ico {
|
||||
color: $green;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Actions ─────────────────────────────────────────────────────── */
|
||||
.sat__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
button {
|
||||
height: 42px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 700 !important;
|
||||
padding: 0 22px !important;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
.sat__cancel {
|
||||
color: $slate !important;
|
||||
border-color: $border !important;
|
||||
}
|
||||
|
||||
.sat__submit {
|
||||
background: linear-gradient(135deg, $green 0%, $green-d 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
filter: grayscale(0.3);
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sat--withdrawal .sat__submit {
|
||||
background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(180 83 9 / 35%) !important;
|
||||
|
||||
&:disabled {
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* ─── Responsive ──────────────────────────────────────────────────── */
|
||||
@media (width <= 720px) {
|
||||
.sat__hero-top {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sat__hero-meta {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sat__ctx-item {
|
||||
flex: 1 1 calc(50% - 14px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 480px) {
|
||||
.sat__ctx-item {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.sat__actions {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+297
-16
@@ -19,16 +19,33 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
||||
|
||||
/** Custom Services */
|
||||
import { SavingsService } from '../../savings.service';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { Currency } from 'app/shared/models/general.model';
|
||||
import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component';
|
||||
import { MatSlideToggle } from '@angular/material/slide-toggle';
|
||||
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TransactionConfirmDialogComponent } from './transaction-confirm-dialog/transaction-confirm-dialog.component';
|
||||
import { IdentityVerifyDialogComponent } from './identity-verify-dialog/identity-verify-dialog.component';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** Single Indian denomination row (note or coin). */
|
||||
interface Denomination {
|
||||
value: number;
|
||||
kind: 'note' | 'coin';
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create savings account transactions component.
|
||||
* Create savings account transactions component (Deposit / Withdraw).
|
||||
*
|
||||
* Banking-grade redesign: gradient hero, denomination breakdown grid
|
||||
* (Indian notes ₹500/₹200/₹100/₹50/₹20/₹10 + coins ₹20/₹10/₹5/₹2/Re 1)
|
||||
* with live total + auto-sync into transactionAmount, payment & note
|
||||
* panel below.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-savings-transactions',
|
||||
@@ -38,7 +55,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
InputAmountComponent,
|
||||
MatSlideToggle,
|
||||
CdkTextareaAutosize
|
||||
CdkTextareaAutosize,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class SavingsAccountTransactionsComponent implements OnInit {
|
||||
@@ -47,7 +65,9 @@ export class SavingsAccountTransactionsComponent implements OnInit {
|
||||
private router = inject(Router);
|
||||
private dateUtils = inject(Dates);
|
||||
private savingsService = inject(SavingsService);
|
||||
private clientsService = inject(ClientsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
/** Minimum Due Date allowed. */
|
||||
minDate = new Date(2000, 0, 1);
|
||||
@@ -73,15 +93,50 @@ export class SavingsAccountTransactionsComponent implements OnInit {
|
||||
savingAccountId: string;
|
||||
currency: Currency | null = null;
|
||||
|
||||
/**
|
||||
* Retrieves the Saving Account transaction template data from `resolve`.
|
||||
* @param {FormBuilder} formBuilder Form Builder.
|
||||
* @param {SavingsService} savingsService Savings Service.
|
||||
* @param {ActivatedRoute} route Activated Route.
|
||||
* @param {Dates} dateUtils Date Utils.
|
||||
* @param {Router} router Router for navigation.
|
||||
* @param {SettingsService} settingsService Settings Service
|
||||
*/
|
||||
/** Indian banknote denominations (descending). */
|
||||
readonly notes: Denomination[] = [
|
||||
{ value: 500, kind: 'note', label: '₹500' },
|
||||
{ value: 200, kind: 'note', label: '₹200' },
|
||||
{ value: 100, kind: 'note', label: '₹100' },
|
||||
{ value: 50, kind: 'note', label: '₹50' },
|
||||
{ value: 20, kind: 'note', label: '₹20' },
|
||||
{ value: 10, kind: 'note', label: '₹10' }
|
||||
];
|
||||
|
||||
/** Indian coin denominations (descending). */
|
||||
readonly coins: Denomination[] = [
|
||||
{ value: 20, kind: 'coin', label: '₹20' },
|
||||
{ value: 10, kind: 'coin', label: '₹10' },
|
||||
{ value: 5, kind: 'coin', label: '₹5' },
|
||||
{ value: 2, kind: 'coin', label: '₹2' },
|
||||
{ value: 1, kind: 'coin', label: 'Re 1' }
|
||||
];
|
||||
|
||||
/** Quantity per denomination — keyed by `${kind}:${value}`. */
|
||||
denominationCounts: Record<string, number> = {};
|
||||
|
||||
// ── Context strip data (populated async) ───────────────────────────
|
||||
/** Account number of the savings account. */
|
||||
accountNo: string | null = null;
|
||||
/** Client / group display name. */
|
||||
clientName: string | null = null;
|
||||
/** Client id (if this savings account is owned by a client, not a group)
|
||||
* — used to open the identity-verify dialog on the withdrawal screen. */
|
||||
clientId: number | null = null;
|
||||
/** Best-effort KYC status: 'Verified' | 'Partial' | 'Pending'. */
|
||||
kycStatus: 'Verified' | 'Partial' | 'Pending' = 'Pending';
|
||||
/** Sum of NEFT/RTGS transactions on the current business day for this
|
||||
* account — used as an AML hint for the cashier. */
|
||||
sameDayNeftRtgs = 0;
|
||||
/** Sum of same-day cheque transactions that are still awaiting
|
||||
* clearing for this account — pre-empts duplicate cash credit. */
|
||||
sameDayChequeClearing = 0;
|
||||
/** Live available balance of the savings account. */
|
||||
availableBalance = 0;
|
||||
/** Tracks whether the cashier has opened the identity-verify pop-up
|
||||
* at least once on this transaction; toggles off the attention pulse. */
|
||||
identityVerified = false;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { savingsAccountActionData: any }) => {
|
||||
this.paymentTypeOptions = data.savingsAccountActionData.paymentTypeOptions;
|
||||
@@ -94,17 +149,118 @@ export class SavingsAccountTransactionsComponent implements OnInit {
|
||||
this.savingAccountId = this.route.snapshot.params['savingAccountId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Saving account transaction form when component loads.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.maxDate = this.settingsService.businessDate;
|
||||
this.createSavingAccountTransactionForm();
|
||||
this.loadContextStrip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create the Saving Account Transaction Form.
|
||||
* Pull the savings account + client to populate the cashier's context
|
||||
* strip (account number, client name, KYC status, risk level, same-day
|
||||
* NEFT/RTGS aggregate). Each fetch fails silently — the strip just
|
||||
* shows '—' for whatever wasn't returned.
|
||||
*/
|
||||
private loadContextStrip(): void {
|
||||
if (!this.savingAccountId) return;
|
||||
this.savingsService.getSavingsAccountData(this.savingAccountId).subscribe({
|
||||
next: (account: any) => {
|
||||
this.accountNo = account?.accountNo ?? null;
|
||||
this.clientName = account?.clientName ?? account?.groupName ?? null;
|
||||
this.clientId = account?.clientId ?? null;
|
||||
if (!this.currency && account?.currency) this.currency = account.currency;
|
||||
this.availableBalance = Number(account?.summary?.availableBalance ?? account?.summary?.accountBalance ?? 0);
|
||||
this.computeSameDayNeftRtgs(account?.transactions);
|
||||
this.computeSameDayChequeClearing(account?.transactions);
|
||||
if (account?.clientId) {
|
||||
this.fetchClientKyc(String(account.clientId));
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
// Leave defaults in place — strip falls back to '—'.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fetchClientKyc(clientId: string): void {
|
||||
this.clientsService.getClientKyc(clientId).subscribe({
|
||||
next: (kyc: any) => {
|
||||
const pan = !!kyc?.panVerified;
|
||||
const aadhaar = !!kyc?.aadhaarVerified;
|
||||
this.kycStatus = pan && aadhaar ? 'Verified' : pan || aadhaar ? 'Partial' : 'Pending';
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter `transactions` to those on today's business date whose payment
|
||||
* type or transaction-type label mentions NEFT or RTGS, and sum them.
|
||||
* The list will be undefined for accounts loaded without the
|
||||
* `transactions` association — in that case we just stay at 0.
|
||||
*/
|
||||
private computeSameDayNeftRtgs(transactions: any[] | undefined): void {
|
||||
if (!Array.isArray(transactions) || transactions.length === 0) {
|
||||
this.sameDayNeftRtgs = 0;
|
||||
return;
|
||||
}
|
||||
const today = this.settingsService.businessDate ?? new Date();
|
||||
const sameDay = (d: any) => {
|
||||
if (!d) return false;
|
||||
const t = Array.isArray(d) && d.length >= 3 ? new Date(d[0], d[1] - 1, d[2]) : new Date(d);
|
||||
return (
|
||||
t.getFullYear() === today.getFullYear() && t.getMonth() === today.getMonth() && t.getDate() === today.getDate()
|
||||
);
|
||||
};
|
||||
const isNeftRtgs = (t: any) => {
|
||||
const txnType = (t?.transactionType?.value ?? t?.transactionType?.code ?? '').toUpperCase();
|
||||
const payType = (t?.paymentDetailData?.paymentType?.name ?? '').toUpperCase();
|
||||
return /NEFT|RTGS/.test(txnType) || /NEFT|RTGS/.test(payType);
|
||||
};
|
||||
this.sameDayNeftRtgs = transactions
|
||||
.filter((t: any) => !t.reversed && sameDay(t.date) && isNeftRtgs(t))
|
||||
.reduce((sum, t: any) => sum + (Number(t.amount) || 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum same-day cheque transactions that are still in clearing for this
|
||||
* account. A row is treated as a cheque-clearing entry when either:
|
||||
* • paymentDetailData carries a cheque/check number, OR
|
||||
* • the transaction type / payment type label mentions "CHEQUE".
|
||||
* Posted (cleared) entries are excluded so we only show the pending
|
||||
* float — that's the AML-relevant figure for the cashier.
|
||||
*/
|
||||
private computeSameDayChequeClearing(transactions: any[] | undefined): void {
|
||||
if (!Array.isArray(transactions) || transactions.length === 0) {
|
||||
this.sameDayChequeClearing = 0;
|
||||
return;
|
||||
}
|
||||
const today = this.settingsService.businessDate ?? new Date();
|
||||
const sameDay = (d: any) => {
|
||||
if (!d) return false;
|
||||
const t = Array.isArray(d) && d.length >= 3 ? new Date(d[0], d[1] - 1, d[2]) : new Date(d);
|
||||
return (
|
||||
t.getFullYear() === today.getFullYear() && t.getMonth() === today.getMonth() && t.getDate() === today.getDate()
|
||||
);
|
||||
};
|
||||
const isCheque = (t: any) => {
|
||||
const txnType = (t?.transactionType?.value ?? t?.transactionType?.code ?? '').toUpperCase();
|
||||
const payType = (t?.paymentDetailData?.paymentType?.name ?? '').toUpperCase();
|
||||
const hasCheckNo = !!t?.paymentDetailData?.checkNumber;
|
||||
return hasCheckNo || /CHEQUE|CHECK/.test(txnType) || /CHEQUE|CHECK/.test(payType);
|
||||
};
|
||||
const isPending = (t: any) => {
|
||||
const cleared = t?.transactionType?.bankTransfer === true || t?.transactionType?.deposit === true;
|
||||
// Treat anything not yet posted as in-clearing — backends differ on
|
||||
// the exact flag, so we look at `cleared` as a heuristic and exclude
|
||||
// it from the float.
|
||||
return !cleared;
|
||||
};
|
||||
this.sameDayChequeClearing = transactions
|
||||
.filter((t: any) => !t.reversed && sameDay(t.date) && isCheque(t) && isPending(t))
|
||||
.reduce((sum, t: any) => sum + (Number(t.amount) || 0), 0);
|
||||
}
|
||||
|
||||
createSavingAccountTransactionForm() {
|
||||
this.savingAccountTransactionForm = this.formBuilder.group({
|
||||
transactionDate: [
|
||||
@@ -123,6 +279,83 @@ export class SavingsAccountTransactionsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Denomination helpers ────────────────────────────────────────────
|
||||
denomKey(d: Denomination): string {
|
||||
return `${d.kind}:${d.value}`;
|
||||
}
|
||||
|
||||
countFor(d: Denomination): number {
|
||||
return this.denominationCounts[this.denomKey(d)] || 0;
|
||||
}
|
||||
|
||||
subtotalFor(d: Denomination): number {
|
||||
return this.countFor(d) * d.value;
|
||||
}
|
||||
|
||||
onDenomChange(d: Denomination, raw: any): void {
|
||||
const n = Math.max(0, Math.floor(Number(raw) || 0));
|
||||
this.denominationCounts[this.denomKey(d)] = n;
|
||||
}
|
||||
|
||||
bumpDenom(d: Denomination, delta: number): void {
|
||||
const next = Math.max(0, this.countFor(d) + delta);
|
||||
this.denominationCounts[this.denomKey(d)] = next;
|
||||
}
|
||||
|
||||
get notesTotal(): number {
|
||||
return this.notes.reduce((s, d) => s + this.subtotalFor(d), 0);
|
||||
}
|
||||
|
||||
get coinsTotal(): number {
|
||||
return this.coins.reduce((s, d) => s + this.subtotalFor(d), 0);
|
||||
}
|
||||
|
||||
get denominationTotal(): number {
|
||||
return this.notesTotal + this.coinsTotal;
|
||||
}
|
||||
|
||||
get totalPieces(): number {
|
||||
return Object.values(this.denominationCounts).reduce((s, n) => s + (n || 0), 0);
|
||||
}
|
||||
|
||||
get denominationMatchesForm(): boolean {
|
||||
const formAmt = Number(this.savingAccountTransactionForm?.controls?.['transactionAmount']?.value || 0);
|
||||
return this.denominationTotal > 0 && formAmt === this.denominationTotal;
|
||||
}
|
||||
|
||||
/** Push the counted total into the form's transactionAmount field. */
|
||||
applyDenominationTotal(): void {
|
||||
this.savingAccountTransactionForm.controls['transactionAmount'].setValue(this.denominationTotal);
|
||||
this.savingAccountTransactionForm.controls['transactionAmount'].markAsDirty();
|
||||
}
|
||||
|
||||
clearDenominations(): void {
|
||||
this.denominationCounts = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the identity-verify pop-up. Only meaningful for withdrawals
|
||||
* (where the cashier needs to match the customer at the counter
|
||||
* against the on-file profile before paying out cash).
|
||||
*/
|
||||
openIdentityVerify(): void {
|
||||
if (!this.clientId) return;
|
||||
// Calm the attention pulse after the first open — the cashier has
|
||||
// acknowledged the verification step.
|
||||
this.identityVerified = true;
|
||||
this.dialog.open(IdentityVerifyDialogComponent, {
|
||||
width: '500px',
|
||||
maxWidth: '92vw',
|
||||
panelClass: 'idv-panel',
|
||||
autoFocus: false,
|
||||
data: {
|
||||
clientId: this.clientId,
|
||||
clientName: this.clientName,
|
||||
accountNo: this.accountNo
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add payment detail fields to the UI.
|
||||
*/
|
||||
@@ -145,8 +378,39 @@ export class SavingsAccountTransactionsComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Method to submit the transaction details.
|
||||
*
|
||||
* Opens a pre-commit confirmation pop-up showing the current balance,
|
||||
* the transaction amount, and the resulting balance. The actual API
|
||||
* call only fires after the cashier confirms.
|
||||
*/
|
||||
submit() {
|
||||
const formAmount = Number(this.savingAccountTransactionForm.value.transactionAmount) || 0;
|
||||
const paymentTypeId = this.savingAccountTransactionForm.value.paymentTypeId;
|
||||
const paymentTypeLabel = this.paymentTypeOptions?.find((p) => p.id === paymentTypeId)?.name ?? null;
|
||||
|
||||
const ref = this.dialog.open(TransactionConfirmDialogComponent, {
|
||||
width: '480px',
|
||||
maxWidth: '92vw',
|
||||
panelClass: 'tcd-panel',
|
||||
autoFocus: false,
|
||||
data: {
|
||||
kind: this.transactionType.deposit ? 'deposit' : 'withdrawal',
|
||||
clientName: this.clientName,
|
||||
accountNo: this.accountNo,
|
||||
currentBalance: this.availableBalance,
|
||||
transactionAmount: formAmount,
|
||||
paymentTypeLabel
|
||||
}
|
||||
});
|
||||
ref.afterClosed().subscribe((result: any) => {
|
||||
if (result?.confirm) {
|
||||
this.postTransaction();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Actual API call — only invoked after the cashier confirms the dialog. */
|
||||
private postTransaction(): void {
|
||||
const savingAccountTransactionFormData = this.savingAccountTransactionForm.value;
|
||||
const locale = this.settingsService.language.code;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
@@ -160,9 +424,26 @@ export class SavingsAccountTransactionsComponent implements OnInit {
|
||||
locale
|
||||
};
|
||||
data['transactionAmount'] = data['transactionAmount'] * 1;
|
||||
// Surface the denomination breakdown on the wire — backend can ignore
|
||||
// unknown fields, but if a cash-handling extension is enabled it can
|
||||
// record the cashier's count for audit.
|
||||
if (this.denominationTotal > 0) {
|
||||
const breakdown = [
|
||||
...this.notes,
|
||||
...this.coins
|
||||
]
|
||||
.map((d) => ({
|
||||
denomination: d.value,
|
||||
kind: d.kind,
|
||||
count: this.countFor(d),
|
||||
subtotal: this.subtotalFor(d)
|
||||
}))
|
||||
.filter((row) => row.count > 0);
|
||||
data['denominationBreakdown'] = breakdown;
|
||||
}
|
||||
this.savingsService
|
||||
.executeSavingsAccountTransactionsCommand(this.savingAccountId, this.transactionCommand, data)
|
||||
.subscribe((res) => {
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['../../transactions'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="tcd" [class.tcd--deposit]="isDeposit" [class.tcd--withdrawal]="!isDeposit">
|
||||
<!-- Header -->
|
||||
<div class="tcd__head">
|
||||
<div class="tcd__head-ico">
|
||||
<fa-icon [icon]="isDeposit ? 'arrow-down' : 'arrow-up'"></fa-icon>
|
||||
</div>
|
||||
<div class="tcd__head-text">
|
||||
<h2 class="tcd__title">Confirm {{ isDeposit ? 'Deposit' : 'Withdrawal' }}</h2>
|
||||
<p class="tcd__sub">Review the figures below — once confirmed the entry will post to the account ledger.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-dialog-content class="tcd__content">
|
||||
<!-- Client / account meta -->
|
||||
<div class="tcd__meta">
|
||||
<div class="tcd__meta-item">
|
||||
<span class="tcd__meta-label">Client</span>
|
||||
<span class="tcd__meta-value">{{ data.clientName || '—' }}</span>
|
||||
</div>
|
||||
<div class="tcd__meta-item">
|
||||
<span class="tcd__meta-label">Account No</span>
|
||||
<span class="tcd__meta-value tcd__meta-value--mono">{{ data.accountNo || '—' }}</span>
|
||||
</div>
|
||||
@if (data.paymentTypeLabel) {
|
||||
<div class="tcd__meta-item">
|
||||
<span class="tcd__meta-label">Payment Type</span>
|
||||
<span class="tcd__meta-value">{{ data.paymentTypeLabel }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Balance breakdown -->
|
||||
<div class="tcd__breakdown">
|
||||
<div class="tcd__brk-row">
|
||||
<span class="tcd__brk-label">Current Balance</span>
|
||||
<span class="tcd__brk-value">₹{{ data.currentBalance | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="tcd__brk-row tcd__brk-row--txn">
|
||||
<span class="tcd__brk-label">
|
||||
{{ isDeposit ? 'Deposit Amount' : 'Withdrawal Amount' }}
|
||||
</span>
|
||||
<span
|
||||
class="tcd__brk-value"
|
||||
[class.tcd__brk-value--positive]="isDeposit"
|
||||
[class.tcd__brk-value--negative]="!isDeposit"
|
||||
>
|
||||
{{ isDeposit ? '+' : '−' }} ₹{{ data.transactionAmount | number: '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tcd__brk-row tcd__brk-row--total">
|
||||
<span class="tcd__brk-label">New Balance</span>
|
||||
<span class="tcd__brk-value">₹{{ newBalance | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overdraft warning -->
|
||||
@if (overdraft) {
|
||||
<div class="tcd__warn">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
Resulting balance is negative — confirm the account permits overdraft before proceeding.
|
||||
</div>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="tcd__actions">
|
||||
<button mat-stroked-button type="button" class="tcd__cancel" (click)="cancel()">
|
||||
<fa-icon icon="times" class="m-r-6"></fa-icon> Cancel
|
||||
</button>
|
||||
<button mat-flat-button type="button" class="tcd__confirm" (click)="confirm()">
|
||||
<fa-icon icon="check" class="m-r-6"></fa-icon>
|
||||
Confirm {{ isDeposit ? 'Deposit' : 'Withdrawal' }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
$green: #059669;
|
||||
$green-d: #047857;
|
||||
$teal: #0f766e;
|
||||
$teal-d: #134e4a;
|
||||
$red: #b91c1c;
|
||||
$amber: #b45309;
|
||||
|
||||
.tcd {
|
||||
width: 460px;
|
||||
max-width: 92vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ─── Header ──────────────────────────────────────────────────────── */
|
||||
.tcd__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 18px 22px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top right, rgb(255 255 255 / 14%), transparent 55%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tcd--deposit .tcd__head {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 45%, #1e40af 100%);
|
||||
}
|
||||
|
||||
.tcd--withdrawal .tcd__head {
|
||||
background: linear-gradient(135deg, $teal-d 0%, #115e59 45%, $teal 100%);
|
||||
}
|
||||
|
||||
.tcd__head-ico {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 28%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tcd__head-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tcd__title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.tcd__sub {
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
color: rgb(255 255 255 / 82%);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ─── Content ─────────────────────────────────────────────────────── */
|
||||
.tcd__content {
|
||||
padding: 18px 22px 6px !important;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tcd__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px dashed $border;
|
||||
}
|
||||
|
||||
.tcd__meta-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tcd__meta-label {
|
||||
color: $slate;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tcd__meta-value {
|
||||
color: $navy;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
|
||||
&--mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Breakdown card ──────────────────────────────────────────────── */
|
||||
.tcd__breakdown {
|
||||
background: $soft;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.tcd__brk-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
color: $slate;
|
||||
|
||||
& + & {
|
||||
border-top: 1px dashed #eef2f7;
|
||||
}
|
||||
|
||||
&--total {
|
||||
padding-top: 12px;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid #cbd5e1 !important;
|
||||
|
||||
.tcd__brk-label {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: $navy;
|
||||
}
|
||||
|
||||
.tcd__brk-value {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tcd__brk-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tcd__brk-value {
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&--positive {
|
||||
color: $green-d;
|
||||
}
|
||||
|
||||
&--negative {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Overdraft warning ───────────────────────────────────────────── */
|
||||
.tcd__warn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
|
||||
fa-icon {
|
||||
color: $amber;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Actions ─────────────────────────────────────────────────────── */
|
||||
.tcd__actions {
|
||||
padding: 14px 22px 18px !important;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
margin: 0 !important;
|
||||
|
||||
button {
|
||||
height: 40px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 700 !important;
|
||||
padding: 0 18px !important;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
.tcd__cancel {
|
||||
color: $slate !important;
|
||||
border-color: $border !important;
|
||||
}
|
||||
|
||||
.tcd__confirm {
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
|
||||
}
|
||||
|
||||
.tcd--deposit .tcd__confirm {
|
||||
background: linear-gradient(135deg, $green 0%, $green-d 100%) !important;
|
||||
}
|
||||
|
||||
.tcd--withdrawal .tcd__confirm {
|
||||
background: linear-gradient(135deg, $teal 0%, $teal-d 100%) !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(15 118 110 / 35%) !important;
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
MatDialogRef,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
} from '@angular/material/dialog';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
export interface TransactionConfirmData {
|
||||
/** 'deposit' | 'withdrawal' — drives the colour palette + sign. */
|
||||
kind: 'deposit' | 'withdrawal';
|
||||
clientName: string | null;
|
||||
accountNo: string | null;
|
||||
currentBalance: number;
|
||||
transactionAmount: number;
|
||||
paymentTypeLabel: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-commit confirmation pop-up showing the cashier exactly how the
|
||||
* deposit / withdrawal will affect the account: current balance, the
|
||||
* transaction amount, and the post-transaction balance.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-transaction-confirm-dialog',
|
||||
standalone: true,
|
||||
templateUrl: './transaction-confirm-dialog.component.html',
|
||||
styleUrls: ['./transaction-confirm-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class TransactionConfirmDialogComponent {
|
||||
dialogRef = inject<MatDialogRef<TransactionConfirmDialogComponent>>(MatDialogRef);
|
||||
data = inject<TransactionConfirmData>(MAT_DIALOG_DATA);
|
||||
|
||||
get isDeposit(): boolean {
|
||||
return this.data.kind === 'deposit';
|
||||
}
|
||||
|
||||
get signedAmount(): number {
|
||||
return this.isDeposit ? this.data.transactionAmount : -this.data.transactionAmount;
|
||||
}
|
||||
|
||||
get newBalance(): number {
|
||||
return this.data.currentBalance + this.signedAmount;
|
||||
}
|
||||
|
||||
get overdraft(): boolean {
|
||||
return !this.isDeposit && this.newBalance < 0;
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
this.dialogRef.close({ confirm: true });
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close({ confirm: false });
|
||||
}
|
||||
}
|
||||
-7
@@ -57,13 +57,6 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (savingsProductSelected) {
|
||||
<mat-form-field appearance="outline" class="sad-field">
|
||||
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
|
||||
<input matInput formControlName="externalId" />
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+47
-4
@@ -82,6 +82,10 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
primaryClientCif = '';
|
||||
primaryClientId: number | null = null;
|
||||
primaryHolderShare = 0;
|
||||
/** Best-effort string label of the client's entity type
|
||||
* (e.g. "Individual", "Corporate", "MSME") — used to auto-fill the
|
||||
* Customer Segment dropdown when the product template lands. */
|
||||
private clientEntityType: string | null = null;
|
||||
|
||||
// ── Status of Account (RBI) ────────────────────────────────────────────
|
||||
readonly accountStatusOptions = [
|
||||
@@ -167,8 +171,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
productId: this.savingsAccountTemplate.savingsProductId,
|
||||
submittedOnDate:
|
||||
this.savingsAccountTemplate.timeline.submittedOnDate &&
|
||||
new Date(this.savingsAccountTemplate.timeline.submittedOnDate),
|
||||
externalId: this.savingsAccountTemplate.externalId
|
||||
new Date(this.savingsAccountTemplate.timeline.submittedOnDate)
|
||||
});
|
||||
} else {
|
||||
this.savingsAccountDetailsForm.patchValue({
|
||||
@@ -193,11 +196,49 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
this.autoDetectAccountStatus();
|
||||
}
|
||||
}
|
||||
this.clientEntityType = this.deriveEntityType(client);
|
||||
// Product template may have already arrived before the client did —
|
||||
// try the segment-apply both here and when the template lands.
|
||||
this.applyInferredCustomerSegment();
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a best-effort label for the client's entity / customer type.
|
||||
* Tries `clientType` first (the explicit master), then falls back to
|
||||
* `clientClassification` and finally `legalForm`.
|
||||
*/
|
||||
private deriveEntityType(client: any): string | null {
|
||||
if (!client) return null;
|
||||
const direct =
|
||||
client.clientType?.value ??
|
||||
client.clientType?.name ??
|
||||
client.clientClassification?.value ??
|
||||
client.clientClassification?.name;
|
||||
if (direct && typeof direct === 'string') return direct;
|
||||
const legalForm = client.legalForm?.value ?? client.legalForm?.code ?? client.legalForm?.name;
|
||||
return typeof legalForm === 'string' ? legalForm : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `clientEntityType` and `customerSegmentOptions` are both ready, try
|
||||
* to match an option (exact then case-insensitive substring) and set it
|
||||
* as the selected Customer Segment. Falls through silently otherwise so
|
||||
* the existing product default stays in place.
|
||||
*/
|
||||
private applyInferredCustomerSegment(): void {
|
||||
if (!this.clientEntityType || !this.customerSegmentOptions?.length) return;
|
||||
const needle = this.clientEntityType.trim().toLowerCase();
|
||||
const match =
|
||||
this.customerSegmentOptions.find((o) => o.toLowerCase() === needle) ||
|
||||
this.customerSegmentOptions.find((o) => o.toLowerCase().includes(needle) || needle.includes(o.toLowerCase()));
|
||||
if (match) {
|
||||
this.selectedCustomerSegment = match;
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse DOB from backend — handles array [yyyy, mm, dd] or string */
|
||||
private parseClientDob(dob: any): Date | null {
|
||||
if (!dob) return null;
|
||||
@@ -263,8 +304,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
fieldOfficerId: [''],
|
||||
externalId: ['']
|
||||
fieldOfficerId: ['']
|
||||
});
|
||||
}
|
||||
|
||||
@@ -279,6 +319,9 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment);
|
||||
this.selectedAccountType = response.accountType ?? null;
|
||||
this.selectedCustomerSegment = response.customerSegment ?? null;
|
||||
// Override the product's default segment with the one inferred
|
||||
// from the client's entity type when an option matches.
|
||||
this.applyInferredCustomerSegment();
|
||||
this.savingsAccountProductTemplate.emit(response);
|
||||
this.fieldOfficerData = response.fieldOfficerOptions;
|
||||
this.savingsProductSelected = true;
|
||||
|
||||
+10
-7
@@ -575,15 +575,18 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="layout-row responsive-column align-center gap-2px margin-t">
|
||||
<button mat-raised-button matStepperPrevious>
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button [routerLink]="['../..']">
|
||||
<!--
|
||||
Submit & Cancel — Previous lives in the parent wizard's tab action bar
|
||||
now that this component renders inside a mat-tab-group rather than a
|
||||
mat-stepper. Re-introducing matStepperPrevious here would require a
|
||||
CdkStepper ancestor and would crash the view on init.
|
||||
-->
|
||||
<div class="preview-submit-row">
|
||||
<button mat-stroked-button [routerLink]="['../..']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="submitEvent.emit()">
|
||||
<button mat-raised-button color="primary" class="preview-submit-row__primary" (click)="submitEvent.emit()">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+25
@@ -13,6 +13,31 @@
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Submit + Cancel row at the bottom of the preview. Previous lives in the
|
||||
parent wizard's tab action bar. */
|
||||
.preview-submit-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
button {
|
||||
height: 42px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 22px !important;
|
||||
}
|
||||
|
||||
&__primary {
|
||||
background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
|
||||
-2
@@ -23,7 +23,6 @@ import {
|
||||
} from '@angular/material/table';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component';
|
||||
import { MatStepperPrevious } from '@angular/material/stepper';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { FindPipe } from '../../../pipes/find.pipe';
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
@@ -52,7 +51,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
MatStepperPrevious,
|
||||
FaIconComponent,
|
||||
FindPipe,
|
||||
DateFormatPipe,
|
||||
|
||||
@@ -6,25 +6,22 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="tab-container mat-typography">
|
||||
<div class="layout-row align-start">
|
||||
<div class="m-b-10">
|
||||
<h3>{{ 'labels.inputs.Charges' | translate }}</h3>
|
||||
</div>
|
||||
@if (this.chargesData) {
|
||||
<div class="action-button m-b-20">
|
||||
<button mat-raised-button color="primary" (click)="toggleCharges()">
|
||||
<div class="chg-tab">
|
||||
<div class="chg-tab__card">
|
||||
<div class="chg-tab__toolbar">
|
||||
<h3 class="chg-tab__title">{{ 'labels.inputs.Charges' | translate }}</h3>
|
||||
<span class="chg-tab__spacer"></span>
|
||||
@if (this.chargesData) {
|
||||
<button mat-flat-button class="chg-tab__toggle" (click)="toggleCharges()">
|
||||
{{
|
||||
showInactiveCharges
|
||||
? viewAllChargeButtons('View Active Charges')
|
||||
: viewAllChargeButtons('View Inactive Charges')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mat-elevation-z1 m-b-25">
|
||||
<table mat-table #chargesTable [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
|
||||
|
||||
@@ -6,30 +6,107 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.tab-container {
|
||||
padding: 1%;
|
||||
margin: 1%;
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
$blue: #1e40af;
|
||||
|
||||
h3 {
|
||||
margin: 1% auto;
|
||||
.chg-tab {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chg-tab__card {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chg-tab__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 18px;
|
||||
background: $soft;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.chg-tab__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.chg-tab__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chg-tab__toggle {
|
||||
height: 34px !important;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 13px !important;
|
||||
padding: 0 14px !important;
|
||||
background: linear-gradient(135deg, #1e88e5 0%, $blue 100%) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::ng-deep .chg-tab__card {
|
||||
.mat-mdc-header-row {
|
||||
background: $soft;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-left: auto;
|
||||
.mat-mdc-header-cell {
|
||||
font-size: 11.5px;
|
||||
font-weight: 800;
|
||||
color: $slate;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
.mat-mdc-cell {
|
||||
font-size: 13px;
|
||||
color: $navy;
|
||||
padding: 11px 14px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.mat-mdc-row:hover {
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Right-align monetary columns. */
|
||||
.mat-column-due,
|
||||
.mat-column-paid,
|
||||
.mat-column-waived,
|
||||
.mat-column-outstanding {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
text-align: center;
|
||||
width: 140px;
|
||||
|
||||
.account-action-button {
|
||||
min-width: 26px;
|
||||
padding: 0 6px;
|
||||
margin: 4px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.select-row:hover {
|
||||
cursor: pointer;
|
||||
min-width: 30px !important;
|
||||
padding: 0 8px !important;
|
||||
margin: 0 3px !important;
|
||||
height: 30px !important;
|
||||
line-height: 30px !important;
|
||||
border-radius: 7px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,355 +13,378 @@
|
||||
}
|
||||
|
||||
@if (!isLoading) {
|
||||
<div class="savings-account-tables layout-row gap-2percent">
|
||||
<div class="flex-49 layout-column gap-4percent">
|
||||
@if (!savingsAccountData.status.rejected && !savingsAccountData.status.submittedAndPendingApproval) {
|
||||
<div>
|
||||
<h4 class="table-headers">{{ 'labels.heading.Savings Details' | translate }}</h4>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.External Id' | translate }}</td>
|
||||
@if (savingsAccountData.externalId) {
|
||||
<td>
|
||||
<mifosx-external-identifier
|
||||
externalId="{{ savingsAccountData.externalId }}"
|
||||
></mifosx-external-identifier>
|
||||
</td>
|
||||
}
|
||||
@if (!savingsAccountData.externalId) {
|
||||
<td>{{ 'labels.inputs.Not Provided' | translate }}</td>
|
||||
}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Activated On' | translate }}</td>
|
||||
<td>
|
||||
{{
|
||||
savingsAccountData.timeline.activatedOnDate
|
||||
? (savingsAccountData.timeline.activatedOnDate | dateFormat)
|
||||
: 'Not Activated'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Field Officer' | translate }}</td>
|
||||
<td>{{ savingsAccountData.fieldOfficerName ? savingsAccountData.fieldOfficerName : 'Unassigned' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Currency' | translate }}</td>
|
||||
<td>{{ savingsAccountData.currency.name }} [{{ savingsAccountData.currency.code }}]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Nominal Interest Rate' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
@if (savingsAccountData.nominalAnnualInterestRate) {
|
||||
<span>{{ savingsAccountData.nominalAnnualInterestRate | formatNumber }} %</span>
|
||||
}
|
||||
@if (!savingsAccountData.nominalAnnualInterestRate) {
|
||||
<span>0 %</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
} @else {
|
||||
<div>
|
||||
<h4 class="table-headers">{{ 'labels.heading.Savings Details' | translate }}</h4>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Field Officer' | translate }}</td>
|
||||
<td>{{ savingsAccountData.fieldOfficerName ? savingsAccountData.fieldOfficerName : 'Unassigned' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Balance' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{ savingsAccountData.summary.accountBalance | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (!savingsAccountData.status.rejected && !savingsAccountData.status.submittedAndPendingApproval) {
|
||||
<div>
|
||||
<h4 class="table-headers">{{ 'labels.heading.Performance History' | translate }}</h4>
|
||||
<table>
|
||||
<tbody>
|
||||
@if (savingsAccountData.summary.totalDeposits) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Total Deposits' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{ savingsAccountData.summary.totalDeposits | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalInterestEarned >= 0) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Total Interest Earned' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalInterestEarned
|
||||
| currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (isActive) {
|
||||
<div class="flex-49">
|
||||
<h4 class="table-headers">{{ 'labels.heading.Account Summary' | translate }}</h4>
|
||||
<table>
|
||||
<tbody>
|
||||
@if (savingsAccountData.summary.totalWithdrawals) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Total Withdrawls' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalWithdrawals | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalWithdrawalFees) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Withdrawals Fees' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalWithdrawalFees | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalAnnualFees) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Annual Fees' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{ savingsAccountData.summary.totalAnnualFees | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalInterestEarned >= 0) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Interest Earned' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalInterestEarned | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalInterestPosted) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Interest Posted' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalInterestPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.interestNotPosted >= 0) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Interest Earned Not Posted' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.interestNotPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalOverdraftInterestDerived) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Interest On Overdraft' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalOverdraftInterestDerived
|
||||
| currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.interestNotPosted < 0) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Overdraft Interest Not Posted' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.interestNotPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.nominalAnnualInterestRate) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Nominal Interest Rate' | translate }}</td>
|
||||
<td class="r-amount">{{ savingsAccountData.nominalAnnualInterestRate | formatNumber }} %</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Interest Compounding Period' | translate }}</td>
|
||||
<td>{{ savingsAccountData.interestCompoundingPeriodType.value | translateKey: 'catalogs' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Interest Posting Period' | translate }}</td>
|
||||
<td>{{ savingsAccountData.interestPostingPeriodType.value | translateKey: 'catalogs' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Interest Calculated Using' | translate }}</td>
|
||||
<td>{{ savingsAccountData.interestCalculationType.value | translateKey: 'catalogs' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Days in Year' | translate }}</td>
|
||||
<td>{{ savingsAccountData.interestCalculationDaysInYearType.value | translateKey: 'catalogs' }}</td>
|
||||
</tr>
|
||||
@if (savingsAccountData.withdrawalFee) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Withdrawal Fee' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{ savingsAccountData.withdrawalFee.amount | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.lastActiveTransactionDate) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Last Active Transaction Date' | translate }}</td>
|
||||
<td>{{ savingsAccountData.lastActiveTransactionDate | dateFormat }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!(savingsAccountData.subStatus.id === 0)) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Substatus' | translate }}</td>
|
||||
<td>{{ savingsAccountData.subStatus.value }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.daysToInactive) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Days to Inactive' | translate }}</td>
|
||||
<td>{{ savingsAccountData.daysToInactive }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.daysToDormancy) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Days to Dormancy' | translate }}</td>
|
||||
<td>{{ savingsAccountData.daysToDormancy }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.daysToEscheat) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Days to Escheat' | translate }}</td>
|
||||
<td>{{ savingsAccountData.daysToEscheat }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.annualFee) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Annual Fee' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{ savingsAccountData.annualFee.amount | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.allowOverdraft) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Overdraft Limit' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{ savingsAccountData.overdraftLimit | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.allowOverdraft) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}</td>
|
||||
<td>
|
||||
{{
|
||||
savingsAccountData.minOverdraftForInterestCalculation
|
||||
| currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.minBalanceForInterestCalculation) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Min Balance Required for Interest Calculation' | translate }}</td>
|
||||
<td>{{ savingsAccountData.minBalanceForInterestCalculation | formatNumber }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.minRequiredBalance) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Minimum Required Balance' | translate }}</td>
|
||||
<td>{{ savingsAccountData.minRequiredBalance | formatNumber }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.enforceMinRequiredBalance) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Enforce Minimum Required Balance' | translate }}</td>
|
||||
<td>{{ savingsAccountData.enforceMinRequiredBalance | yesNo }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.summary.lastInterestCalculationDate) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Interest Recalculation Date' | translate }}</td>
|
||||
<td>{{ savingsAccountData.summary.lastInterestCalculationDate | dateFormat }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.onHoldFunds) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.On Hold Funds' | translate }}</td>
|
||||
<td>
|
||||
<a *mifosxHasPermission="'READ_SAVINGSACCOUNT'">{{
|
||||
savingsAccountData.onHoldFunds | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (savingsAccountData.withHoldTax) {
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Withhold Tax Group' | translate }}</td>
|
||||
<td>{{ savingsAccountData.taxGroup.name }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex-49">
|
||||
<h4 class="table-headers">{{ 'labels.heading.Account Summary' | translate }}</h4>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.External Id' | translate }}</td>
|
||||
@if (savingsAccountData.externalId) {
|
||||
<td>
|
||||
<mifosx-external-identifier
|
||||
externalId="{{ savingsAccountData.externalId }}"
|
||||
></mifosx-external-identifier>
|
||||
</td>
|
||||
}
|
||||
@if (!savingsAccountData.externalId) {
|
||||
<td>{{ 'labels.inputs.Not Provided' | translate }}</td>
|
||||
}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Activated On' | translate }}</td>
|
||||
<td>
|
||||
<div class="gt-grid">
|
||||
<!-- ─── LEFT COLUMN ──────────────────────────────────────────── -->
|
||||
<div class="gt-col">
|
||||
<!-- Savings Details -->
|
||||
<section class="nx-card">
|
||||
<header class="nx-card__head">
|
||||
<fa-icon icon="info-circle" class="nx-card__ico"></fa-icon>
|
||||
<h3 class="nx-card__title">{{ 'labels.heading.Savings Details' | translate }}</h3>
|
||||
</header>
|
||||
<div class="nx-card__body">
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Account Number' | translate }}</span>
|
||||
<span class="nx-row__value nx-mono">{{ savingsAccountData.accountNo }}</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label"
|
||||
>{{ 'labels.text.' + entityType | translate }} {{ 'labels.inputs.name' | translate }}</span
|
||||
>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.clientName || savingsAccountData.groupName || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
@if (!savingsAccountData.status.rejected && !savingsAccountData.status.submittedAndPendingApproval) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Activated On' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{
|
||||
savingsAccountData.timeline.activatedOnDate
|
||||
? (savingsAccountData.timeline.activatedOnDate | dateFormat)
|
||||
: 'Not Activated'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Currency' | translate }}</td>
|
||||
<td>{{ savingsAccountData.currency.name }} [{{ savingsAccountData.currency.code }}]</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Field Officer' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.fieldOfficerName ? savingsAccountData.fieldOfficerName : 'Unassigned' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Currency' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.currency.name }}
|
||||
<span class="nx-mono">[{{ savingsAccountData.currency.code }}]</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Nominal Interest Rate' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
@if (savingsAccountData.nominalAnnualInterestRate) {
|
||||
{{ savingsAccountData.nominalAnnualInterestRate | formatNumber }} %
|
||||
} @else {
|
||||
0 %
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Field Officer' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.fieldOfficerName ? savingsAccountData.fieldOfficerName : 'Unassigned' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Balance' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{ savingsAccountData.summary.accountBalance | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Performance History -->
|
||||
@if (!savingsAccountData.status.rejected && !savingsAccountData.status.submittedAndPendingApproval) {
|
||||
<section class="nx-card">
|
||||
<header class="nx-card__head">
|
||||
<fa-icon icon="chart-line" class="nx-card__ico"></fa-icon>
|
||||
<h3 class="nx-card__title">{{ 'labels.heading.Performance History' | translate }}</h3>
|
||||
</header>
|
||||
<div class="nx-card__body">
|
||||
@if (savingsAccountData.summary.totalDeposits) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Total Deposits' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount nx-row__value--positive">
|
||||
{{ savingsAccountData.summary.totalDeposits | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalInterestEarned >= 0) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Total Interest Earned' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalInterestEarned | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (!savingsAccountData.summary.totalDeposits && !(savingsAccountData.summary.totalInterestEarned >= 0)) {
|
||||
<div class="nx-empty">
|
||||
<fa-icon icon="inbox" class="nx-empty__ico"></fa-icon>
|
||||
<span>No activity yet.</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- ─── RIGHT COLUMN ─────────────────────────────────────────── -->
|
||||
<div class="gt-col">
|
||||
@if (isActive) {
|
||||
<section class="nx-card">
|
||||
<header class="nx-card__head">
|
||||
<fa-icon icon="file-invoice" class="nx-card__ico"></fa-icon>
|
||||
<h3 class="nx-card__title">{{ 'labels.heading.Account Summary' | translate }}</h3>
|
||||
</header>
|
||||
<div class="nx-card__body">
|
||||
@if (savingsAccountData.summary.totalWithdrawals) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Total Withdrawls' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount nx-row__value--negative">
|
||||
{{
|
||||
savingsAccountData.summary.totalWithdrawals | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalWithdrawalFees) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Withdrawals Fees' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalWithdrawalFees | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalAnnualFees) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Annual Fees' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{ savingsAccountData.summary.totalAnnualFees | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalInterestEarned >= 0) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Interest Earned' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalInterestEarned | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalInterestPosted) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Interest Posted' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalInterestPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.interestNotPosted >= 0) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Interest Earned Not Posted' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{
|
||||
savingsAccountData.summary.interestNotPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.totalOverdraftInterestDerived) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Interest On Overdraft' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{
|
||||
savingsAccountData.summary.totalOverdraftInterestDerived
|
||||
| currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.interestNotPosted < 0) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Overdraft Interest Not Posted' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{
|
||||
savingsAccountData.summary.interestNotPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.nominalAnnualInterestRate) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Nominal Interest Rate' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{ savingsAccountData.nominalAnnualInterestRate | formatNumber }} %
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Interest Compounding Period' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.interestCompoundingPeriodType.value | translateKey: 'catalogs' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Interest Posting Period' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.interestPostingPeriodType.value | translateKey: 'catalogs' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Interest Calculated Using' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.interestCalculationType.value | translateKey: 'catalogs' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Days in Year' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.interestCalculationDaysInYearType.value | translateKey: 'catalogs' }}
|
||||
</span>
|
||||
</div>
|
||||
@if (savingsAccountData.withdrawalFee) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Withdrawal Fee' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{ savingsAccountData.withdrawalFee.amount | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.lastActiveTransactionDate) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Last Active Transaction Date' | translate }}</span>
|
||||
<span class="nx-row__value">{{ savingsAccountData.lastActiveTransactionDate | dateFormat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (!(savingsAccountData.subStatus.id === 0)) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Substatus' | translate }}</span>
|
||||
<span class="nx-row__value">{{ savingsAccountData.subStatus.value }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.daysToInactive) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Days to Inactive' | translate }}</span>
|
||||
<span class="nx-row__value">{{ savingsAccountData.daysToInactive }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.daysToDormancy) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Days to Dormancy' | translate }}</span>
|
||||
<span class="nx-row__value">{{ savingsAccountData.daysToDormancy }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.daysToEscheat) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Days to Escheat' | translate }}</span>
|
||||
<span class="nx-row__value">{{ savingsAccountData.daysToEscheat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.annualFee) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Annual Fee' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{ savingsAccountData.annualFee.amount | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.allowOverdraft) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Overdraft Limit' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
{{ savingsAccountData.overdraftLimit | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.allowOverdraft) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">
|
||||
{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}
|
||||
</span>
|
||||
<span class="nx-row__value">
|
||||
{{
|
||||
savingsAccountData.minOverdraftForInterestCalculation
|
||||
| currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.minBalanceForInterestCalculation) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">
|
||||
{{ 'labels.inputs.Min Balance Required for Interest Calculation' | translate }}
|
||||
</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.minBalanceForInterestCalculation | formatNumber }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.minRequiredBalance) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Minimum Required Balance' | translate }}</span>
|
||||
<span class="nx-row__value">{{ savingsAccountData.minRequiredBalance | formatNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.enforceMinRequiredBalance) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Enforce Minimum Required Balance' | translate }}</span>
|
||||
<span class="nx-row__value">{{ savingsAccountData.enforceMinRequiredBalance | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.summary.lastInterestCalculationDate) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Interest Recalculation Date' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.summary.lastInterestCalculationDate | dateFormat }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.onHoldFunds) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.On Hold Funds' | translate }}</span>
|
||||
<span class="nx-row__value nx-row__value--amount">
|
||||
<a *mifosxHasPermission="'READ_SAVINGSACCOUNT'">
|
||||
{{ savingsAccountData.onHoldFunds | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountData.withHoldTax) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Withhold Tax Group' | translate }}</span>
|
||||
<span class="nx-row__value">{{ savingsAccountData.taxGroup.name }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
} @else {
|
||||
<section class="nx-card">
|
||||
<header class="nx-card__head">
|
||||
<fa-icon icon="file-invoice" class="nx-card__ico"></fa-icon>
|
||||
<h3 class="nx-card__title">{{ 'labels.heading.Account Summary' | translate }}</h3>
|
||||
</header>
|
||||
<div class="nx-card__body">
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Activated On' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{
|
||||
savingsAccountData.timeline.activatedOnDate
|
||||
? (savingsAccountData.timeline.activatedOnDate | dateFormat)
|
||||
: 'Not Activated'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Currency' | translate }}</span>
|
||||
<span class="nx-row__value">
|
||||
{{ savingsAccountData.currency.name }}
|
||||
<span class="nx-mono">[{{ savingsAccountData.currency.code }}]</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -6,9 +6,15 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
|
||||
/* ─── Loader keyframes (legacy, untouched) ──────────────────────────── */
|
||||
$bar: 160px;
|
||||
$foo: $bar * 8;
|
||||
$color: #e2e4ec;
|
||||
|
||||
.loader {
|
||||
margin-bottom: 39px;
|
||||
@@ -79,164 +85,136 @@ $color: #e2e4ec;
|
||||
}
|
||||
}
|
||||
|
||||
.general-tab-container {
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
background-color: var(--background-color, #f8f9fa);
|
||||
color: var(--text-color, #333);
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
/* ─── Two-column grid ───────────────────────────────────────────────── */
|
||||
.gt-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 0;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--border-color, #e9ecef);
|
||||
@media (width <= 1024px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.2;
|
||||
color: var(--primary-color, #007bff);
|
||||
position: relative;
|
||||
}
|
||||
.gt-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
color: var(--text-secondary, #6c757d);
|
||||
position: relative;
|
||||
}
|
||||
/* ─── Nexora section card ───────────────────────────────────────────── */
|
||||
.nx-card {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 18px;
|
||||
background: $soft;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-info-card {
|
||||
background: var(--card-background, #fff);
|
||||
&__ico {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
transition:
|
||||
box-shadow 0.3s ease,
|
||||
background-color 0.3s ease;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 16px 20px;
|
||||
background: var(--card-header-bg, #f8f9fa);
|
||||
border-bottom: 1px solid var(--border-color, #e9ecef);
|
||||
position: relative;
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
&__body {
|
||||
padding: 4px 18px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Key/Value row ─────────────────────────────────────────────────── */
|
||||
.nx-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed #eef2f7;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: $slate;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__value {
|
||||
color: $navy;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
|
||||
&--amount {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
&--positive {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-light, #f1f3f4);
|
||||
position: relative;
|
||||
min-height: 24px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #6c757d);
|
||||
flex: 0 0 auto;
|
||||
margin-right: 16px;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #333);
|
||||
text-align: right;
|
||||
flex: 1 1 auto;
|
||||
line-height: 1.4;
|
||||
|
||||
&.currency {
|
||||
color: var(--success-color, #28a745);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
&--negative {
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode styles
|
||||
[data-theme='dark'] .general-tab-container,
|
||||
.dark-theme .general-tab-container {
|
||||
--background-color: #1a1a1a;
|
||||
--text-color: #e9ecef;
|
||||
--text-secondary: #adb5bd;
|
||||
--primary-color: #4dabf7;
|
||||
--card-background: #2d3748;
|
||||
--card-header-bg: #374151;
|
||||
--border-color: #4a5568;
|
||||
--border-light: #2d3748;
|
||||
--success-color: #68d391;
|
||||
.nx-muted {
|
||||
color: $muted;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Responsive adjustments for mobile
|
||||
@media (width <= 768px) {
|
||||
.general-tab-container {
|
||||
padding: 12px 16px;
|
||||
.nx-mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
color: $muted;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px 0;
|
||||
/* ─── Empty state inside a card ─────────────────────────────────────── */
|
||||
.nx-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 24px 16px;
|
||||
color: $muted;
|
||||
|
||||
.header-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
&__ico {
|
||||
font-size: 28px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.account-info-card {
|
||||
.card-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
|
||||
.info-row {
|
||||
padding: 8px 0;
|
||||
|
||||
.label {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
span {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Currency } from 'app/shared/models/general.model';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component';
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
|
||||
import { YesnoPipe } from '../../../pipes/yesno.pipe';
|
||||
@@ -22,7 +21,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./general-tab.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
ExternalIdentifierComponent,
|
||||
CurrencyPipe,
|
||||
DateFormatPipe,
|
||||
FormatNumberPipe,
|
||||
|
||||
@@ -6,239 +6,373 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<mat-card class="account-card">
|
||||
<mat-card-header class="header layout-column">
|
||||
<mat-card-title-group class="header-title-group">
|
||||
<div class="profile-image-container">
|
||||
<div>
|
||||
<img
|
||||
mat-card-md-image
|
||||
class="profile-image"
|
||||
matTooltip="{{ 'tooltips.Savings Account' | translate }}"
|
||||
[src]="'assets/images/savings_account_placeholder.png'"
|
||||
/>
|
||||
</div>
|
||||
<div class="sav-view">
|
||||
<!-- ─── Gradient hero header ─────────────────────────────────────── -->
|
||||
<header class="sav-hero">
|
||||
<div class="sav-hero__left">
|
||||
<div class="sav-hero__avatar">
|
||||
<fa-icon icon="piggy-bank"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div class="mat-typography account-card-title">
|
||||
<mat-card-title class="layout-row layout-lt-md-column">
|
||||
<div class="flex-60">
|
||||
<h3>
|
||||
@if (!savingsAccountData.subStatus.block) {
|
||||
<i
|
||||
class="fa fa-stop"
|
||||
[ngClass]="savingsAccountData.status.code | statusLookup"
|
||||
[matTooltip]="savingsAccountData.status.value"
|
||||
></i>
|
||||
}
|
||||
@if (savingsAccountData.subStatus.block) {
|
||||
<i
|
||||
class="fa fa-stop"
|
||||
[ngClass]="savingsAccountData.subStatus.value | statusLookup"
|
||||
[matTooltip]="savingsAccountData.subStatus.value"
|
||||
></i>
|
||||
}
|
||||
</h3>
|
||||
<!-- Product Name Row -->
|
||||
<span class="product-row">
|
||||
<span class="m-r-5">{{ 'labels.inputs.Savings Product' | translate }} :</span>
|
||||
<span class="m-r-5">
|
||||
<mifosx-long-text textValue="{{ savingsAccountData.savingsProductName }}"></mifosx-long-text>
|
||||
</span>
|
||||
<div class="sav-hero__info">
|
||||
<!-- Status pill (account status, or sub-status if blocked) -->
|
||||
<div class="sav-hero__status-row">
|
||||
@if (!savingsAccountData.subStatus.block) {
|
||||
<span
|
||||
class="sav-status-pill"
|
||||
[ngClass]="savingsAccountData.status.code | statusLookup"
|
||||
[matTooltip]="savingsAccountData.status.value"
|
||||
>
|
||||
<span class="sav-status-pill__dot"></span>
|
||||
{{ savingsAccountData.status.value }}
|
||||
</span>
|
||||
<!-- Account Number Row -->
|
||||
<span class="account-number-row">
|
||||
<span class="m-r-5">{{ 'labels.inputs.Account Number' | translate }} :</span>
|
||||
<span class="m-r-5">
|
||||
<mifosx-account-number
|
||||
accountNo="{{ savingsAccountData.accountNo }}"
|
||||
display="left"
|
||||
></mifosx-account-number>
|
||||
</span>
|
||||
} @else {
|
||||
<span
|
||||
class="sav-status-pill sav-status-pill--warn"
|
||||
[ngClass]="savingsAccountData.subStatus.value | statusLookup"
|
||||
[matTooltip]="savingsAccountData.subStatus.value"
|
||||
>
|
||||
<span class="sav-status-pill__dot"></span>
|
||||
{{ savingsAccountData.subStatus.value }}
|
||||
</span>
|
||||
<!-- External ID Row -->
|
||||
@if (savingsAccountData.externalId) {
|
||||
<span class="external-id-row">
|
||||
<span class="m-r-5">{{ 'labels.inputs.External Id' | translate }} :</span>
|
||||
<span>{{ savingsAccountData.externalId }}</span>
|
||||
</span>
|
||||
}
|
||||
<span class="account-overview">
|
||||
{{ 'labels.text.' + entityType | translate }} {{ 'labels.inputs.name' | translate }}:
|
||||
}
|
||||
</div>
|
||||
|
||||
<h2 class="sav-hero__title">
|
||||
<mifosx-long-text textValue="{{ savingsAccountData.savingsProductName }}"></mifosx-long-text>
|
||||
</h2>
|
||||
|
||||
<div class="sav-hero__meta">
|
||||
<span class="sav-hero__meta-item">
|
||||
<span class="sav-hero__meta-label">{{ 'labels.inputs.Account Number' | translate }}</span>
|
||||
<mifosx-account-number
|
||||
accountNo="{{ savingsAccountData.accountNo }}"
|
||||
display="left"
|
||||
></mifosx-account-number>
|
||||
</span>
|
||||
|
||||
<span class="sav-hero__meta-item">
|
||||
<span class="sav-hero__meta-label">
|
||||
{{ 'labels.text.' + entityType | translate }} {{ 'labels.inputs.name' | translate }}
|
||||
</span>
|
||||
<span class="sav-hero__meta-value">
|
||||
{{ savingsAccountData.clientName || savingsAccountData.groupName }}
|
||||
@if (savingsAccountData.clientAccountNo) {
|
||||
<span class="m-l-10">
|
||||
<mifosx-account-number accountNo="{{ savingsAccountData.clientAccountNo }}"></mifosx-account-number>
|
||||
</span>
|
||||
<mifosx-account-number accountNo="{{ savingsAccountData.clientAccountNo }}"></mifosx-account-number>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (!savingsAccountData.status.rejected && !savingsAccountData.status.submittedAndPendingApproval) {
|
||||
<div class="flex-30">
|
||||
<h3>{{ 'labels.heading.Account Overview' | translate }}</h3>
|
||||
<table class="account-overview">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Current Balance' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.accountBalance | currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'labels.inputs.Available Balance' | translate }}</td>
|
||||
<td class="r-amount">
|
||||
{{
|
||||
savingsAccountData.summary.availableBalance
|
||||
| currency: currency.code : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-5">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
aria-label="Loan account actions"
|
||||
yPosition="below"
|
||||
>
|
||||
<mat-icon matListIcon class="actions-menu">
|
||||
<fa-icon icon="bars" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-card-title>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-menu #accountMenu="matMenu">
|
||||
@for (item of buttonConfig.singleButtons; track item) {
|
||||
<button mat-menu-item *mifosxHasPermission="item.taskPermissionName" (click)="doAction(item.name)">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="{{ item.icon }}" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span> {{ 'labels.menus.' + item.name | translate }} </span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (buttonConfig.options && !savingsAccountData.subStatus.block) {
|
||||
<button mat-menu-item [matMenuTriggerFor]="More">{{ 'labels.menus.More' | translate }}</button>
|
||||
<mat-menu #More="matMenu">
|
||||
@for (option of buttonConfig.options; track option) {
|
||||
<span>
|
||||
<button mat-menu-item *mifosxHasPermission="option.taskPermissionName" (click)="doAction(option.name)">
|
||||
{{ 'labels.menus.' + option.name | translate }}
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</mat-menu>
|
||||
}
|
||||
</mat-menu>
|
||||
</mat-card-title-group>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content class="content">
|
||||
@if (isActive) {
|
||||
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./general']"
|
||||
routerLinkActive
|
||||
#general="routerLinkActive"
|
||||
[active]="general.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.General' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./transactions']"
|
||||
routerLinkActive
|
||||
#transactions="routerLinkActive"
|
||||
[active]="transactions.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Transactions' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./passbook']"
|
||||
routerLinkActive
|
||||
#passbook="routerLinkActive"
|
||||
[active]="passbook.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Passbook' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./charges']"
|
||||
routerLinkActive
|
||||
#charges="routerLinkActive"
|
||||
[active]="charges.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Charges' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./documents']"
|
||||
routerLinkActive
|
||||
#savingsDocuments="routerLinkActive"
|
||||
[active]="savingsDocuments.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Documents' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
*mifosxHasPermission="'READ_SAVINGSNOTE'"
|
||||
[routerLink]="['./notes']"
|
||||
routerLinkActive
|
||||
#notes="routerLinkActive"
|
||||
[active]="notes.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Notes' | translate }}
|
||||
</a>
|
||||
<ng-container *mifosxHasPermission="'READ_SAVINGSACCOUNTCHARGE'">
|
||||
@if (savingsAccountData.clientId) {
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./standing-instructions']"
|
||||
routerLinkActive
|
||||
#standingInstructions="routerLinkActive"
|
||||
[active]="standingInstructions.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Standing Instructions' | translate }}
|
||||
</a>
|
||||
}
|
||||
</ng-container>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nominees="routerLinkActive"
|
||||
[active]="nominees.isActive"
|
||||
>
|
||||
Nominees
|
||||
</a>
|
||||
@for (savingsDatatable of savingsDatatables; track savingsDatatable) {
|
||||
<a
|
||||
mat-tab-link
|
||||
*mifosxHasPermission="'READ_' + savingsDatatable.registeredTableName"
|
||||
[routerLink]="['./datatables', savingsDatatable.registeredTableName]"
|
||||
routerLinkActive
|
||||
#datatable="routerLinkActive"
|
||||
[active]="datatable.isActive"
|
||||
>
|
||||
{{ savingsDatatable.registeredTableName }}
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
<!-- Balance KPI strip (only for approved / active accounts) -->
|
||||
@if (!savingsAccountData.status.rejected && !savingsAccountData.status.submittedAndPendingApproval) {
|
||||
<div class="sav-hero__kpis">
|
||||
<div class="sav-kpi">
|
||||
<span class="sav-kpi__label">{{ 'labels.inputs.Current Balance' | translate }}</span>
|
||||
<span class="sav-kpi__value">
|
||||
{{ savingsAccountData.summary.accountBalance | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="sav-kpi sav-kpi--available">
|
||||
<span class="sav-kpi__label">{{ 'labels.inputs.Available Balance' | translate }}</span>
|
||||
<span class="sav-kpi__value">
|
||||
{{ savingsAccountData.summary.availableBalance | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-tab-nav-panel #tabPanel>
|
||||
<!-- Action menu (kebab) -->
|
||||
<div class="sav-hero__actions">
|
||||
<button mat-icon-button [matMenuTriggerFor]="accountMenu" aria-label="Account actions" yPosition="below">
|
||||
<fa-icon icon="ellipsis-v"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Primary actions menu (Nexora-styled). The grouped sections come
|
||||
from `kindOf(name)` in the component — Transact, Restrict (block),
|
||||
Interest, Admin, Danger. -->
|
||||
<mat-menu #accountMenu="matMenu" class="sav-action-menu" xPosition="before">
|
||||
<div class="sav-menu" (click)="$event.stopPropagation()">
|
||||
<header class="sav-menu__head">
|
||||
<fa-icon icon="bolt" class="sav-menu__head-ico"></fa-icon>
|
||||
<div>
|
||||
<div class="sav-menu__head-title">Account Actions</div>
|
||||
<div class="sav-menu__head-sub">Manage deposits, blocks, interest & more</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Cash movement (Deposit / Withdrawal / Hold Amount) -->
|
||||
<div class="sav-menu__group">
|
||||
<div class="sav-menu__group-title">Transact</div>
|
||||
@for (item of buttonConfig.singleButtons; track item.name) {
|
||||
@if (kindOf(item.name) === 'transact') {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="sav-menu__item sav-menu__item--transact"
|
||||
*mifosxHasPermission="item.taskPermissionName"
|
||||
(click)="doAction(item.name)"
|
||||
>
|
||||
<span class="sav-menu__ico">
|
||||
<fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon>
|
||||
</span>
|
||||
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Restrictions (Block / Unblock variants) -->
|
||||
<div class="sav-menu__group">
|
||||
<div class="sav-menu__group-title">Restrict</div>
|
||||
@for (item of buttonConfig.singleButtons; track item.name) {
|
||||
@if (kindOf(item.name) === 'block') {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="sav-menu__item sav-menu__item--block"
|
||||
*mifosxHasPermission="item.taskPermissionName"
|
||||
(click)="doAction(item.name)"
|
||||
>
|
||||
<span class="sav-menu__ico">
|
||||
<fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon>
|
||||
</span>
|
||||
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Interest -->
|
||||
<div class="sav-menu__group">
|
||||
<div class="sav-menu__group-title">Interest</div>
|
||||
@for (item of buttonConfig.singleButtons; track item.name) {
|
||||
@if (kindOf(item.name) === 'interest') {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="sav-menu__item sav-menu__item--interest"
|
||||
*mifosxHasPermission="item.taskPermissionName"
|
||||
(click)="doAction(item.name)"
|
||||
>
|
||||
<span class="sav-menu__ico">
|
||||
<fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon>
|
||||
</span>
|
||||
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Admin (approval/staff/charges) — drawn from BOTH primary
|
||||
buttons and the historical "More" options. -->
|
||||
@if (buttonConfig.options && !savingsAccountData.subStatus.block) {
|
||||
<div class="sav-menu__group">
|
||||
<div class="sav-menu__group-title">Admin</div>
|
||||
@for (item of buttonConfig.singleButtons; track item.name) {
|
||||
@if (kindOf(item.name) === 'admin') {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="sav-menu__item sav-menu__item--admin"
|
||||
*mifosxHasPermission="item.taskPermissionName"
|
||||
(click)="doAction(item.name)"
|
||||
>
|
||||
<span class="sav-menu__ico">
|
||||
<fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon>
|
||||
</span>
|
||||
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@for (option of buttonConfig.options; track option.name) {
|
||||
@if (kindOf(option.name) === 'admin') {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="sav-menu__item sav-menu__item--admin"
|
||||
*mifosxHasPermission="option.taskPermissionName"
|
||||
(click)="doAction(option.name)"
|
||||
>
|
||||
<span class="sav-menu__ico">
|
||||
<fa-icon [icon]="iconOf(option.name)"></fa-icon>
|
||||
</span>
|
||||
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@for (option of buttonConfig.options; track option.name) {
|
||||
@if (kindOf(option.name) === 'interest') {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="sav-menu__item sav-menu__item--interest"
|
||||
*mifosxHasPermission="option.taskPermissionName"
|
||||
(click)="doAction(option.name)"
|
||||
>
|
||||
<span class="sav-menu__ico">
|
||||
<fa-icon [icon]="iconOf(option.name)"></fa-icon>
|
||||
</span>
|
||||
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@for (option of buttonConfig.options; track option.name) {
|
||||
@if (kindOf(option.name) === 'transact') {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="sav-menu__item sav-menu__item--transact"
|
||||
*mifosxHasPermission="option.taskPermissionName"
|
||||
(click)="doAction(option.name)"
|
||||
>
|
||||
<span class="sav-menu__ico">
|
||||
<fa-icon [icon]="iconOf(option.name)"></fa-icon>
|
||||
</span>
|
||||
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Danger (Close / Reject / Delete) -->
|
||||
@if (buttonConfig.options && !savingsAccountData.subStatus.block) {
|
||||
<div class="sav-menu__group sav-menu__group--danger">
|
||||
<div class="sav-menu__group-title">Danger zone</div>
|
||||
@for (option of buttonConfig.options; track option.name) {
|
||||
@if (kindOf(option.name) === 'danger') {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="sav-menu__item sav-menu__item--danger"
|
||||
*mifosxHasPermission="option.taskPermissionName"
|
||||
(click)="doAction(option.name)"
|
||||
>
|
||||
<span class="sav-menu__ico">
|
||||
<fa-icon [icon]="iconOf(option.name)"></fa-icon>
|
||||
</span>
|
||||
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-menu>
|
||||
</header>
|
||||
|
||||
<!-- ─── Pill-style tab navigation ─────────────────────────────────── -->
|
||||
@if (isActive) {
|
||||
<nav mat-tab-nav-bar class="sav-tabs" [tabPanel]="tabPanel">
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
[routerLink]="['./general']"
|
||||
routerLinkActive
|
||||
#general="routerLinkActive"
|
||||
[active]="general.isActive"
|
||||
>
|
||||
<fa-icon icon="info-circle" class="sav-tab__ic"></fa-icon>
|
||||
<span>{{ 'labels.inputs.General' | translate }}</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
[routerLink]="['./transactions']"
|
||||
routerLinkActive
|
||||
#transactions="routerLinkActive"
|
||||
[active]="transactions.isActive"
|
||||
>
|
||||
<fa-icon icon="exchange-alt" class="sav-tab__ic"></fa-icon>
|
||||
<span>{{ 'labels.inputs.Transactions' | translate }}</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
[routerLink]="['./passbook']"
|
||||
routerLinkActive
|
||||
#passbook="routerLinkActive"
|
||||
[active]="passbook.isActive"
|
||||
>
|
||||
<fa-icon icon="book" class="sav-tab__ic"></fa-icon>
|
||||
<span>{{ 'labels.inputs.Passbook' | translate }}</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
[routerLink]="['./charges']"
|
||||
routerLinkActive
|
||||
#charges="routerLinkActive"
|
||||
[active]="charges.isActive"
|
||||
>
|
||||
<fa-icon icon="receipt" class="sav-tab__ic"></fa-icon>
|
||||
<span>{{ 'labels.inputs.Charges' | translate }}</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
[routerLink]="['./documents']"
|
||||
routerLinkActive
|
||||
#savingsDocuments="routerLinkActive"
|
||||
[active]="savingsDocuments.isActive"
|
||||
>
|
||||
<fa-icon icon="folder-open" class="sav-tab__ic"></fa-icon>
|
||||
<span>{{ 'labels.inputs.Documents' | translate }}</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
*mifosxHasPermission="'READ_SAVINGSNOTE'"
|
||||
[routerLink]="['./notes']"
|
||||
routerLinkActive
|
||||
#notes="routerLinkActive"
|
||||
[active]="notes.isActive"
|
||||
>
|
||||
<fa-icon icon="sticky-note" class="sav-tab__ic"></fa-icon>
|
||||
<span>{{ 'labels.inputs.Notes' | translate }}</span>
|
||||
</a>
|
||||
<ng-container *mifosxHasPermission="'READ_SAVINGSACCOUNTCHARGE'">
|
||||
@if (savingsAccountData.clientId) {
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
[routerLink]="['./standing-instructions']"
|
||||
routerLinkActive
|
||||
#standingInstructions="routerLinkActive"
|
||||
[active]="standingInstructions.isActive"
|
||||
>
|
||||
<fa-icon icon="calendar-check" class="sav-tab__ic"></fa-icon>
|
||||
<span>{{ 'labels.inputs.Standing Instructions' | translate }}</span>
|
||||
</a>
|
||||
}
|
||||
</ng-container>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nominees="routerLinkActive"
|
||||
[active]="nominees.isActive"
|
||||
>
|
||||
<fa-icon icon="user-shield" class="sav-tab__ic"></fa-icon>
|
||||
<span>Nominees</span>
|
||||
</a>
|
||||
@for (savingsDatatable of savingsDatatables; track savingsDatatable) {
|
||||
<a
|
||||
mat-tab-link
|
||||
class="sav-tab"
|
||||
*mifosxHasPermission="'READ_' + savingsDatatable.registeredTableName"
|
||||
[routerLink]="['./datatables', savingsDatatable.registeredTableName]"
|
||||
routerLinkActive
|
||||
#datatable="routerLinkActive"
|
||||
[active]="datatable.isActive"
|
||||
>
|
||||
<fa-icon icon="table" class="sav-tab__ic"></fa-icon>
|
||||
<span>{{ savingsDatatable.registeredTableName }}</span>
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
}
|
||||
|
||||
<mat-tab-nav-panel #tabPanel>
|
||||
<div class="sav-body">
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</mat-tab-nav-panel>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,397 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
mat-card-title {
|
||||
display: flex;
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$purple: #7c3aed;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
|
||||
.sav-view {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 20px 22px;
|
||||
}
|
||||
|
||||
.product-row,
|
||||
.account-number-row,
|
||||
.external-id-row {
|
||||
display: block;
|
||||
/* ─── Gradient hero header ──────────────────────────────────────────── */
|
||||
.sav-hero {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: stretch;
|
||||
gap: 14px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 45%, #4338ca 100%);
|
||||
box-shadow: 0 6px 18px -10px rgb(15 23 42 / 35%);
|
||||
overflow: hidden;
|
||||
|
||||
/* Subtle radial flare top-right for depth. */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top right, rgb(255 255 255 / 12%) 0%, transparent 55%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, rgb(255 255 255 / 22%) 0%, rgb(255 255 255 / 10%) 100%);
|
||||
border: 1px solid rgb(255 255 255 / 28%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 3px 10px -3px rgb(0 0 0 / 25%);
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
color: #fff;
|
||||
line-height: 1.15;
|
||||
|
||||
::ng-deep mifosx-long-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 18px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
&__meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: rgb(255 255 255 / 90%);
|
||||
}
|
||||
|
||||
&__meta-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: rgb(255 255 255 / 60%);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__meta-value {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__kpis {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
button {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-width: 32px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
color: #fff !important;
|
||||
background: rgb(255 255 255 / 10%) !important;
|
||||
border: 1px solid rgb(255 255 255 / 22%) !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
transform 0.12s ease;
|
||||
|
||||
/* Center the FA icon — mat-icon-button can offset child glyphs. */
|
||||
::ng-deep fa-icon {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Strip Material's ripple/state surface that paints a square. */
|
||||
::ng-deep .mat-mdc-button-persistent-ripple,
|
||||
::ng-deep .mat-mdc-button-touch-target {
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgb(255 255 255 / 22%) !important;
|
||||
border-color: rgb(255 255 255 / 38%) !important;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Balance KPI tiles inside the hero */
|
||||
.sav-kpi {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 130px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 9px;
|
||||
background: rgb(255 255 255 / 14%);
|
||||
border: 1px solid rgb(255 255 255 / 22%);
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&__label {
|
||||
font-size: 9.5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
font-weight: 700;
|
||||
color: rgb(255 255 255 / 75%);
|
||||
}
|
||||
|
||||
&__value {
|
||||
margin-top: 2px;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
&--available {
|
||||
background: rgb(16 185 129 / 22%);
|
||||
border-color: rgb(16 185 129 / 35%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Status pill (Active / Pending / Closed / Blocked) */
|
||||
.sav-status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 18px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: rgb(16 185 129 / 24%);
|
||||
border: 1px solid rgb(16 185 129 / 40%);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #d1fae5;
|
||||
|
||||
&__dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 0 2px rgb(16 185 129 / 30%);
|
||||
}
|
||||
|
||||
&--warn {
|
||||
background: rgb(245 158 11 / 24%);
|
||||
border-color: rgb(245 158 11 / 40%);
|
||||
color: #fef3c7;
|
||||
|
||||
.sav-status-pill__dot {
|
||||
background: #f59e0b;
|
||||
box-shadow: 0 0 0 3px rgb(245 158 11 / 30%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme variants driven by `statusLookup` pipe — keeps colour coding
|
||||
consistent with the rest of the app. */
|
||||
&.statussubmitted,
|
||||
&.statuspending {
|
||||
background: rgb(245 158 11 / 24%);
|
||||
border-color: rgb(245 158 11 / 40%);
|
||||
color: #fef3c7;
|
||||
|
||||
.sav-status-pill__dot {
|
||||
background: #f59e0b;
|
||||
}
|
||||
}
|
||||
|
||||
&.statusclosed,
|
||||
&.statusrejected,
|
||||
&.statuswithdrawn {
|
||||
background: rgb(239 68 68 / 24%);
|
||||
border-color: rgb(239 68 68 / 40%);
|
||||
color: #fee2e2;
|
||||
|
||||
.sav-status-pill__dot {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Pill-style tab navigation ─────────────────────────────────────── */
|
||||
.sav-tabs {
|
||||
margin-top: 18px;
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-link-container {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-links {
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mdc-tab-indicator__content--underline,
|
||||
.mat-mdc-tab-header-pagination-controls-enabled .mdc-tab-indicator__content--underline {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sav-tab {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 38px !important;
|
||||
min-width: 0 !important;
|
||||
padding: 0 16px !important;
|
||||
border-radius: 10px !important;
|
||||
font-family: Inter, 'DM Sans', Roboto, sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em !important;
|
||||
color: #64748b !important;
|
||||
background: transparent !important;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent !important;
|
||||
opacity: 1 !important;
|
||||
|
||||
&__ic {
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover:not(.mdc-tab--active) {
|
||||
background: #f1f5f9 !important;
|
||||
color: $navy !important;
|
||||
}
|
||||
|
||||
&.mdc-tab--active,
|
||||
&.mat-mdc-tab-link-active {
|
||||
background: linear-gradient(135deg, $blue 0%, $blue-d 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 2px 6px -1px rgb(30 64 175 / 35%);
|
||||
border-color: $blue-d !important;
|
||||
|
||||
::ng-deep .mdc-tab__text-label,
|
||||
::ng-deep .mat-mdc-tab-link-content,
|
||||
::ng-deep span,
|
||||
.sav-tab__ic {
|
||||
color: #fff !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sav-body {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* ─── Responsive ────────────────────────────────────────────────────── */
|
||||
@media (width <= 1100px) {
|
||||
.sav-hero {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
&__kpis {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.sav-kpi {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 700px) {
|
||||
.sav-view {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sav-hero {
|
||||
padding: 18px;
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
font-size: 22px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.sav-tab {
|
||||
padding: 0 12px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
/** Custom Dialogs */
|
||||
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
||||
import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
|
||||
import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component';
|
||||
import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-interest-dialog.component';
|
||||
import { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component';
|
||||
@@ -26,14 +27,6 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/** Environment Configuration */
|
||||
import { environment } from '../../../environments/environment';
|
||||
import {
|
||||
MatCard,
|
||||
MatCardHeader,
|
||||
MatCardTitleGroup,
|
||||
MatCardMdImage,
|
||||
MatCardTitle,
|
||||
MatCardContent
|
||||
} from '@angular/material/card';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { NgClass, CurrencyPipe } from '@angular/common';
|
||||
import { LongTextComponent } from '../../shared/long-text/long-text.component';
|
||||
@@ -48,6 +41,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Savings Account View Component
|
||||
*
|
||||
* Nexora redesign: gradient hero card with balance KPIs + pill-style tab
|
||||
* navigation. No more `mat-card` chrome — pure custom layout.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-savings-account-view',
|
||||
@@ -55,11 +51,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./savings-account-view.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatCardHeader,
|
||||
MatCardTitleGroup,
|
||||
MatCardMdImage,
|
||||
MatTooltip,
|
||||
MatCardTitle,
|
||||
NgClass,
|
||||
LongTextComponent,
|
||||
AccountNumberComponent,
|
||||
@@ -97,6 +89,87 @@ export class SavingsAccountViewComponent implements OnInit {
|
||||
isActive = false;
|
||||
currency: Currency;
|
||||
|
||||
/**
|
||||
* Categorisation map used by the action menu — every action name is
|
||||
* bucketed into one of: 'transact' (cash movement), 'block' (restrict
|
||||
* access), 'interest' (interest postings), 'admin' (approvals, charges,
|
||||
* staff), 'danger' (close / reject / delete). Drives the section split
|
||||
* and the colour of each icon.
|
||||
*/
|
||||
private readonly ACTION_KIND: Record<string, 'transact' | 'block' | 'interest' | 'admin' | 'danger'> = {
|
||||
Deposit: 'transact',
|
||||
Withdrawal: 'transact',
|
||||
'Hold Amount': 'transact',
|
||||
'Transfer Funds': 'transact',
|
||||
'Interbank Transfer': 'transact',
|
||||
'Block Deposit': 'block',
|
||||
'Unblock Deposit': 'block',
|
||||
'Block Withdrawal': 'block',
|
||||
'Unblock Withdrawal': 'block',
|
||||
'Block Account': 'block',
|
||||
'Unblock Account': 'block',
|
||||
'Calculate Interest': 'interest',
|
||||
'Post Interest': 'interest',
|
||||
'Post Interest As On': 'interest',
|
||||
'Enable Withhold Tax': 'interest',
|
||||
'Disable Withhold Tax': 'interest',
|
||||
'Add Charge': 'admin',
|
||||
'Apply Annual Fees': 'admin',
|
||||
'Modify Application': 'admin',
|
||||
'Assign Staff': 'admin',
|
||||
'Unassign Staff': 'admin',
|
||||
Approve: 'admin',
|
||||
Activate: 'admin',
|
||||
'Undo Approval': 'admin',
|
||||
Close: 'danger',
|
||||
Reject: 'danger',
|
||||
Delete: 'danger',
|
||||
'Withdrawn by Client': 'danger'
|
||||
};
|
||||
|
||||
/** Lightweight icon fallback for actions whose buttonConfig doesn't
|
||||
* carry an icon (the `options` sub-menu items historically didn't). */
|
||||
private readonly ACTION_ICON: Record<string, string> = {
|
||||
Deposit: 'arrow-down',
|
||||
Withdrawal: 'arrow-up',
|
||||
'Hold Amount': 'pause',
|
||||
'Transfer Funds': 'exchange-alt',
|
||||
'Interbank Transfer': 'random',
|
||||
'Block Deposit': 'lock',
|
||||
'Unblock Deposit': 'lock-open',
|
||||
'Block Withdrawal': 'lock',
|
||||
'Unblock Withdrawal': 'lock-open',
|
||||
'Block Account': 'lock',
|
||||
'Unblock Account': 'lock-open',
|
||||
'Calculate Interest': 'calculator',
|
||||
'Post Interest': 'percentage',
|
||||
'Post Interest As On': 'calendar-day',
|
||||
'Enable Withhold Tax': 'percentage',
|
||||
'Disable Withhold Tax': 'percentage',
|
||||
'Add Charge': 'plus-circle',
|
||||
'Apply Annual Fees': 'receipt',
|
||||
'Modify Application': 'edit',
|
||||
'Assign Staff': 'user-plus',
|
||||
'Unassign Staff': 'user-slash',
|
||||
Approve: 'check-circle',
|
||||
Activate: 'check',
|
||||
'Undo Approval': 'undo',
|
||||
Close: 'times-circle',
|
||||
Reject: 'ban',
|
||||
Delete: 'trash',
|
||||
'Withdrawn by Client': 'user-slash'
|
||||
};
|
||||
|
||||
/** Returns the colour kind for a given action name (used to tint the
|
||||
* icon and hover state in the dropdown). */
|
||||
kindOf(name: string): 'transact' | 'block' | 'interest' | 'admin' | 'danger' {
|
||||
return this.ACTION_KIND[name] ?? 'admin';
|
||||
}
|
||||
|
||||
iconOf(name: string, fallback?: string): string {
|
||||
return this.ACTION_ICON[name] ?? fallback ?? 'circle';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches savings account data from `resolve`
|
||||
* @param {ActivatedRoute} route Activated Route
|
||||
@@ -373,9 +446,33 @@ export class SavingsAccountViewComponent implements OnInit {
|
||||
unblockSavingsAccountDialogRef.afterClosed().subscribe((response: { confirm: any }) => {
|
||||
if (response.confirm) {
|
||||
this.savingsService.executeSavingsAccountCommand(this.savingsAccountData.id, command, {}).subscribe(() => {
|
||||
this.reload();
|
||||
this.showUnblockSuccess(action);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the shared success dialog with copy tuned to the unblock variant
|
||||
* the cashier just performed, then reloads the savings view so the
|
||||
* account substatus shows as unblocked.
|
||||
*/
|
||||
private showUnblockSuccess(action: string): void {
|
||||
let title = 'Account Unblocked Successfully';
|
||||
let message = 'Account activity has been restored.';
|
||||
if (action === 'Unblock Deposit') {
|
||||
title = 'Deposits Unblocked Successfully';
|
||||
message = 'Incoming credits to this savings account are accepted again.';
|
||||
} else if (action === 'Unblock Withdrawal') {
|
||||
title = 'Withdrawals Unblocked Successfully';
|
||||
message = 'Outgoing debits from this savings account are accepted again.';
|
||||
}
|
||||
const ref = this.dialog.open(SuccessDialogComponent, {
|
||||
width: '420px',
|
||||
maxWidth: '92vw',
|
||||
autoFocus: false,
|
||||
data: { title, message, buttonLabel: 'Done' }
|
||||
});
|
||||
ref.afterClosed().subscribe(() => this.reload());
|
||||
}
|
||||
}
|
||||
|
||||
+20
-27
@@ -6,23 +6,22 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<div class="mat-elevation-z1 m-b-25" [hidden]="!accountWithTransactions">
|
||||
<div class="layout-row align-end">
|
||||
@if (checkStatus()) {
|
||||
<div class="action-button m-b-20 layout-row gap-20px">
|
||||
<mat-checkbox [formControl]="hideReversedParam" (click)="hideReversed()" class="accruals">
|
||||
{{ 'labels.inputs.Hide Reversed' | translate }}</mat-checkbox
|
||||
>
|
||||
<mat-checkbox [formControl]="hideAccrualsParam" (click)="hideAccruals()" class="accruals">
|
||||
{{ 'labels.inputs.Hide Accruals' | translate }}</mat-checkbox
|
||||
>
|
||||
<button mat-raised-button color="primary" [routerLink]="['export']">
|
||||
{{ 'labels.buttons.Export' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="txn-tab">
|
||||
<div class="txn-tab__card" [hidden]="!accountWithTransactions">
|
||||
@if (checkStatus()) {
|
||||
<div class="txn-tab__toolbar">
|
||||
<mat-checkbox [formControl]="hideReversedParam" (click)="hideReversed()" class="txn-tab__check">
|
||||
{{ 'labels.inputs.Hide Reversed' | translate }}
|
||||
</mat-checkbox>
|
||||
<mat-checkbox [formControl]="hideAccrualsParam" (click)="hideAccruals()" class="txn-tab__check">
|
||||
{{ 'labels.inputs.Hide Accruals' | translate }}
|
||||
</mat-checkbox>
|
||||
<span class="txn-tab__spacer"></span>
|
||||
<button mat-flat-button class="txn-tab__export" [routerLink]="['export']">
|
||||
{{ 'labels.buttons.Export' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="row">
|
||||
@@ -59,13 +58,6 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="externalId">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.External Id' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let transaction" [ngClass]="savingsTransactionColor(transaction)">
|
||||
<mifosx-external-identifier externalId="{{ transaction.externalId }}"></mifosx-external-identifier>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactionType">
|
||||
<th mat-header-cell class="center" *matHeaderCellDef>{{ 'labels.inputs.Transaction Type' | translate }}</th>
|
||||
<td
|
||||
@@ -176,10 +168,11 @@
|
||||
<mat-paginator [pageSize]="50" [pageSizeOptions]="[50, 100, 200]" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<div class="alert" [hidden]="accountWithTransactions">
|
||||
<div class="message">
|
||||
<i class="fa fa-exclamation-circle alert-check"></i>
|
||||
<div class="txn-tab__empty" [hidden]="accountWithTransactions">
|
||||
<fa-icon icon="inbox" class="txn-tab__empty-ico"></fa-icon>
|
||||
<div class="txn-tab__empty-title">
|
||||
{{ 'labels.text.No transaction was found' | translate }}
|
||||
</div>
|
||||
<div class="txn-tab__empty-sub">Posted transactions will appear here.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+138
-22
@@ -6,37 +6,153 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use 'assets/styles/helper';
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
$blue: #1e40af;
|
||||
|
||||
.tab-container {
|
||||
padding: 1%;
|
||||
margin: 1%;
|
||||
.txn-tab {
|
||||
/* Wrapper hero (savings-account-view) already provides the page padding;
|
||||
this tab sits flush so the table can use the full width. */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 1% auto;
|
||||
.txn-tab__card {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ─── Toolbar ─────────────────────────────────────────────────────── */
|
||||
.txn-tab__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 12px 18px;
|
||||
background: $soft;
|
||||
border-bottom: 1px solid $border;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.txn-tab__check {
|
||||
font-size: 13px;
|
||||
color: $slate;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.txn-tab__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.txn-tab__export {
|
||||
height: 36px !important;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 13px !important;
|
||||
padding: 0 16px !important;
|
||||
background: linear-gradient(135deg, #1e88e5 0%, $blue 100%) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* ─── Table ───────────────────────────────────────────────────────── */
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::ng-deep .txn-tab__card {
|
||||
.mat-mdc-header-row {
|
||||
background: $soft;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-left: auto;
|
||||
.mat-mdc-header-cell {
|
||||
font-size: 11.5px;
|
||||
font-weight: 800;
|
||||
color: $slate;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
.mat-mdc-cell {
|
||||
font-size: 13px;
|
||||
color: $navy;
|
||||
padding: 11px 14px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.account-action-button {
|
||||
min-width: 26px;
|
||||
padding: 0 0 3px;
|
||||
margin: 0 10%;
|
||||
line-height: 25px;
|
||||
.mat-mdc-row:hover {
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accruals {
|
||||
padding-top: 1%;
|
||||
}
|
||||
}
|
||||
/* Tabular numerics on the money columns (Debit / Credit / Balance) +
|
||||
right-align them. */
|
||||
.mat-column-debit,
|
||||
.mat-column-credit,
|
||||
.mat-column-balance {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
/* Compact paginator */
|
||||
.mat-mdc-paginator {
|
||||
border-top: 1px solid $border;
|
||||
background: $soft;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
/* Row tint variants — strike-through reversed, dim accrual, blue transfer */
|
||||
.strike td {
|
||||
text-decoration: line-through;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
.accrual td {
|
||||
color: $muted;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.transfer td {
|
||||
color: #6d28d9;
|
||||
}
|
||||
}
|
||||
|
||||
.select-row:hover {
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
/* ─── Empty state ─────────────────────────────────────────────────── */
|
||||
.txn-tab__empty {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
padding: 56px 24px;
|
||||
text-align: center;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
.txn-tab__empty-ico {
|
||||
font-size: 34px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.txn-tab__empty-title {
|
||||
font-size: 14.5px;
|
||||
font-weight: 700;
|
||||
color: $slate;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.txn-tab__empty-sub {
|
||||
font-size: 12.5px;
|
||||
color: $muted;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { Currency } from 'app/shared/models/general.model';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatButton, MatIconButton } from '@angular/material/button';
|
||||
import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component';
|
||||
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
@@ -64,7 +63,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
NgClass,
|
||||
ExternalIdentifierComponent,
|
||||
MatIconButton,
|
||||
MatMenuTrigger,
|
||||
MatIcon,
|
||||
@@ -102,7 +100,6 @@ export class TransactionsTabComponent implements OnInit {
|
||||
'row',
|
||||
'id',
|
||||
'date',
|
||||
'externalId',
|
||||
'transactionType',
|
||||
'debit',
|
||||
'credit',
|
||||
|
||||
+125
-115
@@ -6,123 +6,133 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container transaction-buttons m-b-20 align-end-center gap-2percent" align="end">
|
||||
@if (
|
||||
!(
|
||||
transactionData.interestedPostedAsOn === false &&
|
||||
(transactionData.transactionType.id === 17 || transactionData.transactionType.id === 3)
|
||||
)
|
||||
) {
|
||||
<span>
|
||||
<span *mifosxHasPermission="'ADJUSTTRANSACTION_SAVINGSACCOUNT'">
|
||||
@if (
|
||||
transactionData.transactionType.value === 'Transfer' ||
|
||||
transactionData.reversed === 'true' ||
|
||||
transactionData.transactionType.id === 3 ||
|
||||
transactionData.transactionType.id === 17
|
||||
) {
|
||||
<button mat-raised-button color="primary" [routerLink]="'edit'">
|
||||
<fa-icon icon="edit" class="m-r-10"></fa-icon>{{ 'labels.buttons.Edit' | translate }}
|
||||
</button>
|
||||
}
|
||||
<div class="txn">
|
||||
<!-- ─── Compact toolbar strip ─────────────────────────────────────── -->
|
||||
<div class="txn__toolbar">
|
||||
<button mat-stroked-button class="txn__back" type="button" [routerLink]="['../../']" matTooltip="Back">
|
||||
<fa-icon icon="arrow-left"></fa-icon>
|
||||
</button>
|
||||
|
||||
<div class="txn__title-block">
|
||||
<span class="txn__id">Transaction #{{ transactionData.id }}</span>
|
||||
<span class="txn__type">{{ transactionData.transactionType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
|
||||
@if (transactionData.reversed) {
|
||||
<span class="txn__pill txn__pill--reversed"><span class="txn__pill-dot"></span> Reversed</span>
|
||||
} @else if (transactionData.transactionType.accrual) {
|
||||
<span class="txn__pill txn__pill--accrual"><span class="txn__pill-dot"></span> Accrual</span>
|
||||
} @else if (transactionData.transfer) {
|
||||
<span class="txn__pill txn__pill--transfer"><span class="txn__pill-dot"></span> Transfer</span>
|
||||
} @else {
|
||||
<span class="txn__pill"><span class="txn__pill-dot"></span> Posted</span>
|
||||
}
|
||||
|
||||
<div class="txn__amount" [class.txn__amount--debit]="isDebit()">
|
||||
<span class="txn__amount-label">{{ isDebit() ? 'Debit' : 'Credit' }}</span>
|
||||
<span class="txn__amount-value">
|
||||
{{ isDebit() ? '−' : '+' }}
|
||||
{{ transactionData.amount | currency: transactionData.currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
@if (!transactionData.reversed && transactionData.transactionType.amountHold) {
|
||||
<span>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
*mifosxHasPermission="'RELEASEAMOUNT_SAVINGSACCOUNT'"
|
||||
(click)="releaseAmount()"
|
||||
>
|
||||
<fa-icon icon="lock-open" class="m-r-10"></fa-icon>{{ 'labels.buttons.Release Amount' | translate }}
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
@if (allowUndo()) {
|
||||
<span>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="warn"
|
||||
*mifosxHasPermission="'UNDOTRANSACTION_SAVINGSACCOUNT'"
|
||||
(click)="undoTransaction()"
|
||||
>
|
||||
<fa-icon icon="undo" class="m-r-10"></fa-icon>{{ 'labels.buttons.Undo' | translate }}
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="layout-row-wrap card-content">
|
||||
<div class="flex-100" [ngClass]="transactionColor()"></div>
|
||||
|
||||
<div class="flex-50 mat-body-strong">
|
||||
{{ 'labels.inputs.Id' | translate }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50">
|
||||
{{ transactionData.id }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50 mat-body-strong">
|
||||
{{ 'labels.inputs.Transaction Type' | translate }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50">
|
||||
{{ transactionData.transactionType.value | translateKey: 'catalogs' }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50 mat-body-strong">
|
||||
{{ 'labels.inputs.Transaction Date' | translate }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50">
|
||||
{{ transactionData.date | dateFormat }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50 mat-body-strong">
|
||||
{{ 'labels.inputs.Currency' | translate }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50">
|
||||
{{ transactionData.currency.displayLabel }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50 mat-body-strong">
|
||||
{{ 'labels.inputs.Amount' | translate }}
|
||||
</div>
|
||||
|
||||
<div class="flex-50">
|
||||
{{ transactionData.amount | currency: transactionData.currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</div>
|
||||
|
||||
@if (transactionData.note) {
|
||||
<div class="flex-50 mat-body-strong">
|
||||
{{ 'labels.inputs.Note' | translate }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (transactionData.note) {
|
||||
<div class="flex-50">
|
||||
{{ transactionData.note }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (transactionData.paymentDetailData) {
|
||||
<mifosx-transaction-payment-detail [paymentDetailData]="transactionData.paymentDetailData">
|
||||
</mifosx-transaction-payment-detail>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="layout-row layout-align-center gap-2percent column-on-mobile">
|
||||
<button type="button" mat-raised-button color="primary" [routerLink]="['../../']">
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
<div class="txn__actions">
|
||||
@if (
|
||||
!(
|
||||
transactionData.interestedPostedAsOn === false &&
|
||||
(transactionData.transactionType.id === 17 || transactionData.transactionType.id === 3)
|
||||
)
|
||||
) {
|
||||
<span *mifosxHasPermission="'ADJUSTTRANSACTION_SAVINGSACCOUNT'">
|
||||
@if (
|
||||
transactionData.transactionType.value === 'Transfer' ||
|
||||
transactionData.reversed === 'true' ||
|
||||
transactionData.transactionType.id === 3 ||
|
||||
transactionData.transactionType.id === 17
|
||||
) {
|
||||
<button mat-stroked-button class="txn__btn" [routerLink]="'edit'">
|
||||
<fa-icon icon="edit" class="m-r-6"></fa-icon>{{ 'labels.buttons.Edit' | translate }}
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@if (!transactionData.reversed && transactionData.transactionType.amountHold) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="txn__btn"
|
||||
*mifosxHasPermission="'RELEASEAMOUNT_SAVINGSACCOUNT'"
|
||||
(click)="releaseAmount()"
|
||||
>
|
||||
<fa-icon icon="lock-open" class="m-r-6"></fa-icon>{{ 'labels.buttons.Release Amount' | translate }}
|
||||
</button>
|
||||
}
|
||||
@if (allowUndo()) {
|
||||
<button
|
||||
mat-flat-button
|
||||
class="txn__btn txn__btn--undo"
|
||||
*mifosxHasPermission="'UNDOTRANSACTION_SAVINGSACCOUNT'"
|
||||
(click)="undoTransaction()"
|
||||
>
|
||||
<fa-icon icon="undo" class="m-r-6"></fa-icon>{{ 'labels.buttons.Undo' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Details panel ─────────────────────────────────────────────── -->
|
||||
<section class="txn__panel">
|
||||
<div class="txn__panel-grid">
|
||||
<div class="txn__cell">
|
||||
<span class="txn__cell-label">{{ 'labels.inputs.Id' | translate }}</span>
|
||||
<span class="txn__cell-value txn__cell-value--mono">{{ transactionData.id }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@if (accountNo) {
|
||||
<div class="txn__cell">
|
||||
<span class="txn__cell-label">Account Number</span>
|
||||
<span class="txn__cell-value txn__cell-value--mono">{{ accountNo }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (clientName) {
|
||||
<div class="txn__cell">
|
||||
<span class="txn__cell-label">Client Name</span>
|
||||
<span class="txn__cell-value">{{ clientName }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="txn__cell">
|
||||
<span class="txn__cell-label">{{ 'labels.inputs.Transaction Type' | translate }}</span>
|
||||
<span class="txn__cell-value">{{ transactionData.transactionType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
<div class="txn__cell">
|
||||
<span class="txn__cell-label">{{ 'labels.inputs.Transaction Date' | translate }}</span>
|
||||
<span class="txn__cell-value">{{ transactionData.date | dateFormat }}</span>
|
||||
</div>
|
||||
<div class="txn__cell">
|
||||
<span class="txn__cell-label">{{ 'labels.inputs.Currency' | translate }}</span>
|
||||
<span class="txn__cell-value">{{ transactionData.currency.displayLabel }}</span>
|
||||
</div>
|
||||
<div class="txn__cell">
|
||||
<span class="txn__cell-label">{{ 'labels.inputs.Amount' | translate }}</span>
|
||||
<span
|
||||
class="txn__cell-value txn__cell-value--amount"
|
||||
[class.txn__cell-value--negative]="isDebit()"
|
||||
[class.txn__cell-value--positive]="!isDebit()"
|
||||
>
|
||||
{{ transactionData.amount | currency: transactionData.currency.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
@if (transactionData.note) {
|
||||
<div class="txn__cell txn__cell--wide">
|
||||
<span class="txn__cell-label">{{ 'labels.inputs.Note' | translate }}</span>
|
||||
<span class="txn__cell-value">{{ transactionData.note }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (transactionData.paymentDetailData) {
|
||||
<div class="txn__payment-detail">
|
||||
<mifosx-transaction-payment-detail [paymentDetailData]="transactionData.paymentDetailData">
|
||||
</mifosx-transaction-payment-detail>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+285
-22
@@ -6,40 +6,303 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use 'assets/styles/helper';
|
||||
@use 'assets/styles/colours' as *;
|
||||
/* Compact, banking-grade transaction view: single toolbar strip on top,
|
||||
single wide grid panel below. Designed to fit on one screen on a normal
|
||||
desktop without scrolling, no big gradient hero, no centered narrow
|
||||
column — the layout uses full available width. */
|
||||
|
||||
.container {
|
||||
max-width: 37rem;
|
||||
$navy: #0d1b2a;
|
||||
$slate: #475569;
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$soft: #f8fafc;
|
||||
$blue: #1e40af;
|
||||
$green: #047857;
|
||||
$red: #b91c1c;
|
||||
$amber: #b45309;
|
||||
$purple: #6d28d9;
|
||||
|
||||
.card-content {
|
||||
div {
|
||||
margin: 1rem 0;
|
||||
word-wrap: break-word;
|
||||
.txn {
|
||||
width: 100%;
|
||||
padding: 12px 16px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ─── Toolbar (back · title · status · amount · actions) ──────────── */
|
||||
.txn__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.txn__back {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: 36px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 8px !important;
|
||||
color: $slate !important;
|
||||
border-color: $border !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.txn__title-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.txn__id {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.txn__type {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: $muted;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.txn__pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 22px;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
background: #ecfdf5;
|
||||
color: $green;
|
||||
border: 1px solid #a7f3d0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
|
||||
&-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: $green;
|
||||
}
|
||||
|
||||
&--reversed {
|
||||
background: #fef2f2;
|
||||
color: $red;
|
||||
border-color: #fecaca;
|
||||
|
||||
.txn__pill-dot {
|
||||
background: $red;
|
||||
}
|
||||
}
|
||||
|
||||
mifosx-transaction-payment-detail {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
&--accrual {
|
||||
background: #fffbeb;
|
||||
color: $amber;
|
||||
border-color: #fde68a;
|
||||
|
||||
.txn__pill-dot {
|
||||
background: $amber;
|
||||
}
|
||||
}
|
||||
|
||||
&--transfer {
|
||||
background: #f5f3ff;
|
||||
color: $purple;
|
||||
border-color: #ddd6fe;
|
||||
|
||||
.txn__pill-dot {
|
||||
background: $purple;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-buttons {
|
||||
margin-top: 3%;
|
||||
/* Amount mini-tile inside the toolbar — Credit (green) / Debit (red). */
|
||||
.txn__amount {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-left: auto;
|
||||
padding: 6px 14px;
|
||||
border-radius: 10px;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
|
||||
&--debit {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
|
||||
.txn__amount-value {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.txn__amount-label {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: $status-active;
|
||||
height: 4px;
|
||||
.txn__amount-label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: $green;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.linked {
|
||||
background-color: $status-approved;
|
||||
height: 4px;
|
||||
.txn__amount-value {
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: $green;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.undo {
|
||||
background-color: $status-active-overdue;
|
||||
height: 4px;
|
||||
.txn__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.txn__btn {
|
||||
height: 36px !important;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 13px !important;
|
||||
padding: 0 14px !important;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.txn__btn--undo {
|
||||
background: $red !important;
|
||||
color: #fff !important;
|
||||
|
||||
&:hover {
|
||||
background: #991b1b !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Wide details panel ──────────────────────────────────────────── */
|
||||
.txn__panel {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
/* Multi-column grid — each cell is one label/value pair. Uses the full
|
||||
panel width so wide screens get more columns side-by-side. */
|
||||
.txn__panel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 4px 24px;
|
||||
}
|
||||
|
||||
.txn__cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dashed #eef2f7;
|
||||
font-size: 13px;
|
||||
min-width: 0;
|
||||
|
||||
&--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.txn__cell-label {
|
||||
color: $slate;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.txn__cell-value {
|
||||
color: $navy;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
|
||||
&--mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12.5px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
&--amount {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
&--positive {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
&--negative {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
.txn__payment-detail {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
::ng-deep mat-card,
|
||||
::ng-deep .mat-mdc-card {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* ─── Responsive ──────────────────────────────────────────────────── */
|
||||
@media (width <= 900px) {
|
||||
.txn__toolbar {
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.txn__amount {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.txn__title-block {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.txn__actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
|
||||
.txn__btn {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -45,11 +45,36 @@ export class SavingsTransactionGeneralTabComponent {
|
||||
transactionId: string;
|
||||
transactionData: any;
|
||||
|
||||
/** Parent savings account context — fetched on init so we can show the
|
||||
* account number and client/group name in the hero. */
|
||||
accountNo: string | null = null;
|
||||
clientName: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { savingsAccountTransaction: any }) => {
|
||||
this.accountId = this.route.parent.snapshot.params['savingAccountId'];
|
||||
this.transactionData = data.savingsAccountTransaction;
|
||||
});
|
||||
if (this.accountId) {
|
||||
this.savingsService.getSavingsAccountData(this.accountId).subscribe({
|
||||
next: (account: any) => {
|
||||
this.accountNo = account?.accountNo || null;
|
||||
this.clientName = account?.clientName || account?.groupName || null;
|
||||
},
|
||||
error: () => {
|
||||
this.accountNo = null;
|
||||
this.clientName = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true for transactions that move money out (withdrawal, fees,
|
||||
* overdraft interest, withhold-tax). Used to colour the Amount chip. */
|
||||
isDebit(): boolean {
|
||||
const t = this.transactionData?.transactionType;
|
||||
if (!t) return false;
|
||||
return t.withdrawal === true || t.feeDeduction === true || t.overdraftInterest === true || t.withholdTax === true;
|
||||
}
|
||||
|
||||
allowUndo(): boolean {
|
||||
|
||||
@@ -220,6 +220,12 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (selfSelectError) {
|
||||
<div class="nom-conflict-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
{{ selfSelectError }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -355,6 +361,12 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (guardianSelfSelectError) {
|
||||
<div class="nom-conflict-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
{{ guardianSelfSelectError }}
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<span class="nom-guardian-selected" [class.nom-guardian-selected--error]="guardianSameAsNominee"
|
||||
>{{ selectedGuardian.displayName }}
|
||||
|
||||
@@ -90,6 +90,11 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
editShare: number | null = null;
|
||||
editNomType: string = 'DA1';
|
||||
|
||||
/** Inline error shown when the user picks themselves as the nominee. */
|
||||
selfSelectError: string | null = null;
|
||||
/** Inline error shown when the user picks the host client as guardian. */
|
||||
guardianSelfSelectError: string | null = null;
|
||||
|
||||
get totalShare(): number {
|
||||
return this.nominees.reduce((sum, n) => sum + (n.percentageShare || 0), 0);
|
||||
}
|
||||
@@ -169,8 +174,15 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** True when the given client id is the same as the savings account's
|
||||
* host client — used by the search to refuse self-add as nominee. */
|
||||
isSelfClient(clientId: any): boolean {
|
||||
return clientId != null && this.clientId != null && String(clientId) === String(this.clientId);
|
||||
}
|
||||
|
||||
onSearch(text: string): void {
|
||||
this.searchText = text;
|
||||
this.selfSelectError = null;
|
||||
clearTimeout(this.searchTimer);
|
||||
if (!text || text.length < 2) {
|
||||
this.searchResults = [];
|
||||
@@ -180,7 +192,10 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({
|
||||
next: (data: any) => {
|
||||
this.searchResults = data?.content || [];
|
||||
const raw: any[] = data?.content || [];
|
||||
// Drop the host client from the result list — a client cannot be
|
||||
// their own nominee.
|
||||
this.searchResults = raw.filter((c: any) => !this.isSelfClient(c.id));
|
||||
this.searching = false;
|
||||
this.clientsService.enrichSearchResults(this.searchResults);
|
||||
},
|
||||
@@ -200,6 +215,16 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
}
|
||||
|
||||
selectClient(client: any): void {
|
||||
if (this.isSelfClient(client?.id)) {
|
||||
// Belt-and-braces — even if a stale result sneaks through, refuse
|
||||
// the selection and surface an inline message.
|
||||
this.selfSelectError =
|
||||
'You cannot add the account holder as their own nominee. Please choose a different client.';
|
||||
this.searchResults = [];
|
||||
this.searchText = '';
|
||||
return;
|
||||
}
|
||||
this.selfSelectError = null;
|
||||
this.selectedClient = client;
|
||||
this.searchResults = [];
|
||||
this.searchText = '';
|
||||
@@ -281,6 +306,7 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
|
||||
onGuardianSearch(text: string): void {
|
||||
this.guardianSearchText = text;
|
||||
this.guardianSelfSelectError = null;
|
||||
clearTimeout(this.guardianSearchTimer);
|
||||
if (!text || text.length < 2) {
|
||||
this.guardianSearchResults = [];
|
||||
@@ -290,7 +316,10 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
this.guardianSearchTimer = setTimeout(() => {
|
||||
this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({
|
||||
next: (data: any) => {
|
||||
this.guardianSearchResults = data?.content || [];
|
||||
const raw: any[] = data?.content || [];
|
||||
// Drop the host client from the guardian results too — a client
|
||||
// can't be their own minor-nominee's guardian.
|
||||
this.guardianSearchResults = raw.filter((c: any) => !this.isSelfClient(c.id));
|
||||
this.guardianSearching = false;
|
||||
this.clientsService.enrichSearchResults(this.guardianSearchResults);
|
||||
},
|
||||
@@ -304,6 +333,14 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
|
||||
selectGuardian(client: any): void {
|
||||
if (this.selectedClient && client.id === this.selectedClient.id) return;
|
||||
if (this.isSelfClient(client?.id)) {
|
||||
this.guardianSelfSelectError =
|
||||
'The account holder cannot be selected as the guardian. Please choose a different client.';
|
||||
this.guardianSearchResults = [];
|
||||
this.guardianSearchText = '';
|
||||
return;
|
||||
}
|
||||
this.guardianSelfSelectError = null;
|
||||
this.selectedGuardian = client;
|
||||
this.guardianSearchResults = [];
|
||||
this.guardianSearchText = '';
|
||||
|
||||
@@ -221,7 +221,12 @@ import {
|
||||
faSignature,
|
||||
faPercentage,
|
||||
faCompressAlt,
|
||||
faExpandAlt
|
||||
faExpandAlt,
|
||||
faUserCheck,
|
||||
faUsersCog,
|
||||
faUserCog,
|
||||
faServer,
|
||||
faBox
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
/**
|
||||
@@ -449,7 +454,12 @@ export class IconsModule {
|
||||
faSignature,
|
||||
faPercentage,
|
||||
faCompressAlt,
|
||||
faExpandAlt
|
||||
faExpandAlt,
|
||||
faUserCheck,
|
||||
faUsersCog,
|
||||
faUserCog,
|
||||
faServer,
|
||||
faBox
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+44
-35
@@ -7,40 +7,45 @@
|
||||
-->
|
||||
|
||||
<div class="cv-tab-content">
|
||||
<div class="cv-action-bar">
|
||||
<mat-form-field appearance="outline" class="cv-search-field">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'labels.text.Search by user' | translate }}"
|
||||
(keyup)="applyFilter($event.target.value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="toggleAdvancedSearch()"
|
||||
class="cv-toggle-btn"
|
||||
[attr.aria-label]="
|
||||
showAdvancedSearch
|
||||
? ('labels.buttons.Hide advanced search' | translate)
|
||||
: ('labels.buttons.Show advanced search' | translate)
|
||||
"
|
||||
[attr.aria-expanded]="showAdvancedSearch"
|
||||
>
|
||||
<mat-icon>{{ showAdvancedSearch ? 'arrow_drop_up' : 'arrow_drop_down' }}</mat-icon>
|
||||
</button>
|
||||
<div class="cv-action-buttons">
|
||||
<button mat-stroked-button class="cv-btn cv-btn-approve" (click)="approveChecker()">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>{{ 'labels.buttons.Approve' | translate }}
|
||||
</button>
|
||||
<button mat-stroked-button class="cv-btn cv-btn-delete" (click)="deleteChecker()">
|
||||
<fa-icon icon="trash" class="m-r-10"></fa-icon>{{ 'labels.buttons.Delete' | translate }}
|
||||
</button>
|
||||
<button mat-stroked-button class="cv-btn cv-btn-reject" (click)="rejectChecker()">
|
||||
<fa-icon icon="times" class="m-r-10"></fa-icon>{{ 'labels.buttons.Reject' | translate }}
|
||||
<!-- Action bar (search + advanced toggle + Approve/Delete/Reject) — only
|
||||
rendered when there's actual data to work with. An empty inbox shows
|
||||
the friendly empty-state block below without these controls. -->
|
||||
@if (checkerData) {
|
||||
<div class="cv-action-bar">
|
||||
<mat-form-field appearance="outline" class="cv-search-field">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'labels.text.Search by user' | translate }}"
|
||||
(keyup)="applyFilter($event.target.value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="toggleAdvancedSearch()"
|
||||
class="cv-toggle-btn"
|
||||
[attr.aria-label]="
|
||||
showAdvancedSearch
|
||||
? ('labels.buttons.Hide advanced search' | translate)
|
||||
: ('labels.buttons.Show advanced search' | translate)
|
||||
"
|
||||
[attr.aria-expanded]="showAdvancedSearch"
|
||||
>
|
||||
<mat-icon>{{ showAdvancedSearch ? 'arrow_drop_up' : 'arrow_drop_down' }}</mat-icon>
|
||||
</button>
|
||||
<div class="cv-action-buttons">
|
||||
<button mat-stroked-button class="cv-btn cv-btn-approve" (click)="approveChecker()">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>{{ 'labels.buttons.Approve' | translate }}
|
||||
</button>
|
||||
<button mat-stroked-button class="cv-btn cv-btn-delete" (click)="deleteChecker()">
|
||||
<fa-icon icon="trash" class="m-r-10"></fa-icon>{{ 'labels.buttons.Delete' | translate }}
|
||||
</button>
|
||||
<button mat-stroked-button class="cv-btn cv-btn-reject" (click)="rejectChecker()">
|
||||
<fa-icon icon="times" class="m-r-10"></fa-icon>{{ 'labels.buttons.Reject' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showAdvancedSearch) {
|
||||
<form [formGroup]="makerCheckerSearchForm">
|
||||
@@ -161,16 +166,20 @@
|
||||
}
|
||||
|
||||
@if (checkerData && noSearchedData) {
|
||||
<div class="empty-state">
|
||||
<div class="empty-state empty-state--filter">
|
||||
<i class="fas fa-inbox empty-icon"></i>
|
||||
<p class="empty-text">{{ 'labels.text.No checker inbox data available for this search' | translate }}</p>
|
||||
<p class="empty-title">No matches</p>
|
||||
<p class="empty-text">No checker inbox items match the current search. Try clearing the filters.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!checkerData) {
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox empty-icon"></i>
|
||||
<p class="empty-text">{{ 'labels.text.No checker inbox data available for this account' | translate }}</p>
|
||||
<p class="empty-title">Inbox is empty</p>
|
||||
<p class="empty-text">
|
||||
Currently no items in the Checker Inbox. New maker actions awaiting review will appear here.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -210,3 +210,44 @@ $text-muted: #64748b;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty state — shown when the inbox has no items at all (search/action bar
|
||||
is hidden in that case) or when the current search has no matches. */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 24px;
|
||||
background: #fff;
|
||||
border: 1px dashed #e2e8f0;
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 42px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
max-width: 380px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&--filter {
|
||||
background: #f8fafc;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,66 +6,81 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<div class="table-header">
|
||||
<div class="table-title-group">
|
||||
<h2 class="table-title">
|
||||
<span class="t-logomark"><span></span><span></span><span></span><span></span></span>
|
||||
{{ 'labels.heading.Pending Tasks' | translate }}
|
||||
</h2>
|
||||
<div class="ac-container">
|
||||
<!-- Hero header -->
|
||||
<div class="ac-header">
|
||||
<div class="ac-header__left">
|
||||
<fa-icon icon="check-double" class="ac-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="ac-header__title">Approval Center</h2>
|
||||
<div class="ac-header__sub">
|
||||
Review pending checker actions, client & loan approvals, disbursals, and reschedules.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav mat-tab-nav-bar class="cv-tab-nav" [tabPanel]="tabPanel">
|
||||
<!-- Pill-style tab navigation -->
|
||||
<nav mat-tab-nav-bar class="ac-tabs" [tabPanel]="tabPanel">
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
[routerLink]="['./checker-inbox']"
|
||||
routerLinkActive
|
||||
#checkerInbox="routerLinkActive"
|
||||
[active]="checkerInbox.isActive"
|
||||
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
|
||||
>
|
||||
<i class="fas fa-inbox cv-tab-icon"></i> {{ 'labels.inputs.Checker Inbox' | translate }}
|
||||
<fa-icon icon="inbox" class="ac-tab__ic"></fa-icon>
|
||||
<span>Checker Inbox</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
[routerLink]="['./client-approval']"
|
||||
routerLinkActive
|
||||
#clientApproval="routerLinkActive"
|
||||
[active]="clientApproval.isActive"
|
||||
*mifosxHasPermission="'READ_CLIENT'"
|
||||
>
|
||||
<i class="fas fa-user-check cv-tab-icon"></i> {{ 'labels.inputs.Client Approval' | translate }}
|
||||
<fa-icon icon="user-check" class="ac-tab__ic"></fa-icon>
|
||||
<span>Client Approval</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
[routerLink]="['./loan-approval']"
|
||||
routerLinkActive
|
||||
#loanApproval="routerLinkActive"
|
||||
[active]="loanApproval.isActive"
|
||||
*mifosxHasPermission="'READ_LOAN'"
|
||||
>
|
||||
<i class="fas fa-hand-holding-usd cv-tab-icon"></i> {{ 'labels.inputs.Loan Approval' | translate }}
|
||||
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
|
||||
<span>Loan Approval</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
[routerLink]="['./loan-disbursal']"
|
||||
routerLinkActive
|
||||
#loanDisbursal="routerLinkActive"
|
||||
[active]="loanDisbursal.isActive"
|
||||
*mifosxHasPermission="'READ_LOAN'"
|
||||
>
|
||||
<i class="fas fa-money-bill-wave cv-tab-icon"></i> {{ 'labels.inputs.Loan Disbursal' | translate }}
|
||||
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
|
||||
<span>Loan Disbursal</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
[routerLink]="['./reschedule-loan']"
|
||||
routerLinkActive
|
||||
#rescheduleLoan="routerLinkActive"
|
||||
[active]="rescheduleLoan.isActive"
|
||||
*mifosxHasPermission="'RESCHEDULE_LOAN'"
|
||||
>
|
||||
<i class="fas fa-calendar-alt cv-tab-icon"></i> {{ 'labels.inputs.Reschedule Loan' | translate }}
|
||||
<fa-icon icon="calendar-alt" class="ac-tab__ic"></fa-icon>
|
||||
<span>Reschedule Loan</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -8,24 +8,151 @@
|
||||
|
||||
@use '../../shared/styles/list-layout' as *;
|
||||
|
||||
.cv-tab-icon {
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$purple: #7c3aed;
|
||||
$green: #10b981;
|
||||
$amber: #f59e0b;
|
||||
|
||||
.ac-container {
|
||||
padding: 20px 24px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cv-tab-nav {
|
||||
.ac-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, $blue 0%, $purple 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ───────── Pill-style tab navigation ───────── */
|
||||
.ac-tabs {
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-link {
|
||||
font-family: 'DM Sans', Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
min-width: 0;
|
||||
padding: 0 16px;
|
||||
height: 40px;
|
||||
.mat-mdc-tab-link-container {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-links {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Material draws the active-tab underline indicator with a 2px height —
|
||||
hide it; our pill background already signals the active tab. */
|
||||
.mdc-tab-indicator__content--underline,
|
||||
.mat-mdc-tab-header-pagination-controls-enabled .mdc-tab-indicator__content--underline {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ac-tab {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 38px !important;
|
||||
min-width: 0 !important;
|
||||
padding: 0 18px !important;
|
||||
border-radius: 10px !important;
|
||||
font-family: Inter, 'DM Sans', Roboto, sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em !important;
|
||||
color: #64748b !important;
|
||||
background: transparent !important;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent !important;
|
||||
opacity: 1 !important;
|
||||
|
||||
&__ic {
|
||||
font-size: 13px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover:not(.mdc-tab--active) {
|
||||
background: #f1f5f9 !important;
|
||||
color: $navy !important;
|
||||
}
|
||||
|
||||
/* Material 3 adds `.mdc-tab--active` (and `.mat-mdc-tab-link-active`) to the
|
||||
selected link. Both target the same pill state — bold blue gradient. */
|
||||
&.mdc-tab--active,
|
||||
&.mat-mdc-tab-link-active {
|
||||
background: linear-gradient(135deg, $blue 0%, $blue-d 100%) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 2px 6px -1px rgb(30 64 175 / 35%);
|
||||
border-color: $blue-d !important;
|
||||
|
||||
/* Material 3 wraps the label in its own span with a color rule that
|
||||
beats the parent. Force white on every descendant so the active-tab
|
||||
label is always legible against the dark gradient pill. */
|
||||
::ng-deep .mdc-tab__text-label,
|
||||
::ng-deep .mat-mdc-tab-link-content,
|
||||
::ng-deep span,
|
||||
.ac-tab__ic {
|
||||
color: #fff !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.ac-tabs {
|
||||
overflow-x: auto;
|
||||
|
||||
::ng-deep .mat-mdc-tab-links {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ac-tab {
|
||||
padding: 0 14px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
|
||||
import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Checker Inbox and Tasks Component
|
||||
* Approval Center — checker inbox + client/loan approval workflows.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-checker-inbox-and-tasks',
|
||||
@@ -25,7 +26,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatTabLink,
|
||||
RouterLinkActive,
|
||||
MatTabNavPanel,
|
||||
RouterOutlet
|
||||
RouterOutlet,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class CheckerInboxAndTasksComponent {
|
||||
|
||||
+244
@@ -80,6 +80,23 @@
|
||||
min-width: 1040px;
|
||||
}
|
||||
|
||||
/* Admin Mega Menu — 3-column rectangular panel (Setup · Catalog ·
|
||||
Audit & Compliance). Reuses the deposit-mega-menu grid + group chrome
|
||||
and retints the group-title accent to Admin purple. */
|
||||
.admin-mega-panel {
|
||||
max-width: 760px !important;
|
||||
}
|
||||
|
||||
.admin-grid {
|
||||
grid-template-columns: repeat(3, minmax(220px, 1fr)) !important;
|
||||
min-width: 680px;
|
||||
|
||||
.deposit-group-title {
|
||||
color: #7c3aed !important;
|
||||
border-bottom-color: #7c3aed !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Material Select Panel Styles (Material Design 3) =====
|
||||
.mat-mdc-select-panel {
|
||||
background: var(--md-sys-color-surface-container, #fff);
|
||||
@@ -1026,6 +1043,233 @@ mat-card {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* ─── Savings Account Actions Menu ───
|
||||
Modern Nexora dropdown for the kebab on the savings account view hero.
|
||||
Groups actions semantically (Transact · Restrict · Interest · Admin ·
|
||||
Danger zone) and colour-codes each row's icon by category. */
|
||||
.sav-action-menu.mat-mdc-menu-panel {
|
||||
min-width: 280px !important;
|
||||
max-width: 320px !important;
|
||||
border-radius: 14px !important;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow:
|
||||
0 12px 32px -8px rgb(15 23 42 / 18%),
|
||||
0 4px 12px -4px rgb(15 23 42 / 10%) !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sav-action-menu .mat-mdc-menu-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.sav-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: Inter, 'DM Sans', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.sav-menu__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 45%, #4338ca 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sav-menu__head-ico {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 28%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sav-menu__head-title {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.sav-menu__head-sub {
|
||||
margin-top: 1px;
|
||||
font-size: 10.5px;
|
||||
color: rgb(255 255 255 / 78%);
|
||||
}
|
||||
|
||||
.sav-menu__group {
|
||||
padding: 6px 0;
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: #fefafa;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sav-menu__group-title {
|
||||
font-size: 9.5px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #94a3b8;
|
||||
padding: 4px 14px 2px;
|
||||
}
|
||||
|
||||
.sav-menu__item.mat-mdc-menu-item {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 38px !important;
|
||||
height: 38px !important;
|
||||
padding: 0 14px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 500 !important;
|
||||
color: #0d1b2a !important;
|
||||
background: transparent !important;
|
||||
transition: background 0.12s ease;
|
||||
|
||||
/* Hide Material's default trailing chevron/state container so our
|
||||
icon + label are the only visual elements. */
|
||||
.mat-mdc-menu-item-text {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sav-menu__ico {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11.5px;
|
||||
flex-shrink: 0;
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
transform 0.12s ease,
|
||||
background 0.12s ease,
|
||||
color 0.12s ease;
|
||||
}
|
||||
|
||||
.sav-menu__label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Hover lift on the icon */
|
||||
.sav-menu__item:hover .sav-menu__ico {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
/* Variant tints */
|
||||
.sav-menu__item--transact .sav-menu__ico {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.sav-menu__item--block .sav-menu__ico {
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.sav-menu__item--interest .sav-menu__ico {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.sav-menu__item--admin .sav-menu__ico {
|
||||
background: #f5f3ff;
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
.sav-menu__item--danger.mat-mdc-menu-item {
|
||||
color: #991b1b !important;
|
||||
}
|
||||
|
||||
.sav-menu__item--danger .sav-menu__ico {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.sav-menu__item--danger:hover {
|
||||
background: #fef2f2 !important;
|
||||
}
|
||||
|
||||
/* ─── Identity-Verify Dialog Panel ───
|
||||
Strip Material's default surface padding so the teal gradient header
|
||||
on the Withdraw "Verify Customer Identity" pop-up reaches edge-to-edge
|
||||
(no white gutter around the gradient). */
|
||||
.idv-panel {
|
||||
.mat-mdc-dialog-container {
|
||||
padding: 0 !important;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-surface {
|
||||
padding: 0 !important;
|
||||
border-radius: 14px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-content {
|
||||
padding: 0 !important;
|
||||
max-height: 72vh;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-actions {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Transaction-Confirm Dialog Panel ───
|
||||
Same flush surface treatment for the Deposit/Withdraw confirmation. */
|
||||
.tcd-panel {
|
||||
.mat-mdc-dialog-container {
|
||||
padding: 0 !important;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-surface {
|
||||
padding: 0 !important;
|
||||
border-radius: 14px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-content {
|
||||
padding: 0 !important;
|
||||
max-height: 72vh;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-actions {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Add Beneficiary Dialog Panel ─── */
|
||||
.abd-dialog-panel {
|
||||
.mat-mdc-dialog-container {
|
||||
|
||||
Reference in New Issue
Block a user