feat(dds): DDS product creation in tabs with an Accounting tab
- DDS Product Creation split into Details / Interest & Maturity / Premature Closure / Accounting tabs, like the savings product - Accounting tab picks Customer Deposits (liability), Interest Expense and Agent Commission Expense (expense) GLs; product view shows them - Approval review shows the product's GLs, old -> new on edits - Agent master no longer reads the removed dds-deposit-product setting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,46 @@
|
|||||||
<span class="ddr-chip ddr-chip--edit">{{ group.changed }} changed</span>
|
<span class="ddr-chip ddr-chip--edit">{{ group.changed }} changed</span>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
|
@if (group.name === 'Accounting') {
|
||||||
|
<!--
|
||||||
|
The ledger accounts the product posts through, laid out as the product's own Accounting
|
||||||
|
tab lays them out: the kind of account, what it is for, and the account by name and code.
|
||||||
|
-->
|
||||||
|
<div class="ddr-acct">
|
||||||
|
@for (field of group.fields; track field.key) {
|
||||||
|
<div class="ddr-acct__row" [class.ddr-acct__row--changed]="field.changed">
|
||||||
|
<div class="ddr-acct__meta">
|
||||||
|
<span class="ddr-acct__kind" [ngClass]="'ddr-acct__kind--' + glKind(field.key).toLowerCase()">{{
|
||||||
|
glKind(field.key)
|
||||||
|
}}</span>
|
||||||
|
<span class="ddr-acct__label">
|
||||||
|
{{ field.label }}
|
||||||
|
@if (field.changed) {
|
||||||
|
<span class="ddr-tile__flag">Changed</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ddr-acct__value">
|
||||||
|
@if (field.changed) {
|
||||||
|
<span class="ddr-acct__gl ddr-acct__gl--old">
|
||||||
|
{{ glName(field.current) }}
|
||||||
|
@if (glCode(field.current)) {
|
||||||
|
<span class="ddr-acct__code">{{ glCode(field.current) }}</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<mat-icon class="ddr-acct__arrow">arrow_forward</mat-icon>
|
||||||
|
}
|
||||||
|
<span class="ddr-acct__gl" [class.ddr-acct__gl--none]="!field.value">
|
||||||
|
{{ glName(field.value) }}
|
||||||
|
@if (glCode(field.value)) {
|
||||||
|
<span class="ddr-acct__code">{{ glCode(field.value) }}</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
<div class="ddr-grid">
|
<div class="ddr-grid">
|
||||||
@for (field of group.fields; track field.key) {
|
@for (field of group.fields; track field.key) {
|
||||||
<div class="ddr-tile" [class.ddr-tile--changed]="field.changed">
|
<div class="ddr-tile" [class.ddr-tile--changed]="field.changed">
|
||||||
@@ -149,6 +189,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+110
@@ -451,3 +451,113 @@ $shadow-sm:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── A DDS product's Accounting, as its own Accounting tab shows it ───────────────────────────────────
|
||||||
|
.ddr-acct {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 20px 20px;
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: $bg-white;
|
||||||
|
|
||||||
|
&--changed {
|
||||||
|
border-color: $warning-border;
|
||||||
|
background: $warning-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 720px) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__kind {
|
||||||
|
flex: none;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&--liability {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--expense {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__gl {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $brand;
|
||||||
|
|
||||||
|
&--old {
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-soft;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--none {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__code {
|
||||||
|
padding: 1px 8px;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: $bg-soft;
|
||||||
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-mid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__arrow {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: $warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,6 +123,26 @@ export class DdsApprovalReviewComponent implements OnInit {
|
|||||||
return this.fields.find((field: any) => field.key === key)?.value || '';
|
return this.fields.find((field: any) => field.key === key)?.value || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The kind of ledger account a product's Accounting row names - customers' deposits are owed, the rest are costs. */
|
||||||
|
glKind(key: string): string {
|
||||||
|
return key === 'depositLiabilityGlId' ? 'Liability' : 'Expense';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "DDS CUSTOMER DEPOSITS (200601)" → "DDS CUSTOMER DEPOSITS"; a missing account reads "Not set". */
|
||||||
|
glName(value: string | null | undefined): string {
|
||||||
|
if (!value) {
|
||||||
|
return 'Not set';
|
||||||
|
}
|
||||||
|
const match = /^(.*) (([^()]+))$/.exec(value);
|
||||||
|
return match ? match[1] : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "DDS CUSTOMER DEPOSITS (200601)" → "200601". */
|
||||||
|
glCode(value: string | null | undefined): string {
|
||||||
|
const match = value ? /^(.*) (([^()]+))$/.exec(value) : null;
|
||||||
|
return match ? match[2] : '';
|
||||||
|
}
|
||||||
|
|
||||||
get changedCount(): number {
|
get changedCount(): number {
|
||||||
return this.fields.filter((field: any) => field.changed).length;
|
return this.fields.filter((field: any) => field.changed).length;
|
||||||
}
|
}
|
||||||
@@ -131,6 +151,7 @@ export class DdsApprovalReviewComponent implements OnInit {
|
|||||||
Product: 'inventory_2',
|
Product: 'inventory_2',
|
||||||
Interest: 'trending_up',
|
Interest: 'trending_up',
|
||||||
'Maturity & Premature Closure': 'event_busy',
|
'Maturity & Premature Closure': 'event_busy',
|
||||||
|
Accounting: 'menu_book',
|
||||||
'Customer & Scheme': 'groups',
|
'Customer & Scheme': 'groups',
|
||||||
'Personal & Contact': 'contact_page',
|
'Personal & Contact': 'contact_page',
|
||||||
'Minor / Guardian': 'escalator_warning',
|
'Minor / Guardian': 'escalator_warning',
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { OrganizationService } from 'app/organization/organization.service';
|
import { OrganizationService } from 'app/organization/organization.service';
|
||||||
import { AccountingService } from 'app/accounting/accounting.service';
|
import { AccountingService } from 'app/accounting/accounting.service';
|
||||||
import { SystemService } from 'app/system/system.service';
|
|
||||||
import { ClientsService } from 'app/clients/clients.service';
|
import { ClientsService } from 'app/clients/clients.service';
|
||||||
import { SavingsService } from 'app/savings/savings.service';
|
import { SavingsService } from 'app/savings/savings.service';
|
||||||
import { SettingsService } from 'app/settings/settings.service';
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
@@ -90,7 +89,6 @@ export class CreateAgentComponent implements OnInit {
|
|||||||
private clientsService = inject(ClientsService);
|
private clientsService = inject(ClientsService);
|
||||||
private savingsService = inject(SavingsService);
|
private savingsService = inject(SavingsService);
|
||||||
private accountingService = inject(AccountingService);
|
private accountingService = inject(AccountingService);
|
||||||
private systemService = inject(SystemService);
|
|
||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private dateUtils = inject(Dates);
|
private dateUtils = inject(Dates);
|
||||||
private alertService = inject(AlertService);
|
private alertService = inject(AlertService);
|
||||||
@@ -286,17 +284,6 @@ export class CreateAgentComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The savings product a DDS customer's own scheme account is opened under, read from the setting
|
|
||||||
* that names it rather than guessed from a product name a bank is free to change.
|
|
||||||
*
|
|
||||||
* Held so those accounts can be kept out of the agent's own list. They are savings accounts and
|
|
||||||
* pass every other test, but they belong to a customer's scheme: commission paid into one would be
|
|
||||||
* counted as that customer's deposits by every interest run afterwards. The server refuses it
|
|
||||||
* either way - this is so the choice is never offered rather than taken and then thrown back.
|
|
||||||
*/
|
|
||||||
private ddsProductId: number | null = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The accounts on offer once a type has been chosen. Type is asked first, as the legacy screen
|
* The accounts on offer once a type has been chosen. Type is asked first, as the legacy screen
|
||||||
* did, so the officer narrows to a kind of account and then picks one of that kind.
|
* did, so the officer narrows to a kind of account and then picks one of that kind.
|
||||||
@@ -420,19 +407,6 @@ export class CreateAgentComponent implements OnInit {
|
|||||||
error: () => (this.offices = [])
|
error: () => (this.offices = [])
|
||||||
});
|
});
|
||||||
|
|
||||||
this.systemService.getConfigurationByName('dds-deposit-product').subscribe({
|
|
||||||
next: (config: any) => {
|
|
||||||
this.ddsProductId = config?.enabled && config?.value ? Number(config.value) : null;
|
|
||||||
// The client may already have been keyed while this was in flight, so the list is rebuilt
|
|
||||||
// rather than left showing an account this now knows to exclude.
|
|
||||||
const clientId = this.agentForm.get('clientId')?.value;
|
|
||||||
if (clientId) {
|
|
||||||
this.loadClientSavingsAccounts(String(clientId));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: () => (this.ddsProductId = null)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.accountingService.getChartOfAccounts().subscribe({
|
this.accountingService.getChartOfAccounts().subscribe({
|
||||||
next: (accounts: any) => {
|
next: (accounts: any) => {
|
||||||
const rows = Array.isArray(accounts) ? accounts : accounts?.pageItems || [];
|
const rows = Array.isArray(accounts) ? accounts : accounts?.pageItems || [];
|
||||||
@@ -751,11 +725,8 @@ export class CreateAgentComponent implements OnInit {
|
|||||||
// Savings only. Commission is credited whenever a run posts, and a fixed deposit is a
|
// Savings only. Commission is credited whenever a run posts, and a fixed deposit is a
|
||||||
// single sum placed for a term while a recurring one takes only its own instalments -
|
// single sum placed for a term while a recurring one takes only its own instalments -
|
||||||
// neither accepts the credit, so the posting would fail rather than the choice.
|
// neither accepts the credit, so the posting would fail rather than the choice.
|
||||||
// A DDS scheme account is a savings account too and so passes the test above. It is
|
// A DDS customer's scheme account would pass this too; the server refuses one on save.
|
||||||
// dropped by the product the setting names - not by a product called "DDS", which is a
|
|
||||||
// name the bank may change - so the list offers only accounts the agent can be paid in.
|
|
||||||
account.depositType?.id === CreateAgentComponent.SAVINGS_DEPOSIT_TYPE &&
|
account.depositType?.id === CreateAgentComponent.SAVINGS_DEPOSIT_TYPE &&
|
||||||
(this.ddsProductId === null || account.productId !== this.ddsProductId) &&
|
|
||||||
(account.status?.active || account.status?.submittedAndPendingApproval || account.status?.approved)
|
(account.status?.active || account.status?.submittedAndPendingApproval || account.status?.approved)
|
||||||
);
|
);
|
||||||
this.savingsAccountTypes = [
|
this.savingsAccountTypes = [
|
||||||
|
|||||||
+138
-9
@@ -7,7 +7,6 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<div class="dat-root">
|
<div class="dat-root">
|
||||||
<div class="dat-card">
|
|
||||||
<div class="dat-head">
|
<div class="dat-head">
|
||||||
<div class="dat-head__ic"><fa-icon icon="calendar-day"></fa-icon></div>
|
<div class="dat-head__ic"><fa-icon icon="calendar-day"></fa-icon></div>
|
||||||
<div>
|
<div>
|
||||||
@@ -16,7 +15,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
One form, split into tabs the way the savings product screen is. Every tab stays in the DOM
|
||||||
|
(preserveContent), so a field filled on one tab is still there when the user comes back to it.
|
||||||
|
-->
|
||||||
<form [formGroup]="accountTypeForm">
|
<form [formGroup]="accountTypeForm">
|
||||||
|
<mat-tab-group
|
||||||
|
class="dat-tabs"
|
||||||
|
animationDuration="200ms"
|
||||||
|
[preserveContent]="true"
|
||||||
|
[(selectedIndex)]="selectedTabIndex"
|
||||||
|
>
|
||||||
|
<!-- ── Details ─────────────────────────────────────────────────────────────────────────── -->
|
||||||
|
<mat-tab>
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<i class="fas fa-info-circle dat-tab-icon"></i>
|
||||||
|
Details
|
||||||
|
@if (tabHasError(detailsControls)) {
|
||||||
|
<span class="dat-tab-dot" title="Something on this tab still needs filling in"></span>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
<div class="dat-tab-content">
|
||||||
<div class="dat-grid">
|
<div class="dat-grid">
|
||||||
<mat-form-field appearance="outline" class="dat-field">
|
<mat-form-field appearance="outline" class="dat-field">
|
||||||
<mat-label>Product Group</mat-label>
|
<mat-label>Product Group</mat-label>
|
||||||
@@ -29,14 +48,34 @@
|
|||||||
|
|
||||||
<mat-form-field appearance="outline" class="dat-field">
|
<mat-form-field appearance="outline" class="dat-field">
|
||||||
<mat-label>Product Code</mat-label>
|
<mat-label>Product Code</mat-label>
|
||||||
<input matInput required maxlength="20" formControlName="accountTypeCode" style="text-transform: uppercase" />
|
<input
|
||||||
|
matInput
|
||||||
|
required
|
||||||
|
maxlength="20"
|
||||||
|
formControlName="accountTypeCode"
|
||||||
|
style="text-transform: uppercase"
|
||||||
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="dat-field dat-field--wide">
|
<mat-form-field appearance="outline" class="dat-field dat-field--wide">
|
||||||
<mat-label>Product Description</mat-label>
|
<mat-label>Product Description</mat-label>
|
||||||
<input matInput required maxlength="200" formControlName="accountTypeDescription" />
|
<input matInput required maxlength="200" formControlName="accountTypeDescription" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<!-- ── Interest & Maturity ─────────────────────────────────────────────────────────────── -->
|
||||||
|
<mat-tab>
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<i class="fas fa-percent dat-tab-icon"></i>
|
||||||
|
Interest & Maturity
|
||||||
|
@if (tabHasError(interestControls)) {
|
||||||
|
<span class="dat-tab-dot" title="Something on this tab still needs filling in"></span>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
<div class="dat-tab-content">
|
||||||
|
<div class="dat-grid">
|
||||||
<mat-form-field appearance="outline" class="dat-field">
|
<mat-form-field appearance="outline" class="dat-field">
|
||||||
<mat-label>Interest Rate (%)</mat-label>
|
<mat-label>Interest Rate (%)</mat-label>
|
||||||
<input matInput required type="number" min="0" max="100" step="0.01" formControlName="interestRate" />
|
<input matInput required type="number" min="0" max="100" step="0.01" formControlName="interestRate" />
|
||||||
@@ -76,19 +115,32 @@
|
|||||||
<mat-option [value]="true">Yes</mat-option>
|
<mat-option [value]="true">Yes</mat-option>
|
||||||
<mat-option [value]="false">No</mat-option>
|
<mat-option [value]="false">No</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-hint>Closes the account on its maturity date, if it names a savings account to be paid into</mat-hint>
|
<mat-hint
|
||||||
|
>Closes the account on its maturity date, if it names a savings account to be paid into</mat-hint
|
||||||
|
>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<!-- ── Premature Closure ───────────────────────────────────────────────────────────────── -->
|
||||||
<!--
|
<!--
|
||||||
The terms for closing an account on this scheme early. Set once here rather than asked of the
|
The terms for closing an account on this scheme early. Set once here rather than asked of the
|
||||||
clerk at closing time: they are the bank's terms and the same for every account on the scheme,
|
clerk at closing time: they are the bank's terms and the same for every account on the scheme,
|
||||||
and a figure typed at the counter is a figure that can be typed differently next time.
|
and a figure typed at the counter is a figure that can be typed differently next time.
|
||||||
-->
|
-->
|
||||||
<h3 class="dat-section">Premature Closure</h3>
|
<mat-tab>
|
||||||
<p class="dat-section-note">
|
<ng-template mat-tab-label>
|
||||||
Applied when an account on this scheme is closed before its term ends. The branch chooses at the time whether
|
<i class="fas fa-door-open dat-tab-icon"></i>
|
||||||
the customer or the agent bears the cost; these set what that costs.
|
Premature Closure
|
||||||
|
@if (tabHasError(prematureControls)) {
|
||||||
|
<span class="dat-tab-dot" title="Something on this tab still needs filling in"></span>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
<div class="dat-tab-content">
|
||||||
|
<p class="dat-note">
|
||||||
|
Applied when an account on this scheme is closed before its term ends. The branch chooses at the time
|
||||||
|
whether the customer or the agent bears the cost; these set what that costs.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="dat-grid">
|
<div class="dat-grid">
|
||||||
@@ -126,14 +178,91 @@
|
|||||||
<mat-hint>Charged whoever bears the interest — the round was not kept going.</mat-hint>
|
<mat-hint>Charged whoever bears the interest — the round was not kept going.</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<!-- ── Accounting ──────────────────────────────────────────────────────────────────────── -->
|
||||||
|
<!--
|
||||||
|
The product's own ledger accounts, laid out the way the savings product's Accounting tab is:
|
||||||
|
grouped by kind, each row with the GL search picker. Only accounts of the right kind are offered.
|
||||||
|
-->
|
||||||
|
<mat-tab>
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<i class="fas fa-book dat-tab-icon"></i>
|
||||||
|
Accounting
|
||||||
|
@if (tabHasError(accountingControls)) {
|
||||||
|
<span class="dat-tab-dot" title="Something on this tab still needs filling in"></span>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
<div class="dat-tab-content">
|
||||||
|
<div class="spa-section">
|
||||||
|
<div class="spa-section__header">
|
||||||
|
<span class="spa-section__icon"><i class="fas fa-book"></i></span>
|
||||||
|
<h3>Accounting</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spa-section__body">
|
||||||
|
@if (glLoadFailed) {
|
||||||
|
<div class="dat-gl-error">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
The chart of accounts could not be loaded, so no ledger account can be picked. Reload the page to try
|
||||||
|
again.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (section of accountingSections; track section.key) {
|
||||||
|
<div class="spa-group">
|
||||||
|
<h4>{{ section.title }}</h4>
|
||||||
|
<div class="spa-list">
|
||||||
|
@for (row of section.rows; track row.controlName) {
|
||||||
|
<div class="spa-row">
|
||||||
|
<div class="spa-row__meta">
|
||||||
|
<span class="spa-pill" [ngClass]="'spa-pill--' + row.pill">{{ row.pillLabel }}</span>
|
||||||
|
<div class="spa-row__text">
|
||||||
|
<span class="spa-row__label">
|
||||||
|
{{ row.label }}
|
||||||
|
@if (row.required) {
|
||||||
|
<span class="spa-req">*</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="spa-row__hint">{{ row.hint }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mifosx-gl-account-selector
|
||||||
|
class="spa-row__input"
|
||||||
|
[inputFormControl]="accountTypeForm.get(row.controlName)"
|
||||||
|
[glAccountList]="row.options"
|
||||||
|
[required]="row.required"
|
||||||
|
[inputLabel]="row.inputLabel"
|
||||||
|
></mifosx-gl-account-selector>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
|
||||||
<div class="dat-actions">
|
<div class="dat-actions">
|
||||||
<button mat-raised-button type="button" (click)="cancel()">Cancel</button>
|
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
|
||||||
|
<span class="dat-actions__spacer"></span>
|
||||||
|
<button mat-stroked-button type="button" [disabled]="selectedTabIndex === 0" (click)="previousTab()">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
@if (selectedTabIndex < lastTabIndex) {
|
||||||
|
<button mat-stroked-button color="primary" type="button" (click)="nextTab()">
|
||||||
|
Next
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<button mat-raised-button color="primary" type="button" [disabled]="accountTypeForm.invalid" (click)="submit()">
|
<button mat-raised-button color="primary" type="button" [disabled]="accountTypeForm.invalid" (click)="submit()">
|
||||||
<fa-icon icon="check" style="margin-right: 6px"></fa-icon>
|
<fa-icon icon="check" style="margin-right: 6px"></fa-icon>
|
||||||
{{ isEdit ? 'Update' : 'Submit' }}
|
{{ isEdit ? 'Update' : 'Submit' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+249
-48
@@ -6,57 +6,104 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.dat-root {
|
// Laid out like the savings product screen - a header, then the form in tabs - in the DDS screens'
|
||||||
padding: 20px 24px 40px;
|
// own teal.
|
||||||
}
|
$teal: #0f766e;
|
||||||
|
$teal-dark: #134e4a;
|
||||||
|
$teal-soft: #e6f4ee;
|
||||||
|
$border: #e2e8f0;
|
||||||
|
$text: #0f172a;
|
||||||
|
$text-mid: #475569;
|
||||||
|
$text-soft: #94a3b8;
|
||||||
|
|
||||||
.dat-card {
|
.dat-root {
|
||||||
background: #fff;
|
max-width: 960px;
|
||||||
border: 1px solid rgb(0 0 0 / 8%);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 880px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
padding: 24px 16px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dat-head {
|
.dat-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 20px 24px;
|
margin-bottom: 18px;
|
||||||
border-bottom: 1px solid rgb(0 0 0 / 7%);
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 2px 0 0;
|
margin: 2px 0 0;
|
||||||
font-size: 0.83rem;
|
font-size: 0.83rem;
|
||||||
color: rgb(0 0 0 / 55%);
|
color: $text-mid;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dat-head__ic {
|
&__ic {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
flex: none;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: linear-gradient(135deg, #0f766e, #134e4a);
|
background: linear-gradient(135deg, $teal, $teal-dark);
|
||||||
flex: none;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tabs ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
.dat-tabs {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.mat-mdc-tab-header {
|
||||||
|
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab {
|
||||||
|
min-width: 130px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab.mdc-tab--active .mdc-tab__text-label {
|
||||||
|
color: $teal;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-tab-indicator__content--underline {
|
||||||
|
border-color: $teal !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dat-tab-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A tab with something still to fill in.
|
||||||
|
.dat-tab-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
margin-left: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dat-tab-content {
|
||||||
|
padding: 24px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dat-grid {
|
.dat-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||||
gap: 8px 20px;
|
gap: 8px 20px;
|
||||||
padding: 26px 24px 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dat-field {
|
.dat-field {
|
||||||
@@ -67,42 +114,196 @@
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
.dat-note {
|
||||||
* Divides the scheme's own settings from the terms it charges for closing early. A rule rather than a
|
margin: 0 0 18px;
|
||||||
* second card, because it is the same record being filled in - the break is to stop three money
|
max-width: 72ch; /* stylelint-disable-line unit-allowed-list */
|
||||||
* figures reading as more of the posting settings above them.
|
|
||||||
*/
|
|
||||||
.dat-section {
|
|
||||||
margin: 20px 0 0;
|
|
||||||
padding: 18px 24px 0;
|
|
||||||
border-top: 1px solid rgb(0 0 0 / 7%);
|
|
||||||
font-size: 0.82rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #0f766e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dat-section-note {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
padding: 0 24px;
|
|
||||||
max-width: 68ch; /* stylelint-disable-line unit-allowed-list */
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: rgb(0 0 0 / 60%);
|
color: $text-mid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The note already carries the spacing, so the grid under it does not add its own again. */
|
.dat-gl-error {
|
||||||
.dat-section-note + .dat-grid {
|
margin: 0 0 16px;
|
||||||
padding-top: 14px;
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fef2f2;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #b91c1c;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Accounting tab, as the savings product's ──────────────────────────────────────────────────────────
|
||||||
|
.spa-section {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 1px solid #e8eaed;
|
||||||
|
background: #f8f9fa;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #202124;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: $teal-soft;
|
||||||
|
color: $teal;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
padding: 16px 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spa-group {
|
||||||
|
& + & {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: $text-soft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spa-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spa-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: $text-mid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 22rem;
|
||||||
|
min-width: 18rem;
|
||||||
|
max-width: 22rem;
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.layout-column {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spa-req {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spa-pill {
|
||||||
|
flex: none;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--amber {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Footer ───────────────────────────────────────────────────────────────────────────────────────────
|
||||||
.dat-actions {
|
.dat-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
padding: 16px 24px;
|
margin-top: 16px;
|
||||||
border-top: 1px solid rgb(0 0 0 / 7%);
|
padding: 14px 0;
|
||||||
|
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||||
|
|
||||||
|
&__spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 720px) {
|
@media (width <= 720px) {
|
||||||
|
|||||||
+164
-4
@@ -11,19 +11,40 @@ import { Component, OnInit, inject } from '@angular/core';
|
|||||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component';
|
||||||
|
import { GLAccount } from 'app/shared/models/general.model';
|
||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { ProductsService } from 'app/products/products.service';
|
import { ProductsService } from 'app/products/products.service';
|
||||||
import { AlertService } from 'app/core/alert/alert.service';
|
import { AlertService } from 'app/core/alert/alert.service';
|
||||||
import { OrganizationService } from 'app/organization/organization.service';
|
import { OrganizationService } from 'app/organization/organization.service';
|
||||||
import { SettingsService } from 'app/settings/settings.service';
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
|
import { AccountingService } from 'app/accounting/accounting.service';
|
||||||
import { announceHeldForApproval, heldForApproval } from 'app/deposits/dds-approvals/dds-approval-notice';
|
import { announceHeldForApproval, heldForApproval } from 'app/deposits/dds-approvals/dds-approval-notice';
|
||||||
|
|
||||||
|
/** acc_gl_account classification: 2 liability, 5 expense. */
|
||||||
|
const GL_LIABILITY = 2;
|
||||||
|
const GL_EXPENSE = 5;
|
||||||
|
|
||||||
|
/** One ledger account the product names, as a row on the Accounting tab. */
|
||||||
|
interface AccountingRow {
|
||||||
|
controlName: string;
|
||||||
|
label: string;
|
||||||
|
inputLabel: string;
|
||||||
|
hint: string;
|
||||||
|
pill: 'blue' | 'amber';
|
||||||
|
pillLabel: string;
|
||||||
|
required: boolean;
|
||||||
|
options: GLAccount[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account Type Creation — the legacy DDS screen this replaces, field for field: group, code,
|
* Account Type Creation — the legacy DDS screen this replaces, field for field: group, code,
|
||||||
* description, and whether interest posts automatically and how often. Nothing else: DDS never used
|
* description, and whether interest posts automatically and how often - laid out in tabs the way
|
||||||
* the deposit-product engine, so there are no terms, charges or accounting tabs behind this.
|
* the savings product screen is, with an Accounting tab for the ledger accounts the product posts
|
||||||
|
* through.
|
||||||
*
|
*
|
||||||
* The same form handles create and edit; an `accountTypeId` in the route switches it to edit.
|
* The same form handles create and edit; an `accountTypeId` in the route switches it to edit.
|
||||||
*/
|
*/
|
||||||
@@ -33,7 +54,9 @@ import { announceHeldForApproval, heldForApproval } from 'app/deposits/dds-appro
|
|||||||
styleUrls: ['./create-dds-account-type.component.scss'],
|
styleUrls: ['./create-dds-account-type.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
...STANDALONE_SHARED_IMPORTS,
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
FaIconComponent
|
FaIconComponent,
|
||||||
|
MatTabsModule,
|
||||||
|
GlAccountSelectorComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CreateDdsAccountTypeComponent implements OnInit {
|
export class CreateDdsAccountTypeComponent implements OnInit {
|
||||||
@@ -44,6 +67,38 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
private alertService = inject(AlertService);
|
private alertService = inject(AlertService);
|
||||||
private organizationService = inject(OrganizationService);
|
private organizationService = inject(OrganizationService);
|
||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
|
private accountingService = inject(AccountingService);
|
||||||
|
|
||||||
|
/** The tab on show; Previous and Next move it. */
|
||||||
|
selectedTabIndex = 0;
|
||||||
|
readonly lastTabIndex = 3;
|
||||||
|
|
||||||
|
/** Which controls sit on which tab - a tab whose controls are invalid shows a dot on its label. */
|
||||||
|
readonly detailsControls = [
|
||||||
|
'accountTypeGroup',
|
||||||
|
'accountTypeCode',
|
||||||
|
'accountTypeDescription'
|
||||||
|
];
|
||||||
|
readonly interestControls = [
|
||||||
|
'interestRate',
|
||||||
|
'autoInterestPost',
|
||||||
|
'autoInterestPostWhen',
|
||||||
|
'maturityAutoClose'
|
||||||
|
];
|
||||||
|
readonly prematureControls = [
|
||||||
|
'prematureLockinMonths',
|
||||||
|
'prematureInterestReduction',
|
||||||
|
'prematureAgentCommissionPenalty'
|
||||||
|
];
|
||||||
|
readonly accountingControls = [
|
||||||
|
'depositLiabilityGlId',
|
||||||
|
'interestExpenseGlId',
|
||||||
|
'commissionExpenseGlId'
|
||||||
|
];
|
||||||
|
|
||||||
|
/** The Accounting tab's rows, grouped by kind as the savings product's are. */
|
||||||
|
accountingSections: { key: string; title: string; rows: AccountingRow[] }[] = [];
|
||||||
|
glLoadFailed = false;
|
||||||
|
|
||||||
accountTypeGroups = ['DDS'];
|
accountTypeGroups = ['DDS'];
|
||||||
|
|
||||||
@@ -124,6 +179,23 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
Validators.min(0),
|
Validators.min(0),
|
||||||
Validators.max(100)
|
Validators.max(100)
|
||||||
]
|
]
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
* The product's ledger accounts (Accounting tab) - the only accounts DDS posts through for this
|
||||||
|
* product; nothing is taken from Global Configurations. All three are required, as a savings
|
||||||
|
* product's control and interest accounts are.
|
||||||
|
*/
|
||||||
|
depositLiabilityGlId: [
|
||||||
|
null,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
interestExpenseGlId: [
|
||||||
|
null,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
commissionExpenseGlId: [
|
||||||
|
null,
|
||||||
|
Validators.required
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,6 +204,7 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.loadLedgerAccounts();
|
||||||
const routeId = this.route.snapshot.paramMap.get('accountTypeId');
|
const routeId = this.route.snapshot.paramMap.get('accountTypeId');
|
||||||
if (!routeId) {
|
if (!routeId) {
|
||||||
return;
|
return;
|
||||||
@@ -150,17 +223,104 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
// that charges nothing - and must not be replaced by the default.
|
// that charges nothing - and must not be replaced by the default.
|
||||||
prematureLockinMonths: accountType.prematureLockinMonths ?? 12,
|
prematureLockinMonths: accountType.prematureLockinMonths ?? 12,
|
||||||
prematureInterestReduction: accountType.prematureInterestReduction ?? 1,
|
prematureInterestReduction: accountType.prematureInterestReduction ?? 1,
|
||||||
prematureAgentCommissionPenalty: accountType.prematureAgentCommissionPenalty ?? 1
|
prematureAgentCommissionPenalty: accountType.prematureAgentCommissionPenalty ?? 1,
|
||||||
|
depositLiabilityGlId: accountType.depositLiabilityGlId ?? null,
|
||||||
|
interestExpenseGlId: accountType.interestExpenseGlId ?? null,
|
||||||
|
commissionExpenseGlId: accountType.commissionExpenseGlId ?? null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** True once a control on the tab is invalid and the user has been there or tried to submit. */
|
||||||
|
tabHasError(controls: string[]): boolean {
|
||||||
|
return controls.some((name) => {
|
||||||
|
const control = this.accountTypeForm.get(name);
|
||||||
|
return !!control && control.invalid && (control.touched || control.dirty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
previousTab(): void {
|
||||||
|
this.selectedTabIndex = Math.max(0, this.selectedTabIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTab(): void {
|
||||||
|
this.selectedTabIndex = Math.min(this.lastTabIndex, this.selectedTabIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detail accounts that can take a posting, split by kind so each row offers only the kind its job
|
||||||
|
* needs - the backend refuses any other kind, and this keeps the wrong one from being picked at all.
|
||||||
|
*/
|
||||||
|
private loadLedgerAccounts(): void {
|
||||||
|
this.accountingService.getChartOfAccounts().subscribe({
|
||||||
|
next: (accounts: any) => {
|
||||||
|
const rows: GLAccount[] = (Array.isArray(accounts) ? accounts : accounts?.pageItems || []).filter(
|
||||||
|
(a: any) => a.usage?.id === 1 && !a.disabled
|
||||||
|
);
|
||||||
|
const ofType = (...types: number[]) => rows.filter((a: any) => types.includes(a.type?.id));
|
||||||
|
this.buildAccountingSections(ofType(GL_LIABILITY), ofType(GL_EXPENSE));
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.glLoadFailed = true;
|
||||||
|
this.buildAccountingSections([], []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildAccountingSections(liabilities: GLAccount[], expenses: GLAccount[]): void {
|
||||||
|
this.accountingSections = [
|
||||||
|
{
|
||||||
|
key: 'liabilities',
|
||||||
|
title: 'Liabilities',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
controlName: 'depositLiabilityGlId',
|
||||||
|
label: 'DDS Customer Deposits',
|
||||||
|
inputLabel: 'Customer deposits account',
|
||||||
|
hint: 'What the bank owes the customers on this product - credited on collection, debited on payout.',
|
||||||
|
pill: 'blue',
|
||||||
|
pillLabel: 'Liability',
|
||||||
|
required: true,
|
||||||
|
options: liabilities
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'expenses',
|
||||||
|
title: 'Expenses',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
controlName: 'interestExpenseGlId',
|
||||||
|
label: 'Interest Expense',
|
||||||
|
inputLabel: 'Interest expense account',
|
||||||
|
hint: 'What interest paid to the customers on this product costs the bank.',
|
||||||
|
pill: 'amber',
|
||||||
|
pillLabel: 'Expense',
|
||||||
|
required: true,
|
||||||
|
options: expenses
|
||||||
|
},
|
||||||
|
{
|
||||||
|
controlName: 'commissionExpenseGlId',
|
||||||
|
label: 'Agent Commission Expense',
|
||||||
|
inputLabel: 'Commission expense account',
|
||||||
|
hint: "Agents' commission on collecting for this product. An agent collecting for several products is shared out by what was collected for each.",
|
||||||
|
pill: 'amber',
|
||||||
|
pillLabel: 'Expense',
|
||||||
|
required: true,
|
||||||
|
options: expenses
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
cancel(): void {
|
cancel(): void {
|
||||||
this.backToList();
|
this.backToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
submit(): void {
|
submit(): void {
|
||||||
if (this.accountTypeForm.invalid) {
|
if (this.accountTypeForm.invalid) {
|
||||||
|
this.accountTypeForm.markAllAsTouched();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const raw = this.accountTypeForm.getRawValue();
|
const raw = this.accountTypeForm.getRawValue();
|
||||||
|
|||||||
+29
@@ -160,5 +160,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ACCOUNTING: the only ledger accounts DDS posts through for this product -->
|
||||||
|
<section class="vsp-section">
|
||||||
|
<div class="vsp-section-h">
|
||||||
|
<div class="vsp-section-ic vsp-section-ic--blue"><fa-icon icon="book"></fa-icon></div>
|
||||||
|
<h3>Accounting</h3>
|
||||||
|
</div>
|
||||||
|
<div class="vsp-grid">
|
||||||
|
<div class="vsp-kv">
|
||||||
|
<span class="vsp-k">DDS Customer Deposits (Liability)</span>
|
||||||
|
<span class="vsp-v">{{
|
||||||
|
glLabel(accountType.depositLiabilityGlCode, accountType.depositLiabilityGlName)
|
||||||
|
}}</span>
|
||||||
|
<span class="vdt-note">Credited on collection, debited on payout</span>
|
||||||
|
</div>
|
||||||
|
<div class="vsp-kv">
|
||||||
|
<span class="vsp-k">Interest Expense</span>
|
||||||
|
<span class="vsp-v">{{ glLabel(accountType.interestExpenseGlCode, accountType.interestExpenseGlName) }}</span>
|
||||||
|
<span class="vdt-note">Debited when interest is posted to customers</span>
|
||||||
|
</div>
|
||||||
|
<div class="vsp-kv">
|
||||||
|
<span class="vsp-k">Agent Commission Expense</span>
|
||||||
|
<span class="vsp-v">{{
|
||||||
|
glLabel(accountType.commissionExpenseGlCode, accountType.commissionExpenseGlName)
|
||||||
|
}}</span>
|
||||||
|
<span class="vdt-note">Debited with this product's share of agents' commission</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
@@ -77,6 +77,11 @@ export class ViewDdsAccountTypeComponent implements OnInit {
|
|||||||
return `${Number(value)}%`;
|
return `${Number(value)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A ledger account as "NAME (code)", or a plain "Not set" when the product names none. */
|
||||||
|
glLabel(code: string | null | undefined, name: string | null | undefined): string {
|
||||||
|
return code ? `${name} (${code})` : 'Not set';
|
||||||
|
}
|
||||||
|
|
||||||
back(): void {
|
back(): void {
|
||||||
this.router.navigate(['/products/dds-account-types']);
|
this.router.navigate(['/products/dds-account-types']);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user