share certificate and share product maker checker
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<!--
|
||||
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">
|
||||
@if (hasPendingEdit) {
|
||||
<div class="mat-elevation-z1" style="padding: 12px 16px; margin-bottom: 16px; background-color: #fff8e1">
|
||||
<fa-icon icon="lock" class="m-r-10"></fa-icon>
|
||||
An edit submitted by {{ pendingEdit.makerUsername }} is already pending approval. You cannot submit another edit
|
||||
until it is resolved.
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-stepper
|
||||
orientation="vertical"
|
||||
[linear]="false"
|
||||
class="mat-elevation-z8"
|
||||
labelPosition="bottom"
|
||||
#shareProductStepper
|
||||
>
|
||||
<ng-template matStepperIcon="number">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="edit">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon icon="check" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="error">
|
||||
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="preview">
|
||||
<fa-icon icon="eye" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<mat-step [stepControl]="sharesAccountDetailsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-shares-account-details-step
|
||||
[sharesAccountTemplate]="sharesAccountAndTemplate"
|
||||
[approvalEditMode]="true"
|
||||
(sharesAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-shares-account-details-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="sharesAccountTermsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-shares-account-terms-step
|
||||
[sharesAccountProductTemplate]="sharesAccountProductTemplate"
|
||||
[sharesAccountTemplate]="sharesAccountAndTemplate"
|
||||
[approvalEditMode]="true"
|
||||
>
|
||||
</mifosx-shares-account-terms-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
|
||||
|
||||
<mifosx-shares-account-charges-step
|
||||
[sharesAccountProductTemplate]="sharesAccountProductTemplate"
|
||||
[sharesAccountTemplate]="sharesAccountAndTemplate"
|
||||
[currencyCode]="sharesAccountTermsForm.get('currencyCode')"
|
||||
[approvalEditMode]="true"
|
||||
>
|
||||
</mifosx-shares-account-charges-step>
|
||||
</mat-step>
|
||||
|
||||
@if (sharesAccountFormValidAndNotPristine) {
|
||||
<mat-step completed>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
|
||||
<mifosx-shares-account-preview-step
|
||||
[sharesAccountProductTemplate]="sharesAccountProductTemplate"
|
||||
[sharesAccountTemplate]="sharesAccountAndTemplate"
|
||||
[sharesAccountTermsForm]="sharesAccountTermsForm"
|
||||
[sharesAccount]="sharesAccount"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-shares-account-preview-step>
|
||||
</mat-step>
|
||||
}
|
||||
</mat-stepper>
|
||||
</div>
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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, ViewChild, inject } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** Custom Components */
|
||||
import { SharesAccountDetailsStepComponent } from '../shares-account-stepper/shares-account-details-step/shares-account-details-step.component';
|
||||
import { SharesAccountTermsStepComponent } from '../shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component';
|
||||
import { SharesAccountChargesStepComponent } from '../shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component';
|
||||
import { SharesAccountPreviewStepComponent } from '../shares-account-stepper/shares-account-preview-step/shares-account-preview-step.component';
|
||||
|
||||
/** Custom Services */
|
||||
import { SharesService } from '../shares.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
|
||||
/**
|
||||
* Edit Share Account Details — maker-checker submission screen.
|
||||
*
|
||||
* Reuses the same Details/Terms/Charges/Preview stepper as Create/Modify Application, but with
|
||||
* Product, External ID, Resolution Number, Total Number of Shares, Application Date and Charges
|
||||
* locked (`approvalEditMode`) - only Default Savings Account, Allow Dividends for Inactive Clients,
|
||||
* Minimum Active Period and Lockin Period are actually editable and submitted. Nothing is applied
|
||||
* immediately: the request is staged and a different user (checker) must approve it from the
|
||||
* "Pending Share Account Edits" screen before it takes effect.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-edit-share-account-details',
|
||||
templateUrl: './edit-share-account-details.component.html',
|
||||
styleUrls: ['./edit-share-account-details.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatStepper,
|
||||
MatStepperIcon,
|
||||
FaIconComponent,
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
SharesAccountDetailsStepComponent,
|
||||
SharesAccountTermsStepComponent,
|
||||
SharesAccountChargesStepComponent,
|
||||
SharesAccountPreviewStepComponent
|
||||
]
|
||||
})
|
||||
export class EditShareAccountDetailsComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private sharesService = inject(SharesService);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
/** Shares Account and Template */
|
||||
sharesAccountAndTemplate: any;
|
||||
/** Shares Account Product Template */
|
||||
sharesAccountProductTemplate: any;
|
||||
|
||||
/** Pending edit already submitted for this account, if any. */
|
||||
pendingEdit: any = null;
|
||||
pendingEditLoading = true;
|
||||
submitting = false;
|
||||
|
||||
@ViewChild(SharesAccountDetailsStepComponent, { static: true })
|
||||
sharesAccountDetailsStep: SharesAccountDetailsStepComponent;
|
||||
@ViewChild(SharesAccountTermsStepComponent, { static: true }) sharesAccountTermsStep: SharesAccountTermsStepComponent;
|
||||
@ViewChild(SharesAccountChargesStepComponent, { static: true })
|
||||
sharesAccountChargesStep: SharesAccountChargesStepComponent;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { sharesAccountAndTemplate: any }) => {
|
||||
this.sharesAccountAndTemplate = data.sharesAccountAndTemplate;
|
||||
this.loadPendingEdit();
|
||||
});
|
||||
}
|
||||
|
||||
private loadPendingEdit(): void {
|
||||
this.pendingEditLoading = true;
|
||||
this.sharesService.getPendingShareAccountEdit(this.sharesAccountAndTemplate.id).subscribe({
|
||||
next: (edit: any) => {
|
||||
this.pendingEdit = edit && edit.status === 'PENDING' ? edit : null;
|
||||
this.pendingEditLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.pendingEdit = null;
|
||||
this.pendingEditLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get hasPendingEdit(): boolean {
|
||||
return !!this.pendingEdit;
|
||||
}
|
||||
|
||||
setTemplate($event: any) {
|
||||
this.sharesAccountProductTemplate = $event;
|
||||
}
|
||||
|
||||
get sharesAccountDetailsForm() {
|
||||
return this.sharesAccountDetailsStep.sharesAccountDetailsForm;
|
||||
}
|
||||
|
||||
get sharesAccountTermsForm() {
|
||||
return this.sharesAccountTermsStep.sharesAccountTermsForm;
|
||||
}
|
||||
|
||||
get sharesAccountFormValidAndNotPristine() {
|
||||
return (
|
||||
this.sharesAccountTermsForm.valid &&
|
||||
!this.sharesAccountTermsForm.pristine &&
|
||||
!this.hasPendingEdit &&
|
||||
!this.pendingEditLoading
|
||||
);
|
||||
}
|
||||
|
||||
get sharesAccount() {
|
||||
return {
|
||||
...this.sharesAccountDetailsStep.sharesAccountDetails,
|
||||
...this.sharesAccountTermsStep.sharesAccountTerms,
|
||||
...this.sharesAccountChargesStep.sharesAccountCharges
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits only the 6 fields this flow supports for approval - Product, External ID, Resolution
|
||||
* Number, Total Number of Shares, Application Date and Charges are locked in the UI and are never
|
||||
* part of this payload.
|
||||
*/
|
||||
submit() {
|
||||
if (!this.sharesAccountFormValidAndNotPristine || this.submitting) {
|
||||
return;
|
||||
}
|
||||
const locale = this.settingsService.language.code;
|
||||
const terms = this.sharesAccountTermsForm.value;
|
||||
const payload: any = {
|
||||
locale,
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
savingsAccountId: terms.savingsAccountId,
|
||||
allowDividendCalculationForInactiveClients: terms.allowDividendCalculationForInactiveClients
|
||||
};
|
||||
if (terms.minimumActivePeriod) {
|
||||
payload.minimumActivePeriod = terms.minimumActivePeriod;
|
||||
}
|
||||
if (terms.minimumActivePeriodFrequencyType) {
|
||||
payload.minimumActivePeriodFrequencyType = terms.minimumActivePeriodFrequencyType;
|
||||
}
|
||||
if (terms.lockinPeriodFrequency) {
|
||||
payload.lockinPeriodFrequency = terms.lockinPeriodFrequency;
|
||||
}
|
||||
if (terms.lockinPeriodFrequencyType) {
|
||||
payload.lockinPeriodFrequencyType = terms.lockinPeriodFrequencyType;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
this.sharesService.submitShareAccountEdit(this.sharesAccountAndTemplate.id, payload).subscribe({
|
||||
next: () => {
|
||||
this.submitting = false;
|
||||
this.router.navigate(['../'], { relativeTo: this.route });
|
||||
},
|
||||
error: () => {
|
||||
this.submitting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+28
-22
@@ -7,23 +7,25 @@
|
||||
-->
|
||||
|
||||
<div class="layout-row-wrap gap-2px responsive-column">
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
|
||||
<mat-select #charge>
|
||||
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
|
||||
<mat-option [value]="charge">
|
||||
{{ charge.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (!approvalEditMode) {
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
|
||||
<mat-select #charge>
|
||||
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
|
||||
<mat-option [value]="charge">
|
||||
{{ charge.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="flex-48 align-center">
|
||||
<button type="button" mat-raised-button color="primary" (click)="addCharge(charge)" [disabled]="!charge.value">
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Add' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-48 align-center">
|
||||
<button type="button" mat-raised-button color="primary" (click)="addCharge(charge)" [disabled]="!charge.value">
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Add' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<table
|
||||
class="flex-98 mat-elevation-z1"
|
||||
@@ -49,9 +51,11 @@
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let charge">
|
||||
{{ charge.amount || charge.amountOrPercentage }}
|
||||
<button mat-icon-button color="primary" (click)="editCharge(charge)">
|
||||
<fa-icon icon="pen"></fa-icon>
|
||||
</button>
|
||||
@if (!approvalEditMode) {
|
||||
<button mat-icon-button color="primary" (click)="editCharge(charge)">
|
||||
<fa-icon icon="pen"></fa-icon>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -65,9 +69,11 @@
|
||||
<ng-container matColumnDef="action">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let charge">
|
||||
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
|
||||
<fa-icon icon="trash"></fa-icon>
|
||||
</button>
|
||||
@if (!approvalEditMode) {
|
||||
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
|
||||
<fa-icon icon="trash"></fa-icon>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
+3
@@ -73,6 +73,9 @@ export class SharesAccountChargesStepComponent implements OnInit, OnChanges {
|
||||
@Input() sharesAccountTemplate: any;
|
||||
/** Currency Code */
|
||||
@Input() currencyCode: UntypedFormControl;
|
||||
/** When true (Share Account "Edit Details" maker-checker flow), charges are locked - they aren't
|
||||
* part of that flow's editable field set. */
|
||||
@Input() approvalEditMode = false;
|
||||
|
||||
/** Charge Data */
|
||||
chargeData: any = [];
|
||||
|
||||
+14
-1
@@ -45,6 +45,15 @@
|
||||
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
|
||||
<input matInput formControlName="externalId" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Resolution Number' | translate }}</mat-label>
|
||||
<input matInput formControlName="resolutionNumber" required />
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Resolution Number' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||||
@@ -56,7 +65,11 @@
|
||||
mat-raised-button
|
||||
matStepperNext
|
||||
[disabled]="
|
||||
!(sharesAccountDetailsForm.get('productId')?.value && sharesAccountDetailsForm.get('submittedDate')?.value)
|
||||
!(
|
||||
sharesAccountDetailsForm.get('productId')?.value &&
|
||||
sharesAccountDetailsForm.get('submittedDate')?.value &&
|
||||
sharesAccountDetailsForm.get('resolutionNumber')?.value
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
|
||||
+13
-2
@@ -38,6 +38,9 @@ export class SharesAccountDetailsStepComponent implements OnInit {
|
||||
|
||||
/** Shares Account Template */
|
||||
@Input() sharesAccountTemplate: any;
|
||||
/** When true (Share Account "Edit Details" maker-checker flow), Product/Submitted On/External ID/
|
||||
* Resolution Number are locked - none of them are part of that flow's editable field set. */
|
||||
@Input() approvalEditMode = false;
|
||||
|
||||
/** Minimum date allowed. */
|
||||
minDate = new Date(2000, 0, 1);
|
||||
@@ -72,10 +75,14 @@ export class SharesAccountDetailsStepComponent implements OnInit {
|
||||
submittedDate:
|
||||
this.sharesAccountTemplate.timeline.submittedOnDate &&
|
||||
new Date(this.sharesAccountTemplate.timeline.submittedOnDate),
|
||||
externalId: this.sharesAccountTemplate.externalId
|
||||
externalId: this.sharesAccountTemplate.externalId,
|
||||
resolutionNumber: this.sharesAccountTemplate.resolutionNumber
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.approvalEditMode) {
|
||||
this.sharesAccountDetailsForm.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +98,11 @@ export class SharesAccountDetailsStepComponent implements OnInit {
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
externalId: ['']
|
||||
externalId: [''],
|
||||
resolutionNumber: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -36,6 +36,11 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex-fill">
|
||||
<span class="flex-40">{{ 'labels.inputs.Resolution Number' | translate }}</span>
|
||||
<span class="flex-60">{{ sharesAccount.resolutionNumber }}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
|
||||
|
||||
<mat-divider class="flex-fill"></mat-divider>
|
||||
|
||||
+8
@@ -41,6 +41,10 @@ export class SharesAccountTermsStepComponent implements OnChanges, OnInit {
|
||||
@Input() sharesAccountProductTemplate: any;
|
||||
/** [Optional] Shares Account Template */
|
||||
@Input() sharesAccountTemplate: any;
|
||||
/** When true (Share Account "Edit Details" maker-checker flow), only Default Savings Account, Allow
|
||||
* Dividends, Minimum Active Period and Lockin Period stay editable - Total Number of Shares and
|
||||
* Application Date are transactional/historical and aren't part of that flow's editable field set. */
|
||||
@Input() approvalEditMode = false;
|
||||
|
||||
/** Minimum date allowed. */
|
||||
minDate = new Date(2000, 0, 1);
|
||||
@@ -105,6 +109,10 @@ export class SharesAccountTermsStepComponent implements OnChanges, OnInit {
|
||||
this.sharesAccountTemplate.allowDividendCalculationForInactiveClients
|
||||
});
|
||||
}
|
||||
if (this.approvalEditMode) {
|
||||
this.sharesAccountTermsForm.get('requestedShares').disable();
|
||||
this.sharesAccountTermsForm.get('applicationDate').disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
-->
|
||||
|
||||
<div class="tab-container mat-typography">
|
||||
@if (pendingEdit) {
|
||||
<div class="mat-elevation-z1" style="padding: 12px 16px; margin-bottom: 16px; background-color: #fff8e1">
|
||||
<fa-icon icon="lock" class="m-r-10"></fa-icon>
|
||||
A details edit submitted by {{ pendingEdit.makerUsername }} is awaiting checker approval. You cannot submit
|
||||
another edit until it is resolved.
|
||||
</div>
|
||||
}
|
||||
<div class="shares-account-tables layout-row gap-2percent">
|
||||
<div class="flex-49">
|
||||
<h4 class="table-headers">{{ 'labels.heading.Shares Details' | translate }}</h4>
|
||||
|
||||
@@ -13,6 +13,8 @@ import { AccountNumberComponent } from '../../../shared/account-number/account-n
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { SharesService } from '../../shares.service';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-general-tab',
|
||||
@@ -23,15 +25,20 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
ExternalIdentifierComponent,
|
||||
AccountNumberComponent,
|
||||
DateFormatPipe,
|
||||
FormatNumberPipe
|
||||
FormatNumberPipe,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class GeneralTabComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private sharesService = inject(SharesService);
|
||||
|
||||
/** Shares Account Data */
|
||||
sharesAccountData: any;
|
||||
|
||||
/** Pending details edit awaiting checker approval, if any. */
|
||||
pendingEdit: any = null;
|
||||
|
||||
/**
|
||||
* Fetches shares account data from `resolve`
|
||||
* @param {ActivatedRoute} route Activated Route
|
||||
@@ -39,6 +46,14 @@ export class GeneralTabComponent {
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { sharesAccountData: any }) => {
|
||||
this.sharesAccountData = data.sharesAccountData;
|
||||
this.sharesService.getPendingShareAccountEdit(this.sharesAccountData.id).subscribe({
|
||||
next: (edit: any) => {
|
||||
this.pendingEdit = edit && edit.status === 'PENDING' ? edit : null;
|
||||
},
|
||||
error: () => {
|
||||
this.pendingEdit = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<h2 mat-dialog-title class="dialog-header no-print">
|
||||
<span>Share Certificate View</span>
|
||||
<span class="flex-auto"></span>
|
||||
</h2>
|
||||
|
||||
<div mat-dialog-content class="dialog-content-container">
|
||||
<!-- Certificate Page Frame -->
|
||||
<div class="certificate-container" id="share-certificate-print">
|
||||
<div class="certificate-border-outer">
|
||||
<div class="certificate-border-inner">
|
||||
<!-- Certificate Watermark Icon/Seal -->
|
||||
<div class="certificate-watermark">
|
||||
<mat-icon class="watermark-icon">verified_user</mat-icon>
|
||||
</div>
|
||||
|
||||
<div class="certificate-header">
|
||||
<div class="bank-name">Nexora Co-operative Bank Ltd.</div>
|
||||
<div class="bank-sub">Mumbai, Maharashtra, India</div>
|
||||
<div class="cert-title">SHARE CERTIFICATE</div>
|
||||
<div class="cert-subtitle">Issued under the Common Seal of the Bank</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-meta-row">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Certificate No.</span>
|
||||
<span class="meta-value">{{ certificateNo }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Folio No.</span>
|
||||
<span class="meta-value">{{ folioNo }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Date of Issue</span>
|
||||
<span class="meta-value">{{ issueDate | dateFormat }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-body-section">
|
||||
<p class="cert-main-text">
|
||||
This is to certify that <strong class="highlight-text">{{ sharesAccountData.clientName }}</strong> is the
|
||||
registered holder of
|
||||
<strong class="highlight-text">{{ sharesAccountData.summary?.totalApprovedShares | formatNumber }}</strong>
|
||||
({{ sharesAccountData.productName || 'Ordinary' }}) shares of
|
||||
<strong class="highlight-text">{{
|
||||
sharesAccountData.currentMarketPrice | currency: sharesAccountData.currency.code : 'symbol-narrow'
|
||||
}}</strong>
|
||||
each, fully paid up, in <strong class="highlight-text">Nexora Co-operative Bank Ltd.</strong>, subject to
|
||||
the memorandum, articles of association, and bye-laws of the bank.
|
||||
</p>
|
||||
|
||||
<div class="issued-to-details">
|
||||
<div class="issued-to-title">ISSUED TO:</div>
|
||||
<div class="client-name-details">{{ sharesAccountData.clientName }}</div>
|
||||
<div class="client-ids">
|
||||
<span *ngIf="memberId"><strong>Member ID:</strong> {{ memberId }}</span>
|
||||
<span *ngIf="panNumber && panNumber !== 'Not Available'">
|
||||
· <strong>PAN:</strong> {{ panNumber }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="client-address" *ngIf="clientAddress">
|
||||
{{ clientAddress }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Grid -->
|
||||
<table class="certificate-details-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="lbl-col">Branch</td>
|
||||
<td class="val-col">{{ clientData?.officeName || 'Mumbai Main Branch' }}</td>
|
||||
<td class="lbl-col">Member Since</td>
|
||||
<td class="val-col">
|
||||
{{
|
||||
(clientData?.activationDate
|
||||
? (clientData.activationDate | dateFormat)
|
||||
: (clientData?.timeline?.submittedOnDate | dateFormat)) || 'N/A'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="lbl-col">Share Class</td>
|
||||
<td class="val-col">{{ sharesAccountData.productName || 'Ordinary' }}</td>
|
||||
<td class="lbl-col">Date of Issue</td>
|
||||
<td class="val-col">{{ issueDate | dateFormat }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="lbl-col">Share Numbers</td>
|
||||
<td class="val-col">{{ shareNumbersRange }}</td>
|
||||
<td class="lbl-col">Dividend Paid (Cumulative)</td>
|
||||
<td class="val-col">
|
||||
{{ cumulativeDividend | currency: sharesAccountData.currency.code : 'symbol-narrow' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="highlight-row">
|
||||
<td colspan="2" class="lbl-col-highlight">TOTAL FACE VALUE</td>
|
||||
<td colspan="2" class="val-col-highlight">
|
||||
{{ totalFaceValue | currency: sharesAccountData.currency.code : 'symbol-narrow' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="cert-footer">
|
||||
<div class="seal-container">
|
||||
<div class="seal-ring">
|
||||
<div class="seal-text">COMMON SEAL</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="closing-text">
|
||||
Given under the common seal of the bank this {{ todayDate | date: 'd' }} day of
|
||||
{{ todayDate | date: 'MMMM yyyy' }}.
|
||||
</div>
|
||||
<div class="signatures-container">
|
||||
<div class="signature-block">
|
||||
<div class="signature-line"></div>
|
||||
<div class="signatory-title">Authorised Signatory</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="end" class="no-print">
|
||||
<button mat-button mat-dialog-close>{{ 'labels.buttons.Close' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="printCertificate()">
|
||||
<mat-icon class="m-r-5">print</mat-icon>
|
||||
Print Certificate
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* 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 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
/* Hide non-overlay contents during print */
|
||||
body > :not(.cdk-overlay-container) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Make certificate fill page */
|
||||
#share-certificate-print {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100% !important;
|
||||
height: 100% !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;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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 { FormatNumberPipe } from '../../../pipes/format-number.pipe';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-share-certificate-dialog',
|
||||
templateUrl: './share-certificate-dialog.component.html',
|
||||
styleUrls: ['./share-certificate-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
DateFormatPipe,
|
||||
FormatNumberPipe,
|
||||
CurrencyPipe,
|
||||
MatButton,
|
||||
MatIcon
|
||||
]
|
||||
})
|
||||
export class ShareCertificateDialogComponent implements OnInit {
|
||||
dialogRef = inject<MatDialogRef<ShareCertificateDialogComponent>>(MatDialogRef);
|
||||
data = inject(MAT_DIALOG_DATA);
|
||||
clientsService = inject(ClientsService);
|
||||
|
||||
sharesAccountData: any;
|
||||
clientId: string;
|
||||
|
||||
clientData: any;
|
||||
clientAddress: string = '';
|
||||
panNumber: string = 'Not Available';
|
||||
memberId: string = '';
|
||||
|
||||
certificateNo: string = '';
|
||||
folioNo: string = '';
|
||||
shareNumbersRange: string = '';
|
||||
cumulativeDividend: number = 0;
|
||||
totalFaceValue: number = 0;
|
||||
issueDate: Date = new Date();
|
||||
todayDate: Date = new Date();
|
||||
|
||||
ngOnInit() {
|
||||
this.sharesAccountData = this.data.sharesAccountData;
|
||||
this.clientId = String(this.data.clientId);
|
||||
|
||||
// Initialize shares certificate values
|
||||
this.issueDate = this.sharesAccountData.timeline?.activatedDate
|
||||
? new Date(this.sharesAccountData.timeline.activatedDate)
|
||||
: new Date();
|
||||
|
||||
this.certificateNo = `SC-2026-${this.sharesAccountData.accountNo || String(this.sharesAccountData.id).padStart(6, '0')}`;
|
||||
|
||||
// Calculate total face value
|
||||
const approvedShares = this.sharesAccountData.summary?.totalApprovedShares || 0;
|
||||
const price = this.sharesAccountData.currentMarketPrice || 0;
|
||||
this.totalFaceValue = approvedShares * price;
|
||||
|
||||
// Calculate share numbers range (seeded dynamically from account ID)
|
||||
const startNum = this.sharesAccountData.id * 5000 + 201;
|
||||
const endNum = startNum + approvedShares - 1;
|
||||
this.shareNumbersRange = approvedShares > 0 ? `${startNum.toLocaleString()} – ${endNum.toLocaleString()}` : 'None';
|
||||
|
||||
// Calculate cumulative dividends
|
||||
if (this.sharesAccountData.dividends) {
|
||||
this.cumulativeDividend = this.sharesAccountData.dividends.reduce(
|
||||
(sum: number, d: any) => sum + (d.amount || 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch client details
|
||||
this.clientsService.getClientData(this.clientId).subscribe({
|
||||
next: (client: any) => {
|
||||
this.clientData = client;
|
||||
this.memberId = client.accountNo || '';
|
||||
this.folioNo = `F-${client.accountNo || String(client.id).padStart(5, '0')}`;
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
|
||||
// Fetch address details
|
||||
this.clientsService.getClientAddressData(this.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: () => {}
|
||||
});
|
||||
|
||||
// Fetch identifiers (for PAN number)
|
||||
this.clientsService.getClientIdentifiers(this.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: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
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(', ');
|
||||
}
|
||||
|
||||
printCertificate() {
|
||||
window.print();
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
|
||||
<span class="flex-auto"></span>
|
||||
<div class="flex-40">
|
||||
<div class="flex-10" style="display: flex; justify-content: flex-end; align-items: flex-start">
|
||||
@if (buttonConfig.singleButtons.length > 0) {
|
||||
<button
|
||||
mat-icon-button
|
||||
@@ -105,7 +105,15 @@
|
||||
<mat-menu #More="matMenu">
|
||||
@for (option of buttonConfig.options; track option) {
|
||||
<span>
|
||||
<button mat-menu-item *mifosxHasPermission="option.taskPermissionName" (click)="doAction(option.name)">
|
||||
<button
|
||||
mat-menu-item
|
||||
*mifosxHasPermission="option.taskPermissionName"
|
||||
[disabled]="option.name === 'Edit Details'"
|
||||
[matTooltip]="
|
||||
option.name === 'Edit Details' ? ('tooltips.Editing details is disabled' | translate) : ''
|
||||
"
|
||||
(click)="doAction(option.name)"
|
||||
>
|
||||
{{ 'labels.menus.' + option.name | translate }}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
/** Custom Dialogs */
|
||||
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
||||
import { ShareCertificateDialogComponent } from './share-certificate-dialog/share-certificate-dialog.component';
|
||||
|
||||
/** Custom Services */
|
||||
import { SharesService } from '../shares.service';
|
||||
@@ -98,6 +99,9 @@ export class SharesAccountViewComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether this account has a details edit awaiting checker approval. */
|
||||
hasPendingEdit = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.setConditionalButtons();
|
||||
if (this.router.url.includes('clients')) {
|
||||
@@ -107,6 +111,17 @@ export class SharesAccountViewComponent implements OnInit {
|
||||
} else if (this.router.url.includes('centers')) {
|
||||
this.entityType = 'Center';
|
||||
}
|
||||
this.sharesService.getPendingShareAccountEdit(this.sharesAccountData.id).subscribe({
|
||||
next: (edit: any) => {
|
||||
this.hasPendingEdit = !!edit && edit.status === 'PENDING';
|
||||
if (this.hasPendingEdit) {
|
||||
this.buttonConfig.removeOption('Edit Details');
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.hasPendingEdit = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,6 +179,12 @@ export class SharesAccountViewComponent implements OnInit {
|
||||
case 'Modify Application':
|
||||
this.router.navigate(['edit'], { relativeTo: this.route });
|
||||
break;
|
||||
case 'Edit Details':
|
||||
this.router.navigate(['edit-details'], { relativeTo: this.route });
|
||||
break;
|
||||
case 'Share Certificate':
|
||||
this.viewShareCertificate();
|
||||
break;
|
||||
case 'Delete':
|
||||
this.deleteSharesAccount();
|
||||
break;
|
||||
@@ -185,4 +206,15 @@ export class SharesAccountViewComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private viewShareCertificate() {
|
||||
this.dialog.open(ShareCertificateDialogComponent, {
|
||||
data: {
|
||||
sharesAccountData: this.sharesAccountData,
|
||||
clientId: this.sharesAccountData.clientId
|
||||
},
|
||||
width: '850px',
|
||||
maxWidth: '95vw'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,14 @@ export class SharesButtonsConfiguration {
|
||||
switch (status) {
|
||||
case 'Active':
|
||||
this.optionArray = [
|
||||
{
|
||||
name: 'Edit Details',
|
||||
taskPermissionName: 'SUBMITEDIT_SHAREACCOUNT'
|
||||
},
|
||||
{
|
||||
name: 'Share Certificate',
|
||||
taskPermissionName: 'READ_SHAREACCOUNTPURCHASE'
|
||||
},
|
||||
{
|
||||
name: 'Close',
|
||||
taskPermissionName: 'CLOSE_SHAREACCOUNT'
|
||||
@@ -128,4 +136,12 @@ export class SharesButtonsConfiguration {
|
||||
const index = buttonNames.indexOf(name);
|
||||
this.buttonsArray.splice(index, 1);
|
||||
}
|
||||
|
||||
removeOption(name: string) {
|
||||
const optionNames = this.optionArray.map((entry) => entry.name);
|
||||
const index = optionNames.indexOf(name);
|
||||
if (index > -1) {
|
||||
this.optionArray.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ export class TransactionsTabComponent implements OnInit {
|
||||
constructor() {
|
||||
this.route.parent.data.subscribe((data: { sharesAccountData: any }) => {
|
||||
this.shareAccountData = data.sharesAccountData;
|
||||
this.transactionsData = this.shareAccountData.purchasedShares;
|
||||
this.transactionsData = this.shareAccountData.purchasedShares.filter(
|
||||
(transaction: any) => transaction.type.value !== 'Charge Payment'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { DividendsTabComponent } from './shares-account-view/dividends-tab/divid
|
||||
import { CreateSharesAccountComponent } from './create-shares-account/create-shares-account.component';
|
||||
import { EditSharesAccountComponent } from './edit-shares-account/edit-shares-account.component';
|
||||
import { SharesAccountActionsComponent } from './shares-account-actions/shares-account-actions.component';
|
||||
import { EditShareAccountDetailsComponent } from './edit-share-account-details/edit-share-account-details.component';
|
||||
|
||||
/** Custom Resolvers */
|
||||
import { SharesAccountViewResolver } from './common-resolvers/share-account-view.resolver';
|
||||
@@ -89,6 +90,14 @@ const routes: Routes = [
|
||||
sharesAccountAndTemplate: SharesAccountAndTemplateResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'edit-details',
|
||||
data: { title: 'Edit Share Account Details', breadcrumb: 'Edit Details', routeParamBreadcrumb: false },
|
||||
component: EditShareAccountDetailsComponent,
|
||||
resolve: {
|
||||
sharesAccountAndTemplate: SharesAccountAndTemplateResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'actions/:name',
|
||||
data: { title: 'Shares Account Actions', breadcrumb: 'Actions', routeParamBreadcrumb: 'name' },
|
||||
|
||||
@@ -32,6 +32,16 @@ export class SharesService {
|
||||
return this.http.get(`/accounts/share/${accountId}`, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset Paging offset.
|
||||
* @param {number} limit Paging size limit.
|
||||
* @returns {Observable<any>} A page of all (active) share accounts, backed by m_share_account.
|
||||
*/
|
||||
getAllShareAccounts(offset: number = 0, limit: number = 200): Observable<any> {
|
||||
const httpParams = new HttpParams().set('offset', offset.toString()).set('limit', limit.toString());
|
||||
return this.http.get('/accounts/share', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} clientId Client Id assosciated with shares account.
|
||||
* @param {string} productId Product Id
|
||||
@@ -78,4 +88,45 @@ export class SharesService {
|
||||
const httpParams = new HttpParams().set('command', command);
|
||||
return this.http.post(`/accounts/share/${accountId}`, data, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accountId Shares Account Id.
|
||||
* @param {any} data Details edit payload.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
submitShareAccountEdit(accountId: string, data: any): Observable<any> {
|
||||
return this.http.post(`/shareaccounts/${accountId}/edit`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accountId Shares Account Id.
|
||||
* @returns {Observable<any>} The pending details edit for this account, if any.
|
||||
*/
|
||||
getPendingShareAccountEdit(accountId: string): Observable<any> {
|
||||
return this.http.get(`/shareaccounts/${accountId}/edit/pending`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<any>} All Share Account details edits pending approval.
|
||||
*/
|
||||
getAllPendingShareAccountEdits(): Observable<any> {
|
||||
return this.http.get(`/shareaccounts/edit/pending`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accountId Shares Account Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
approveShareAccountEdit(accountId: string): Observable<any> {
|
||||
return this.http.post(`/shareaccounts/${accountId}/edit/approve`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accountId Shares Account Id.
|
||||
* @param {string} rejectReason Optional reason for rejection.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
rejectShareAccountEdit(accountId: string, rejectReason?: string): Observable<any> {
|
||||
return this.http.post(`/shareaccounts/${accountId}/edit/reject`, { rejectReason: rejectReason || '' });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user