diff --git a/src/app/savings/saving-account-actions/activate-savings-account/activate-savings-account.component.ts b/src/app/savings/saving-account-actions/activate-savings-account/activate-savings-account.component.ts
index e9add4f43..d7a41c94a 100644
--- a/src/app/savings/saving-account-actions/activate-savings-account/activate-savings-account.component.ts
+++ b/src/app/savings/saving-account-actions/activate-savings-account/activate-savings-account.component.ts
@@ -7,7 +7,7 @@
*/
/** Angular Imports */
-import { Component, OnInit, Input, inject } from '@angular/core';
+import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
@@ -41,8 +41,6 @@ export class ActivateSavingsAccountComponent implements OnInit {
private router = inject(Router);
private settingsService = inject(SettingsService);
- @Input() minRequiredOpeningBalance: number | null = null;
-
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
@@ -76,18 +74,10 @@ export class ActivateSavingsAccountComponent implements OnInit {
* Creates the activate savings account form.
*/
createActivateSavingsAccountForm() {
- const minBalance = this.minRequiredOpeningBalance ?? 0;
this.activateSavingsAccountForm = this.formBuilder.group({
activatedOnDate: [
'',
Validators.required
- ],
- openingBalance: [
- minBalance,
- [
- Validators.required,
- Validators.min(minBalance)
- ]
]
});
}
@@ -109,9 +99,6 @@ export class ActivateSavingsAccountComponent implements OnInit {
dateFormat,
locale
};
- if (data.openingBalance === 0 || data.openingBalance === null) {
- delete data.openingBalance;
- }
this.savingsService.executeSavingsAccountCommand(this.accountId, 'activate', data).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
diff --git a/src/app/savings/saving-account-actions/approve-savings-account/approve-savings-account.component.html b/src/app/savings/saving-account-actions/approve-savings-account/approve-savings-account.component.html
index 8f61d900d..b6d897fb1 100644
--- a/src/app/savings/saving-account-actions/approve-savings-account/approve-savings-account.component.html
+++ b/src/app/savings/saving-account-actions/approve-savings-account/approve-savings-account.component.html
@@ -10,6 +10,7 @@
diff --git a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts
index 30da85e32..6add60514 100644
--- a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts
+++ b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts
@@ -50,12 +50,11 @@ export class SavingsAccountDigitalStepComponent {
}
}
+ // Note: a new account always starts with every digital facility off, even if the product has
+ // its own defaults configured -- those are enabled deliberately, one at a time, after the
+ // account is created and active, not inherited automatically at opening.
@Input()
- set savingsAccountProductTemplate(value: any) {
- if (!this.hasExistingAccount && value?.accountFacilityConfig) {
- this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
- }
- }
+ savingsAccountProductTemplate: any;
get savingsAccountDigital() {
return { accountFacilityConfig: { ...this.facilityConfig } };
diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts
index f8837abfc..32b227bcd 100644
--- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts
+++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts
@@ -124,13 +124,11 @@ export class SavingsAccountPreviewStepComponent implements OnChanges {
}
}
- /** Returns user-modified digital config (from digital step), falling back to product template */
+ /** Digital facility config as actually captured by a digital step in this wizard -- no fallback
+ * to the product's own defaults, since account creation no longer has a digital step at all and
+ * must not preview facilities the officer never actually chose. */
get digitalFacilityConfig(): any {
- return (
- this.savingsAccountDigital?.accountFacilityConfig ??
- this.savingsAccountProductTemplate?.accountFacilityConfig ??
- null
- );
+ return this.savingsAccountDigital?.accountFacilityConfig ?? null;
}
get hasDigitalSection(): boolean {
diff --git a/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.html b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.html
new file mode 100644
index 000000000..f5d32347b
--- /dev/null
+++ b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.html
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+
+ {{ accountType === 'FD' ? 'savings' : 'event_repeat' }}
+
+
+
+
+
+
+
+
+ This is to certify that
+ {{ accountData.clientName || accountData.groupName }}
+ holds a {{ accountType === 'FD' ? 'Fixed Deposit' : 'Recurring Deposit' }} account bearing account number
+ {{ accountData.fdAccountNumber || accountData.accountNo }}
+ with Nexora Co-operative Bank Ltd. , as per the particulars set out
+ below.
+
+ This account was renewed on maturity {{ renewalCount }} {{ renewalCount === 1 ? 'time' : 'times' }}; the
+ full renewal history is set out below.
+
+
+
+
+
ISSUED TO:
+
{{ accountData.clientName || accountData.groupName }}
+
+ Customer ID: {{ accountData.clientAccountNo }}
+
+ · PAN: {{ panNumber }}
+
+
+ {{ clientAddress }}
+
+
+
+
+
+
+
+
+ Product
+ {{ accountData.depositProductName }}
+ {{ accountType === 'FD' ? 'Deposit Date' : 'First Deposit Date' }}
+ {{ depositDate | dateFormat }}
+
+
+ Deposit Amount
+
+ {{ accountData.depositAmount | currency: accountData.currency.code : 'symbol-narrow' }}
+
+ Tenure
+ {{ tenureLabel }}
+
+
+ Installment Amount
+
+ {{
+ accountData.mandatoryRecommendedDepositAmount | currency: accountData.currency.code : 'symbol-narrow'
+ }}
+
+ Installment Frequency
+
+ Every {{ accountData.recurringFrequency }} {{ accountData.recurringFrequencyType?.value }}
+
+
+
+ Tenure
+ {{ tenureLabel }}
+ Interest Rate (p.a.)
+ {{ accountData.nominalAnnualInterestRate }}%
+
+
+ Interest Rate (p.a.)
+ {{ accountData.nominalAnnualInterestRate }}%
+ Interest Compounding
+ {{ accountData.interestCompoundingPeriodType?.value }}
+
+
+ Maturity Date
+ {{ accountData.maturityDate | dateFormat }}
+ Current Balance
+
+ {{ accountData.summary?.accountBalance | currency: accountData.currency.code : 'symbol-narrow' }}
+
+
+
+ MATURITY AMOUNT
+
+ {{ accountData.maturityAmount | currency: accountData.currency.code : 'symbol-narrow' }}
+
+
+
+
+
+
+
+
+
+ RENEWAL HISTORY
+
+
+ Term
+ From
+ To
+ Rate
+ Principal
+ Interest
+ Maturity Amount
+
+
+
+
+ {{ term.renewalcycleno === 0 ? 'Original' : 'Renewal ' + term.renewalcycleno }}
+ {{ term.activatedondate | dateFormat }}
+ {{ term.maturitydate | dateFormat }}
+ {{ term.interestrate }}%
+ {{ term.depositamount | currency: accountData.currency.code : 'symbol-narrow' }}
+
+ {{
+ term.interestearned === null || term.interestearned === undefined
+ ? '—'
+ : (term.interestearned | currency: accountData.currency.code : 'symbol-narrow')
+ }}
+
+ {{ maturityFor(term) | currency: accountData.currency.code : 'symbol-narrow' }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'labels.buttons.Close' | translate }}
+
+ print
+ Print Certificate
+
+
diff --git a/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.scss b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.scss
new file mode 100644
index 000000000..744d3b9df
--- /dev/null
+++ b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.scss
@@ -0,0 +1,556 @@
+/**
+ * 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/.
+ */
+
+.dialog-header {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid #e0e0e0;
+ padding-bottom: 12px;
+ margin-bottom: 16px;
+ font-weight: 500;
+ color: #1a252c;
+}
+
+.dialog-content-container {
+ max-height: 80vh;
+ overflow-y: auto;
+ padding: 10px;
+}
+
+/* Certificate Outer Container */
+.certificate-container {
+ background-color: #faf8f5;
+ padding: 20px;
+ font-family: Georgia, 'Times New Roman', Times, serif;
+ color: #2c2520;
+ box-shadow: 0 4px 20px rgb(0 0 0 / 8%);
+ border-radius: 8px;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+/* Classic Ornate Border System */
+.certificate-border-outer {
+ border: 5px double #c5a059; /* Gold double border */
+ padding: 8px;
+ background-color: #fdfcf7; /* Textured light cream */
+ border-radius: 4px;
+ position: relative;
+}
+
+.certificate-border-inner {
+ border: 1px solid #dcd1bd;
+ padding: 30px;
+ border-radius: 2px;
+ position: relative;
+ overflow: hidden;
+}
+
+/* Watermark */
+.certificate-watermark {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ opacity: 0.03;
+ pointer-events: none;
+ z-index: 1;
+
+ .watermark-icon {
+ font-size: 280px;
+ width: 280px;
+ height: 280px;
+ color: #c5a059;
+ }
+}
+
+.certificate-header,
+.cert-meta-row,
+.cert-body-section,
+.certificate-details-table,
+.cert-footer {
+ position: relative;
+ z-index: 2; /* Ensure content is above watermark */
+}
+
+/* Header styling */
+.certificate-header {
+ text-align: center;
+ margin-bottom: 30px;
+ border-bottom: 2px solid #f2edd5;
+ padding-bottom: 20px;
+
+ .bank-name {
+ font-size: 26px;
+ font-weight: bold;
+ color: #0b3c5d; /* Dark Premium Blue */
+ text-transform: uppercase;
+ letter-spacing: 1.5px;
+ margin-bottom: 4px;
+ }
+
+ .bank-sub {
+ font-size: 13px;
+ color: #7b6f63;
+ font-style: italic;
+ letter-spacing: 1px;
+ margin-bottom: 20px;
+ }
+
+ .cert-title {
+ font-size: 28px;
+ font-weight: 400;
+ color: #c5a059; /* Classic Gold */
+ letter-spacing: 4px;
+ margin-bottom: 5px;
+ font-family: Georgia, serif;
+ }
+
+ .cert-subtitle {
+ font-size: 11px;
+ color: #7b6f63;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ }
+}
+
+/* Meta Data Grid (Top of cert) */
+.cert-meta-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 30px;
+ border-bottom: 1px solid #f2edd5;
+ padding-bottom: 15px;
+
+ .meta-item {
+ display: flex;
+ flex-direction: column;
+
+ .meta-label {
+ font-size: 11px;
+ text-transform: uppercase;
+ color: #928374;
+ letter-spacing: 1px;
+ margin-bottom: 3px;
+ }
+
+ .meta-value {
+ font-size: 15px;
+ font-weight: bold;
+ color: #322f2b;
+ }
+ }
+}
+
+/* Main Body text */
+.cert-body-section {
+ margin-bottom: 35px;
+
+ .cert-main-text {
+ font-size: 15px;
+ line-height: 1.8;
+ text-align: center;
+ color: #4a453f;
+ margin-bottom: 25px;
+
+ .highlight-text {
+ color: #1a202c;
+ font-weight: 600;
+ border-bottom: 1px dotted #c5a059;
+ padding-bottom: 1px;
+ }
+ }
+}
+
+/* Issued To Details Block */
+.issued-to-details {
+ border: 1px solid #f0e6d2;
+ background-color: #fbfaf5;
+ padding: 15px 20px;
+ border-radius: 4px;
+ margin: 20px auto;
+ max-width: 90%;
+ text-align: left;
+
+ .issued-to-title {
+ font-size: 10px;
+ font-weight: bold;
+ color: #c5a059;
+ letter-spacing: 1.5px;
+ margin-bottom: 6px;
+ }
+
+ .client-name-details {
+ font-size: 18px;
+ font-weight: bold;
+ color: #0b3c5d;
+ margin-bottom: 4px;
+ }
+
+ .client-ids {
+ font-size: 12px;
+ color: #666;
+ margin-bottom: 6px;
+ }
+
+ .client-address {
+ font-size: 12px;
+ color: #7b6f63;
+ line-height: 1.4;
+ }
+}
+
+/* Certificate Data Table */
+.certificate-details-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 40px;
+
+ td {
+ padding: 10px 12px;
+ border: 1px solid #ede8db;
+ font-size: 13px;
+ }
+
+ .lbl-col {
+ background-color: #f7f5ee;
+ font-weight: bold;
+ color: #7b6f63;
+ width: 25%;
+ }
+
+ .val-col {
+ color: #322f2b;
+ width: 25%;
+ }
+
+ .highlight-row {
+ background-color: #f5f2e9;
+ border-top: 2px solid #c5a059;
+ }
+
+ .lbl-col-highlight {
+ font-weight: bold;
+ color: #0b3c5d;
+ font-size: 14px;
+ letter-spacing: 1px;
+ text-align: right;
+ padding-right: 20px;
+ }
+
+ .val-col-highlight {
+ font-size: 16px;
+ font-weight: bold;
+ color: #b8860b; /* Dark Goldenrod */
+ }
+}
+
+/* Renewal History Table -- lists each earlier term this FD was renewed from */
+.renewal-history-table {
+ th {
+ padding: 8px 10px;
+ border: 1px solid #ede8db;
+ font-size: 11px;
+ text-align: left;
+ background-color: #f7f5ee;
+ color: #7b6f63;
+ }
+
+ .renewal-history-title {
+ text-align: center;
+ background-color: #f5f2e9;
+ color: #0b3c5d;
+ font-weight: bold;
+ letter-spacing: 1.5px;
+ font-size: 12px;
+ }
+
+ .current-term-row {
+ background-color: #fbfaf5;
+ font-weight: bold;
+ }
+}
+
+/* Certificate Footer Seal / Signatures */
+.cert-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 20px;
+
+ .seal-container {
+ width: 25%;
+
+ .seal-ring {
+ width: 90px;
+ height: 90px;
+ border: 2px dashed #b8860b;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto;
+ transform: rotate(-15deg);
+
+ .seal-text {
+ font-size: 10px;
+ font-weight: bold;
+ color: #b8860b;
+ text-align: center;
+ line-height: 1.2;
+ letter-spacing: 1px;
+ }
+ }
+ }
+
+ .closing-text {
+ width: 45%;
+ font-size: 12px;
+ color: #7b6f63;
+ font-style: italic;
+ line-height: 1.5;
+ text-align: center;
+ }
+
+ .signatures-container {
+ width: 30%;
+ text-align: center;
+
+ .signature-block {
+ display: inline-block;
+ width: 80%;
+
+ .signature-line {
+ border-bottom: 1px solid #7b6f63;
+ margin-bottom: 8px;
+ height: 40px; /* Space for signature */
+ }
+
+ .signatory-title {
+ font-size: 12px;
+ color: #7b6f63;
+ font-weight: bold;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+ }
+ }
+}
+
+/* Print CSS */
+@media print {
+ /* Everything below `::ng-deep` reaches outside this component's own template -- html/body and
+ the whole Angular Material CDK overlay chain never carry this component's scoping attribute
+ under the default emulated encapsulation, so a plain (scoped) selector would silently never
+ match any of them. `::ng-deep` is deprecated but still the correct tool for a component that
+ genuinely needs to override its host application's shell during print. */
+
+ /* The app shell pins html/body to a fixed viewport height with overflow hidden (its inner
+ panels handle their own scrolling on screen) -- left in place, that caps every printed page
+ at one viewport's worth of content and silently cuts off the rest instead of paginating. */
+ ::ng-deep html,
+ ::ng-deep body {
+ height: auto !important;
+ overflow: visible !important;
+ }
+
+ /* Hide non-overlay contents during print */
+ ::ng-deep body > :not(.cdk-overlay-container) {
+ display: none !important;
+ }
+
+ /* The dialog's own scroll region clips anything past 80vh -- printing only captures what a
+ scrollable/max-height box shows on screen, so both constraints must be lifted or the
+ certificate gets cut off after its first visible slice. */
+ .dialog-content-container {
+ max-height: none !important;
+ overflow: visible !important;
+ }
+
+ /* CDK's overlay wrapper/backdrop/pane are all `position: fixed` (needed to center the dialog
+ on screen) and the wrapper flex-centers its content -- during print, a fixed-position
+ ancestor is clipped to one page's height, so anything taller than that gets cut off and only
+ a thin cross-section (here, just the certificate's own border) survives. Every ancestor in
+ that chain has to become a normal, statically-flowing block for the full certificate to print. */
+ ::ng-deep .cdk-overlay-backdrop {
+ display: none !important;
+ }
+
+ ::ng-deep .cdk-overlay-container,
+ ::ng-deep .cdk-global-overlay-wrapper,
+ ::ng-deep .cdk-overlay-pane,
+ ::ng-deep .mat-mdc-dialog-container,
+ ::ng-deep .mdc-dialog__container,
+ ::ng-deep .mdc-dialog__surface {
+ position: static !important;
+ display: block !important;
+ max-height: none !important;
+ max-width: none !important;
+ height: auto !important;
+ width: auto !important;
+ overflow: visible !important;
+ transform: none !important;
+ }
+
+ /* Make certificate fill page */
+ #deposit-certificate-print {
+ position: static !important;
+ width: 100% !important;
+ height: auto !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ background-color: #fff !important;
+ box-shadow: none !important;
+ }
+
+ .certificate-border-outer {
+ border-width: 8px !important;
+ background-color: #fff !important;
+ }
+
+ .no-print {
+ display: none !important;
+ }
+
+ ::ng-deep .mat-mdc-dialog-container {
+ padding: 0 !important;
+ }
+
+ /* Compact every spacing/font value so the whole certificate fits a single printed page --
+ the on-screen sizing is generous by design, but that same height doesn't fit one page. */
+ .certificate-container {
+ padding: 6px !important;
+ }
+
+ .certificate-border-inner {
+ padding: 14px !important;
+ }
+
+ .certificate-header {
+ margin-bottom: 10px !important;
+ padding-bottom: 10px !important;
+
+ .bank-name {
+ font-size: 19px !important;
+ margin-bottom: 2px !important;
+ }
+
+ .bank-sub {
+ font-size: 10px !important;
+ margin-bottom: 8px !important;
+ }
+
+ .cert-title {
+ font-size: 19px !important;
+ letter-spacing: 2px !important;
+ margin-bottom: 2px !important;
+ }
+
+ .cert-subtitle {
+ font-size: 9px !important;
+ }
+ }
+
+ .cert-meta-row {
+ margin-bottom: 10px !important;
+ padding-bottom: 8px !important;
+
+ .meta-label {
+ font-size: 9px !important;
+ margin-bottom: 1px !important;
+ }
+
+ .meta-value {
+ font-size: 12px !important;
+ }
+ }
+
+ .cert-body-section {
+ margin-bottom: 10px !important;
+
+ .cert-main-text {
+ font-size: 11px !important;
+ line-height: 1.4 !important;
+ margin-bottom: 10px !important;
+ }
+ }
+
+ .issued-to-details {
+ padding: 8px 14px !important;
+ margin: 8px auto !important;
+
+ .client-name-details {
+ font-size: 14px !important;
+ }
+
+ .client-ids,
+ .client-address {
+ font-size: 10px !important;
+ }
+ }
+
+ .certificate-details-table {
+ margin-bottom: 12px !important;
+
+ td {
+ padding: 5px 10px !important;
+ font-size: 10px !important;
+ }
+
+ .lbl-col-highlight {
+ font-size: 12px !important;
+ }
+
+ .val-col-highlight {
+ font-size: 13px !important;
+ }
+ }
+
+ .renewal-history-table {
+ margin-bottom: 12px !important;
+
+ th {
+ padding: 4px 8px !important;
+ font-size: 8px !important;
+ }
+
+ td {
+ padding: 4px 8px !important;
+ font-size: 9px !important;
+ }
+
+ .renewal-history-title {
+ font-size: 10px !important;
+ }
+ }
+
+ .cert-footer {
+ margin-top: 8px !important;
+
+ .seal-ring {
+ width: 60px !important;
+ height: 60px !important;
+ }
+
+ .seal-text {
+ font-size: 8px !important;
+ }
+
+ .closing-text {
+ font-size: 10px !important;
+ }
+
+ .signature-line {
+ height: 22px !important;
+ }
+
+ .signatory-title {
+ font-size: 10px !important;
+ }
+ }
+}
diff --git a/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.ts b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.ts
new file mode 100644
index 000000000..27ad5f9f3
--- /dev/null
+++ b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.ts
@@ -0,0 +1,157 @@
+/**
+ * Copyright since 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import { Component, OnInit, inject } from '@angular/core';
+import {
+ MAT_DIALOG_DATA,
+ MatDialogRef,
+ MatDialogTitle,
+ MatDialogContent,
+ MatDialogActions,
+ MatDialogClose
+} from '@angular/material/dialog';
+import { ClientsService } from 'app/clients/clients.service';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { DateFormatPipe } from '../../pipes/date-format.pipe';
+import { CurrencyPipe } from '@angular/common';
+import { MatButton } from '@angular/material/button';
+import { MatIcon } from '@angular/material/icon';
+
+/**
+ * Printable deposit certificate for a Fixed Deposit or Recurring Deposit account -- handed to the
+ * customer as physical proof of their deposit. Shared between FD and RD since both account shapes
+ * carry the same core fields (depositAmount, nominalAnnualInterestRate, depositPeriod, maturityDate,
+ * maturityAmount); RD additionally shows its installment/frequency details.
+ */
+@Component({
+ selector: 'mifosx-deposit-certificate-dialog',
+ templateUrl: './deposit-certificate-dialog.component.html',
+ styleUrls: ['./deposit-certificate-dialog.component.scss'],
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ MatDialogTitle,
+ MatDialogContent,
+ MatDialogActions,
+ MatDialogClose,
+ DateFormatPipe,
+ CurrencyPipe,
+ MatButton,
+ MatIcon
+ ]
+})
+export class DepositCertificateDialogComponent implements OnInit {
+ dialogRef = inject
>(MatDialogRef);
+ data = inject(MAT_DIALOG_DATA);
+ clientsService = inject(ClientsService);
+
+ /** 'FD' or 'RD' -- controls the title and which extra rows show. */
+ accountType: 'FD' | 'RD';
+ accountData: any;
+
+ clientData: any;
+ clientAddress = '';
+ panNumber = 'Not Available';
+
+ certificateNo = '';
+ issueDate: Date = new Date();
+
+ /**
+ * Every term of this FD, oldest first, as resolved by the backend -- a renewal opens each term as
+ * its own account, so the certificate lists them all to show the deposit's full history. Empty for
+ * an FD that has never been renewed. Keys are lower-cased, arriving from SQL column labels.
+ */
+ renewalHistory: any[] = [];
+
+ /**
+ * How many times the deposit has actually rolled over. {@link renewalHistory} lists every TERM, and the
+ * first of those is the original deposit rather than a renewal, so the count is one less than the number
+ * of rows. Stated on a customer-facing certificate, so an off-by-one here misreports the deposit.
+ */
+ get renewalCount(): number {
+ return Math.max(this.renewalHistory.length - 1, 0);
+ }
+
+ ngOnInit() {
+ this.accountType = this.data.accountType;
+ this.accountData = this.data.accountData;
+ const clientId = this.data.clientId != null ? String(this.data.clientId) : null;
+
+ this.certificateNo = `${this.accountType}-CERT-${this.accountData.fdAccountNumber || this.accountData.accountNo}`;
+ this.renewalHistory = this.accountData?.renewalHistory ?? [];
+
+ if (!clientId) {
+ return;
+ }
+
+ this.clientsService.getClientData(clientId).subscribe({
+ next: (client: any) => {
+ this.clientData = client;
+ },
+ error: () => {}
+ });
+
+ this.clientsService.getClientAddressData(clientId).subscribe({
+ next: (addresses: any[]) => {
+ if (addresses && addresses.length > 0) {
+ const permanent = addresses.find((a) => /permanent/i.test(a?.addressType || '')) || addresses[0];
+ this.clientAddress = this.formatAddress(permanent);
+ }
+ },
+ error: () => {}
+ });
+
+ this.clientsService.getClientIdentifiers(clientId).subscribe({
+ next: (identifiers: any[]) => {
+ const idList = Array.isArray(identifiers) ? identifiers : (identifiers as any)?.pageItems || [];
+ const panId = idList.find((id: any) => /pan/i.test(id?.documentType?.name || ''));
+ if (panId) {
+ this.panNumber = panId.documentKey;
+ }
+ },
+ error: () => {}
+ });
+ }
+
+ /**
+ * A term's settled maturity amount, or its projection while it is still running. Resolved here rather
+ * than inline so the choice isn't left to operator precedence between `??` and the currency pipe.
+ */
+ maturityFor(term: any): number | null {
+ return term?.maturedamount ?? term?.projectedmaturityamount ?? null;
+ }
+
+ formatAddress(addr: any): string {
+ if (!addr) return '';
+ return [
+ addr.addressLine1,
+ addr.addressLine2,
+ addr.townVillage,
+ addr.city,
+ addr.stateName,
+ addr.countryName,
+ addr.postalCode
+ ]
+ .filter((p) => !!p && String(p).trim())
+ .join(', ');
+ }
+
+ get depositDate(): any {
+ return this.accountData?.timeline?.activatedOnDate ?? this.accountData?.timeline?.submittedOnDate ?? null;
+ }
+
+ get tenureLabel(): string {
+ const period = this.accountData?.depositPeriod;
+ const freq = this.accountData?.depositPeriodFrequency?.value;
+ if (period == null) return 'N/A';
+ return freq ? `${period} ${freq}` : String(period);
+ }
+
+ printCertificate() {
+ window.print();
+ }
+}
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-products/pending-savings-products.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-products/pending-savings-products.component.html
index 5016189b8..ffccfe7d7 100644
--- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-products/pending-savings-products.component.html
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-products/pending-savings-products.component.html
@@ -49,8 +49,13 @@
Actions
-
- Compare
+
+ {{ loadingDiffFor === edit ? 'Loading…' : 'Compare' }}
Approve
@@ -65,37 +70,6 @@