feat(dds): upgrade DDS UI - agent, party, daily entry, approvals, product view

- Agent master: tabbed layout, customer search dialog, GL dropdown, account card, view page
- Party master: tabbed layout, expected party code, nominee cards, relation from code values
- Daily transaction entry: compact style, collections popup, Apply DDS Receipt placement
- Approvals: new review modal, Agent Approval tab, DDS routing into product/pending account
- DDS product: view page and list buttons; drop Auto Interest Post Day

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 10:50:03 +05:30
co-authored by Claude Sonnet 5.5
parent 357f666421
commit 293b671966
45 changed files with 5973 additions and 1292 deletions
@@ -11,14 +11,20 @@
<button mat-stroked-button [routerLink]="['../']" class="dte-back">
<i class="fas fa-arrow-left"></i> Deposits
</button>
<h2 class="dte-header__title">Daily Transaction Detail</h2>
<div class="dte-header__icon"><i class="fas fa-coins"></i></div>
<div class="dte-header__text">
<h2 class="dte-header__title">Daily Transaction Detail</h2>
<p class="dte-header__sub">
An agent's round - what was collected, the cash handed in and applied - and transfers between accounts.
</p>
</div>
</div>
<div class="dte-card">
<form [formGroup]="entryForm">
<div class="dte-info-panels">
<div class="dte-info-panel">
<div class="dte-info-panel__title">Agent Information</div>
<div class="dte-info-panel__title"><i class="fas fa-user-tie"></i>Agent Information</div>
<dl>
<dt>Name</dt>
<dd>{{ agentInfo.name || '—' }}</dd>
@@ -28,17 +34,22 @@
<dd>{{ agentInfo.city || '—' }}</dd>
<dt>A/C</dt>
<dd>{{ agentInfo.accountNo || '—' }}</dd>
<dt>Guarantor</dt>
<dd>{{ agentInfo.guarantor1 || '—' }}</dd>
<dt>Guarantor</dt>
<dd>{{ agentInfo.guarantor2 || '—' }}</dd>
<!-- One line for each guarantor the agent actually has - not two fixed lines, one of them empty. -->
@for (guarantor of agentInfo.guarantors; track $index; let i = $index) {
<dt>{{ agentInfo.guarantors.length > 1 ? 'Guarantor ' + (i + 1) : 'Guarantor' }}</dt>
<dd>{{ guarantor }}</dd>
} @empty {
<dt>Guarantor</dt>
<dd>—</dd>
}
<dt>Curr Bal</dt>
<dd>{{ agentInfo.currentBalance | number: '1.2-2' }}</dd>
</dl>
</div>
<div class="dte-info-panel">
<div class="dte-info-panel__title">Party Information</div>
<!-- Wider than the agent's card, with its details two pairs to a line, so a party's account is read without scrolling. -->
<div class="dte-info-panel dte-info-panel--party">
<div class="dte-info-panel__title"><i class="fas fa-user"></i>Party Information</div>
@if (partyInfo) {
<dl>
<dt>Client Id</dt>
@@ -79,15 +90,62 @@
</dl>
} @else {
<p class="dte-info-panel__hint">
Pick a party in a collection row below — this panel shows whose account it is and what the balance
<i class="fas fa-info-circle"></i>
Click an entry in the collections list below — this panel shows whose account it is and what the balance
becomes.
</p>
}
</div>
</div>
<!-- The round at a glance, once an agent is picked: nothing here is typed or saved. -->
@if (agentInfo.name) {
<div class="dte-kpis">
<div class="dte-kpi">
<span class="dte-kpi__label"><i class="fas fa-users"></i>Parties</span>
<strong class="dte-kpi__value">{{ agentParties.length }}</strong>
<span class="dte-kpi__hint">to collect from</span>
</div>
@if (isPartyTran) {
<div class="dte-kpi">
<span class="dte-kpi__label"><i class="fas fa-list-ol"></i>Entries</span>
<strong class="dte-kpi__value">{{ pendingEntries.length }}</strong>
<span class="dte-kpi__hint">written down, not handed in</span>
</div>
<div class="dte-kpi dte-kpi--accent">
<span class="dte-kpi__label"><i class="fas fa-rupee-sign"></i>To count</span>
<strong class="dte-kpi__value">{{ pendingTotal | number: '1.2-2' }}</strong>
<span class="dte-kpi__hint">the agent should hand over</span>
</div>
}
<!-- Agent Tran: what the agent owes on everything written down, what the cashier has taken in, and what is left. -->
@if (isAgentTran) {
<div class="dte-kpi dte-kpi--owed">
<span class="dte-kpi__label"><i class="fas fa-file-invoice"></i>To take</span>
<strong class="dte-kpi__value">{{ agentOwed | number: '1.2-2' }}</strong>
<span class="dte-kpi__hint">written down, not handed in</span>
</div>
<div class="dte-kpi dte-kpi--received">
<span class="dte-kpi__label"><i class="fas fa-check-circle"></i>Received (approved)</span>
<strong class="dte-kpi__value">{{ agentReceived | number: '1.2-2' }}</strong>
<span class="dte-kpi__hint">taken in by the cashier</span>
</div>
<div class="dte-kpi" [class.dte-kpi--due]="agentBalance > 0">
<span class="dte-kpi__label"><i class="fas fa-hourglass-half"></i>Still to take</span>
<strong class="dte-kpi__value">{{ agentBalance | number: '1.2-2' }}</strong>
<span class="dte-kpi__hint">left to receive</span>
</div>
}
<div class="dte-kpi">
<span class="dte-kpi__label"><i class="fas fa-wallet"></i>Agent Balance</span>
<strong class="dte-kpi__value">{{ agentInfo.currentBalance | number: '1.2-2' }}</strong>
<span class="dte-kpi__hint">held by {{ agentInfo.name }}</span>
</div>
</div>
}
<div class="dte-controls">
<mat-form-field appearance="outline" class="dte-field dte-field--agent">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="dte-field dte-field--agent">
<mat-label>Agent</mat-label>
<mat-select required formControlName="agentId" (selectionChange)="loadAgent()">
@for (agent of selectableAgents; track agent.id) {
@@ -112,7 +170,7 @@
}
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="dte-field">
<mat-label>Date</mat-label>
<!-- The date picks out a round, not just a posting date: a Party Tran settles whatever
the agent wrote down for that day, so changing it has to fetch that day's list. -->
@@ -127,7 +185,7 @@
<mat-datepicker #txnDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="dte-field">
<mat-label>Tran. Type</mat-label>
<mat-select required formControlName="transactionType">
@for (type of transactionTypes; track type.label) {
@@ -140,100 +198,43 @@
}
</mat-select>
</mat-form-field>
</div>
<!--
What the agent wrote down on their round, waiting to be counted.
This used to be an empty grid the clerk typed the round into, and typing it posted it: a
customer was credited the moment a line was keyed, whether or not the cash behind it ever
reached the branch. The agent records their own collections now, and what the clerk does here
is agree with them - so there is nothing to type, only a list to check against a bag of cash.
-->
@if (isPartyTran && agentInfo.name) {
<div class="dte-section">
<div class="dte-section__header">
<i class="fas fa-list"></i>
<h3>Collections up to {{ entryForm.get('transactionDate')?.value | dateFormat }}</h3>
<button mat-stroked-button type="button" class="dte-add-row" (click)="loadPendingEntries()">
<!-- Agent Tran: the cashier's receipts are applied to the customers from here - the dialog shows the days and receipts. -->
@if (isAgentTran && agentInfo.name) {
<div class="dte-agent-actions">
<button mat-stroked-button type="button" class="dte-refresh-btn" (click)="loadAgentMoney()">
<i class="fas fa-sync"></i> Refresh
</button>
</div>
@if (pendingLoading) {
<p class="dte-empty">Reading this agent's entries...</p>
} @else if (pendingEntries.length === 0) {
<p class="dte-empty">{{ agentInfo.name }} has nothing outstanding up to this date.</p>
} @else {
@for (entry of pendingEntries; track entry.id) {
<div class="dte-pending-row">
<!-- The day the agent took it, on the line itself. All the lines carry the date
chosen above, but a row that says so can be read, printed or shown to the agent
on its own without the header to explain it. -->
<span class="dte-pending-row__date">{{ entry.entryDate | dateFormat }}</span>
<span class="dte-pending-row__code">{{ entry.partyCode }}</span>
<span class="dte-pending-row__name">{{ entry.partyName }}</span>
<span class="dte-pending-row__mode"
>{{ entry.transactionMode }}
@if (entry.advanceDays) {
· {{ entry.advanceDays }}d advance
}
@if (entry.arrearsDays) {
· {{ entry.arrearsDays }}d previous
}
</span>
<span class="dte-pending-row__amount">{{ entry.amount | number: '1.2-2' }}</span>
</div>
}
<div class="dte-total">
<span>{{ pendingEntries.length }} {{ pendingEntries.length === 1 ? 'entry' : 'entries' }}</span>
<strong>{{ pendingTotal | number: '1.2-2' }}</strong>
</div>
<!--
The figure the clerk counts against, said in words rather than left as a column total -
it is the one number they are acting on, and the rest of the panel is working out.
It says what the agent wrote down and stops there. The money side - what is owed, what the
cashier has taken in, and applying it - is Agent Tran, so each type has one job.
-->
<p class="dte-expected dte-expected--note">
Everything {{ agentInfo.name }} wrote down and has not handed in yet, up to this date, customer by
customer. The cash and its receipts are under Agent Tran.
</p>
}
</div>
}
<!--
The agent's money: what they owe on everything written down, what the cashier has taken in
against it, and what is left.
This used to be a form the clerk typed an amount into to take the agent's cash in here. The
cash is taken at the cashier's counter now, into the agent's own GL account, and one way to take
the same cash in is the only safe number of ways - so there is nothing to type. What is done here
is applying the cashier's receipts to the customers, in Apply DDS Receipt.
-->
@if (isAgentTran && agentInfo.name) {
<div class="dte-section">
<div class="dte-section__header">
<i class="fas fa-hand-holding-usd"></i>
<h3>Agent's cash</h3>
<button mat-stroked-button type="button" class="dte-add-row" (click)="loadAgentMoney()">
<i class="fas fa-sync"></i> Refresh
</button>
<button
mat-raised-button
color="primary"
type="button"
class="dte-apply-receipt"
(click)="openApplyReceipt()"
>
<button mat-raised-button color="primary" type="button" class="dte-apply-btn" (click)="openApplyReceipt()">
<i class="fas fa-receipt"></i> Apply DDS Receipt
</button>
</div>
}
<!-- The agent's written-down round, in a window of its own and paged. -->
@if (isPartyTran && agentInfo.name) {
<button mat-stroked-button type="button" class="dte-view-btn" (click)="openCollections()">
<i class="fas fa-list"></i> View Collections
<span class="dte-view-btn__count">{{ pendingEntries.length }}</span>
</button>
}
</div>
<!--
What the agent wrote down on their round, waiting to be counted, is no longer listed down this
page - a long round pushed everything else out of sight. It opens from View Collections above, in a
window of its own and paged, and a line clicked there shows that customer in Party Information.
-->
<!--
The agent's money used to have a section of its own down the page: what they owe on everything
written down, what the cashier has taken in, what is left, then the outstanding days and the
cashier's receipts. Apply DDS Receipt shows the same days and receipts, so the section is gone -
the three figures sit in the summary above, the button is beside the Date, and what is below is
only what the dialog does not say: which GL the cash goes into and anything that looks wrong.
-->
@if (isAgentTran && agentInfo.name) {
<div class="dte-notes">
@if (!agentGlCode) {
<p class="dte-expected dte-expected--note">
{{ agentInfo.name }} has no GL account, so the cashier has nowhere to take their cash. Set one on Agent
@@ -246,21 +247,6 @@
takes {{ agentInfo.name }}'s cash into this GL (Entry Type DDS).
</p>
}
<div class="dte-money">
<div class="dte-money__box">
<span class="dte-money__label">To take</span>
<strong>{{ agentOwed | number: '1.2-2' }}</strong>
</div>
<div class="dte-money__box">
<span class="dte-money__label">Received (approved)</span>
<strong>{{ agentReceived | number: '1.2-2' }}</strong>
</div>
<div class="dte-money__box" [class.dte-money__box--due]="agentBalance > 0">
<span class="dte-money__label">Still to take</span>
<strong>{{ agentBalance | number: '1.2-2' }}</strong>
</div>
</div>
@if (agentReceivedBeyondDay > 0) {
<p class="dte-expected dte-expected--note">
{{ agentReceivedBeyondDay | number: '1.2-2' }} received at the counter is more than everything outstanding
@@ -273,66 +259,6 @@
{{ agentWaiting | number: '1.2-2' }} more is at the counter awaiting the manager's approval.
</p>
}
<h4 class="dte-subhead">
Outstanding entries up to {{ entryForm.get('transactionDate')?.value | dateFormat }}
</h4>
@if (agentMoneyLoading) {
<p class="dte-empty">Reading this agent's entries...</p>
} @else if (agentDays.length === 0) {
<p class="dte-empty">{{ agentInfo.name }} has nothing outstanding.</p>
} @else {
@for (day of agentDays; track day.date) {
<div class="dte-pending-row dte-pending-row--day" (click)="toggleAgentDay(day.date)">
<span class="dte-pending-row__date">{{ day.date | dateFormat }}</span>
<span class="dte-pending-row__name">
{{ day.entries.length }} {{ day.entries.length === 1 ? 'entry' : 'entries' }}
</span>
<span class="dte-pending-row__amount">{{ day.total | number: '1.2-2' }}</span>
<i [class]="agentOpenDay === day.date ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
</div>
@if (agentOpenDay === day.date) {
<div class="dte-day-lines">
@for (entry of day.entries; track entry.id) {
<div class="dte-pending-row">
<span class="dte-pending-row__code">{{ entry.partyCode }}</span>
<span class="dte-pending-row__name">{{ entry.partyName }}</span>
<span class="dte-pending-row__mode"
>{{ entry.transactionMode }}
@if (entry.advanceDays) {
· {{ entry.advanceDays }}d advance
}
@if (entry.arrearsDays) {
· {{ entry.arrearsDays }}d previous
}
</span>
<span class="dte-pending-row__amount">{{ entry.amount | number: '1.2-2' }}</span>
</div>
}
</div>
}
}
}
<h4 class="dte-subhead">Cashier receipts</h4>
@if (agentReceipts.length === 0) {
<p class="dte-empty">
No DDS receipt on {{ agentInfo.name }}'s GL account{{ agentGlCode ? ' ' + agentGlCode : '' }} waiting to
be applied.
</p>
} @else {
@for (receipt of agentReceipts; track receipt.id) {
<div class="dte-pending-row">
<span class="dte-pending-row__code">{{ receipt.voucherNo }}</span>
<span class="dte-pending-row__date">{{ receipt.txnDate | dateFormat }}</span>
<span class="dte-pending-row__name">{{ receipt.cashierName || '—' }}</span>
<span class="dte-status" [class.dte-status--ok]="receipt.status === 'APPROVED'">
{{ receipt.status === 'APPROVED' ? 'Approved' : 'Awaiting manager' }}
</span>
<span class="dte-pending-row__amount">{{ receipt.amount | number: '1.2-2' }}</span>
</div>
}
}
</div>
}
@@ -342,84 +268,102 @@
<i class="fas fa-exchange-alt"></i>
<h3>Withdraw Deposite</h3>
</div>
<div class="dte-grid">
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Party Code</mat-label>
<input
matInput
formControlName="creditorPartyCode"
[matAutocomplete]="creditorAuto"
#creditorTrigger="matAutocompleteTrigger"
(click)="creditorTrigger.openPanel()"
(focus)="creditorTrigger.openPanel()"
(blur)="settleTransferParty('creditor')"
placeholder="Search by code or name…"
autocomplete="off"
/>
<mat-icon matSuffix class="dte-list-arrow" (click)="$event.stopPropagation(); creditorTrigger.openPanel()"
>arrow_drop_down</mat-icon
>
<mat-autocomplete #creditorAuto="matAutocomplete" panelWidth="min(30rem, 92vw)">
@for (party of transferPartyOptions('creditor'); track party.id) {
<mat-option [value]="party.partyCode">
{{ party.partyCode }} · {{ party.partyName }} · ₹{{ party.currentBalance | number: '1.2-2' }}
</mat-option>
}
@if (transferPartyOptions('creditor').length === 0) {
<mat-option disabled>No open DDS account matches</mat-option>
}
</mat-autocomplete>
<mat-hint>Account the money goes into</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Party Name</mat-label>
<input matInput formControlName="creditorPartyName" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Curr Bal</mat-label>
<input matInput formControlName="creditorPartyBalance" readonly />
</mat-form-field>
<div class="dte-grid dte-grid--transfer">
<!-- The account the money goes into. -->
<div class="dte-party-card dte-party-card--to">
<div class="dte-party-card__title"><i class="fas fa-arrow-circle-down"></i>Money goes into</div>
<div class="dte-party-card__fields">
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Party Code</mat-label>
<input
matInput
formControlName="creditorPartyCode"
[matAutocomplete]="creditorAuto"
#creditorTrigger="matAutocompleteTrigger"
(click)="creditorTrigger.openPanel()"
(focus)="creditorTrigger.openPanel()"
(blur)="settleTransferParty('creditor')"
placeholder="Search by code or name…"
autocomplete="off"
/>
<mat-icon
matSuffix
class="dte-list-arrow"
(click)="$event.stopPropagation(); creditorTrigger.openPanel()"
>arrow_drop_down</mat-icon
>
<mat-autocomplete #creditorAuto="matAutocomplete" panelWidth="min(30rem, 92vw)">
@for (party of transferPartyOptions('creditor'); track party.id) {
<mat-option [value]="party.partyCode">
{{ party.partyCode }} · {{ party.partyName }} · ₹{{ party.currentBalance | number: '1.2-2' }}
</mat-option>
}
@if (transferPartyOptions('creditor').length === 0) {
<mat-option disabled>No open DDS account matches</mat-option>
}
</mat-autocomplete>
<mat-hint>Account the money goes into</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Party Name</mat-label>
<input matInput formControlName="creditorPartyName" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Curr Bal</mat-label>
<input matInput formControlName="creditorPartyBalance" readonly />
</mat-form-field>
</div>
</div>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Party Code</mat-label>
<input
matInput
formControlName="debitPartyCode"
[matAutocomplete]="debitAuto"
#debitTrigger="matAutocompleteTrigger"
(click)="debitTrigger.openPanel()"
(focus)="debitTrigger.openPanel()"
(blur)="settleTransferParty('debit')"
placeholder="Search by code or name…"
autocomplete="off"
/>
<mat-icon matSuffix class="dte-list-arrow" (click)="$event.stopPropagation(); debitTrigger.openPanel()"
>arrow_drop_down</mat-icon
>
<mat-autocomplete #debitAuto="matAutocomplete" panelWidth="min(30rem, 92vw)">
@for (party of transferPartyOptions('debit'); track party.id) {
<mat-option [value]="party.partyCode">
{{ party.partyCode }} · {{ party.partyName }} · ₹{{ party.currentBalance | number: '1.2-2' }}
</mat-option>
}
@if (transferPartyOptions('debit').length === 0) {
<mat-option disabled>No open DDS account matches</mat-option>
}
</mat-autocomplete>
@if (transferToItself) {
<mat-hint class="dte-same-party">Pick a different account - it cannot transfer to itself</mat-hint>
} @else {
<mat-hint>Account the money is taken from</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Party Name</mat-label>
<input matInput formControlName="debitPartyName" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Curr Bal</mat-label>
<input matInput formControlName="debitPartyBalance" readonly />
</mat-form-field>
<!-- The account the money is taken from. -->
<div class="dte-party-card dte-party-card--from">
<div class="dte-party-card__title"><i class="fas fa-arrow-circle-up"></i>Money is taken from</div>
<div class="dte-party-card__fields">
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Party Code</mat-label>
<input
matInput
formControlName="debitPartyCode"
[matAutocomplete]="debitAuto"
#debitTrigger="matAutocompleteTrigger"
(click)="debitTrigger.openPanel()"
(focus)="debitTrigger.openPanel()"
(blur)="settleTransferParty('debit')"
placeholder="Search by code or name…"
autocomplete="off"
/>
<mat-icon
matSuffix
class="dte-list-arrow"
(click)="$event.stopPropagation(); debitTrigger.openPanel()"
>arrow_drop_down</mat-icon
>
<mat-autocomplete #debitAuto="matAutocomplete" panelWidth="min(30rem, 92vw)">
@for (party of transferPartyOptions('debit'); track party.id) {
<mat-option [value]="party.partyCode">
{{ party.partyCode }} · {{ party.partyName }} · ₹{{ party.currentBalance | number: '1.2-2' }}
</mat-option>
}
@if (transferPartyOptions('debit').length === 0) {
<mat-option disabled>No open DDS account matches</mat-option>
}
</mat-autocomplete>
@if (transferToItself) {
<mat-hint class="dte-same-party">Pick a different account - it cannot transfer to itself</mat-hint>
} @else {
<mat-hint>Account the money is taken from</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Party Name</mat-label>
<input matInput formControlName="debitPartyName" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Curr Bal</mat-label>
<input matInput formControlName="debitPartyBalance" readonly />
</mat-form-field>
</div>
</div>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Amount</mat-label>