static update

This commit is contained in:
2026-07-31 15:09:18 +05:30
parent 0bf337b962
commit f2525080c8
20 changed files with 1443 additions and 360 deletions
@@ -0,0 +1,169 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="fdrd-dlg">
<!-- HEADER -->
<div class="fdrd-head">
<div class="fdrd-head-left">
<div class="fdrd-head-ic"><fa-icon icon="piggy-bank"></fa-icon></div>
<div>
<h2 class="fdrd-title">FD / RD Entry</h2>
<p class="fdrd-sub">Open a new Fixed Deposit or Recurring Deposit account.</p>
</div>
</div>
<div class="fdrd-head-actions">
<span class="fdrd-meta"> <fa-icon icon="university"></fa-icon>&nbsp;{{ bank.name }} </span>
<span class="fdrd-meta"> <fa-icon icon="map-marker-alt"></fa-icon>&nbsp;{{ bank.branch }} </span>
<button class="fdrd-close" (click)="close()" matTooltip="Close" aria-label="Close">&#10005;</button>
</div>
</div>
<div class="fdrd-body">
<!-- Success banner -->
<div class="fdrd-success" *ngIf="saved">
<fa-icon icon="check-circle"></fa-icon>
<div>
<strong>{{ lastAccountNo }}</strong> created for <strong>{{ lastClientName }}</strong>
<div class="fdrd-success-sub">You can enter another deposit below, or close this dialog.</div>
</div>
</div>
<!-- FORM -->
<div class="fdrd-section">
<!-- Row 1: Deposit Info | Client & Amount -->
<div class="ws-group-row">
<div class="ws-group">
<span class="ws-caption">Deposit Info</span>
<div class="ws-group__fields">
<div class="ws-f" style="width: 200px">
<label class="ws-f__lbl">Deposit Type</label>
<select class="ws-f__ctl" [(ngModel)]="depositType" (ngModelChange)="onDepositTypeChange()">
<option *ngFor="let dt of depositTypes" [value]="dt.code">{{ dt.label }}</option>
</select>
</div>
<div class="ws-f" style="width: 150px">
<label class="ws-f__lbl">Pay Mode</label>
<select class="ws-f__ctl" [(ngModel)]="payMode">
<option *ngFor="let m of payModes" [value]="m">{{ m }}</option>
</select>
</div>
</div>
</div>
<div class="ws-vdivider"></div>
<div class="ws-group ws-group--flex">
<span class="ws-caption">Client & Amount</span>
<div class="ws-group__fields">
<div class="ws-f" style="flex: 1.5; min-width: 180px">
<label class="ws-f__lbl">Client</label>
<select class="ws-f__ctl" [(ngModel)]="clientId">
<option value="" disabled>-- Select Client --</option>
<option *ngFor="let c of clients" [value]="c.id">{{ c.name }} ({{ c.id }})</option>
</select>
</div>
<div class="ws-f" style="flex: 1; min-width: 120px">
<label class="ws-f__lbl">{{ isRecurring ? 'Monthly Instalment (₹)' : 'Deposit Amount (₹)' }}</label>
<input
class="ws-f__ctl ws-f__ctl--mono"
type="number"
min="0"
[(ngModel)]="principal"
placeholder="e.g. 100000"
/>
</div>
</div>
</div>
</div>
<!-- Row 2: Term & Rate | Instructions -->
<div class="ws-group-row ws-group-row--mt">
<div class="ws-group">
<span class="ws-caption">Interest & Tenure</span>
<div class="ws-group__fields">
<div class="ws-f" style="width: 110px">
<label class="ws-f__lbl">Int. Rate (% p.a.)</label>
<input class="ws-f__ctl" type="number" min="0" step="0.05" [(ngModel)]="interestRate" />
</div>
<div class="ws-f" style="width: 240px">
<label class="ws-f__lbl">Tenure</label>
<div class="fdrd-tenure-row">
<input class="ws-f__ctl" type="number" min="0" [(ngModel)]="tenureYears" />
<span>Yrs</span>
<input class="ws-f__ctl" type="number" min="0" max="11" [(ngModel)]="tenureMonths" />
<span>Mo</span>
<input class="ws-f__ctl" type="number" min="0" max="30" [(ngModel)]="tenureDays" />
<span>Days</span>
</div>
</div>
</div>
</div>
<div class="ws-vdivider"></div>
<div class="ws-group ws-group--flex">
<span class="ws-caption">Nominee & Maturity Details</span>
<div class="ws-group__fields">
<div class="ws-f" style="flex: 1; min-width: 120px">
<label class="ws-f__lbl">Interest Payout</label>
<select class="ws-f__ctl" [(ngModel)]="interestPayout">
<option *ngFor="let o of interestPayoutOptions" [value]="o">{{ o }}</option>
</select>
</div>
<div class="ws-f" style="flex: 1; min-width: 130px">
<label class="ws-f__lbl">Nominee Name (optional)</label>
<input class="ws-f__ctl" type="text" [(ngModel)]="nomineeName" placeholder="Spouse / Child name" />
</div>
<div class="ws-f" style="flex: 1.2; min-width: 160px">
<label class="ws-f__lbl">Maturity Instruction</label>
<select class="ws-f__ctl" [(ngModel)]="maturityInstruction">
<option *ngFor="let m of maturityInstructions" [value]="m">{{ m }}</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- SUMMARY -->
<div class="fdrd-summary">
<div class="fdrd-stat">
<div class="fdrd-stat-l">Principal</div>
<div class="fdrd-stat-v">&#8377; {{ principal || 0 | number: '1.0-2' }}</div>
</div>
<div class="fdrd-stat">
<div class="fdrd-stat-l">Tenure</div>
<div class="fdrd-stat-v">{{ tenureYears }}y {{ tenureMonths }}m {{ tenureDays }}d</div>
</div>
<div class="fdrd-stat">
<div class="fdrd-stat-l">Maturity Date</div>
<div class="fdrd-stat-v">{{ maturityDate | date: 'dd-MM-yyyy' }}</div>
</div>
<div class="fdrd-stat fdrd-stat-good">
<div class="fdrd-stat-l">Maturity Amount</div>
<div class="fdrd-stat-v">&#8377; {{ maturityAmount | number: '1.2-2' }}</div>
</div>
</div>
</div>
<!-- FOOTER -->
<div class="fdrd-foot">
<div class="fdrd-foot-l">
<fa-icon icon="info-circle"></fa-icon>&nbsp; Demo entry — quarterly-compounded interest, no real account is
created.
</div>
<div class="fdrd-foot-actions">
<button mat-stroked-button class="fdrd-btn-ghost" (click)="resetForm()">
<fa-icon icon="undo"></fa-icon>&nbsp; Reset
</button>
<button mat-stroked-button class="fdrd-btn-save" [disabled]="!isValid" (click)="save()">
<fa-icon icon="save"></fa-icon>&nbsp; Save
</button>
</div>
</div>
</div>
@@ -0,0 +1,395 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
:host {
display: block;
font-family: 'Hanken Grotesk', Inter, sans-serif;
color: #1a2a3e;
}
.fdrd-dlg {
background: #fff;
border-radius: 0;
display: flex;
flex-direction: column;
overflow: hidden;
height: 100vh;
}
/* HEADER - Matches .ws-header in clerk dashboard */
.fdrd-head {
padding: 0 12px;
height: 32px;
background: linear-gradient(90deg, #0d1b2e 0%, #162542 100%);
border-bottom: 2px solid #e87722;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-shrink: 0;
}
.fdrd-head-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.fdrd-head-ic {
width: 22px;
height: 22px;
border-radius: 6px;
background: #e87722;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
flex-shrink: 0;
}
.fdrd-title {
margin: 0;
font-family: Newsreader, Georgia, serif;
font-size: 14px;
font-weight: 500;
color: #e8f0fb;
letter-spacing: 0.01em;
}
.fdrd-sub {
display: none; /* Hide subtitle in header to match clean style */
}
.fdrd-head-actions {
display: flex;
align-items: center;
gap: 12px;
}
.fdrd-meta {
display: inline-flex;
align-items: center;
font-size: 11px;
color: #7a95b0;
background: transparent;
border: none;
padding: 0;
fa-icon {
color: #5a7589;
margin-right: 4px;
}
}
.fdrd-close {
background: transparent;
border: none;
color: #7a95b0;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
transition: all 0.15s;
&:hover {
background: rgb(255 255 255 / 10%);
color: #fff;
}
}
/* BODY - Styled like canvas + card grid */
.fdrd-body {
padding: 16px 20px;
overflow-y: auto;
flex: 1;
background: #f0f4f9;
}
.fdrd-success {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px;
border-radius: 10px;
background: #dcfce7;
color: #15803d;
margin-bottom: 14px;
font-size: 13px;
fa-icon {
font-size: 16px;
margin-top: 1px;
}
}
.fdrd-success-sub {
margin-top: 2px;
font-size: 11px;
font-weight: 400;
opacity: 0.85;
}
/* Card container for form */
.fdrd-section {
background: #fff;
border: 1px solid #dce4ef;
border-radius: 10px;
padding: 16px 20px;
margin-bottom: 16px;
}
/* Transaction Entry Style Layout mappings */
.ws-group-row {
display: flex;
align-items: stretch;
gap: 0;
&--mt {
margin-top: 12px;
}
}
.ws-group {
display: flex;
flex-direction: column;
gap: 4px;
flex-shrink: 0;
&--flex {
flex: 1;
min-width: 0;
}
&__fields {
display: flex;
align-items: flex-end;
gap: 8px;
flex-wrap: nowrap;
height: 100%;
align-content: flex-end;
}
}
.ws-caption {
font-family: Newsreader, Georgia, serif;
font-style: italic;
font-size: 11px;
color: #7a95b0;
padding-bottom: 2px;
}
.ws-vdivider {
width: 1px;
background: #dce4ef;
margin: 12px 10px 0;
flex-shrink: 0;
}
.ws-f {
display: flex;
flex-direction: column;
gap: 3px;
flex-shrink: 0;
&__lbl {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #7a95b0;
white-space: nowrap;
}
&__ctl {
height: 26px;
padding: 0 7px;
border: 1px solid #c8d8eb;
border-radius: 6px;
font-family: 'Hanken Grotesk', Inter, sans-serif;
font-size: 11.5px;
color: #1a2a3e;
background: #f4f7fc;
box-sizing: border-box;
width: 100%;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:hover {
border-color: #9ab5d0;
}
&:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgb(37 99 235 / 12%);
background: #fff;
}
&--mono {
font-family: 'Roboto Mono', monospace;
font-size: 11.5px;
letter-spacing: 0.03em;
}
}
}
.fdrd-tenure-row {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
.ws-f__ctl {
width: 100%;
min-width: 0;
}
span {
font-size: 10px;
font-weight: 600;
color: #7a95b0;
text-transform: uppercase;
}
}
/* SUMMARY - Styled like info summary block */
.fdrd-summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 12px 16px;
background: #f4f7fc;
border: 1px solid #c8d8eb;
border-radius: 10px;
margin-top: 16px;
}
.fdrd-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.fdrd-stat-l {
font-size: 10px;
font-weight: 600;
color: #7a95b0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.fdrd-stat-v {
font-size: 14px;
font-weight: 700;
color: #1a2a3e;
}
.fdrd-stat-good .fdrd-stat-v {
color: #1a7a4a;
}
/* FOOTER - Styled like action panel */
.fdrd-foot {
padding: 10px 16px;
border-top: 1px solid #dce4ef;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-shrink: 0;
background: #f0f4f9;
}
.fdrd-foot-l {
font-size: 11px;
color: #7a95b0;
display: flex;
align-items: center;
gap: 4px;
}
.fdrd-foot-actions {
display: flex;
gap: 8px;
}
.fdrd-btn-ghost {
display: inline-flex;
align-items: center;
gap: 4px;
height: 28px;
padding: 0 12px !important;
border-radius: 7px;
border: 1px solid #dce4ef !important;
background: #fff !important;
color: #1a2a3e !important;
font-family: 'Hanken Grotesk', sans-serif;
font-size: 11.5px !important;
font-weight: 600 !important;
cursor: pointer;
box-shadow: none !important;
&:hover {
background: #f0f4f9 !important;
border-color: #a8bdd4 !important;
}
}
.fdrd-btn-save {
display: inline-flex;
align-items: center;
gap: 4px;
height: 28px;
padding: 0 16px !important;
border-radius: 7px;
border: none !important;
background: #e87722 !important;
color: #fff !important;
font-family: 'Hanken Grotesk', sans-serif;
font-size: 11.5px !important;
font-weight: 700 !important;
cursor: pointer;
box-shadow: 0 2px 8px rgb(232 119 34 / 30%) !important;
&:hover:not(:disabled) {
filter: brightness(1.08);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
background: #c8d8eb !important;
box-shadow: none !important;
}
}
@media (width <= 768px) {
.ws-group-row {
flex-direction: column;
gap: 12px;
}
.ws-vdivider {
display: none;
}
.ws-group__fields {
flex-wrap: wrap;
}
.fdrd-summary {
grid-template-columns: repeat(2, 1fr);
}
}
@@ -0,0 +1,146 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { CommonModule, DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSnackBar } from '@angular/material/snack-bar';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
type DepositType = 'FD-SINGLE' | 'FD-JOINT' | 'RD-MONTHLY' | 'RD-QUARTERLY';
interface DemoClient {
id: string;
name: string;
}
@Component({
selector: 'mifosx-fd-rd-entry-dialog',
standalone: true,
templateUrl: './fd-rd-entry-dialog.component.html',
styleUrls: ['./fd-rd-entry-dialog.component.scss'],
imports: [CommonModule, DecimalPipe, FormsModule, MatDialogModule, MatButtonModule, MatTooltipModule, FaIconComponent]
})
export class FdRdEntryDialogComponent {
private dialogRef = inject(MatDialogRef<FdRdEntryDialogComponent>);
private snackBar = inject(MatSnackBar);
bank = {
name: 'CBSNexora Bank Ltd',
branch: 'Head Office'
};
depositTypes: { code: DepositType; label: string; defaultRate: number }[] = [
{ code: 'FD-SINGLE', label: 'Fixed Deposit — Single A/c', defaultRate: 7.25 },
{ code: 'FD-JOINT', label: 'Fixed Deposit — Joint A/c', defaultRate: 7.25 },
{ code: 'RD-MONTHLY', label: 'Recurring Deposit — Monthly', defaultRate: 7.0 },
{ code: 'RD-QUARTERLY', label: 'Recurring Deposit — Quarterly', defaultRate: 7.0 }
];
clients: DemoClient[] = [
{ id: 'C-10231', name: 'Ramesh Kumar Sharma' },
{ id: 'C-10344', name: 'Priya Enterprises' },
{ id: 'C-10412', name: 'Anjali Verma' },
{ id: 'C-10567', name: 'Suresh & Sons HUF' },
{ id: 'C-10689', name: 'Vikram Traders' }
];
payModes = ['Cash', 'Cheque', 'Transfer from Savings A/c'];
interestPayoutOptions = ['Cumulative (Paid on Maturity)', 'Monthly Payout', 'Quarterly Payout'];
maturityInstructions = [
'Renew Principal & Interest',
'Renew Principal Only, Payout Interest',
'Payout on Maturity (No Renewal)'
];
depositType: DepositType = 'FD-SINGLE';
clientId = '';
principal: number | null = null;
interestRate = 7.25;
tenureYears = 1;
tenureMonths = 0;
tenureDays = 0;
interestPayout = this.interestPayoutOptions[0];
payMode = this.payModes[0];
nomineeName = '';
maturityInstruction = this.maturityInstructions[0];
saved = false;
lastAccountNo = '';
lastClientName = '';
get selectedClient(): DemoClient | undefined {
return this.clients.find((c) => c.id === this.clientId);
}
onDepositTypeChange(): void {
const dt = this.depositTypes.find((d) => d.code === this.depositType);
if (dt) this.interestRate = dt.defaultRate;
}
get isRecurring(): boolean {
return this.depositType.startsWith('RD');
}
get tenureInYears(): number {
return this.tenureYears + this.tenureMonths / 12 + this.tenureDays / 365;
}
get maturityDate(): Date {
const d = new Date();
d.setFullYear(d.getFullYear() + this.tenureYears);
d.setMonth(d.getMonth() + this.tenureMonths);
d.setDate(d.getDate() + this.tenureDays);
return d;
}
/** Quarterly-compounded maturity value, the standard FD/RD convention */
get maturityAmount(): number {
const p = this.principal || 0;
const t = this.tenureInYears;
if (p <= 0 || t <= 0) return 0;
return p * Math.pow(1 + this.interestRate / 100 / 4, 4 * t);
}
get isValid(): boolean {
return !!this.clientId && (this.principal ?? 0) > 0 && this.tenureInYears > 0;
}
save(): void {
if (!this.isValid) return;
const prefix = this.isRecurring ? 'RD' : 'FD';
this.lastAccountNo = `${prefix}${Math.floor(1000000 + Math.random() * 8999999)}`;
this.lastClientName = this.selectedClient?.name ?? '';
this.saved = true;
this.snackBar.open(`${prefix} account ${this.lastAccountNo} created for ${this.lastClientName}`, 'Close', {
duration: 4000
});
}
resetForm(): void {
this.depositType = 'FD-SINGLE';
this.clientId = '';
this.principal = null;
this.interestRate = 7.25;
this.tenureYears = 1;
this.tenureMonths = 0;
this.tenureDays = 0;
this.nomineeName = '';
this.interestPayout = this.interestPayoutOptions[0];
this.payMode = this.payModes[0];
this.maturityInstruction = this.maturityInstructions[0];
this.saved = false;
}
close(): void {
this.dialogRef.close();
}
}