feat(fd): convert Premature Close to a dialog, with its full calculation shown
Was a full-page form at a dedicated route, reached by navigating away from the account -- now a popup dialog in the same visual language as the Payment/Renew dialog, just in this Danger Zone action's own red accent. Also fixes a real gap in the form it replaces: the premature amount/action options never loaded until a maker happened to touch the date picker, since the calculation was only ever triggered on value change, never on open. Added a breakdown panel showing exactly how the shown Maturity Amount was reached -- Deposit Amount, Contracted Interest Rate, Pre-Closure Penalty (when applicable), and the resulting Interest Earned -- using data the account-view page already had, since the calculation endpoint itself only ever returns the final number. Removed the now-dead full-page component/route target it replaces. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+194
@@ -0,0 +1,194 @@
|
|||||||
|
<!--
|
||||||
|
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="pcd">
|
||||||
|
<div class="pcd__header">
|
||||||
|
<div class="pcd__header-left">
|
||||||
|
<span class="pcd__header-ico"><fa-icon icon="times-circle"></fa-icon></span>
|
||||||
|
<div class="pcd__header-text">
|
||||||
|
<h2 class="pcd__title">{{ 'labels.heading.Premature Close' | translate }}</h2>
|
||||||
|
<p class="pcd__subtitle">
|
||||||
|
Close {{ data.accountNo ?? 'this account' }} before maturity -- interest is recalculated as of the date below,
|
||||||
|
usually at a lower, penalty rate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="pcd__header-close" mat-dialog-close [disabled]="submitting" title="Close">
|
||||||
|
<fa-icon icon="times"></fa-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div mat-dialog-content class="pcd__content">
|
||||||
|
@if (loadFailed) {
|
||||||
|
<div class="pcd__error" role="alert">
|
||||||
|
<fa-icon icon="times-circle" class="m-r-10"></fa-icon>
|
||||||
|
Could not calculate the premature-closure amount for this account. Close this dialog and try again.
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<form [formGroup]="prematureCloseForm">
|
||||||
|
<mat-form-field appearance="outline" class="pcd__date" (click)="closedOnDatePicker.open()">
|
||||||
|
<mat-label>{{ 'labels.inputs.Premature Close Date' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[min]="minDate"
|
||||||
|
[max]="maxDate"
|
||||||
|
[matDatepicker]="closedOnDatePicker"
|
||||||
|
required
|
||||||
|
formControlName="closedOnDate"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="closedOnDatePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #closedOnDatePicker></mat-datepicker>
|
||||||
|
@if (prematureCloseForm.controls.closedOnDate.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
{{ 'labels.inputs.Premature Close Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||||
|
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (loadingAmount) {
|
||||||
|
<div class="pcd__loading">
|
||||||
|
<mat-spinner diameter="28"></mat-spinner>
|
||||||
|
<span>Calculating premature-closure amount...</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
@if (maturityAmount !== null && maturityAmount !== undefined) {
|
||||||
|
<div class="pcd__preview">
|
||||||
|
@if (data.depositAmount) {
|
||||||
|
<div class="pcd__preview-row">
|
||||||
|
<span class="pcd__preview-label">Deposit Amount</span>
|
||||||
|
<span class="pcd__preview-value">{{ data.depositAmount | formatNumber }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (data.nominalAnnualInterestRate) {
|
||||||
|
<div class="pcd__preview-row">
|
||||||
|
<span class="pcd__preview-label">Contracted Interest Rate</span>
|
||||||
|
<span class="pcd__preview-value">{{ data.nominalAnnualInterestRate }}%</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (data.preClosurePenalApplicable) {
|
||||||
|
<div class="pcd__preview-row">
|
||||||
|
<span class="pcd__preview-label"
|
||||||
|
>Pre-Closure Penalty (on {{ data.preClosurePenalInterestOnType?.value }})</span
|
||||||
|
>
|
||||||
|
<span class="pcd__preview-value">− {{ data.preClosurePenalInterest }}%</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (interestEarned !== null) {
|
||||||
|
<div class="pcd__preview-row pcd__preview-row--highlight">
|
||||||
|
<span class="pcd__preview-label"
|
||||||
|
>Interest Earned (to {{ 'labels.inputs.Premature Close Date' | translate }})</span
|
||||||
|
>
|
||||||
|
<span class="pcd__preview-value">+ {{ interestEarned | formatNumber }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="pcd__preview-row pcd__preview-row--total">
|
||||||
|
<span class="pcd__preview-label">{{ 'labels.inputs.Maturity Amount' | translate }}</span>
|
||||||
|
<span class="pcd__preview-value">{{ maturityAmount | formatNumber }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="pcd__section-label" id="pcd-action-label">{{ 'labels.inputs.Action' | translate }}</div>
|
||||||
|
<div class="pcd__options" role="radiogroup" aria-labelledby="pcd-action-label">
|
||||||
|
@for (closureOption of onAccountClosureOptions; track closureOption) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pcd__option"
|
||||||
|
role="radio"
|
||||||
|
[attr.aria-checked]="prematureCloseForm.get('onAccountClosureId').value === closureOption.id"
|
||||||
|
[class.pcd__option--active]="prematureCloseForm.get('onAccountClosureId').value === closureOption.id"
|
||||||
|
(click)="selectClosureType(closureOption.id)"
|
||||||
|
>
|
||||||
|
<span class="pcd__option-ico"><fa-icon [icon]="iconFor(closureOption.id)"></fa-icon></span>
|
||||||
|
<span class="pcd__option-text">
|
||||||
|
<span class="pcd__option-title">{{ closureOption.value }}</span>
|
||||||
|
<span class="pcd__option-desc">{{ descriptionFor(closureOption.id) }}</span>
|
||||||
|
</span>
|
||||||
|
@if (prematureCloseForm.get('onAccountClosureId').value === closureOption.id) {
|
||||||
|
<fa-icon icon="check-circle" class="pcd__option-check"></fa-icon>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (
|
||||||
|
prematureCloseForm.controls.onAccountClosureId?.hasError('required') &&
|
||||||
|
prematureCloseForm.controls.onAccountClosureId?.touched
|
||||||
|
) {
|
||||||
|
<div class="pcd__option-error">
|
||||||
|
{{ 'labels.inputs.Account action' | translate }} {{ 'labels.commons.is' | translate }}
|
||||||
|
{{ 'labels.commons.required' | translate }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (noDestinationAccounts) {
|
||||||
|
<p class="pcd__hint pcd__hint--warn" role="alert">
|
||||||
|
<fa-icon icon="info-circle" class="m-r-5"></fa-icon>
|
||||||
|
This customer has no active savings account to transfer into. Open one first, or choose an action that
|
||||||
|
pays out in cash instead.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (prematureCloseForm.contains('toSavingsAccountId')) {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Transfer to Savings' | translate }}</mat-label>
|
||||||
|
<mat-select required formControlName="toSavingsAccountId">
|
||||||
|
@for (account of savingsAccountsData; track account) {
|
||||||
|
<mat-option [value]="account.id">
|
||||||
|
{{ account.accountNo }}
|
||||||
|
</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
@if (prematureCloseForm.controls.toSavingsAccountId?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
{{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }}
|
||||||
|
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (prematureCloseForm.contains('transferDescription')) {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Transfer Description' | translate }}</mat-label>
|
||||||
|
<input matInput formControlName="transferDescription" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (prematureCloseForm.contains('note')) {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
||||||
|
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (submitError) {
|
||||||
|
<p class="pcd__error" role="alert">
|
||||||
|
<fa-icon icon="times-circle" class="m-r-5"></fa-icon>
|
||||||
|
{{ submitError }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div mat-dialog-actions align="end" class="pcd__actions">
|
||||||
|
<button mat-button mat-dialog-close [disabled]="submitting" class="pcd__btn-cancel">
|
||||||
|
{{ 'labels.buttons.Cancel' | translate }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
class="pcd__btn-submit"
|
||||||
|
[disabled]="!prematureCloseForm?.valid || submitting || loadFailed || loadingAmount"
|
||||||
|
(click)="submit()"
|
||||||
|
>
|
||||||
|
{{ submitting ? ('labels.buttons.Processing' | translate) : ('labels.buttons.Confirm' | translate) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+388
@@ -0,0 +1,388 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Premature Close Dialog Styles
|
||||||
|
* CBSNexora Core Banking System
|
||||||
|
*
|
||||||
|
* Same shape/spacing/typography as the Payment dialog (.pmt__*), in this Danger Zone action's own red
|
||||||
|
* accent on the header and Confirm button, rather than Payment's blue -- an irreversible, usually
|
||||||
|
* penalty-bearing action reads differently from a routine one, everywhere else stays identical so the
|
||||||
|
* two dialogs feel like the same product.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.pcd {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 92vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family:
|
||||||
|
'DM Sans',
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
color: #0f172a;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 24px 48px -12px rgb(15 23 42 / 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 26px 30px;
|
||||||
|
background: linear-gradient(135deg, #7f1d1d 0%, #b91c1c 55%, #dc2626 100%);
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(circle at 88% -20%, rgb(255 255 255 / 22%), transparent 55%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-ico {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgb(255 255 255 / 16%);
|
||||||
|
border: 1px solid rgb(255 255 255 / 25%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-close {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: rgb(255 255 255 / 14%);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: rgb(255 255 255 / 28%);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.25;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__subtitle {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgb(255 255 255 / 88%);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__content {
|
||||||
|
padding: 26px 30px !important;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
background: #fbfcfe;
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 14px 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 4px 18px;
|
||||||
|
margin: 0 0 18px;
|
||||||
|
|
||||||
|
&-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 11px 0;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--highlight {
|
||||||
|
.pcd__preview-value {
|
||||||
|
color: #b91c1c;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--total {
|
||||||
|
margin: 0 -18px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
|
||||||
|
.pcd__preview-label {
|
||||||
|
color: #b91c1c;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__preview-value {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-value {
|
||||||
|
font-family: 'JetBrains Mono', 'DM Sans', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__section-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #475569;
|
||||||
|
margin: 10px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Options Grid - 2 Equal Columns */
|
||||||
|
.pcd__options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
|
||||||
|
@media (width <= 560px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__option {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
text-align: left;
|
||||||
|
background: #fff;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #fca5a5;
|
||||||
|
background: #f8fafc;
|
||||||
|
box-shadow: 0 6px 16px rgb(15 23 42 / 6%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
border-color: #dc2626;
|
||||||
|
background: #fef2f2;
|
||||||
|
box-shadow: 0 0 0 1.5px #dc2626;
|
||||||
|
|
||||||
|
.pcd__option-ico {
|
||||||
|
background: #dc2626;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-ico {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
min-width: 38px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #dc2626;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-check {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 17px;
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
right: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__option-error {
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: -10px 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
border-left: 4px solid #0284c7;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #0369a1;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 0 0 18px;
|
||||||
|
|
||||||
|
&--warn {
|
||||||
|
background: #fffbeb;
|
||||||
|
border-color: #fde68a;
|
||||||
|
border-left-color: #d97706;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__error {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-left: 4px solid #ef4444;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__actions {
|
||||||
|
padding: 18px 30px !important;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.pcd__btn-cancel {
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd__btn-submit {
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 26px;
|
||||||
|
height: 42px;
|
||||||
|
background: linear-gradient(135deg, #b91c1c 0%, #dc2626 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 20px rgb(220 38 38 / 30%);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
box-shadow: 0 10px 24px rgb(220 38 38 / 40%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #cbd5e1;
|
||||||
|
box-shadow: none;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+218
@@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Angular Imports */
|
||||||
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
|
import {
|
||||||
|
MAT_DIALOG_DATA,
|
||||||
|
MatDialogRef,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose
|
||||||
|
} from '@angular/material/dialog';
|
||||||
|
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||||
|
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
|
||||||
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { FixedDepositsService } from '../../../fixed-deposits.service';
|
||||||
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
|
import { Dates } from 'app/core/utils/dates';
|
||||||
|
import { FormatNumberPipe } from '../../../../../pipes/format-number.pipe';
|
||||||
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Premature Close" dialog -- Active-only, no reinvest option (the deposit hasn't matured, so there's
|
||||||
|
* nothing to renew): pay out in cash, or transfer the pre-maturity balance to a savings account. Same
|
||||||
|
* `.pcd__*` visual language as the Payment dialog ({@link PaymentDialogComponent}), in this Danger Zone
|
||||||
|
* action's own red accent rather than Payment's blue, since it's an irreversible, usually
|
||||||
|
* penalty-bearing action rather than a routine one.
|
||||||
|
* <p>
|
||||||
|
* The premature amount is date-dependent (unlike Payment's one-shot template load): every change to
|
||||||
|
* Closed On Date re-calls `calculatePrematureAmount`, same as the full-page form this replaces did.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-premature-close-dialog',
|
||||||
|
templateUrl: './premature-close-dialog.component.html',
|
||||||
|
styleUrls: ['./premature-close-dialog.component.scss'],
|
||||||
|
imports: [
|
||||||
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
|
CdkScrollable,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose,
|
||||||
|
CdkTextareaAutosize,
|
||||||
|
FaIconComponent,
|
||||||
|
FormatNumberPipe
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class PrematureCloseDialogComponent implements OnInit {
|
||||||
|
/** The `onAccountClosureId` codes Premature Close can offer -- same codes Payment/Close use. */
|
||||||
|
private static readonly WITHDRAW_DEPOSIT_ID = 100;
|
||||||
|
private static readonly TRANSFER_TO_SAVINGS_ID = 200;
|
||||||
|
|
||||||
|
private static readonly ACTION_ICONS: Record<number, string> = {
|
||||||
|
[PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'money-bill-wave',
|
||||||
|
[PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'wallet'
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly ACTION_DESCRIPTIONS: Record<number, string> = {
|
||||||
|
[PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'Pay out the full balance in cash',
|
||||||
|
[PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'Move the full balance to another savings account'
|
||||||
|
};
|
||||||
|
|
||||||
|
dialogRef = inject<MatDialogRef<PrematureCloseDialogComponent>>(MatDialogRef);
|
||||||
|
data: {
|
||||||
|
accountId: number | string;
|
||||||
|
accountNo?: string;
|
||||||
|
depositAmount?: number;
|
||||||
|
nominalAnnualInterestRate?: number;
|
||||||
|
preClosurePenalApplicable?: boolean;
|
||||||
|
preClosurePenalInterest?: number;
|
||||||
|
preClosurePenalInterestOnType?: { value: string };
|
||||||
|
} = inject(MAT_DIALOG_DATA);
|
||||||
|
private formBuilder = inject(UntypedFormBuilder);
|
||||||
|
private fixedDepositsService = inject(FixedDepositsService);
|
||||||
|
private settingsService = inject(SettingsService);
|
||||||
|
private dateUtils = inject(Dates);
|
||||||
|
|
||||||
|
prematureCloseForm: UntypedFormGroup;
|
||||||
|
onAccountClosureOptions: any[] = [];
|
||||||
|
savingsAccountsData: any[] = [];
|
||||||
|
maturityAmount: number = null;
|
||||||
|
loadingAmount = true;
|
||||||
|
loadFailed = false;
|
||||||
|
submitting = false;
|
||||||
|
submitError: string = null;
|
||||||
|
minDate = new Date(2000, 0, 1);
|
||||||
|
maxDate: Date;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.maxDate = this.settingsService.businessDate;
|
||||||
|
this.prematureCloseForm = this.formBuilder.group({
|
||||||
|
closedOnDate: [
|
||||||
|
this.maxDate,
|
||||||
|
Validators.required
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.prematureCloseForm
|
||||||
|
.get('closedOnDate')
|
||||||
|
.valueChanges.subscribe((date: Date) => this.calculatePrematureAmount(date));
|
||||||
|
// Same date the form already starts with -- without this the amount/options never load unless the
|
||||||
|
// maker happens to touch the date picker, which the full-page form this replaces required too.
|
||||||
|
this.calculatePrematureAmount(this.maxDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculatePrematureAmount(date: Date): void {
|
||||||
|
this.loadingAmount = true;
|
||||||
|
this.loadFailed = false;
|
||||||
|
const locale = this.settingsService.language.code;
|
||||||
|
const dateFormat = this.settingsService.dateFormat;
|
||||||
|
const data = {
|
||||||
|
closedOnDate: this.dateUtils.formatDate(date, dateFormat),
|
||||||
|
dateFormat,
|
||||||
|
locale
|
||||||
|
};
|
||||||
|
this.fixedDepositsService
|
||||||
|
.executeFixedDepositsAccountCommand(this.data.accountId as string, 'calculatePrematureAmount', data)
|
||||||
|
.subscribe({
|
||||||
|
next: (response: any) => {
|
||||||
|
this.savingsAccountsData = response.savingsAccounts ?? [];
|
||||||
|
this.onAccountClosureOptions = response.onAccountClosureOptions ?? [];
|
||||||
|
this.maturityAmount = response.maturityAmount;
|
||||||
|
this.loadingAmount = false;
|
||||||
|
if (!this.prematureCloseForm.contains('onAccountClosureId')) {
|
||||||
|
this.prematureCloseForm.addControl('onAccountClosureId', new UntypedFormControl('', Validators.required));
|
||||||
|
this.prematureCloseForm.addControl('note', new UntypedFormControl(''));
|
||||||
|
this.prematureCloseForm
|
||||||
|
.get('onAccountClosureId')
|
||||||
|
.valueChanges.subscribe((id: number) => this.onClosureTypeChange(id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Without this the spinner would run forever on a failed load, leaving Cancel as the only way out.
|
||||||
|
error: () => {
|
||||||
|
this.loadingAmount = false;
|
||||||
|
this.loadFailed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds/removes the destination-account control to match what the chosen type needs. */
|
||||||
|
private onClosureTypeChange(id: number): void {
|
||||||
|
if (id === PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID) {
|
||||||
|
if (!this.prematureCloseForm.contains('toSavingsAccountId')) {
|
||||||
|
this.prematureCloseForm.addControl('toSavingsAccountId', new UntypedFormControl('', Validators.required));
|
||||||
|
this.prematureCloseForm.addControl('transferDescription', new UntypedFormControl(''));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.prematureCloseForm.removeControl('toSavingsAccountId');
|
||||||
|
this.prematureCloseForm.removeControl('transferDescription');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The gap between the deposit and the computed maturity amount -- the actual interest this premature
|
||||||
|
* closure earns, at whatever rate (contracted, or the lower pre-closure-penalty one below) applied. */
|
||||||
|
get interestEarned(): number | null {
|
||||||
|
if (this.maturityAmount === null || this.maturityAmount === undefined || !this.data.depositAmount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Number(this.maturityAmount) - Number(this.data.depositAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the chosen action needs a destination account but the customer has none to pick. */
|
||||||
|
get noDestinationAccounts(): boolean {
|
||||||
|
return (
|
||||||
|
!this.loadingAmount &&
|
||||||
|
this.prematureCloseForm?.contains('toSavingsAccountId') &&
|
||||||
|
this.savingsAccountsData.length === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
iconFor(id: number): string {
|
||||||
|
return PrematureCloseDialogComponent.ACTION_ICONS[id] ?? 'circle';
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptionFor(id: number): string {
|
||||||
|
return PrematureCloseDialogComponent.ACTION_DESCRIPTIONS[id] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
selectClosureType(id: number): void {
|
||||||
|
this.prematureCloseForm.get('onAccountClosureId').setValue(id);
|
||||||
|
this.prematureCloseForm.get('onAccountClosureId').markAsTouched();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(): void {
|
||||||
|
const formValue = { ...this.prematureCloseForm.value };
|
||||||
|
const locale = this.settingsService.language.code;
|
||||||
|
const dateFormat = this.settingsService.dateFormat;
|
||||||
|
if (formValue.closedOnDate instanceof Date) {
|
||||||
|
formValue.closedOnDate = this.dateUtils.formatDate(formValue.closedOnDate, dateFormat);
|
||||||
|
}
|
||||||
|
const data = { ...formValue, dateFormat, locale };
|
||||||
|
this.submitting = true;
|
||||||
|
this.submitError = null;
|
||||||
|
this.fixedDepositsService
|
||||||
|
.executeFixedDepositsAccountCommand(this.data.accountId as string, 'prematureClose', data)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.dialogRef.close({ confirm: true });
|
||||||
|
},
|
||||||
|
// Keep the dialog open with the entered values and say what went wrong, rather than leaving a
|
||||||
|
// permanently disabled Confirm behind whatever modal the global interceptor threw up.
|
||||||
|
error: (error: any) => {
|
||||||
|
this.submitting = false;
|
||||||
|
this.submitError =
|
||||||
|
error?.error?.defaultUserMessage ??
|
||||||
|
error?.error?.errors?.[0]?.defaultUserMessage ??
|
||||||
|
'This account could not be prematurely closed. Please review the details and try again.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
-3
@@ -17,6 +17,7 @@ import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-int
|
|||||||
import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-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';
|
import { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component';
|
||||||
import { PaymentDialogComponent } from './custom-dialogs/payment-dialog/payment-dialog.component';
|
import { PaymentDialogComponent } from './custom-dialogs/payment-dialog/payment-dialog.component';
|
||||||
|
import { PrematureCloseDialogComponent } from './custom-dialogs/premature-close-dialog/premature-close-dialog.component';
|
||||||
import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component';
|
import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component';
|
||||||
|
|
||||||
/** Custom Button Config. */
|
/** Custom Button Config. */
|
||||||
@@ -238,7 +239,6 @@ export class FixedDepositAccountViewComponent implements OnInit {
|
|||||||
case 'Undo Activation':
|
case 'Undo Activation':
|
||||||
case 'Add Charge':
|
case 'Add Charge':
|
||||||
case 'Withdrawn by Client':
|
case 'Withdrawn by Client':
|
||||||
case 'Premature Close':
|
|
||||||
case 'Deposit':
|
case 'Deposit':
|
||||||
case 'Withdrawal':
|
case 'Withdrawal':
|
||||||
this.router.navigate([`actions/${name}`], { relativeTo: this.route });
|
this.router.navigate([`actions/${name}`], { relativeTo: this.route });
|
||||||
@@ -267,12 +267,17 @@ export class FixedDepositAccountViewComponent implements OnInit {
|
|||||||
case 'Payment':
|
case 'Payment':
|
||||||
this.openPaymentDialog();
|
this.openPaymentDialog();
|
||||||
break;
|
break;
|
||||||
|
case 'Premature Close':
|
||||||
|
this.openPrematureCloseDialog();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the Payment action -- unlike Premature Close/Close, works whether the account is Active or
|
* Opens the Payment action -- only offered once the FD has actually Matured (see
|
||||||
* Matured, and is the only place a Partial Reinvest can be done.
|
* FixedDepositsButtonsConfiguration), and the only place a Partial Reinvest can be done. Also the
|
||||||
|
* exact same dialog the FD Dashboard's Renew action opens (see fd-dashboard.component.ts's
|
||||||
|
* openRenewDialog), just without dashboardRenewal data, so its date picker stays visible.
|
||||||
*/
|
*/
|
||||||
private openPaymentDialog(): void {
|
private openPaymentDialog(): void {
|
||||||
const paymentDialogRef = this.dialog.open(PaymentDialogComponent, {
|
const paymentDialogRef = this.dialog.open(PaymentDialogComponent, {
|
||||||
@@ -293,6 +298,34 @@ export class FixedDepositAccountViewComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the Premature Close action -- Active-only, no reinvest option, since the deposit hasn't
|
||||||
|
* matured yet.
|
||||||
|
*/
|
||||||
|
private openPrematureCloseDialog(): void {
|
||||||
|
const prematureCloseDialogRef = this.dialog.open(PrematureCloseDialogComponent, {
|
||||||
|
width: '720px',
|
||||||
|
maxWidth: '95vw',
|
||||||
|
data: {
|
||||||
|
accountId: this.fixedDepositsAccountData.id,
|
||||||
|
accountNo: this.fixedDepositsAccountData.fdAccountNumber || this.fixedDepositsAccountData.accountNo,
|
||||||
|
// calculatePrematureAmount only ever returns the final maturityAmount, not how it was reached --
|
||||||
|
// these come from the account's own data (already loaded for this page) so the dialog can show
|
||||||
|
// the breakdown (deposit, contracted rate, penalty, interest earned) behind that one number.
|
||||||
|
depositAmount: this.fixedDepositsAccountData.depositAmount,
|
||||||
|
nominalAnnualInterestRate: this.fixedDepositsAccountData.nominalAnnualInterestRate,
|
||||||
|
preClosurePenalApplicable: this.fixedDepositsAccountData.preClosurePenalApplicable,
|
||||||
|
preClosurePenalInterest: this.fixedDepositsAccountData.preClosurePenalInterest,
|
||||||
|
preClosurePenalInterestOnType: this.fixedDepositsAccountData.preClosurePenalInterestOnType
|
||||||
|
}
|
||||||
|
});
|
||||||
|
prematureCloseDialogRef.afterClosed().subscribe((response: any) => {
|
||||||
|
if (response?.confirm) {
|
||||||
|
this.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes Fixed Deposits Account.
|
* Deletes Fixed Deposits Account.
|
||||||
*/
|
*/
|
||||||
|
|||||||
-3
@@ -24,9 +24,6 @@
|
|||||||
@if (actions['Add Charge']) {
|
@if (actions['Add Charge']) {
|
||||||
<mifosx-add-charge-fixed-deposits-account></mifosx-add-charge-fixed-deposits-account>
|
<mifosx-add-charge-fixed-deposits-account></mifosx-add-charge-fixed-deposits-account>
|
||||||
}
|
}
|
||||||
@if (actions['Premature Close']) {
|
|
||||||
<mifosx-premature-close-fixed-deposits-account></mifosx-premature-close-fixed-deposits-account>
|
|
||||||
}
|
|
||||||
@if (actions['Close']) {
|
@if (actions['Close']) {
|
||||||
<mifosx-close-fixed-deposits-account></mifosx-close-fixed-deposits-account>
|
<mifosx-close-fixed-deposits-account></mifosx-close-fixed-deposits-account>
|
||||||
}
|
}
|
||||||
|
|||||||
-4
@@ -15,7 +15,6 @@ import { ActivateFixedDepositsAccountComponent } from './activate-fixed-deposits
|
|||||||
import { UndoApprovalFixedDepositsAccountComponent } from './undo-approval-fixed-deposits-account/undo-approval-fixed-deposits-account.component';
|
import { UndoApprovalFixedDepositsAccountComponent } from './undo-approval-fixed-deposits-account/undo-approval-fixed-deposits-account.component';
|
||||||
import { WithdrawByClientFixedDepositsAccountComponent } from './withdraw-by-client-fixed-deposits-account/withdraw-by-client-fixed-deposits-account.component';
|
import { WithdrawByClientFixedDepositsAccountComponent } from './withdraw-by-client-fixed-deposits-account/withdraw-by-client-fixed-deposits-account.component';
|
||||||
import { AddChargeFixedDepositsAccountComponent } from './add-charge-fixed-deposits-account/add-charge-fixed-deposits-account.component';
|
import { AddChargeFixedDepositsAccountComponent } from './add-charge-fixed-deposits-account/add-charge-fixed-deposits-account.component';
|
||||||
import { PrematureCloseFixedDepositsAccountComponent } from './premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component';
|
|
||||||
import { CloseFixedDepositsAccountComponent } from './close-fixed-deposits-account/close-fixed-deposits-account.component';
|
import { CloseFixedDepositsAccountComponent } from './close-fixed-deposits-account/close-fixed-deposits-account.component';
|
||||||
import { FixedDepositsCashTransactionComponent } from './fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component';
|
import { FixedDepositsCashTransactionComponent } from './fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
@@ -35,7 +34,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
UndoApprovalFixedDepositsAccountComponent,
|
UndoApprovalFixedDepositsAccountComponent,
|
||||||
WithdrawByClientFixedDepositsAccountComponent,
|
WithdrawByClientFixedDepositsAccountComponent,
|
||||||
AddChargeFixedDepositsAccountComponent,
|
AddChargeFixedDepositsAccountComponent,
|
||||||
PrematureCloseFixedDepositsAccountComponent,
|
|
||||||
CloseFixedDepositsAccountComponent,
|
CloseFixedDepositsAccountComponent,
|
||||||
FixedDepositsCashTransactionComponent
|
FixedDepositsCashTransactionComponent
|
||||||
]
|
]
|
||||||
@@ -52,7 +50,6 @@ export class FixedDepositsAccountActionsComponent {
|
|||||||
'Undo Approval': boolean;
|
'Undo Approval': boolean;
|
||||||
'Undo Activation': boolean;
|
'Undo Activation': boolean;
|
||||||
'Add Charge': boolean;
|
'Add Charge': boolean;
|
||||||
'Premature Close': boolean;
|
|
||||||
'Withdrawn by Client': boolean;
|
'Withdrawn by Client': boolean;
|
||||||
Withdrawal: boolean;
|
Withdrawal: boolean;
|
||||||
Deposit: boolean;
|
Deposit: boolean;
|
||||||
@@ -64,7 +61,6 @@ export class FixedDepositsAccountActionsComponent {
|
|||||||
'Undo Approval': false,
|
'Undo Approval': false,
|
||||||
'Undo Activation': false,
|
'Undo Activation': false,
|
||||||
'Add Charge': false,
|
'Add Charge': false,
|
||||||
'Premature Close': false,
|
|
||||||
'Withdrawn by Client': false,
|
'Withdrawn by Client': false,
|
||||||
Withdrawal: false,
|
Withdrawal: false,
|
||||||
Deposit: false
|
Deposit: false
|
||||||
|
|||||||
-105
@@ -1,105 +0,0 @@
|
|||||||
<!--
|
|
||||||
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="container">
|
|
||||||
<mat-card>
|
|
||||||
<form [formGroup]="prematureCloseAccountForm" (ngSubmit)="submit()">
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="layout-column">
|
|
||||||
<mat-form-field (click)="closedOnDatePicker.open()">
|
|
||||||
<mat-label>{{ 'labels.inputs.Premature Close Date' | translate }}</mat-label>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
[min]="minDate"
|
|
||||||
[max]="maxDate"
|
|
||||||
[matDatepicker]="closedOnDatePicker"
|
|
||||||
required
|
|
||||||
formControlName="closedOnDate"
|
|
||||||
/>
|
|
||||||
<mat-datepicker-toggle matSuffix [for]="closedOnDatePicker"></mat-datepicker-toggle>
|
|
||||||
<mat-datepicker #closedOnDatePicker></mat-datepicker>
|
|
||||||
@if (prematureCloseAccountForm.controls.closedOnDate.hasError('required')) {
|
|
||||||
<mat-error>
|
|
||||||
{{ 'labels.inputs.Premature Close Date' | translate }} {{ 'labels.commons.is' | translate }}
|
|
||||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.controls.maturityAmount) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Maturity Amount' | translate }}</mat-label>
|
|
||||||
<input type="number" matInput formControlName="maturityAmount" />
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.contains('onAccountClosureId')) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Action' | translate }}</mat-label>
|
|
||||||
<mat-select required formControlName="onAccountClosureId">
|
|
||||||
@for (closureOption of onAccountClosureOptions; track closureOption) {
|
|
||||||
<mat-option [value]="closureOption.id">
|
|
||||||
{{ closureOption.value }}
|
|
||||||
</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
@if (prematureCloseAccountForm.controls.onAccountClosureId.hasError('required')) {
|
|
||||||
<mat-error>
|
|
||||||
{{ 'labels.inputs.Account action' | translate }} {{ 'labels.commons.is' | translate }}
|
|
||||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.contains('toSavingsAccountId')) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Transfer to Savings' | translate }}</mat-label>
|
|
||||||
<mat-select required formControlName="toSavingsAccountId">
|
|
||||||
@for (account of savingsAccountsData; track account) {
|
|
||||||
<mat-option [value]="account.id">
|
|
||||||
{{ account.accountNo }}
|
|
||||||
</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
@if (prematureCloseAccountForm.controls.toSavingsAccountId.hasError('required')) {
|
|
||||||
<mat-error>
|
|
||||||
{{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }}
|
|
||||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.contains('transferDescription')) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Transfer Description' | translate }}</mat-label>
|
|
||||||
<input matInput formControlName="transferDescription" />
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.contains('note')) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
|
||||||
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
|
|
||||||
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
|
|
||||||
<button type="button" mat-raised-button [routerLink]="['../../']">
|
|
||||||
{{ 'labels.buttons.Cancel' | translate }}
|
|
||||||
</button>
|
|
||||||
<button mat-raised-button color="warn" [disabled]="!prematureCloseAccountForm.valid">
|
|
||||||
{{ 'labels.buttons.Confirm' | translate }}
|
|
||||||
</button>
|
|
||||||
</mat-card-actions>
|
|
||||||
</form>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 37rem;
|
|
||||||
}
|
|
||||||
-179
@@ -1,179 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Angular Imports */
|
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
|
||||||
import {
|
|
||||||
UntypedFormGroup,
|
|
||||||
UntypedFormBuilder,
|
|
||||||
Validators,
|
|
||||||
UntypedFormControl,
|
|
||||||
ReactiveFormsModule
|
|
||||||
} from '@angular/forms';
|
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
|
||||||
|
|
||||||
/** Custom Services */
|
|
||||||
import { FixedDepositsService } from '../../fixed-deposits.service';
|
|
||||||
import { SettingsService } from 'app/settings/settings.service';
|
|
||||||
import { Dates } from 'app/core/utils/dates';
|
|
||||||
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
|
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Premature Close Fixed Deposits Account Component
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'mifosx-premature-close-fixed-deposits-account',
|
|
||||||
templateUrl: './premature-close-fixed-deposits-account.component.html',
|
|
||||||
styleUrls: ['./premature-close-fixed-deposits-account.component.scss'],
|
|
||||||
imports: [
|
|
||||||
...STANDALONE_SHARED_IMPORTS,
|
|
||||||
CdkTextareaAutosize
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class PrematureCloseFixedDepositsAccountComponent implements OnInit {
|
|
||||||
private formBuilder = inject(UntypedFormBuilder);
|
|
||||||
private fixedDepositsService = inject(FixedDepositsService);
|
|
||||||
private dateUtils = inject(Dates);
|
|
||||||
private route = inject(ActivatedRoute);
|
|
||||||
private router = inject(Router);
|
|
||||||
private settingsService = inject(SettingsService);
|
|
||||||
|
|
||||||
/** Minimum date allowed. */
|
|
||||||
minDate = new Date(2000, 0, 1);
|
|
||||||
/** Maximum date allowed. */
|
|
||||||
maxDate = new Date();
|
|
||||||
/** Premature Close FD Account form. */
|
|
||||||
prematureCloseAccountForm: UntypedFormGroup;
|
|
||||||
/** Savings Account Data */
|
|
||||||
savingsAccountsData: any;
|
|
||||||
/** On account Closure Options */
|
|
||||||
onAccountClosureOptions: any;
|
|
||||||
/** Fixed Deposits Account Id */
|
|
||||||
accountId: any;
|
|
||||||
/** Form submission event */
|
|
||||||
isSubmitted = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {FormBuilder} formBuilder Form Builder
|
|
||||||
* @param {FixedDepositsService} fixedDepositsService Fixed Deposits Service
|
|
||||||
* @param {Dates} dateUtils Date Utils
|
|
||||||
* @param {ActivatedRoute} route Activated Route
|
|
||||||
* @param {Router} router Router
|
|
||||||
* @param {SettingsService} settingsService Settings Service
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
this.accountId = this.route.parent.snapshot.params['fixedDepositAccountId'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the premature close fd account form.
|
|
||||||
*/
|
|
||||||
ngOnInit() {
|
|
||||||
this.maxDate = this.settingsService.businessDate;
|
|
||||||
this.createPrematureCloseAccountForm();
|
|
||||||
this.buildDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the premature close fd account form.
|
|
||||||
*/
|
|
||||||
createPrematureCloseAccountForm() {
|
|
||||||
this.prematureCloseAccountForm = this.formBuilder.group({
|
|
||||||
closedOnDate: [
|
|
||||||
'',
|
|
||||||
Validators.required
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribes to value changes of parent control.
|
|
||||||
*/
|
|
||||||
buildDependencies() {
|
|
||||||
this.prematureCloseAccountForm.get('closedOnDate').valueChanges.subscribe((value: Date) => {
|
|
||||||
if (!this.isSubmitted) {
|
|
||||||
this.calculatePrematureAmount(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates prematue amount based on closed on date.
|
|
||||||
* @param {Date} date Premature Close Date
|
|
||||||
*/
|
|
||||||
calculatePrematureAmount(date: Date) {
|
|
||||||
const locale = this.settingsService.language.code;
|
|
||||||
const dateFormat = this.settingsService.dateFormat;
|
|
||||||
const data = {
|
|
||||||
closedOnDate: this.dateUtils.formatDate(date, dateFormat),
|
|
||||||
dateFormat,
|
|
||||||
locale
|
|
||||||
};
|
|
||||||
this.fixedDepositsService
|
|
||||||
.executeFixedDepositsAccountCommand(this.accountId, 'calculatePrematureAmount', data)
|
|
||||||
.subscribe((response: any) => {
|
|
||||||
this.savingsAccountsData = response.savingsAccounts;
|
|
||||||
this.onAccountClosureOptions = response.onAccountClosureOptions;
|
|
||||||
this.prematureCloseAccountForm.addControl(
|
|
||||||
'maturityAmount',
|
|
||||||
new UntypedFormControl({ value: '', disabled: true })
|
|
||||||
);
|
|
||||||
this.prematureCloseAccountForm.addControl(
|
|
||||||
'onAccountClosureId',
|
|
||||||
new UntypedFormControl('', Validators.required)
|
|
||||||
);
|
|
||||||
this.prematureCloseAccountForm.addControl('note', new UntypedFormControl(''));
|
|
||||||
this.prematureCloseAccountForm.get('maturityAmount').patchValue(response.maturityAmount);
|
|
||||||
this.addTransferDetails();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribes to value changes of `onAccountClosureId` adds and removes transfer details accordingly.
|
|
||||||
*/
|
|
||||||
addTransferDetails() {
|
|
||||||
this.prematureCloseAccountForm.get('onAccountClosureId').valueChanges.subscribe((id: any) => {
|
|
||||||
if (id === 200) {
|
|
||||||
this.prematureCloseAccountForm.addControl(
|
|
||||||
'toSavingsAccountId',
|
|
||||||
new UntypedFormControl('', Validators.required)
|
|
||||||
);
|
|
||||||
this.prematureCloseAccountForm.addControl('transferDescription', new UntypedFormControl(''));
|
|
||||||
} else {
|
|
||||||
this.prematureCloseAccountForm.removeControl('toSavingsAccountId');
|
|
||||||
this.prematureCloseAccountForm.removeControl('transferDescription');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits the form and premature closes the fd account,
|
|
||||||
* if successful redirects to the fd account.
|
|
||||||
*/
|
|
||||||
submit() {
|
|
||||||
const prematureCloseAccountFormData = this.prematureCloseAccountForm.value;
|
|
||||||
this.isSubmitted = true;
|
|
||||||
const locale = this.settingsService.language.code;
|
|
||||||
const dateFormat = this.settingsService.dateFormat;
|
|
||||||
const prevClosedDate: Date = this.prematureCloseAccountForm.value.closedOnDate;
|
|
||||||
if (prematureCloseAccountFormData.closedOnDate instanceof Date) {
|
|
||||||
prematureCloseAccountFormData.closedOnDate = this.dateUtils.formatDate(prevClosedDate, dateFormat);
|
|
||||||
}
|
|
||||||
const data = {
|
|
||||||
...prematureCloseAccountFormData,
|
|
||||||
dateFormat,
|
|
||||||
locale
|
|
||||||
};
|
|
||||||
this.fixedDepositsService
|
|
||||||
.executeFixedDepositsAccountCommand(this.accountId, 'prematureClose', data)
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,6 @@ import { ViewTransactionComponent } from './fixed-deposit-account-view/view-tran
|
|||||||
import { WaiveChargeDialogComponent } from './fixed-deposit-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component';
|
import { WaiveChargeDialogComponent } from './fixed-deposit-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component';
|
||||||
import { InactivateChargeDialogComponent } from './fixed-deposit-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component';
|
import { InactivateChargeDialogComponent } from './fixed-deposit-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component';
|
||||||
import { AddChargeFixedDepositsAccountComponent } from './fixed-deposits-account-actions/add-charge-fixed-deposits-account/add-charge-fixed-deposits-account.component';
|
import { AddChargeFixedDepositsAccountComponent } from './fixed-deposits-account-actions/add-charge-fixed-deposits-account/add-charge-fixed-deposits-account.component';
|
||||||
import { PrematureCloseFixedDepositsAccountComponent } from './fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component';
|
|
||||||
import { CloseFixedDepositsAccountComponent } from './fixed-deposits-account-actions/close-fixed-deposits-account/close-fixed-deposits-account.component';
|
import { CloseFixedDepositsAccountComponent } from './fixed-deposits-account-actions/close-fixed-deposits-account/close-fixed-deposits-account.component';
|
||||||
import { CreateFixedDepositAccountComponent } from './create-fixed-deposit-account/create-fixed-deposit-account.component';
|
import { CreateFixedDepositAccountComponent } from './create-fixed-deposit-account/create-fixed-deposit-account.component';
|
||||||
import { FixedDepositAccountDetailsStepComponent } from './fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component';
|
import { FixedDepositAccountDetailsStepComponent } from './fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component';
|
||||||
@@ -78,7 +77,6 @@ import { FixedDepositsCashTransactionComponent } from './fixed-deposits-account-
|
|||||||
WaiveChargeDialogComponent,
|
WaiveChargeDialogComponent,
|
||||||
InactivateChargeDialogComponent,
|
InactivateChargeDialogComponent,
|
||||||
AddChargeFixedDepositsAccountComponent,
|
AddChargeFixedDepositsAccountComponent,
|
||||||
PrematureCloseFixedDepositsAccountComponent,
|
|
||||||
CloseFixedDepositsAccountComponent,
|
CloseFixedDepositsAccountComponent,
|
||||||
CreateFixedDepositAccountComponent,
|
CreateFixedDepositAccountComponent,
|
||||||
FixedDepositAccountDetailsStepComponent,
|
FixedDepositAccountDetailsStepComponent,
|
||||||
|
|||||||
Reference in New Issue
Block a user