feat: KYC re-verify UX, family/nominee tab gating, savings dialogs, cfcfrms shell
- KYC tab: fix Verify (PAN) and Send OTP (Aadhaar) being disabled in edit mode; unmask Aadhaar input and show on-file masked value as hint; resolve kycId from id/kycId/resourceId fallbacks. - Client stepper/view: add familyAllowed / nomineeAllowed predicates + CanActivate route guards. Family tab hidden for non-individuals, Nominee tab hidden for non-proprietor entities. Family-members step accepts a nomineeOnly input for proprietorship clients. Add/Edit/Delete buttons inside family-members and nominee tabs gated by canAdd. - cfcfrms: add cfcfrms-shell wrapper, restyle complaint/fraud/mule/escalate pages and dashboard. - Savings: identity-verify and transaction-confirm dialogs, manage / activate / add-charge flows, account preview styling. - Tasks, core shell, account-nominees, icons: minor style and behaviour updates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
bf3e26a0d0
commit
b149fef72e
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user