implement user, role, product, and task management components along with associated services and UI updates
This commit is contained in:
+20
-8
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+183
-153
@@ -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>
|
||||
|
||||
+105
-9
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
||||
|
||||
/** Custom Services */
|
||||
import { SystemService } from '../../system.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** Custom Service Zitadel */
|
||||
@@ -36,6 +37,7 @@ export class AddRoleComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private authService = inject(AuthService);
|
||||
private alertService = inject(AlertService);
|
||||
|
||||
/** Role form. */
|
||||
roleForm: UntypedFormGroup;
|
||||
@@ -69,7 +71,12 @@ export class AddRoleComponent implements OnInit {
|
||||
*/
|
||||
submit() {
|
||||
this.systemService.createRole(this.roleForm.value).subscribe((response: any) => {
|
||||
if (environment.OIDC.oidcServerEnabled) {
|
||||
if (response?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'Role creation request submitted successfully for approval.'
|
||||
});
|
||||
} else if (environment.OIDC.oidcServerEnabled) {
|
||||
this.authService.createRole(response.resourceId, this.roleForm.value.name, this.roleForm.value.description);
|
||||
}
|
||||
this.router.navigate(['../'], { relativeTo: this.route });
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
|
||||
/** Custom Services */
|
||||
import { SystemService } from '../../system.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** Custom Service Zitadel */
|
||||
@@ -36,6 +37,7 @@ export class EditRoleComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private authService = inject(AuthService);
|
||||
private alertService = inject(AlertService);
|
||||
|
||||
/** Role Form */
|
||||
roleForm: UntypedFormGroup;
|
||||
@@ -83,8 +85,13 @@ export class EditRoleComponent implements OnInit {
|
||||
* if successful redirects to view updated roles and permissions.
|
||||
*/
|
||||
submit() {
|
||||
this.systemService.updateRole(this.roleForm.value, this.roleData.id).subscribe(() => {
|
||||
if (environment.OIDC.oidcServerEnabled) {
|
||||
this.systemService.updateRole(this.roleForm.value, this.roleData.id).subscribe((response: any) => {
|
||||
if (response?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'Role update request submitted successfully for approval.'
|
||||
});
|
||||
} else if (environment.OIDC.oidcServerEnabled) {
|
||||
this.authService.updateRole(
|
||||
this.roleData.id,
|
||||
this.roleForm.get('name')?.value,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { EnableDialogComponent } from '../../../shared/enable-dialog/enable-dial
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
|
||||
/** Custom Service Zitadel */
|
||||
import { environment } from '../../../../environments/environment';
|
||||
@@ -48,6 +49,7 @@ export class ViewRoleComponent implements OnInit {
|
||||
private translateService = inject(TranslateService);
|
||||
dialog = inject(MatDialog);
|
||||
private authService = inject(AuthService);
|
||||
private alertService = inject(AlertService);
|
||||
|
||||
/** Role Permissions Data */
|
||||
rolePermissionService: any;
|
||||
@@ -720,7 +722,14 @@ export class ViewRoleComponent implements OnInit {
|
||||
this.formGroup.controls.roster.disable();
|
||||
this.checkboxesChanged = false;
|
||||
this.isDisabled = true;
|
||||
this.systemService.updateRolePermission(this.roleId, permissionData).subscribe((response: any) => {});
|
||||
this.systemService.updateRolePermission(this.roleId, permissionData).subscribe((response: any) => {
|
||||
if (response?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'Role permissions update request submitted successfully for approval.'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -756,8 +765,13 @@ export class ViewRoleComponent implements OnInit {
|
||||
});
|
||||
deleteRoleDialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.delete) {
|
||||
this.systemService.deleteRole(this.roleId).subscribe(() => {
|
||||
if (environment.OIDC.oidcServerEnabled) {
|
||||
this.systemService.deleteRole(this.roleId).subscribe((res: any) => {
|
||||
if (res?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'Role deletion request submitted successfully for approval.'
|
||||
});
|
||||
} else if (environment.OIDC.oidcServerEnabled) {
|
||||
this.authService.deleteRole(this.roleId);
|
||||
}
|
||||
this.router.navigate(['/system/roles-and-permissions']);
|
||||
@@ -776,7 +790,13 @@ export class ViewRoleComponent implements OnInit {
|
||||
});
|
||||
enableRoleDialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.enable) {
|
||||
this.systemService.enableRole(this.roleId).subscribe(() => {
|
||||
this.systemService.enableRole(this.roleId).subscribe((res: any) => {
|
||||
if (res?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'Role enablement request submitted successfully for approval.'
|
||||
});
|
||||
}
|
||||
this.router.navigate(['/system/roles-and-permissions']);
|
||||
});
|
||||
} else {
|
||||
@@ -793,7 +813,13 @@ export class ViewRoleComponent implements OnInit {
|
||||
});
|
||||
deleteRoleDialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.disable) {
|
||||
this.systemService.disableRole(this.roleId).subscribe(() => {
|
||||
this.systemService.disableRole(this.roleId).subscribe((res: any) => {
|
||||
if (res?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'Role disablement request submitted successfully for approval.'
|
||||
});
|
||||
}
|
||||
this.router.navigate(['/system/roles-and-permissions']);
|
||||
});
|
||||
} else {
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
<!--
|
||||
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="pce-tab-content">
|
||||
<div class="pce-action-bar">
|
||||
<div class="pce-search-box">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input #filterInput type="text" placeholder="Search by username" (keyup.enter)="applyFilter(filterInput.value)" />
|
||||
</div>
|
||||
|
||||
<select class="pce-filter-select" [(ngModel)]="filterStatus" (change)="onFilterChange()">
|
||||
@for (opt of statusOptions; track opt.value) {
|
||||
<option [value]="opt.value">{{ opt.label }}</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<span class="pce-fb-sep"></span>
|
||||
|
||||
<div class="pce-presets">
|
||||
@for (p of datePresets; track p.key) {
|
||||
<button
|
||||
type="button"
|
||||
class="pce-preset-chip"
|
||||
[class.pce-preset-chip--active]="activePreset === p.key"
|
||||
(click)="applyPreset(p.key)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button type="button" class="pce-btn pce-btn-refresh" (click)="load()" [disabled]="loading">
|
||||
<fa-icon icon="sync-alt"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (loading) {
|
||||
<div class="empty-state">
|
||||
<p class="empty-title">Loading…</p>
|
||||
</div>
|
||||
} @else if (loaded && requests.length === 0) {
|
||||
<div class="empty-state">
|
||||
<fa-icon icon="hourglass-half" class="empty-icon"></fa-icon>
|
||||
<p class="empty-title">No {{ filterStatus | lowercase }} user requests</p>
|
||||
<p class="empty-text">No pending user operations found for this filter.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="requests" class="modern-table">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>#</th>
|
||||
<td mat-cell *matCellDef="let req">{{ req.id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="requestType">
|
||||
<th mat-header-cell *matHeaderCellDef>Action</th>
|
||||
<td mat-cell *matCellDef="let req">
|
||||
<span
|
||||
class="type-badge"
|
||||
[class.type-create]="req.requestType === 'CREATE_USER'"
|
||||
[class.type-update]="req.requestType === 'UPDATE_USER'"
|
||||
>
|
||||
{{ req.requestType === 'CREATE_USER' ? 'Create User' : 'Update User' }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="username">
|
||||
<th mat-header-cell *matHeaderCellDef>Username</th>
|
||||
<td mat-cell *matCellDef="let req" class="font-medium">{{ req.username || '—' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="fullname">
|
||||
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||
<td mat-cell *matCellDef="let req">{{ req.firstname || '' }} {{ req.lastname || '' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="email">
|
||||
<th mat-header-cell *matHeaderCellDef>Email</th>
|
||||
<td mat-cell *matCellDef="let req">{{ req.email || '—' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="office">
|
||||
<th mat-header-cell *matHeaderCellDef>Office ID</th>
|
||||
<td mat-cell *matCellDef="let req">{{ req.officeId || '—' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="makerUsername">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
|
||||
<td mat-cell *matCellDef="let req">{{ req.makerUsername }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="submittedDate">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
|
||||
<td mat-cell *matCellDef="let req">{{ formatDate(req.submittedDate) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let req">
|
||||
<span class="status-badge" [ngClass]="statusClass(req.status)">{{ req.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let req">
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-action btn-action--compare"
|
||||
(click)="compare(req)"
|
||||
title="View / Compare Details"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
<span>View</span>
|
||||
</button>
|
||||
|
||||
@if (req.status === 'PENDING') {
|
||||
<button
|
||||
type="button"
|
||||
class="btn-action btn-action--approve"
|
||||
(click)="approve(req)"
|
||||
title="Approve User Request"
|
||||
>
|
||||
<fa-icon icon="check"></fa-icon>
|
||||
<span>Approve</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-action btn-action--reject"
|
||||
(click)="reject(req)"
|
||||
title="Reject User Request"
|
||||
>
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
<span>Reject</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator
|
||||
[length]="totalRecords"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="[10, 20, 50]"
|
||||
(page)="onPageChange($event)"
|
||||
></mat-paginator>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.pce-tab-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.pce-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pce-search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
gap: 6px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pce-filter-select {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #fff;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.pce-fb-sep {
|
||||
height: 24px;
|
||||
width: 1px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.pce-presets {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pce-preset-chip {
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #f8fafc;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&--active {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.pce-btn-refresh {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modern-table {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 14px;
|
||||
font-size: 0.85rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
|
||||
&.type-create {
|
||||
background-color: #e0f2fe;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
&.type-update {
|
||||
background-color: #f3e8ff;
|
||||
color: #7e22ce;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
|
||||
&--pending {
|
||||
background-color: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
&--approved {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
&--rejected {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&--compare {
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
border-color: #cbd5e1;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
&--approve {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
border-color: #a7f3d0;
|
||||
|
||||
&:hover {
|
||||
background: #d1fae5;
|
||||
}
|
||||
}
|
||||
|
||||
&--reject {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border-color: #fecaca;
|
||||
|
||||
&:hover {
|
||||
background: #fee2e2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px 16px;
|
||||
color: #64748b;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 0.88rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* 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 { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { UsersService } from 'app/users/users.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
|
||||
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
||||
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import {
|
||||
UserEditCompareDialogComponent,
|
||||
UserEditCompareRow,
|
||||
UserEditCompareDialogData
|
||||
} from './user-edit-compare-dialog/user-edit-compare-dialog.component';
|
||||
|
||||
const USER_FIELD_LABELS: Record<string, string> = {
|
||||
username: 'Username',
|
||||
firstname: 'First Name',
|
||||
lastname: 'Last Name',
|
||||
email: 'Email Address',
|
||||
mobileNo: 'Mobile Number',
|
||||
officeId: 'Office',
|
||||
officeName: 'Office Name',
|
||||
staffId: 'Staff Member',
|
||||
roles: 'Assigned Roles',
|
||||
roleNames: 'Assigned Roles',
|
||||
otpDeliveryMethod: '2FA / OTP Channel',
|
||||
sendPasswordToEmail: 'Send Password To Email',
|
||||
passwordNeverExpires: 'Password Never Expires',
|
||||
password: 'Password'
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-pending-user-edits',
|
||||
templateUrl: './pending-user-edits.component.html',
|
||||
styleUrls: ['./pending-user-edits.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
MatIcon,
|
||||
MatPaginatorModule
|
||||
]
|
||||
})
|
||||
export class PendingUserEditsComponent implements OnInit {
|
||||
private usersService = inject(UsersService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
requests: any[] = [];
|
||||
loading = false;
|
||||
loaded = false;
|
||||
|
||||
filterStatus = 'PENDING';
|
||||
filterUsername = '';
|
||||
fromDate: Date | null = null;
|
||||
toDate: Date | null = null;
|
||||
maxDate = new Date();
|
||||
activePreset = 'all';
|
||||
|
||||
pageIndex = 0;
|
||||
pageSize = 20;
|
||||
totalRecords = 0;
|
||||
|
||||
displayedColumns = [
|
||||
'id',
|
||||
'requestType',
|
||||
'username',
|
||||
'fullname',
|
||||
'email',
|
||||
'office',
|
||||
'makerUsername',
|
||||
'submittedDate',
|
||||
'status',
|
||||
'actions'
|
||||
];
|
||||
|
||||
statusOptions = [
|
||||
{ value: 'PENDING', label: 'Pending Approval' },
|
||||
{ value: 'APPROVED', label: 'Approved' },
|
||||
{ value: 'REJECTED', label: 'Rejected' },
|
||||
{ value: '', label: 'All Statuses' }
|
||||
];
|
||||
|
||||
datePresets = [
|
||||
{ key: 'today', label: 'Today' },
|
||||
{ key: '7d', label: 'Last 7 Days' },
|
||||
{ key: '30d', label: 'Last 30 Days' },
|
||||
{ key: 'all', label: 'All Dates' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
const fromStr = this.fromDate ? this.formatIsoDate(this.fromDate) : undefined;
|
||||
const toStr = this.toDate ? this.formatIsoDate(this.toDate) : undefined;
|
||||
const offset = this.pageIndex * this.pageSize;
|
||||
|
||||
this.usersService
|
||||
.getPendingUsers(this.filterStatus, this.filterUsername, fromStr, toStr, offset, this.pageSize)
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
this.requests = response.pageItems || [];
|
||||
this.totalRecords = response.totalFilteredRecords || this.requests.length;
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
},
|
||||
error: () => {
|
||||
this.requests = [];
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onFilterChange(): void {
|
||||
this.pageIndex = 0;
|
||||
this.load();
|
||||
}
|
||||
|
||||
applyFilter(query: string): void {
|
||||
this.filterUsername = query;
|
||||
this.pageIndex = 0;
|
||||
this.load();
|
||||
}
|
||||
|
||||
applyPreset(key: string): void {
|
||||
this.activePreset = key;
|
||||
const now = new Date();
|
||||
if (key === 'today') {
|
||||
this.fromDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
this.toDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
} else if (key === '7d') {
|
||||
const past = new Date();
|
||||
past.setDate(past.getDate() - 7);
|
||||
this.fromDate = past;
|
||||
this.toDate = now;
|
||||
} else if (key === '30d') {
|
||||
const past = new Date();
|
||||
past.setDate(past.getDate() - 30);
|
||||
this.fromDate = past;
|
||||
this.toDate = now;
|
||||
} else {
|
||||
this.fromDate = null;
|
||||
this.toDate = null;
|
||||
}
|
||||
this.pageIndex = 0;
|
||||
this.load();
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
this.pageIndex = event.pageIndex;
|
||||
this.pageSize = event.pageSize;
|
||||
this.load();
|
||||
}
|
||||
|
||||
formatDate(val: any): string {
|
||||
if (!val) {
|
||||
return '—';
|
||||
}
|
||||
const d = new Date(val);
|
||||
return isNaN(d.getTime()) ? String(val) : d.toLocaleString();
|
||||
}
|
||||
|
||||
private formatIsoDate(d: Date): string {
|
||||
const yyyy = d.getFullYear();
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
statusClass(status: string): string {
|
||||
if (status === 'PENDING') {
|
||||
return 'status-badge--pending';
|
||||
}
|
||||
if (status === 'APPROVED') {
|
||||
return 'status-badge--approved';
|
||||
}
|
||||
if (status === 'REJECTED') {
|
||||
return 'status-badge--rejected';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
compare(req: any): void {
|
||||
this.usersService.getPendingUser(req.id).subscribe((detail: any) => {
|
||||
const rows: UserEditCompareRow[] = [];
|
||||
const proposed = detail.proposedValues || {};
|
||||
const original = detail.originalValues || {};
|
||||
|
||||
const allKeys = Array.from(
|
||||
new Set([
|
||||
...Object.keys(original),
|
||||
...Object.keys(proposed)
|
||||
])
|
||||
);
|
||||
for (const k of allKeys) {
|
||||
if (k === 'repeatPassword' || k === 'pendingLockKey' || k === 'id') {
|
||||
continue;
|
||||
}
|
||||
const label = USER_FIELD_LABELS[k] || k;
|
||||
let curVal = original[k];
|
||||
let propVal = proposed[k];
|
||||
|
||||
if (Array.isArray(curVal)) {
|
||||
curVal = curVal.join(', ');
|
||||
}
|
||||
if (Array.isArray(propVal)) {
|
||||
propVal = propVal.join(', ');
|
||||
}
|
||||
|
||||
rows.push({
|
||||
key: k,
|
||||
label,
|
||||
current: curVal,
|
||||
value: propVal
|
||||
});
|
||||
}
|
||||
|
||||
const data: UserEditCompareDialogData = {
|
||||
requestId: detail.id,
|
||||
requestType: detail.requestType,
|
||||
username: detail.username || req.username,
|
||||
rows,
|
||||
status: detail.status,
|
||||
makerUsername: detail.makerUsername,
|
||||
submittedDate: this.formatDate(detail.submittedDate),
|
||||
checkerUsername: detail.checkerUsername,
|
||||
checkedDate: this.formatDate(detail.checkedDate),
|
||||
rejectReason: detail.rejectReason
|
||||
};
|
||||
|
||||
this.dialog.open(UserEditCompareDialogComponent, {
|
||||
data,
|
||||
width: '650px'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
approve(req: any): void {
|
||||
const actionLabel = req.requestType === 'CREATE_USER' ? 'creation' : 'update';
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
heading: `Approve User ${actionLabel}`,
|
||||
dialogContext: `Are you sure you want to approve this user ${actionLabel} for '${req.username}'?`
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((confirmed: boolean) => {
|
||||
if (confirmed) {
|
||||
this.usersService.approvePendingUser(req.id).subscribe(() => {
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reject(req: any): void {
|
||||
const formfields: FormfieldBase[] = [
|
||||
new InputBase({
|
||||
controlType: 'input',
|
||||
key: 'reason',
|
||||
label: 'Rejection Reason',
|
||||
required: true,
|
||||
order: 1
|
||||
})
|
||||
];
|
||||
|
||||
const dialogRef = this.dialog.open(FormDialogComponent, {
|
||||
data: {
|
||||
title: `Reject User Request`,
|
||||
layout: { addButtonText: 'Reject' },
|
||||
formfields
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (result && result.data && result.data.reason) {
|
||||
this.usersService.rejectPendingUser(req.id, result.data.reason).subscribe(() => {
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<!--
|
||||
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-title">
|
||||
<span class="dialog-title__action"
|
||||
>{{ data.requestType === 'CREATE_USER' ? 'Create User' : 'Update User' }} Request</span
|
||||
>
|
||||
<span class="dialog-title__user">— {{ data.username }}</span>
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-body">
|
||||
<div class="meta-banner">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Request ID</span>
|
||||
<span class="meta-val">#{{ data.requestId }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Submitted By</span>
|
||||
<span class="meta-val">{{ data.makerUsername }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Submitted On</span>
|
||||
<span class="meta-val">{{ data.submittedDate }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Status</span>
|
||||
<span
|
||||
class="status-badge"
|
||||
[class.badge-pending]="data.status === 'PENDING'"
|
||||
[class.badge-approved]="data.status === 'APPROVED'"
|
||||
[class.badge-rejected]="data.status === 'REJECTED'"
|
||||
>
|
||||
{{ data.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (data.rejectReason) {
|
||||
<div class="reject-banner"><strong>Rejection Reason:</strong> {{ data.rejectReason }}</div>
|
||||
}
|
||||
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
@if (data.requestType === 'UPDATE_USER') {
|
||||
<th>Current Value</th>
|
||||
}
|
||||
<th>Proposed Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of data.rows; track r.key) {
|
||||
<tr>
|
||||
<td class="col-field">{{ r.label }}</td>
|
||||
@if (data.requestType === 'UPDATE_USER') {
|
||||
<td class="col-curr">{{ r.current != null && r.current !== '' ? r.current : '—' }}</td>
|
||||
}
|
||||
<td class="col-prop" [class.highlight-change]="data.requestType === 'UPDATE_USER' && r.current !== r.value">
|
||||
{{ r.value != null && r.value !== '' ? r.value : '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close class="btn-close">Close</button>
|
||||
</mat-dialog-actions>
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
&__action {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
&__user {
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 16px 20px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.meta-banner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.meta-val {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
|
||||
&.badge-pending {
|
||||
background-color: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
&.badge-approved {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
&.badge-rejected {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
.reject-banner {
|
||||
background-color: #fff1f2;
|
||||
border: 1px solid #fecdd3;
|
||||
color: #9f1239;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.compare-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #cbd5e1;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.col-field {
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
width: 28%;
|
||||
}
|
||||
|
||||
.col-curr {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.col-prop {
|
||||
color: #0f172a;
|
||||
font-weight: 500;
|
||||
|
||||
&.highlight-change {
|
||||
background-color: #f0fdf4;
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
color: #64748b;
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
MatDialogRef,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
} from '@angular/material/dialog';
|
||||
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
export interface UserEditCompareRow {
|
||||
key: string;
|
||||
label: string;
|
||||
current: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface UserEditCompareDialogData {
|
||||
requestId: string | number;
|
||||
requestType: string;
|
||||
username: string;
|
||||
rows: UserEditCompareRow[];
|
||||
status?: string;
|
||||
makerUsername?: string;
|
||||
submittedDate?: string | null;
|
||||
checkerUsername?: string;
|
||||
checkedDate?: string | null;
|
||||
rejectReason?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-user-edit-compare-dialog',
|
||||
templateUrl: './user-edit-compare-dialog.component.html',
|
||||
styleUrls: ['./user-edit-compare-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatDialogTitle,
|
||||
CdkScrollable,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
]
|
||||
})
|
||||
export class UserEditCompareDialogComponent {
|
||||
dialogRef = inject<MatDialogRef<UserEditCompareDialogComponent>>(MatDialogRef);
|
||||
data = inject<UserEditCompareDialogData>(MAT_DIALOG_DATA);
|
||||
}
|
||||
@@ -108,6 +108,15 @@
|
||||
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
|
||||
<span>Pending Client Edits</span>
|
||||
</a>
|
||||
<a
|
||||
class="ac-tab"
|
||||
routerLink="./pending-user-edits"
|
||||
routerLinkActive="ac-tab--active"
|
||||
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
|
||||
>
|
||||
<fa-icon icon="user-cog" class="ac-tab__ic"></fa-icon>
|
||||
<span>Pending User Approvals</span>
|
||||
</a>
|
||||
<a
|
||||
class="ac-tab"
|
||||
routerLink="./rejected-account"
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ChequebookRequestsComponent } from './checker-inbox-and-tasks-tabs/cheq
|
||||
import { PendingSavingsAccountEditsComponent } from './checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component';
|
||||
import { PendingShareAccountEditsComponent } from './checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component';
|
||||
import { PendingClientEditsComponent } from './checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component';
|
||||
import { PendingUserEditsComponent } from './checker-inbox-and-tasks-tabs/pending-user-edits/pending-user-edits.component';
|
||||
import { ViewCheckerInboxComponent } from './view-checker-inbox/view-checker-inbox.component';
|
||||
|
||||
/** Custom Resolvers */
|
||||
@@ -156,6 +157,11 @@ const routes: Routes = [
|
||||
component: PendingClientEditsComponent,
|
||||
data: { title: 'Pending Client Edits' }
|
||||
},
|
||||
{
|
||||
path: 'pending-user-edits',
|
||||
component: PendingUserEditsComponent,
|
||||
data: { title: 'Pending User Approvals' }
|
||||
},
|
||||
{
|
||||
path: 'withdrawal-requests',
|
||||
component: WithdrawalRequestsComponent,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
/** Custom Services */
|
||||
import { UsersService } from '../users.service';
|
||||
import { PopoverService } from '../../configuration-wizard/popover/popover.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
|
||||
/** Custom Dialog Component */
|
||||
import { PasswordsUtility } from 'app/core/utils/passwords-utility';
|
||||
@@ -56,6 +57,7 @@ export class CreateUserComponent implements OnInit, AfterViewInit {
|
||||
private configurationWizardService = inject(ConfigurationWizardService);
|
||||
private dialog = inject(MatDialog);
|
||||
private passwordsUtility = inject(PasswordsUtility);
|
||||
private alertService = inject(AlertService);
|
||||
|
||||
/** User form. */
|
||||
userForm: UntypedFormGroup;
|
||||
@@ -211,6 +213,12 @@ export class CreateUserComponent implements OnInit, AfterViewInit {
|
||||
if (this.configurationWizardService.showUsersForm) {
|
||||
this.configurationWizardService.showUsersForm = false;
|
||||
this.openDialog();
|
||||
} else if (response?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'User creation request submitted successfully for approval.'
|
||||
});
|
||||
this.router.navigate(['../'], { relativeTo: this.route });
|
||||
} else {
|
||||
this.router.navigate(
|
||||
[
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
|
||||
import { ErrorDialogComponent } from 'app/shared/error-dialog/error-dialog.component';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
|
||||
/**
|
||||
* Edit User Component.
|
||||
@@ -39,6 +40,7 @@ export class EditUserComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private alertService = inject(AlertService);
|
||||
|
||||
/** User Data */
|
||||
userData: any;
|
||||
@@ -142,27 +144,35 @@ export class EditUserComponent implements OnInit {
|
||||
const editedUser = this.editUserForm.value;
|
||||
this.usersService.editUser(this.userData.id, editedUser).subscribe({
|
||||
next: (response: any) => {
|
||||
// Fineract sometimes omits resourceId in PUT responses — fall back to
|
||||
// the user id we're editing so navigation never silently no-ops.
|
||||
const targetId = response?.resourceId ?? this.userData.id;
|
||||
const dialogRef = this.dialog.open(SuccessDialogComponent, {
|
||||
data: {
|
||||
title: 'User updated',
|
||||
message: `Changes to ${this.userData.username} have been saved successfully.`,
|
||||
buttonLabel: 'View user'
|
||||
},
|
||||
panelClass: 'sd-panel',
|
||||
disableClose: true
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.router.navigate(
|
||||
[
|
||||
'../../',
|
||||
targetId
|
||||
],
|
||||
{ relativeTo: this.route }
|
||||
);
|
||||
});
|
||||
if (response?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'User update request submitted successfully for approval.'
|
||||
});
|
||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||
} else {
|
||||
// Fineract sometimes omits resourceId in PUT responses — fall back to
|
||||
// the user id we're editing so navigation never silently no-ops.
|
||||
const targetId = response?.resourceId ?? this.userData.id;
|
||||
const dialogRef = this.dialog.open(SuccessDialogComponent, {
|
||||
data: {
|
||||
title: 'User updated',
|
||||
message: `Changes to ${this.userData.username} have been saved successfully.`,
|
||||
buttonLabel: 'View user'
|
||||
},
|
||||
panelClass: 'sd-panel',
|
||||
disableClose: true
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.router.navigate(
|
||||
[
|
||||
'../../',
|
||||
targetId
|
||||
],
|
||||
{ relativeTo: this.route }
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('[EditUser] update failed', err);
|
||||
|
||||
@@ -87,4 +87,62 @@ export class UsersService {
|
||||
const httpParams = new HttpParams().set('officeId', officeId.toString()).set('status', 'all');
|
||||
return this.http.get('/staff', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} status Status filter (e.g. 'PENDING')
|
||||
* @param {string} username Username filter
|
||||
* @param {string} fromDate From date filter
|
||||
* @param {string} toDate To date filter
|
||||
* @param {number} offset Offset
|
||||
* @param {number} limit Limit
|
||||
* @returns {Observable<any>} Pending users
|
||||
*/
|
||||
getPendingUsers(
|
||||
status?: string,
|
||||
username?: string,
|
||||
fromDate?: string,
|
||||
toDate?: string,
|
||||
offset: number = 0,
|
||||
limit: number = 20
|
||||
): Observable<any> {
|
||||
let httpParams = new HttpParams().set('offset', offset.toString()).set('limit', limit.toString());
|
||||
if (status) {
|
||||
httpParams = httpParams.set('status', status);
|
||||
}
|
||||
if (username) {
|
||||
httpParams = httpParams.set('username', username);
|
||||
}
|
||||
if (fromDate) {
|
||||
httpParams = httpParams.set('fromDate', fromDate);
|
||||
}
|
||||
if (toDate) {
|
||||
httpParams = httpParams.set('toDate', toDate);
|
||||
}
|
||||
return this.http.get('/users/pending', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number} id Pending request ID
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getPendingUser(id: string | number): Observable<any> {
|
||||
return this.http.get(`/users/pending/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number} id Pending request ID
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
approvePendingUser(id: string | number): Observable<any> {
|
||||
return this.http.post(`/users/pending/${id}/approve`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number} id Pending request ID
|
||||
* @param {string} reason Rejection reason
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
rejectPendingUser(id: string | number, reason: string): Observable<any> {
|
||||
return this.http.post(`/users/pending/${id}/reject`, { reason });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { MatIconButton } from '@angular/material/button';
|
||||
|
||||
/** Custom Services */
|
||||
import { UsersService } from '../users.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
|
||||
/** Custom Components */
|
||||
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
||||
@@ -42,6 +43,7 @@ export class ViewUserComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private alertService = inject(AlertService);
|
||||
|
||||
/** User Data. */
|
||||
userData: any;
|
||||
@@ -68,7 +70,13 @@ export class ViewUserComponent {
|
||||
});
|
||||
deleteUserDialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.delete) {
|
||||
this.usersService.deleteUser(this.userData.id).subscribe(() => {
|
||||
this.usersService.deleteUser(this.userData.id).subscribe((res: any) => {
|
||||
if (res?.rollbackTransaction) {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: 'User deletion request submitted successfully for approval.'
|
||||
});
|
||||
}
|
||||
this.router.navigate(['/appusers']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -357,6 +357,8 @@
|
||||
|
||||
&.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
width: 1%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +371,8 @@
|
||||
|
||||
&.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
width: 1%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,8 +500,10 @@
|
||||
.pl-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
height: 30px;
|
||||
min-width: 80px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid;
|
||||
|
||||
Reference in New Issue
Block a user