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:
2026-10-28 12:43:51 +05:30
co-authored by Claude Opus 5.5
parent 293b671966
commit 53bb97d6ed
9 changed files with 885 additions and 218 deletions
@@ -128,27 +128,68 @@
<span class="ddr-chip ddr-chip--edit">{{ group.changed }} changed</span>
}
</header>
<div class="ddr-grid">
@for (field of group.fields; track field.key) {
<div class="ddr-tile" [class.ddr-tile--changed]="field.changed">
<span class="ddr-tile__label">
{{ field.label }}
@if (field.changed) {
<span class="ddr-tile__flag">Changed</span>
}
</span>
@if (field.changed) {
<span class="ddr-tile__diff">
<span class="ddr-tile__old">{{ field.current || '—' }}</span>
<mat-icon>arrow_forward</mat-icon>
<ng-container *ngTemplateOutlet="valueTpl; context: { field: field }"></ng-container>
@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">
@for (field of group.fields; track field.key) {
<div class="ddr-tile" [class.ddr-tile--changed]="field.changed">
<span class="ddr-tile__label">
{{ field.label }}
@if (field.changed) {
<span class="ddr-tile__flag">Changed</span>
}
</span>
} @else {
<ng-container *ngTemplateOutlet="valueTpl; context: { field: field }"></ng-container>
}
</div>
}
</div>
@if (field.changed) {
<span class="ddr-tile__diff">
<span class="ddr-tile__old">{{ field.current || '—' }}</span>
<mat-icon>arrow_forward</mat-icon>
<ng-container *ngTemplateOutlet="valueTpl; context: { field: field }"></ng-container>
</span>
} @else {
<ng-container *ngTemplateOutlet="valueTpl; context: { field: field }"></ng-container>
}
</div>
}
</div>
}
</section>
}
@@ -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 || '';
}
/** 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 {
return this.fields.filter((field: any) => field.changed).length;
}
@@ -131,6 +151,7 @@ export class DdsApprovalReviewComponent implements OnInit {
Product: 'inventory_2',
Interest: 'trending_up',
'Maturity & Premature Closure': 'event_busy',
Accounting: 'menu_book',
'Customer & Scheme': 'groups',
'Personal & Contact': 'contact_page',
'Minor / Guardian': 'escalator_warning',