implement user, role, product, and task management components along with associated services and UI updates

This commit is contained in:
2026-08-27 18:30:45 +05:30
parent fa2522e678
commit abc966d507
20 changed files with 1471 additions and 202 deletions
@@ -33,6 +33,7 @@ import {
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { Dates } from 'app/core/utils/dates';
import { Accounting } from 'app/core/utils/accounting';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@@ -70,6 +71,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export class CreateFixedDepositProductComponent {
private route = inject(ActivatedRoute);
private productsService = inject(ProductsService);
private alertService = inject(AlertService);
private router = inject(Router);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
@@ -265,14 +267,24 @@ export class CreateFixedDepositProductComponent {
locale: this.settingsService.language.code
});
delete fixedDepositProduct.advancedAccountingRules;
this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe((response: any) => {
this.router.navigate(
[
'../',
response.resourceId
],
{ relativeTo: this.route }
);
this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe({
next: (response: any) => {
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message: 'Fixed deposit product creation request submitted successfully for approval.'
});
this.router.navigate(['../'], { relativeTo: this.route });
} else {
this.router.navigate(
[
'../',
response.resourceId
],
{ relativeTo: this.route }
);
}
}
});
}
}
@@ -21,6 +21,7 @@ import { FixedDepositProductAccountingStepComponent } from '../fixed-deposit-pro
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { MatTabsModule } from '@angular/material/tabs';
import {
MatTable,
@@ -70,6 +71,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export class EditFixedDepositProductComponent {
private route = inject(ActivatedRoute);
private productsService = inject(ProductsService);
private alertService = inject(AlertService);
private router = inject(Router);
private settingsService = inject(SettingsService);
private accounting = inject(Accounting);
@@ -280,8 +282,14 @@ export class EditFixedDepositProductComponent {
delete fixedDepositProduct.advancedAccountingRules;
this.productsService
.updateFixedDepositProduct(this.fixedDepositProductsTemplate.id, fixedDepositProduct)
.subscribe((response: any) => {
this.router.navigate(['../'], { relativeTo: this.route });
.subscribe({
next: () => {
this.alertService.alert({
type: 'Success',
message: 'Fixed deposit product update submitted successfully for Checker approval.'
});
this.router.navigate(['../'], { relativeTo: this.route });
}
});
}
@@ -28,7 +28,7 @@
}
</div>
</div>
<div style="display: flex; gap: 10px; flex-shrink: 0">
<div class="pv-hero-actions">
<button
class="pv-edit-btn"
type="button"
@@ -43,111 +43,141 @@
</div>
</div>
<div class="pv-legacy">
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Details' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
<span class="flex-60">{{ shareProduct.shortName }}</span>
<!-- DETAILS -->
<section class="pv-section">
<div class="pv-section-h">
<div class="pv-section-ic pv-section-ic--blue"><fa-icon icon="info-circle"></fa-icon></div>
<h3>{{ 'labels.inputs.Details' | translate }}</h3>
</div>
@if (shareProduct.description) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ shareProduct.description }}</span>
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Currency' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}:</span>
<span class="flex-60">{{ shareProduct.currency.name }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ shareProduct.currency.decimalPlaces }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
<span class="flex-60">{{ shareProduct.currency.inMultiplesOf }}</span>
</div>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Total Number of Shares' | translate }}:</span>
<span class="flex-60">{{ shareProduct.totalShares | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Shares to be Issued' | translate }}:</span>
<span class="flex-60">{{ shareProduct.totalSharesIssued | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal/Unit Price' | translate }}:</span>
<span class="flex-60">{{ shareProduct.unitPrice | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Capital Value' | translate }}:</span>
<span class="flex-60">{{ shareProduct.shareCapital | formatNumber }}</span>
</div>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Shares per Client' | translate }}:</span>
<span class="flex-60">{{
shareProduct.nominalShares +
' (Min: ' +
(shareProduct.minimumShares ? shareProduct.minimumShares : shareProduct.nominalShares) +
', Max: ' +
(shareProduct.maximumShares ? shareProduct.maximumShares : shareProduct.nominalShares) +
')'
}}</span>
</div>
@if (shareProduct.minimumActivePeriod) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Active Period' | translate }}:</span>
<span class="flex-60"
>{{ shareProduct.minimumActivePeriod }}
{{ shareProduct.minimumActivePeriodForDividendsTypeEnum.value | translateKey: 'catalogs' }}</span
<div class="pv-grid">
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Short Name' | translate }}</span>
<span class="pv-v"
><strong>{{ shareProduct.shortName }}</strong></span
>
</div>
}
@if (shareProduct.description) {
<div class="pv-kv pv-kv--span">
<span class="pv-k">{{ 'labels.inputs.Description' | translate }}</span>
<span class="pv-v">{{ shareProduct.description }}</span>
</div>
}
</div>
</section>
@if (shareProduct.lockinPeriod) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60"
>{{ shareProduct.lockinPeriod }} {{ shareProduct.lockPeriodTypeEnum.value | translateKey: 'catalogs' }}</span
<!-- CURRENCY -->
<section class="pv-section">
<div class="pv-section-h">
<div class="pv-section-ic pv-section-ic--teal"><fa-icon icon="coins"></fa-icon></div>
<h3>{{ 'labels.heading.Currency' | translate }}</h3>
</div>
<div class="pv-grid">
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Currency' | translate }}</span>
<span class="pv-v"
><strong>{{ shareProduct.currency.name }}</strong></span
>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Allow dividends for inactive clients' | translate }}:</span>
<span class="flex-60">{{ shareProduct.allowDividendCalculationForInactiveClients | yesNo }}</span>
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="pv-v">{{ shareProduct.currency.decimalPlaces }}</span>
</div>
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Currency in multiples of' | translate }}</span>
<span class="pv-v">{{ shareProduct.currency.inMultiplesOf }}</span>
</div>
</div>
</section>
@if (shareProduct.marketPrice?.length) {
<div class="layout-row-wrap responsive-column flex-fill">
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Market Price Period' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="shareProduct.marketPrice">
<!-- TERMS -->
<section class="pv-section">
<div class="pv-section-h">
<div class="pv-section-ic pv-section-ic--amber"><fa-icon icon="percentage"></fa-icon></div>
<h3>{{ 'labels.heading.Terms' | translate }}</h3>
</div>
<div class="pv-grid">
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Total Number of Shares' | translate }}</span>
<span class="pv-v">{{ shareProduct.totalShares | formatNumber }}</span>
</div>
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Shares to be Issued' | translate }}</span>
<span class="pv-v">{{ shareProduct.totalSharesIssued | formatNumber }}</span>
</div>
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Nominal/Unit Price' | translate }}</span>
<span class="pv-v">{{ shareProduct.unitPrice | formatNumber }}</span>
</div>
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Capital Value' | translate }}</span>
<span class="pv-v">{{ shareProduct.shareCapital | formatNumber }}</span>
</div>
</div>
</section>
<!-- SETTINGS -->
<section class="pv-section">
<div class="pv-section-h">
<div class="pv-section-ic pv-section-ic--indigo"><fa-icon icon="sliders-h"></fa-icon></div>
<h3>{{ 'labels.heading.Settings' | translate }}</h3>
</div>
<div class="pv-grid">
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Shares per Client' | translate }}</span>
<span class="pv-v">{{
shareProduct.nominalShares +
' (Min: ' +
(shareProduct.minimumShares ? shareProduct.minimumShares : shareProduct.nominalShares) +
', Max: ' +
(shareProduct.maximumShares ? shareProduct.maximumShares : shareProduct.nominalShares) +
')'
}}</span>
</div>
@if (shareProduct.minimumActivePeriod) {
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Minimum Active Period' | translate }}</span>
<span class="pv-v"
>{{ shareProduct.minimumActivePeriod }}
{{ shareProduct.minimumActivePeriodForDividendsTypeEnum.value | translateKey: 'catalogs' }}</span
>
</div>
}
@if (shareProduct.lockinPeriod) {
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Lock-in Period' | translate }}</span>
<span class="pv-v"
>{{ shareProduct.lockinPeriod }}
{{ shareProduct.lockPeriodTypeEnum.value | translateKey: 'catalogs' }}</span
>
</div>
}
<div class="pv-kv">
<span class="pv-k">{{ 'labels.inputs.Allow dividends for inactive clients' | translate }}</span>
<span class="pv-v">
<span
class="pv-pill"
[class.pv-pill--green]="shareProduct.allowDividendCalculationForInactiveClients"
[class.pv-pill--gray]="!shareProduct.allowDividendCalculationForInactiveClients"
>
{{ shareProduct.allowDividendCalculationForInactiveClients | yesNo }}
</span>
</span>
</div>
</div>
</section>
<!-- MARKET PRICE -->
@if (shareProduct.marketPrice?.length) {
<section class="pv-section">
<div class="pv-section-h">
<div class="pv-section-ic pv-section-ic--teal"><fa-icon icon="chart-line"></fa-icon></div>
<h3>{{ 'labels.inputs.Market Price Period' | translate }}</h3>
</div>
<div class="pv-table-wrap">
<table class="pv-table" mat-table [dataSource]="shareProduct.marketPrice">
<ng-container matColumnDef="fromDate">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.From Date' | translate }}</th>
<td mat-cell *matCellDef="let marketPrice">
@@ -164,13 +194,18 @@
<tr mat-row *matRowDef="let row; columns: marketPriceDisplayedColumns"></tr>
</table>
</div>
}
</section>
}
@if (shareProduct.charges.length) {
<div class="layout-row-wrap responsive-column flex-fill">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Charges' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="shareProduct.charges">
<!-- CHARGES -->
@if (shareProduct.charges.length) {
<section class="pv-section">
<div class="pv-section-h">
<div class="pv-section-ic pv-section-ic--red"><fa-icon icon="receipt"></fa-icon></div>
<h3>{{ 'labels.heading.Charges' | translate }}</h3>
</div>
<div class="pv-table-wrap">
<table class="pv-table" mat-table [dataSource]="shareProduct.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@@ -199,62 +234,57 @@
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
}
</section>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Accounting' | translate }}</h3>
<!-- ACCOUNTING -->
<section class="pv-section pv-accounting">
<div class="pv-section-h">
<div class="pv-section-ic pv-section-ic--green"><fa-icon icon="book"></fa-icon></div>
<h3>{{ 'labels.heading.Accounting' | translate }}</h3>
</div>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Type' | translate }}:</span>
<span class="flex-60">{{
<div class="pv-accounting-row">
<span class="pv-accounting-label">{{ 'labels.inputs.Type' | translate }}</span>
<span class="pv-accounting-value">{{
'labels.accounting.' + getAccountingRuleName(shareProduct.accountingRule.value) | translate
}}</span>
</div>
@if (shareProduct.accountingRule.id === 2) {
<div class="layout-row-wrap responsive-column flex-fill">
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Assets' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Share reference'"
[glAccount]="shareProduct.accountingMappings.shareReferenceId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Liabilities' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Share suspense control'"
[glAccount]="shareProduct.accountingMappings.shareSuspenseId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Share Equity' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Equity'"
[glAccount]="shareProduct.accountingMappings.shareEquityId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Income' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Income from fees'"
[glAccount]="shareProduct.accountingMappings.incomeFromFeeAccountId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<div class="pv-accounting-details">
<h4 class="pv-sub-h">{{ 'labels.heading.Assets' | translate }}</h4>
<mifosx-gl-account-display
[accountTitle]="'Share reference'"
[glAccount]="shareProduct.accountingMappings.shareReferenceId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
<h4 class="pv-sub-h">{{ 'labels.heading.Liabilities' | translate }}</h4>
<mifosx-gl-account-display
[accountTitle]="'Share suspense control'"
[glAccount]="shareProduct.accountingMappings.shareSuspenseId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
<h4 class="pv-sub-h">{{ 'labels.heading.Share Equity' | translate }}</h4>
<mifosx-gl-account-display
[accountTitle]="'Equity'"
[glAccount]="shareProduct.accountingMappings.shareEquityId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
<h4 class="pv-sub-h">{{ 'labels.heading.Income' | translate }}</h4>
<mifosx-gl-account-display
[accountTitle]="'Income from fees'"
[glAccount]="shareProduct.accountingMappings.incomeFromFeeAccountId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
}
</div>
</section>
</div>
@@ -6,8 +6,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
@use 'assets/styles/product-view' as pv;
@include pv.product-view-styles;
@@ -18,13 +16,111 @@
--pv-accent-l: #fce7f3;
}
.tab-container {
padding: 1%;
margin: 1%;
.pv-hero-actions {
display: flex;
gap: 10px;
flex-shrink: 0;
flex-wrap: wrap;
align-items: flex-start;
}
.mat-h3 {
margin-top: 10px !important;
font-weight: 300;
color: $status-approved;
/* ───────── COMPACT ACCOUNTING SECTION ───────── */
.pv-accounting {
.pv-accounting-row {
display: grid;
grid-template-columns: minmax(220px, 40%) 1fr;
gap: 14px;
align-items: baseline;
padding: 8px 0;
border-bottom: 1px dashed var(--pv-g100);
min-height: unset;
line-height: 1.4;
}
.pv-accounting-label {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--pv-g500);
margin: 0;
flex: none;
}
.pv-accounting-value {
font-size: 13.5px;
color: var(--pv-g900);
font-weight: 500;
word-break: break-word;
margin: 0;
flex: none;
}
.pv-accounting-details {
margin-top: 0;
h4.pv-sub-h {
margin: 16px 0 8px;
font-size: 12px;
font-weight: 700;
color: var(--pv-g700);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1.2;
}
mifosx-gl-account-display {
display: block;
padding: 8px 0;
min-height: unset;
line-height: 1.4;
border-bottom: 1px dashed var(--pv-g100);
&:last-child {
border-bottom: 0;
}
::ng-deep .layout-row {
display: grid;
grid-template-columns: minmax(220px, 40%) 1fr;
gap: 14px;
align-items: baseline;
margin: 0;
}
::ng-deep div {
margin: 0 !important;
line-height: 1.4;
}
::ng-deep .header {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--pv-g500);
margin: 0 !important;
max-width: 100%;
flex: none;
}
::ng-deep div:not(.header) {
font-size: 13.5px;
color: var(--pv-g900);
font-weight: 500;
word-break: break-word;
margin: 0 !important;
max-width: 100%;
flex: none;
}
}
}
@media (width <= 600px) {
.pv-accounting-row,
.pv-accounting-details mifosx-gl-account-display ::ng-deep .layout-row {
grid-template-columns: 1fr;
gap: 4px;
}
}
}