diff --git a/src/app/deposits/dds-approvals/dds-approval-review/dds-approval-review.component.html b/src/app/deposits/dds-approvals/dds-approval-review/dds-approval-review.component.html
index fc3137e05..7a8ca9bbb 100644
--- a/src/app/deposits/dds-approvals/dds-approval-review/dds-approval-review.component.html
+++ b/src/app/deposits/dds-approvals/dds-approval-review/dds-approval-review.component.html
@@ -128,27 +128,68 @@
{{ group.changed }} changed
}
-
-
-
-
-
-
DDS Product Creation
-
{{ isEdit ? 'Update this Daily Deposit product.' : 'Register a Daily Deposit product.' }}
-
+
+
+
+
DDS Product Creation
+
{{ isEdit ? 'Update this Daily Deposit product.' : 'Register a Daily Deposit product.' }}
+
-
+
diff --git a/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.scss b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.scss
index a6a629710..fe521eece 100644
--- a/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.scss
+++ b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.scss
@@ -6,57 +6,104 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-.dat-root {
- padding: 20px 24px 40px;
-}
+// Laid out like the savings product screen - a header, then the form in tabs - in the DDS screens'
+// own teal.
+$teal: #0f766e;
+$teal-dark: #134e4a;
+$teal-soft: #e6f4ee;
+$border: #e2e8f0;
+$text: #0f172a;
+$text-mid: #475569;
+$text-soft: #94a3b8;
-.dat-card {
- background: #fff;
- 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;
+.dat-root {
+ max-width: 960px;
margin: 0 auto;
+ padding: 24px 16px 40px;
}
.dat-head {
display: flex;
align-items: center;
gap: 14px;
- padding: 20px 24px;
- border-bottom: 1px solid rgb(0 0 0 / 7%);
+ margin-bottom: 18px;
h2 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
+ color: $text;
}
p {
margin: 2px 0 0;
font-size: 0.83rem;
- color: rgb(0 0 0 / 55%);
+ color: $text-mid;
+ }
+
+ &__ic {
+ display: grid;
+ place-items: center;
+ flex: none;
+ width: 46px;
+ height: 46px;
+ border-radius: 12px;
+ font-size: 1.05rem;
+ color: #fff;
+ background: linear-gradient(135deg, $teal, $teal-dark);
}
}
-.dat-head__ic {
- width: 46px;
- height: 46px;
- border-radius: 12px;
- display: grid;
- place-items: center;
- font-size: 1.05rem;
- color: #fff;
- background: linear-gradient(135deg, #0f766e, #134e4a);
- 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 {
display: grid;
grid-template-columns: repeat(2, minmax(220px, 1fr));
gap: 8px 20px;
- padding: 26px 24px 8px;
}
.dat-field {
@@ -67,42 +114,196 @@
grid-column: 1 / -1;
}
-/*
- * Divides the scheme's own settings from the terms it charges for closing early. A rule rather than a
- * second card, because it is the same record being filled in - the break is to stop three money
- * 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 */
+.dat-note {
+ margin: 0 0 18px;
+ max-width: 72ch; /* stylelint-disable-line unit-allowed-list */
font-size: 0.8rem;
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-section-note + .dat-grid {
- padding-top: 14px;
+.dat-gl-error {
+ margin: 0 0 16px;
+ 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 {
display: flex;
- justify-content: flex-end;
- gap: 12px;
- padding: 16px 24px;
- border-top: 1px solid rgb(0 0 0 / 7%);
+ align-items: center;
+ gap: 10px;
+ margin-top: 16px;
+ 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) {
diff --git a/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.ts b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.ts
index c6e5d4003..17cb0a873 100644
--- a/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.ts
+++ b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.ts
@@ -11,19 +11,40 @@ import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { MatTabsModule } from '@angular/material/tabs';
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 */
import { ProductsService } from 'app/products/products.service';
import { AlertService } from 'app/core/alert/alert.service';
import { OrganizationService } from 'app/organization/organization.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';
+/** 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,
- * description, and whether interest posts automatically and how often. Nothing else: DDS never used
- * the deposit-product engine, so there are no terms, charges or accounting tabs behind this.
+ * description, and whether interest posts automatically and how often - laid out in tabs the way
+ * 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.
*/
@@ -33,7 +54,9 @@ import { announceHeldForApproval, heldForApproval } from 'app/deposits/dds-appro
styleUrls: ['./create-dds-account-type.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
- FaIconComponent
+ FaIconComponent,
+ MatTabsModule,
+ GlAccountSelectorComponent
]
})
export class CreateDdsAccountTypeComponent implements OnInit {
@@ -44,6 +67,38 @@ export class CreateDdsAccountTypeComponent implements OnInit {
private alertService = inject(AlertService);
private organizationService = inject(OrganizationService);
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'];
@@ -124,6 +179,23 @@ export class CreateDdsAccountTypeComponent implements OnInit {
Validators.min(0),
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 {
+ this.loadLedgerAccounts();
const routeId = this.route.snapshot.paramMap.get('accountTypeId');
if (!routeId) {
return;
@@ -150,17 +223,104 @@ export class CreateDdsAccountTypeComponent implements OnInit {
// that charges nothing - and must not be replaced by the default.
prematureLockinMonths: accountType.prematureLockinMonths ?? 12,
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 {
this.backToList();
}
submit(): void {
if (this.accountTypeForm.invalid) {
+ this.accountTypeForm.markAllAsTouched();
return;
}
const raw = this.accountTypeForm.getRawValue();
diff --git a/src/app/products/dds-account-types/view-dds-account-type/view-dds-account-type.component.html b/src/app/products/dds-account-types/view-dds-account-type/view-dds-account-type.component.html
index fd228ac69..3ce3dae2d 100644
--- a/src/app/products/dds-account-types/view-dds-account-type/view-dds-account-type.component.html
+++ b/src/app/products/dds-account-types/view-dds-account-type/view-dds-account-type.component.html
@@ -160,5 +160,34 @@
+
+
+