implemented a Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits
This commit is contained in:
@@ -135,12 +135,19 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pl-action-btn pl-action-btn--edit"
|
||||
[routerLink]="[p.id, 'edit']"
|
||||
[class.pl-action-btn--disabled]="p.isPendingApproval"
|
||||
[disabled]="p.isPendingApproval"
|
||||
[routerLink]="p.isPendingApproval ? null : [p.id, 'edit']"
|
||||
(click)="$event.stopPropagation()"
|
||||
matTooltip="Edit this product"
|
||||
[matTooltip]="
|
||||
p.isPendingApproval
|
||||
? 'This product is locked because an edit is pending approval'
|
||||
: 'Edit this product'
|
||||
"
|
||||
*mifosxHasPermission="'UPDATE_CURRENTPRODUCT'"
|
||||
>
|
||||
<fa-icon icon="edit"></fa-icon><span>Edit</span>
|
||||
<fa-icon [icon]="p.isPendingApproval ? 'lock' : 'edit'"></fa-icon>
|
||||
<span>{{ p.isPendingApproval ? 'Locked' : 'Edit' }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -137,12 +137,19 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pl-action-btn pl-action-btn--edit"
|
||||
[routerLink]="[p.id, 'edit']"
|
||||
[class.pl-action-btn--disabled]="p.isPendingApproval"
|
||||
[disabled]="p.isPendingApproval"
|
||||
[routerLink]="p.isPendingApproval ? null : [p.id, 'edit']"
|
||||
(click)="$event.stopPropagation()"
|
||||
matTooltip="Edit this product"
|
||||
[matTooltip]="
|
||||
p.isPendingApproval
|
||||
? 'This product is locked because an edit is pending approval'
|
||||
: 'Edit this product'
|
||||
"
|
||||
*mifosxHasPermission="'UPDATE_FIXEDDEPOSITPRODUCT'"
|
||||
>
|
||||
<fa-icon icon="edit"></fa-icon><span>Edit</span>
|
||||
<fa-icon [icon]="p.isPendingApproval ? 'lock' : 'edit'"></fa-icon>
|
||||
<span>{{ p.isPendingApproval ? 'Locked' : 'Edit' }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -74,6 +74,26 @@ export class ProductsService {
|
||||
return this.http.put(`/savingsproducts/${savingProductId}`, savingProduct);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submitting a savings product update (above) puts it in the pending-approval temp table instead of
|
||||
* applying it directly. These calls drive the Approvals -> Pending Savings Product Edits screen.
|
||||
*/
|
||||
getPendingSavingsProductEdits(): Observable<any> {
|
||||
return this.http.get('/savingsproducts/pending');
|
||||
}
|
||||
|
||||
getPendingSavingsProductEdit(savingsProductId: string): Observable<any> {
|
||||
return this.http.get(`/savingsproducts/${savingsProductId}/pending`);
|
||||
}
|
||||
|
||||
approveSavingsProductEdit(savingsProductId: string): Observable<any> {
|
||||
return this.http.post(`/savingsproducts/${savingsProductId}/approve`, {});
|
||||
}
|
||||
|
||||
rejectSavingsProductEdit(savingsProductId: string, rejectReason: string): Observable<any> {
|
||||
return this.http.post(`/savingsproducts/${savingsProductId}/reject`, { rejectReason });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<any>} Current products data
|
||||
*/
|
||||
@@ -102,6 +122,26 @@ export class ProductsService {
|
||||
return this.http.delete(`/currentproducts/${currentProductId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submitting a current product update (above) puts it in the pending-approval temp table instead of
|
||||
* applying it directly. These calls drive the Approvals -> Pending Product Edits screen.
|
||||
*/
|
||||
getPendingCurrentProductEdits(): Observable<any> {
|
||||
return this.http.get('/currentproducts/pending');
|
||||
}
|
||||
|
||||
getPendingCurrentProductEdit(currentProductId: string): Observable<any> {
|
||||
return this.http.get(`/currentproducts/${currentProductId}/pending`);
|
||||
}
|
||||
|
||||
approveCurrentProductEdit(currentProductId: string): Observable<any> {
|
||||
return this.http.post(`/currentproducts/${currentProductId}/approve`, {});
|
||||
}
|
||||
|
||||
rejectCurrentProductEdit(currentProductId: string, rejectReason: string): Observable<any> {
|
||||
return this.http.post(`/currentproducts/${currentProductId}/reject`, { rejectReason });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<any>} Share products data
|
||||
*/
|
||||
@@ -126,6 +166,26 @@ export class ProductsService {
|
||||
return this.http.put(`/products/share/${shareProductId}`, shareProduct);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submitting a share product update (above) puts it in the pending-approval temp table instead of
|
||||
* applying it directly. These calls drive the Approvals -> Pending Product Edits screen.
|
||||
*/
|
||||
getPendingShareProductEdits(): Observable<any> {
|
||||
return this.http.get('/products/share/pending');
|
||||
}
|
||||
|
||||
getPendingShareProductEdit(shareProductId: string): Observable<any> {
|
||||
return this.http.get(`/products/share/${shareProductId}/pending`);
|
||||
}
|
||||
|
||||
approveShareProductEdit(shareProductId: string): Observable<any> {
|
||||
return this.http.post(`/products/share/${shareProductId}/approve`, {});
|
||||
}
|
||||
|
||||
rejectShareProductEdit(shareProductId: string, rejectReason: string): Observable<any> {
|
||||
return this.http.post(`/products/share/${shareProductId}/reject`, { rejectReason });
|
||||
}
|
||||
|
||||
getDividends(shareProductId: string): Observable<any> {
|
||||
return this.http.get(`/shareproduct/${shareProductId}/dividend`);
|
||||
}
|
||||
@@ -251,6 +311,26 @@ export class ProductsService {
|
||||
return this.http.put(`/fixeddepositproducts/${fixedDepositProductId}`, fixedDepositProduct);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submitting a fixed deposit product update (above) puts it in the pending-approval temp table instead of
|
||||
* applying it directly. These calls drive the Approvals -> Pending Product Edits screen.
|
||||
*/
|
||||
getPendingFixedDepositProductEdits(): Observable<any> {
|
||||
return this.http.get('/fixeddepositproducts/pending');
|
||||
}
|
||||
|
||||
getPendingFixedDepositProductEdit(fixedDepositProductId: string): Observable<any> {
|
||||
return this.http.get(`/fixeddepositproducts/${fixedDepositProductId}/pending`);
|
||||
}
|
||||
|
||||
approveFixedDepositProductEdit(fixedDepositProductId: string): Observable<any> {
|
||||
return this.http.post(`/fixeddepositproducts/${fixedDepositProductId}/approve`, {});
|
||||
}
|
||||
|
||||
rejectFixedDepositProductEdit(fixedDepositProductId: string, rejectReason: string): Observable<any> {
|
||||
return this.http.post(`/fixeddepositproducts/${fixedDepositProductId}/reject`, { rejectReason });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<any>} Tax Components data
|
||||
*/
|
||||
@@ -526,6 +606,26 @@ export class ProductsService {
|
||||
return this.http.put(`/recurringdepositproducts/${recurringDepositProductId}`, recurringDepositProduct);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submitting a recurring deposit product update (above) puts it in the pending-approval temp table instead of
|
||||
* applying it directly. These calls drive the Approvals -> Pending Product Edits screen.
|
||||
*/
|
||||
getPendingRecurringDepositProductEdits(): Observable<any> {
|
||||
return this.http.get('/recurringdepositproducts/pending');
|
||||
}
|
||||
|
||||
getPendingRecurringDepositProductEdit(recurringDepositProductId: string): Observable<any> {
|
||||
return this.http.get(`/recurringdepositproducts/${recurringDepositProductId}/pending`);
|
||||
}
|
||||
|
||||
approveRecurringDepositProductEdit(recurringDepositProductId: string): Observable<any> {
|
||||
return this.http.post(`/recurringdepositproducts/${recurringDepositProductId}/approve`, {});
|
||||
}
|
||||
|
||||
rejectRecurringDepositProductEdit(recurringDepositProductId: string, rejectReason: string): Observable<any> {
|
||||
return this.http.post(`/recurringdepositproducts/${recurringDepositProductId}/reject`, { rejectReason });
|
||||
}
|
||||
|
||||
/** Returns the active CBS base interest-rate version(s). */
|
||||
getAllRdInterestRates(): Observable<any> {
|
||||
return this.getRdBaseInterestRates();
|
||||
|
||||
+5
-2
@@ -176,7 +176,11 @@ export class EditRecurringDepositProductComponent {
|
||||
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
|
||||
...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
|
||||
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting,
|
||||
charts: this.recurringDepositProductCharts,
|
||||
// charts intentionally omitted here - the backend no longer requires a charts payload for RD
|
||||
// updates once rdSlabs is present, and this getter's chart data comes from the shared CBS
|
||||
// base-rate template (not this product's own chart), which caused spurious
|
||||
// "chart.overlapping.from.and.end.dates" errors when a product's real chart had different
|
||||
// dates. Still included in recurringDepositProductPreview below for the read-only display tab.
|
||||
rdSlabs: this.activeBaseSlabs
|
||||
};
|
||||
}
|
||||
@@ -240,7 +244,6 @@ export class EditRecurringDepositProductComponent {
|
||||
submit() {
|
||||
const recurringDepositProduct: any = this.pruneEmptyValues({
|
||||
...this.recurringDepositProduct,
|
||||
charts: this.recurringDepositProductCharts,
|
||||
charges: (this.recurringDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
|
||||
locale: this.settingsService.language.code
|
||||
});
|
||||
|
||||
+5
-1
@@ -91,7 +91,11 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
|
||||
otherGlMappingsMap: this.recurringDepositProductsTemplate?.otherGlMappingsMap
|
||||
};
|
||||
const accountingMappingOptions = this.recurringDepositProductsTemplate?.accountingMappingOptions || {};
|
||||
const accountingRuleId = 2; // Always Cash
|
||||
// Defaults to Cash (2) for a brand-new product (no existing accountingRule yet), but preserves
|
||||
// the product's own current accounting rule when editing - previously this was hardcoded to 2
|
||||
// unconditionally, which silently forced every edited product onto Cash-based accounting (and
|
||||
// its required GL mappings) even for products that were and should remain "None".
|
||||
const accountingRuleId = this.recurringDepositProductsTemplate?.accountingRule?.id ?? 2;
|
||||
|
||||
this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || [];
|
||||
this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || [];
|
||||
|
||||
+10
-3
@@ -147,12 +147,19 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pl-action-btn pl-action-btn--edit"
|
||||
[routerLink]="[p.id, 'edit']"
|
||||
[class.pl-action-btn--disabled]="p.isPendingApproval"
|
||||
[disabled]="p.isPendingApproval"
|
||||
[routerLink]="p.isPendingApproval ? null : [p.id, 'edit']"
|
||||
(click)="$event.stopPropagation()"
|
||||
matTooltip="Edit this product"
|
||||
[matTooltip]="
|
||||
p.isPendingApproval
|
||||
? 'This product is locked because an edit is pending approval'
|
||||
: 'Edit this product'
|
||||
"
|
||||
*mifosxHasPermission="'UPDATE_RECURRINGDEPOSITPRODUCT'"
|
||||
>
|
||||
<fa-icon icon="edit"></fa-icon><span>Edit</span>
|
||||
<fa-icon [icon]="p.isPendingApproval ? 'lock' : 'edit'"></fa-icon>
|
||||
<span>{{ p.isPendingApproval ? 'Locked' : 'Edit' }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -145,13 +145,19 @@
|
||||
<button
|
||||
type="button"
|
||||
class="sp-action-btn sp-action-btn--edit"
|
||||
[routerLink]="[p.id, 'edit']"
|
||||
[class.sp-action-btn--disabled]="p.isPendingApproval"
|
||||
[disabled]="p.isPendingApproval"
|
||||
[routerLink]="p.isPendingApproval ? null : [p.id, 'edit']"
|
||||
(click)="$event.stopPropagation()"
|
||||
matTooltip="Edit this product"
|
||||
[matTooltip]="
|
||||
p.isPendingApproval
|
||||
? 'This product is locked because an edit is pending approval'
|
||||
: 'Edit this product'
|
||||
"
|
||||
*mifosxHasPermission="'UPDATE_SAVINGSPRODUCT'"
|
||||
>
|
||||
<fa-icon icon="edit"></fa-icon>
|
||||
<span>Edit</span>
|
||||
<fa-icon [icon]="p.isPendingApproval ? 'lock' : 'edit'"></fa-icon>
|
||||
<span>{{ p.isPendingApproval ? 'Locked' : 'Edit' }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -481,6 +481,14 @@
|
||||
border-color: var(--sp-amber);
|
||||
}
|
||||
}
|
||||
|
||||
&--disabled,
|
||||
&--disabled:hover {
|
||||
background: #f1f5f9;
|
||||
color: #94a3b8;
|
||||
border-color: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* No match strip */
|
||||
|
||||
@@ -135,12 +135,19 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pl-action-btn pl-action-btn--edit"
|
||||
[routerLink]="[p.id, 'edit']"
|
||||
[class.pl-action-btn--disabled]="p.isPendingApproval"
|
||||
[disabled]="p.isPendingApproval"
|
||||
[routerLink]="p.isPendingApproval ? null : [p.id, 'edit']"
|
||||
(click)="$event.stopPropagation()"
|
||||
matTooltip="Edit this product"
|
||||
[matTooltip]="
|
||||
p.isPendingApproval
|
||||
? 'This product is locked because an edit is pending approval'
|
||||
: 'Edit this product'
|
||||
"
|
||||
*mifosxHasPermission="'UPDATE_SHAREPRODUCT'"
|
||||
>
|
||||
<fa-icon icon="edit"></fa-icon><span>Edit</span>
|
||||
<fa-icon [icon]="p.isPendingApproval ? 'lock' : 'edit'"></fa-icon>
|
||||
<span>{{ p.isPendingApproval ? 'Locked' : 'Edit' }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
/** Custom Services */
|
||||
import { PopoverService } from '../../configuration-wizard/popover/popover.service';
|
||||
import { ConfigurationWizardService } from '../../configuration-wizard/configuration-wizard.service';
|
||||
import { ProductsService } from 'app/products/products.service';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
@@ -60,6 +61,7 @@ export class ShareProductsComponent implements OnInit, AfterViewInit {
|
||||
private router = inject(Router);
|
||||
private configurationWizardService = inject(ConfigurationWizardService);
|
||||
private popoverService = inject(PopoverService);
|
||||
private productsService = inject(ProductsService);
|
||||
|
||||
shareProductsData: any[] = [];
|
||||
displayedColumns: string[] = [
|
||||
@@ -101,6 +103,18 @@ export class ShareProductsComponent implements OnInit, AfterViewInit {
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { shareProducts: any }) => {
|
||||
this.shareProductsData = data.shareProducts.pageItems;
|
||||
// ShareProductData is immutable on the backend, so isPendingApproval isn't embedded per-row like the
|
||||
// other product lists - cross-reference against the pending-edits list instead.
|
||||
this.productsService.getPendingShareProductEdits().subscribe((pendingEdits: any[]) => {
|
||||
const pendingProductIds = new Set((pendingEdits || []).map((e) => e.savingsProductId));
|
||||
this.shareProductsData = this.shareProductsData.map((p) => ({
|
||||
...p,
|
||||
isPendingApproval: pendingProductIds.has(p.id)
|
||||
}));
|
||||
this.dataSource = new MatTableDataSource(this.shareProductsData);
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user