fix: tenant-config fixes, cashier/share/savings UI bugs, and menu-access hardcoding

- Add Balance Sheet report and disabled-features accounting config
- Point menu-access tenant header and env.js/environment.ts tenant identifier
  at env config instead of hardcoded 'default'/'lincpay' literals
- Surface the real backend error (not a generic "no reference id") when an
  OTP send fails in contact-otp-block
- Fix branch-day "₹null" display bug (null vs undefined) and add a guard
  blocking a branch's first-ever day open before cash denomination migration
- Dynamically size the denomination count input and block re-opening an
  already-open business day from Migrate Cash Denomination
- Lock the denomination-managed cash GL row in Migrate Opening Balances
  (read-only, with a link back to Migrate Cash Denomination)
- Remove duplicate Cancel/Submit action bars on the Share Product and
  Savings Account creation preview steps; align Previous into the same row
- Hide "Apply Collection" on a share account once its advance collection
  is already settled (further purchases go through Apply Additional Shares)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 15:19:21 +05:30
co-authored by Claude Sonnet 5
parent 42424dff8b
commit 7b77166b19
39 changed files with 1800 additions and 266 deletions
@@ -49,6 +49,18 @@
</div>
}
@if (firstOpenNeedsCashMigration && !missedOnly) {
<div class="bd-locked">
<mat-icon>lock</mat-icon>
<span>
This branch has never opened a business day, and its Cash at Vault GL account has no opening balance yet. Count
the vault and open the first business day together from
<a [routerLink]="['/accounting/migrate-cash-denomination']">Migrate Cash Denomination</a> — Open Day here is
locked until that is done, so today's cash tracking never starts out of step with the ledger.
</span>
</div>
}
<!-- Missed closes first: these are the ones that need action. -->
@if (missed.length > 0) {
<div class="db-exceptions">
@@ -703,10 +715,10 @@
<span class="db-muted">· {{ timeOnly(d.closedAt) }}</span>
}
</td>
<td class="num">{{ d.closingCash != null ? '₹' + (d.closingCash | number: '1.2-2') : '—' }}</td>
<td class="num">{{ d.bookCash != null ? '₹' + (d.bookCash | number: '1.2-2') : '—' }}</td>
<td class="num">{{ hasValue(d.closingCash) ? '₹' + (d.closingCash | number: '1.2-2') : '—' }}</td>
<td class="num">{{ hasValue(d.bookCash) ? '₹' + (d.bookCash | number: '1.2-2') : '—' }}</td>
<td>
@if (d.cashDifference == null) {
@if (!hasValue(d.cashDifference)) {
—
} @else if (d.cashDifference === 0) {
<span class="db-badge db-badge--ok">Tallied</span>
@@ -30,6 +30,36 @@
}
}
/* Blocks Open Day until the branch's first-ever open has gone through Migrate Cash Denomination —
see BranchDayComponent.checkFirstOpenCashMigration(). Same shape as .ve-note, in the error palette
the rest of this screen already uses for a missed close (.bd-missed). */
.bd-locked {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 14px;
margin-bottom: 14px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #b91c1c;
font-size: 12px;
line-height: 1.5;
mat-icon {
flex: none;
font-size: 18px;
width: 18px;
height: 18px;
}
a {
color: inherit;
font-weight: 600;
text-decoration: underline;
}
}
.bd-success {
padding: 10px 14px;
margin-bottom: 14px;
@@ -10,7 +10,7 @@ import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { catchError, forkJoin, map, of, switchMap } from 'rxjs';
import { CashierService } from '../cashier.service';
@@ -61,7 +61,8 @@ interface BranchDay {
imports: [
CommonModule,
FormsModule,
MatIconModule
MatIconModule,
RouterLink
]
})
export class BranchDayComponent implements OnInit {
@@ -85,6 +86,12 @@ export class BranchDayComponent implements OnInit {
loaded = false;
errorMsg = '';
successMsg = '';
/** True only for a branch that has never opened a business day and whose Cash at Vault / Cash at
* Teller GL account has never had anything posted to it — Open Day here would let day-to-day cash
* tracking begin while the ledger silently stays wherever it already was. The backend enforces
* this independently too (see BranchDayRegisterApiResource#requireCashMigrationBeforeFirstEverOpen);
* this flag only steers the user to the right screen before they hit that rejection. */
firstOpenNeedsCashMigration = false;
// Open-day form
showOpenForm = false;
@@ -202,6 +209,7 @@ export class BranchDayComponent implements OnInit {
this.today = res?.today ?? '';
this.canOperate = !!res?.canOperate;
this.openDate = this.today;
this.checkFirstOpenCashMigration();
});
this.service
@@ -212,9 +220,42 @@ export class BranchDayComponent implements OnInit {
});
}
/**
* Only relevant the moment this branch has never opened a business day before — every later
* open/close on this screen is untouched, so there is nothing to re-check once history exists.
*/
private checkFirstOpenCashMigration(): void {
if (this.days.length > 0 || this.openDay) {
this.firstOpenNeedsCashMigration = false;
return;
}
this.service.cashGlStatus().subscribe({
next: (status: any) => {
// configured === false means this tenant has no Cash at Vault / Cash at Teller account
// mapped at all — nothing for this check to reconcile against, so it does not block.
this.firstOpenNeedsCashMigration = status?.configured !== false && !status?.hasBalance;
},
error: () => {
this.firstOpenNeedsCashMigration = false;
}
});
}
/** True when the branch has no open day at all, so today can be opened. */
get canOpenToday(): boolean {
return this.canOperate && !this.openDay;
return this.canOperate && !this.openDay && !this.firstOpenNeedsCashMigration;
}
/**
* A still-open day's closing/book figures are dropped from the API response entirely (the
* backend's Gson instance omits a null map value rather than sending it as JSON null), so the
* field arrives as undefined, not null. Checking only `!== null` against that — which a stricter
* pass of this file made the mistake of doing — leaves undefined on the "has a value" branch,
* which then renders `'₹' + null` (the number pipe's own null output, string-concatenated) as the
* literal text "₹null". This checks for either, the way an open day actually shows up.
*/
hasValue(value: number | null | undefined): boolean {
return value !== null && value !== undefined;
}
startOpen(): void {
@@ -3189,7 +3189,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
// Step 1: search on the correct endpoint for this account type
this.http
.get<any>(
`${cfg.list}?${cfg.listQueryParam}=${encodeURIComponent(acNo)}${cfg.supportsPagination ? `&offset=0&limit=${cfg.maxLimit}` : ''}`
`${cfg.list}?${cfg.listQueryParam}=${encodeURIComponent(acNo)}${cfg.supportsPagination ? `&offset=0&limit=${cfg.maxLimit}` : ''}${cfg.extraListQueryParams ? `&${cfg.extraListQueryParams}` : ''}`
)
.pipe(catchError(() => of(null)))
.subscribe((res: any) => {
@@ -3278,6 +3278,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
matchField: string;
detailParams: string;
supportsPagination: boolean;
/** Extra fixed query params (e.g. "usage=1") appended to every list-search request for this
* account type — used to keep Header GL accounts out of the results a clerk/cashier can pick
* for a transaction; a Header can never receive a posting, so it should never be offered. */
extraListQueryParams?: string;
} {
const t = acTyp?.toUpperCase();
if (t === 'LA') {
@@ -3331,7 +3335,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
listQueryParam: 'searchParam',
matchField: 'glCode',
detailParams: '?fetchRunningBalance=true',
supportsPagination: false
supportsPagination: false,
// Detail-only: a Header account is a pure grouping node and can never receive a
// transaction, so it must never be a selectable match here in the first place.
extraListQueryParams: 'usage=1'
};
}
if (t === 'CA' || t === 'CB') {