Merge branch 'feature/dds' into merge/dds-into-dev-latest
Brings the DDS (Daily Deposit Scheme) UI from feature/dds onto the current dev-latest: - Agent settlement (Agent Tran), Close Party account, DDS reports, Party-Savings link field - Agent collection entry screen, per-agent GL account, maturity auto close - Approvals tab, vouchers, payout & close, Sundry GL search, closure tab and fixes - Menu: DDS Party Master, DDS Accounts, DDS Collection, DDS Reports and Locker links placed in the Deposits mega panel groups; Agent Master, Commission Slab and DDS Batch Processing under Organization; DDS Approvals tab in the checker inbox No content conflicts. Nothing from dev-latest is removed. Merge adjustments: - package-lock.json: kept dev-latest's lock file. feature/dds does not add or change any dependency in package.json; its lock file was only regenerated (about 18k lines removed and 12k added), which would have changed resolved versions for packages already on dev-latest. - Dropped src/app/deposits/party-master/CBS-Backend.code-workspace, a VS Code workspace file holding local folder paths, not app code. - angular.json: bundle budget raised to 18mb warning / 20mb error, as on feature/dds. Lint fixes required by the pre-commit hook (no behaviour change): - Prettier formatting on 12 DDS files. - create-party.component.scss: the dense data-entry section deliberately re-targets the base selectors to override them, so no-duplicate-selectors is disabled for that section instead of merging rules and changing the cascade. - notice-dialog / create-dds-account-type scss: stylelint-disable-line for the "ch" width unit. - commission-slab-master.component.scss: stylelint auto-fix. Verified with tsc -p src/tsconfig.app.json --noEmit, stylelint, prettier and eslint on all merged files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
<!--
|
||||
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="csm-container">
|
||||
<div class="csm-header">
|
||||
<button mat-stroked-button [routerLink]="['../']" class="csm-back">
|
||||
<i class="fas fa-arrow-left"></i> {{ 'labels.heading.Organization' | translate }}
|
||||
</button>
|
||||
<h2 class="csm-header__title">Commission Slab Master</h2>
|
||||
</div>
|
||||
|
||||
<div class="csm-card">
|
||||
<table class="csm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slab Type</th>
|
||||
<th>Slab From</th>
|
||||
<th>Slab To</th>
|
||||
<th>Comm Per</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (slab of slabs; track slab; let i = $index) {
|
||||
@if (editingIndex === i) {
|
||||
<!-- The same row turned into inputs, so a correction is made where the value is read. -->
|
||||
<tr [formGroup]="editSlabForm" class="csm-new-row">
|
||||
<td>
|
||||
<mat-form-field appearance="outline" class="csm-inline-field">
|
||||
<mat-select formControlName="slabType">
|
||||
@for (type of slabTypes; track type) {
|
||||
<mat-option [value]="type">{{ type }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td>
|
||||
<mat-form-field appearance="outline" class="csm-inline-field">
|
||||
<input matInput type="number" formControlName="slabFrom" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td>
|
||||
<mat-form-field appearance="outline" class="csm-inline-field">
|
||||
<input matInput type="number" formControlName="slabTo" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td>
|
||||
<mat-form-field appearance="outline" class="csm-inline-field">
|
||||
<input matInput type="number" formControlName="commissionPercent" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td class="csm-row-actions">
|
||||
<button mat-icon-button color="primary" type="button" (click)="saveEdit()" matTooltip="Save correction">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
<button mat-icon-button type="button" (click)="cancelEdit()" matTooltip="Cancel">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
<tr>
|
||||
<td>{{ slab.slabType }}</td>
|
||||
<td>{{ slab.slabFrom }}</td>
|
||||
<td>{{ slab.slabTo }}</td>
|
||||
<td>{{ slab.commissionPercent }}%</td>
|
||||
<td class="csm-row-actions">
|
||||
@if (slab.id) {
|
||||
<!--
|
||||
A saved slab is corrected, never removed. Deleting a range would leave a hole
|
||||
that pays nothing to any agent whose volume lands in it, with nothing on screen
|
||||
to say why - and there is deliberately no delete endpoint behind it.
|
||||
-->
|
||||
<button mat-icon-button color="primary" type="button" (click)="startEdit(i)" matTooltip="Edit slab">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
} @else {
|
||||
<!-- Not saved yet, so taking it back off the table costs nothing. -->
|
||||
<button mat-icon-button type="button" (click)="removeSlab(i)" matTooltip="Remove before saving">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
<tr [formGroup]="newSlabForm" class="csm-new-row">
|
||||
<td>
|
||||
<mat-form-field appearance="outline" class="csm-inline-field">
|
||||
<mat-select required formControlName="slabType" placeholder="Slab Type">
|
||||
@for (type of slabTypes; track type) {
|
||||
<mat-option [value]="type">{{ type }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td>
|
||||
<mat-form-field appearance="outline" class="csm-inline-field">
|
||||
<input matInput required type="number" formControlName="slabFrom" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td>
|
||||
<mat-form-field appearance="outline" class="csm-inline-field">
|
||||
<input matInput required type="number" formControlName="slabTo" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td>
|
||||
<mat-form-field appearance="outline" class="csm-inline-field">
|
||||
<input matInput required type="number" formControlName="commissionPercent" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td class="csm-row-actions">
|
||||
<!--
|
||||
Deliberately not disabled while the row is incomplete. A greyed-out button says the
|
||||
screen will not accept this, but never which field it is waiting for; letting the
|
||||
click through means addSlab() can say so.
|
||||
-->
|
||||
<button mat-icon-button color="primary" type="button" (click)="addSlab()" matTooltip="Add slab">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if (slabs.length === 0) {
|
||||
<p class="csm-empty">No commission slabs defined yet — add one using the row above.</p>
|
||||
}
|
||||
|
||||
@if (coverage.length > 0) {
|
||||
<div class="csm-coverage">
|
||||
@for (entry of coverage; track entry.slabType) {
|
||||
<div class="csm-coverage__row">
|
||||
<span class="csm-coverage__type">{{ entry.slabType }}</span>
|
||||
<span class="csm-coverage__range"> covers {{ entry.from | number }} to {{ entry.to | number }} </span>
|
||||
@if (entry.gaps.length > 0) {
|
||||
<span class="csm-coverage__gap">
|
||||
nothing set for {{ entry.gaps.join(', ') }} — an agent landing there earns no commission
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="csm-action-bar">
|
||||
<button mat-raised-button type="button" (click)="exit()">Exit</button>
|
||||
<button mat-raised-button color="primary" type="button" (click)="save()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
$primary: #0f766e;
|
||||
|
||||
.csm-container {
|
||||
// Four inputs sit side by side in the entry row; 760px squeezed them out of the card.
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.csm-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.csm-back {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.csm-card {
|
||||
background: var(--md-sys-color-surface, #fff);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.csm-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
|
||||
// Sized rather than left to the browser, so the entry row keeps its shape whatever is typed in.
|
||||
th:nth-child(1),
|
||||
td:nth-child(1) {
|
||||
width: 26%;
|
||||
}
|
||||
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
th:nth-child(3),
|
||||
td:nth-child(3) {
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
th:nth-child(4),
|
||||
td:nth-child(4) {
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
th:nth-child(5),
|
||||
td:nth-child(5) {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgb(0 0 0 / 55%);
|
||||
padding: 8px 10px;
|
||||
border-bottom: 2px solid rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid rgb(0 0 0 / 6%);
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.csm-inline-field {
|
||||
width: 100%;
|
||||
// Without this the field holds its own 180px floor and pushes the next column out of the card.
|
||||
min-width: 0;
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.csm-row-actions {
|
||||
text-align: center;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.csm-new-row {
|
||||
background: rgb(15 118 110 / 4%);
|
||||
}
|
||||
|
||||
.csm-empty {
|
||||
color: rgb(0 0 0 / 50%);
|
||||
font-size: 14px;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
.csm-action-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
/*
|
||||
* What the slabs actually cover, and any figure between them that none of them claims.
|
||||
*
|
||||
* A gap is legitimate - a bank may set no commission below some floor - so this reports rather than
|
||||
* warns. But it has to be said out loud: the commission run finds no slab for a volume in a gap and
|
||||
* moves on, so the agent is simply paid nothing that month and nothing on screen explains it.
|
||||
*/
|
||||
.csm-coverage {
|
||||
margin-top: 14px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid rgb(0 0 0 / 12%);
|
||||
border-radius: 6px;
|
||||
background: rgb(0 0 0 / 2.5%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
&__type {
|
||||
font-weight: 600;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
&__range {
|
||||
color: rgb(0 0 0 / 62%);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__gap {
|
||||
color: #b45309;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** Custom Services */
|
||||
import { OrganizationService } from 'app/organization/organization.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { announceHeldForApproval, heldForApproval } from 'app/deposits/dds-approvals/dds-approval-notice';
|
||||
|
||||
/**
|
||||
* Commission Slab Master component.
|
||||
*
|
||||
* Two kinds of slab: ACCOUNT (commission percentage on collection volume)
|
||||
* and COMMISSION (a senior-agent override on another agent's commission -
|
||||
* deferred: there is no agent hierarchy modelled yet, see the DDS punch
|
||||
* list). Modelled directly on the legacy Commission Slab Master screen this
|
||||
* replaces.
|
||||
*
|
||||
* Backed by its own DDS Commission Slab API (`/dds/commissionslabs`) - an
|
||||
* append-only chart, same as the legacy screen: existing rows load on open,
|
||||
* newly-added rows post on Save, and there is no edit/delete of a
|
||||
* previously-saved row.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-commission-slab-master',
|
||||
templateUrl: './commission-slab-master.component.html',
|
||||
styleUrls: ['./commission-slab-master.component.scss'],
|
||||
imports: [...STANDALONE_SHARED_IMPORTS]
|
||||
})
|
||||
export class CommissionSlabMasterComponent implements OnInit {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private router = inject(Router);
|
||||
private organizationService = inject(OrganizationService);
|
||||
private alertService = inject(AlertService);
|
||||
|
||||
private static readonly SLAB_TYPE_CODES: Record<string, string> = {
|
||||
Account: 'ACCOUNT',
|
||||
Commission: 'COMMISSION'
|
||||
};
|
||||
private static readonly SLAB_TYPE_LABELS: Record<string, string> = {
|
||||
ACCOUNT: 'Account',
|
||||
COMMISSION: 'Commission'
|
||||
};
|
||||
|
||||
slabTypes = [
|
||||
'Account',
|
||||
'Commission'
|
||||
];
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'slabType',
|
||||
'slabFrom',
|
||||
'slabTo',
|
||||
'commissionPercent',
|
||||
'actions'
|
||||
];
|
||||
|
||||
slabs: Array<{ id?: number; slabType: string; slabFrom: number; slabTo: number; commissionPercent: number }> = [];
|
||||
|
||||
newSlabForm: UntypedFormGroup = this.formBuilder.group({
|
||||
slabType: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
slabFrom: [
|
||||
0,
|
||||
Validators.required
|
||||
],
|
||||
slabTo: [
|
||||
0,
|
||||
Validators.required
|
||||
],
|
||||
commissionPercent: [
|
||||
0,
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadSlabs();
|
||||
}
|
||||
|
||||
private loadSlabs(): void {
|
||||
this.organizationService.getCommissionSlabs().subscribe((slabs: any[]) => {
|
||||
this.slabs = (slabs || []).map((slab) => ({
|
||||
id: slab.id,
|
||||
slabType: CommissionSlabMasterComponent.SLAB_TYPE_LABELS[slab.slabType] || slab.slabType,
|
||||
slabFrom: slab.slabFrom,
|
||||
slabTo: slab.slabTo,
|
||||
commissionPercent: slab.commissionPercent
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Where each slab type's coverage begins and ends, and any figure in between that no slab claims.
|
||||
*
|
||||
* A gap is not an error - a bank may well decide there is no commission below some floor - but it
|
||||
* has to be visible. The commission run simply finds no slab for a volume that falls in a gap and
|
||||
* moves on, so the agent is paid nothing for that month with nothing on screen to say why.
|
||||
*/
|
||||
get coverage(): Array<{ slabType: string; from: number; to: number; gaps: string[] }> {
|
||||
return this.slabTypes
|
||||
.map((slabType) => {
|
||||
const rows = this.slabs
|
||||
.filter((slab) => slab.slabType === slabType)
|
||||
.map((slab) => ({ from: Number(slab.slabFrom), to: Number(slab.slabTo) }))
|
||||
.sort((a, b) => a.from - b.from);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gaps: string[] = [];
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const previousEnd = rows[i - 1].to;
|
||||
const nextStart = rows[i].from;
|
||||
if (nextStart > previousEnd + 1) {
|
||||
gaps.push(`${previousEnd + 1} - ${nextStart - 1}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { slabType, from: rows[0].from, to: rows[rows.length - 1].to, gaps };
|
||||
})
|
||||
.filter((entry): entry is { slabType: string; from: number; to: number; gaps: string[] } => entry !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The slab already on the table that this range would collide with, if any.
|
||||
*
|
||||
* The backend refuses overlapping slabs, but only when Save is pressed - by then the officer may
|
||||
* have keyed several rows and has to work out which one it objected to. Catching it as the row is
|
||||
* added names the clash while it is still the row in front of them.
|
||||
*/
|
||||
private clashingSlab(candidate: { slabType: string; slabFrom: number; slabTo: number }): any {
|
||||
return this.slabs.find(
|
||||
(slab) =>
|
||||
slab.slabType === candidate.slabType &&
|
||||
Number(candidate.slabFrom) <= Number(slab.slabTo) &&
|
||||
Number(candidate.slabTo) >= Number(slab.slabFrom)
|
||||
);
|
||||
}
|
||||
|
||||
addSlab(): void {
|
||||
if (this.newSlabForm.invalid) {
|
||||
// Marking the controls touched is what makes Material show which one is missing; without it
|
||||
// the button simply does nothing and the officer has no idea what the screen is waiting for.
|
||||
this.newSlabForm.markAllAsTouched();
|
||||
this.alertService.alert({ type: 'Error', message: 'Fill the slab type, range and percentage first' });
|
||||
return;
|
||||
}
|
||||
|
||||
const candidate = this.newSlabForm.getRawValue();
|
||||
|
||||
if (Number(candidate.slabFrom) > Number(candidate.slabTo)) {
|
||||
this.alertService.alert({ type: 'Error', message: 'Slab From cannot be greater than Slab To' });
|
||||
return;
|
||||
}
|
||||
if (Number(candidate.commissionPercent) > 100) {
|
||||
this.alertService.alert({ type: 'Error', message: 'Commission cannot be more than 100%' });
|
||||
return;
|
||||
}
|
||||
|
||||
const clash = this.clashingSlab(candidate);
|
||||
if (clash) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: `Overlaps the existing ${clash.slabType} slab ${clash.slabFrom} to ${clash.slabTo}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.slabs.push(candidate);
|
||||
this.newSlabForm.reset({
|
||||
slabType: '',
|
||||
slabFrom: 0,
|
||||
slabTo: 0,
|
||||
commissionPercent: 0
|
||||
});
|
||||
}
|
||||
|
||||
/** Only ever an unsaved row - a saved slab is corrected, not taken away. */
|
||||
removeSlab(index: number): void {
|
||||
this.slabs.splice(index, 1);
|
||||
}
|
||||
|
||||
/** Which saved row is currently open for correction, or -1 when none is. */
|
||||
editingIndex = -1;
|
||||
|
||||
editSlabForm: UntypedFormGroup = this.formBuilder.group({
|
||||
slabType: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
slabFrom: [
|
||||
0,
|
||||
Validators.required
|
||||
],
|
||||
slabTo: [
|
||||
0,
|
||||
Validators.required
|
||||
],
|
||||
commissionPercent: [
|
||||
0,
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
|
||||
startEdit(index: number): void {
|
||||
const slab = this.slabs[index];
|
||||
this.editingIndex = index;
|
||||
this.editSlabForm.setValue({
|
||||
slabType: slab.slabType,
|
||||
slabFrom: slab.slabFrom,
|
||||
slabTo: slab.slabTo,
|
||||
commissionPercent: slab.commissionPercent
|
||||
});
|
||||
}
|
||||
|
||||
cancelEdit(): void {
|
||||
this.editingIndex = -1;
|
||||
}
|
||||
|
||||
saveEdit(): void {
|
||||
const slab = this.slabs[this.editingIndex];
|
||||
const corrected = this.editSlabForm.getRawValue();
|
||||
|
||||
if (this.editSlabForm.invalid) {
|
||||
this.editSlabForm.markAllAsTouched();
|
||||
this.alertService.alert({ type: 'Error', message: 'Fill the slab type, range and percentage' });
|
||||
return;
|
||||
}
|
||||
if (Number(corrected.slabFrom) > Number(corrected.slabTo)) {
|
||||
this.alertService.alert({ type: 'Error', message: 'Slab From cannot be greater than Slab To' });
|
||||
return;
|
||||
}
|
||||
if (Number(corrected.commissionPercent) > 100) {
|
||||
this.alertService.alert({ type: 'Error', message: 'Commission cannot be more than 100%' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.organizationService
|
||||
.updateCommissionSlab(slab.id, {
|
||||
slabType: CommissionSlabMasterComponent.SLAB_TYPE_CODES[corrected.slabType] || corrected.slabType,
|
||||
slabFrom: corrected.slabFrom,
|
||||
slabTo: corrected.slabTo,
|
||||
commissionPercent: corrected.commissionPercent,
|
||||
locale: 'en'
|
||||
})
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
if (heldForApproval(response)) {
|
||||
announceHeldForApproval(response, 'The slab correction', this.organizationService, this.alertService);
|
||||
} else {
|
||||
this.alertService.alert({ type: 'Success', message: 'Slab corrected' });
|
||||
}
|
||||
this.editingIndex = -1;
|
||||
this.loadSlabs();
|
||||
},
|
||||
// The backend refuses a correction that overlaps another slab; the reason has to reach the
|
||||
// officer, not vanish.
|
||||
error: (error: any) => {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message:
|
||||
error?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
error?.error?.defaultUserMessage ||
|
||||
'Could not correct the slab'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exit(): void {
|
||||
this.router.navigate(['/organization']);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const newSlabs = this.slabs.filter((slab) => !slab.id);
|
||||
if (newSlabs.length === 0) {
|
||||
// Save used to return here without a word, so a slab typed into the entry row but never added
|
||||
// looked exactly like a slab that had been saved.
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Nothing new to save — add the slab to the table with + first'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = newSlabs.map((slab) => ({
|
||||
slabType: CommissionSlabMasterComponent.SLAB_TYPE_CODES[slab.slabType] || slab.slabType,
|
||||
slabFrom: slab.slabFrom,
|
||||
slabTo: slab.slabTo,
|
||||
commissionPercent: slab.commissionPercent
|
||||
}));
|
||||
|
||||
this.organizationService.createCommissionSlabs(payload).subscribe({
|
||||
next: (response: any) => {
|
||||
if (heldForApproval(response)) {
|
||||
announceHeldForApproval(
|
||||
response,
|
||||
`${newSlabs.length} ${newSlabs.length === 1 ? 'slab' : 'slabs'}`,
|
||||
this.organizationService,
|
||||
this.alertService
|
||||
);
|
||||
} else {
|
||||
this.alertService.alert({
|
||||
type: 'Success',
|
||||
message: `${newSlabs.length} ${newSlabs.length === 1 ? 'slab' : 'slabs'} saved`
|
||||
});
|
||||
}
|
||||
this.loadSlabs();
|
||||
},
|
||||
// The backend refuses an overlapping range; the officer needs to read why, not watch nothing
|
||||
// happen and assume the save went through.
|
||||
error: (error: any) => {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message:
|
||||
error?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
error?.error?.defaultUserMessage ||
|
||||
'Could not save the slabs'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user