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:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user