Compare commits

...
Author SHA1 Message Date
niteshandClaude Opus 5.5 55f5c1cf32 feat(ui): longer startup splash and updated loading animation
The startup splash now holds for at least 4 seconds (progress 2% per tick up to 90%)
instead of 2, and the loading animation is updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:31:24 +05:30
niteshandClaude Opus 5.5 6507e68203 feat(cashier): FD is credit-only on the transaction entry, and a Bank Transfer can fund an FD
- A Fixed Deposit row is credit-only on the clerk and cashier screens (isFdCreditOnly):
  its DR button is disabled, and Save refuses any FD row left on DR (for example by a
  Withdrawal Slip TYPE). Money leaves an FD only through its maturity or premature
  closure; the clerk's "Close this account" flow is the one exception.
- Bank Transfer / Fund Transfer accepts an FD on the CR side, so an FD can be funded from
  the customer's own Savings or Current account. The server enforces the funding rules
  (first and only funding, exactly the principal, the source account's owner, freeze
  status and available balance).

Requires the backend change "FD funding rules and no FD withdrawals from clerk/cashier requests".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:31:03 +05:30
4 changed files with 86 additions and 54 deletions
@@ -184,7 +184,7 @@
class="ws-f__ctl" class="ws-f__ctl"
[(ngModel)]="row.acTyp" [(ngModel)]="row.acTyp"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
(change)="isGlCreditOnly(row) && (row.dbCr = 'Cr')" (change)="isCreditOnly(row) && (row.dbCr = 'Cr')"
> >
@for (a of acTypOptionsFor(row); track a) { @for (a of acTypOptionsFor(row); track a) {
<option [value]="a">{{ a }}</option> <option [value]="a">{{ a }}</option>
@@ -267,7 +267,7 @@
type="button" type="button"
class="ws-seg__btn ws-seg__btn--dr" class="ws-seg__btn ws-seg__btn--dr"
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'" [class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
[disabled]="isGlCreditOnly(row)" [disabled]="isCreditOnly(row)"
(click)="row.dbCr = 'Dr'" (click)="row.dbCr = 'Dr'"
> >
DR DR
@@ -1644,7 +1644,7 @@
class="ws-f__ctl" class="ws-f__ctl"
[(ngModel)]="row.acTyp" [(ngModel)]="row.acTyp"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
(change)="isGlCreditOnly(row) && (row.dbCr = 'Cr')" (change)="isCreditOnly(row) && (row.dbCr = 'Cr')"
> >
@for (a of acTypOptionsFor(row); track a) { @for (a of acTypOptionsFor(row); track a) {
<option [value]="a">{{ a }}</option> <option [value]="a">{{ a }}</option>
@@ -1715,7 +1715,7 @@
type="button" type="button"
class="ws-seg__btn ws-seg__btn--dr" class="ws-seg__btn ws-seg__btn--dr"
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'" [class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
[disabled]="isGlCreditOnly(row)" [disabled]="isCreditOnly(row)"
(click)="$event.stopPropagation(); row.dbCr = 'Dr'" (click)="$event.stopPropagation(); row.dbCr = 'Dr'"
> >
DR DR
@@ -2344,6 +2344,20 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
); );
} }
/**
* A Fixed Deposit row is credit-only on both the clerk and the cashier screens: an FD is funded once, with
* exactly its principal, and money leaves it only through its maturity or premature closure. The one
* exception is the clerk's own "Close this account" flow, which raises a closure request.
*/
isFdCreditOnly(row: TxnRow): boolean {
return row.acTyp === 'FD' && !this.pendingClosure;
}
/** DR is not offered for this row (see isGlCreditOnly and isFdCreditOnly). */
isCreditOnly(row: TxnRow): boolean {
return this.isGlCreditOnly(row) || this.isFdCreditOnly(row);
}
/** Called when the TYPE dropdown changes — sets sensible defaults for TXN TP and DR/CR */ /** Called when the TYPE dropdown changes — sets sensible defaults for TXN TP and DR/CR */
/** /**
* A cashier row that was filled from an approved token (withdrawal / account-closure) is cash * A cashier row that was filled from an approved token (withdrawal / account-closure) is cash
@@ -5272,6 +5286,20 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
} }
onSave(): void { onSave(): void {
// Money never leaves a Fixed Deposit from this screen, clerk or cashier: no withdrawal slip, payment,
// collection or transfer debits an FD. It is paid out only through its maturity or premature closure
// (the clerk's "Close this account" flow). The DR button is already disabled for FD rows (isFdCreditOnly);
// this also catches a row set to DR by its TYPE, such as a Withdrawal Slip.
const fdDebitRows = this.rows.filter((r) => this.isFdCreditOnly(r) && r.dbCr === 'Dr');
if (fdDebitRows.length > 0) {
this.showAlert('error', 'Fixed Deposit Cannot Be Debited', [
`FD account(s) ${fdDebitRows.map((r) => r.acNo).join(', ')} cannot be debited or withdrawn from.`,
'Money leaves a Fixed Deposit only through its maturity or premature closure.',
'An FD can only be credited — once, with exactly its principal.'
]);
return;
}
// Demand drafts commit here like everything else on the worksheet. Checked first because a DD // Demand drafts commit here like everything else on the worksheet. Checked first because a DD
// row shares its shape with the ordinary entries below — without this it would fall through and // row shares its shape with the ordinary entries below — without this it would fall through and
// post as a plain transaction, with the draft never raised and the charges silently dropped. // post as a plain transaction, with the draft never raised and the charges silently dropped.
@@ -5349,7 +5377,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
return; return;
} }
// Bank transfer: only savings (SD) or current (CA/CB) accounts allowed // Bank transfer: savings (SD) or current (CA/CB) accounts on either side, plus a Fixed Deposit (FD) on
// the CR side only -- funding it from the customer's own account. The server checks the funding rules
// (first and only funding, exactly the principal, the source account's owner and available balance).
const allowedTypes = new Set([ const allowedTypes = new Set([
'SD', 'SD',
'CA', 'CA',
@@ -5357,13 +5387,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
]); ]);
const invalidAccounts = [ const invalidAccounts = [
...drRows, ...drRows,
...crRows ...crRows.filter((r) => r.acTyp !== 'FD')
].filter((r) => !allowedTypes.has(r.acTyp)); ].filter((r) => !allowedTypes.has(r.acTyp));
if (invalidAccounts.length > 0) { if (invalidAccounts.length > 0) {
this.showAlert('error', 'Invalid Account Type', [ this.showAlert('error', 'Invalid Account Type', [
`Account type(s) not allowed in Bank Transfer: ${invalidAccounts.map((r) => r.acTyp + ' (' + r.acNo + ')').join(', ')}`, `Account type(s) not allowed in Bank Transfer: ${invalidAccounts.map((r) => r.acTyp + ' (' + r.acNo + ')').join(', ')}`,
'Only Savings (SD) and Current (CA) accounts are permitted.', 'Debit side: only Savings (SD) and Current (CA) accounts. Credit side: SD, CA or a Fixed Deposit (FD).',
'GL, FD and SHR accounts are not allowed — use Disbursement/Collection instead.', 'GL and SHR accounts are not allowed — use Disbursement/Collection instead.',
'RD installments are paid through a cashier Receipt or Auto Debit.' 'RD installments are paid through a cashier Receipt or Auto Debit.'
]); ]);
return; return;
+42 -40
View File
@@ -45,15 +45,17 @@
"hasMask": false, "hasMask": false,
"ao": 0, "ao": 0,
"ks": { "a": { "a": 0, "k": [ "ks": { "a": { "a": 0, "k": [
-17.052704811096184, 0,
-15.0467209815979 0,
0
], "ix": 1 }, "s": { "a": 0, "k": [ ], "ix": 1 }, "s": { "a": 0, "k": [
362.228, 394,
362.228, 394,
100 100
], "ix": 6 }, "sk": { "a": 0, "k": 0 }, "p": { "a": 0, "k": [ ], "ix": 6 }, "sk": { "a": 0, "k": 0 }, "p": { "a": 0, "k": [
188.2303284168625, 250,
195.49656352277756 250,
0
], "ix": 2 }, "r": { "a": 0, "k": 0, "ix": 10 }, "sa": { "a": 0, "k": 0 }, "o": { "a": 0, "k": 100, "ix": 11 } }, ], "ix": 2 }, "r": { "a": 0, "k": 0, "ix": 10 }, "sa": { "a": 0, "k": 0 }, "o": { "a": 0, "k": 100, "ix": 11 } },
"shapes": [ "shapes": [
{ {
@@ -341,16 +343,16 @@
"g": { "p": 3, "k": { "a": 0, "k": [ "g": { "p": 3, "k": { "a": 0, "k": [
0, 0,
1, 1,
0.4470588235294118, 0.6,
0, 0.2,
0.485, 0.485,
0.9725490196078431,
0.2235294117647059,
0.5019607843137255,
1, 1,
0.9411764705882353, 0.6,
0, 0.2,
1 1,
1,
0.6,
0.2
], "ix": 8 } }, ], "ix": 8 } },
"t": 1, "t": 1,
"a": { "a": 0, "k": 0 }, "a": { "a": 0, "k": 0 },
@@ -363,7 +365,7 @@
"lj": 1, "lj": 1,
"ml": 4, "ml": 4,
"o": { "a": 0, "k": 100, "ix": 9 }, "o": { "a": 0, "k": 100, "ix": 9 },
"w": { "a": 0, "k": 2, "ix": 10 } "w": { "a": 0, "k": 2.5, "ix": 10 }
}, },
{ {
"ty": "fl", "ty": "fl",
@@ -402,8 +404,8 @@
"o": { "o": {
"a": 1, "a": 1,
"k": [ "k": [
{ "o": { "x": 0.167, "y": 0.167 }, "i": { "x": 0.833, "y": 0.833 }, "s": [0], "t": 0 }, { "o": { "x": 0.167, "y": 0.167 }, "i": { "x": 0.833, "y": 0.833 }, "s": [28], "t": 0 },
{ "s": [360], "t": 90.0000036657751 } { "s": [388], "t": 90.0000036657751 }
], ],
"ix": 3 "ix": 3
}, },
@@ -437,7 +439,7 @@
250, 250,
250, 250,
0 0
], "ix": 2 }, "r": { "a": 0, "k": 60, "ix": 10 }, "sa": { "a": 0, "k": 0 }, "o": { "a": 0, "k": 100, "ix": 11 } }, ], "ix": 2 }, "r": { "a": 0, "k": 0, "ix": 10 }, "sa": { "a": 0, "k": 0 }, "o": { "a": 0, "k": 100, "ix": 11 } },
"shapes": [ "shapes": [
{ {
"ty": "gr", "ty": "gr",
@@ -723,16 +725,16 @@
], "ix": 5 }, ], "ix": 5 },
"g": { "p": 3, "k": { "a": 0, "k": [ "g": { "p": 3, "k": { "a": 0, "k": [
0, 0,
0, 1,
0.7764705882352941, 1,
1, 1,
0.485, 0.485,
0.24705882352941178,
0.38823529411764707,
1, 1,
1, 1,
0.49411764705882355, 1,
0, 1,
1,
1,
1 1
], "ix": 8 } }, ], "ix": 8 } },
"t": 1, "t": 1,
@@ -746,7 +748,7 @@
"lj": 1, "lj": 1,
"ml": 4, "ml": 4,
"o": { "a": 0, "k": 100, "ix": 9 }, "o": { "a": 0, "k": 100, "ix": 9 },
"w": { "a": 0, "k": 0.5, "ix": 10 } "w": { "a": 0, "k": 2.5, "ix": 10 }
}, },
{ {
"ty": "fl", "ty": "fl",
@@ -813,14 +815,14 @@
0, 0,
0 0
], "ix": 1 }, "s": { "a": 0, "k": [ ], "ix": 1 }, "s": { "a": 0, "k": [
362.228, 330,
362.228, 330,
100 100
], "ix": 6 }, "sk": { "a": 0, "k": 0 }, "p": { "a": 0, "k": [ ], "ix": 6 }, "sk": { "a": 0, "k": 0 }, "p": { "a": 0, "k": [
250, 250,
250, 250,
0 0
], "ix": 2 }, "r": { "a": 0, "k": 30, "ix": 10 }, "sa": { "a": 0, "k": 0 }, "o": { "a": 0, "k": 100, "ix": 11 } }, ], "ix": 2 }, "r": { "a": 0, "k": 0, "ix": 10 }, "sa": { "a": 0, "k": 0 }, "o": { "a": 0, "k": 100, "ix": 11 } },
"shapes": [ "shapes": [
{ {
"ty": "gr", "ty": "gr",
@@ -1106,17 +1108,17 @@
], "ix": 5 }, ], "ix": 5 },
"g": { "p": 3, "k": { "a": 0, "k": [ "g": { "p": 3, "k": { "a": 0, "k": [
0, 0,
1, 0.0784,
0.8235294117647058, 0.7059,
0, 0.2118,
0.485, 0.485,
0.5019607843137255, 0.0784,
0.8117647058823529, 0.7059,
0.5019607843137255, 0.2118,
1, 1,
0, 0.0784,
0.8, 0.7059,
1 0.2118
], "ix": 8 } }, ], "ix": 8 } },
"t": 1, "t": 1,
"a": { "a": 0, "k": 0 }, "a": { "a": 0, "k": 0 },
@@ -1129,7 +1131,7 @@
"lj": 1, "lj": 1,
"ml": 4, "ml": 4,
"o": { "a": 0, "k": 100, "ix": 9 }, "o": { "a": 0, "k": 100, "ix": 9 },
"w": { "a": 0, "k": 0.5, "ix": 10 } "w": { "a": 0, "k": 2.5, "ix": 10 }
}, },
{ {
"ty": "fl", "ty": "fl",
@@ -1168,8 +1170,8 @@
"o": { "o": {
"a": 1, "a": 1,
"k": [ "k": [
{ "o": { "x": 0.167, "y": 0.167 }, "i": { "x": 0.833, "y": 0.833 }, "s": [0], "t": 0 }, { "o": { "x": 0.167, "y": 0.167 }, "i": { "x": 0.833, "y": 0.833 }, "s": [-28], "t": 0 },
{ "s": [360], "t": 90.0000036657751 } { "s": [332], "t": 90.0000036657751 }
], ],
"ix": 3 "ix": 3
}, },
+6 -6
View File
@@ -325,10 +325,10 @@
}; };
document.head.appendChild(script); document.head.appendChild(script);
// Progress simulation stages (Guaranteed ~2 seconds duration on refresh) // Progress simulation stages (Guaranteed ~4 seconds duration on refresh: 2% every 90ms reaches 90% in about 4s)
var interval = setInterval(function () { var interval = setInterval(function () {
if (progress < 90) { if (progress < 90) {
progress += 5; progress += 2;
} else if (angularReady && progress < 100) { } else if (angularReady && progress < 100) {
progress += 5; progress += 5;
} }
@@ -351,7 +351,7 @@
} }
}, 90); }, 90);
// Enforce minimum hold time (2000ms = 2 seconds) so refresh always shows the animation // Enforce minimum hold time (4000ms = 4 seconds) so refresh always shows the animation
setTimeout(function () { setTimeout(function () {
minTimePassed = true; minTimePassed = true;
if (angularReady) { if (angularReady) {
@@ -359,7 +359,7 @@
if (bar) bar.style.width = '100%'; if (bar) bar.style.width = '100%';
dismissSplash(); dismissSplash();
} }
}, 2000); }, 4000);
function dismissSplash() { function dismissSplash() {
if (dismissed) return; if (dismissed) return;
@@ -387,12 +387,12 @@
} }
}; };
// Safety fallback (Max 7s in case of backend network failure) // Safety fallback (Max 9s in case of backend network failure)
setTimeout(function () { setTimeout(function () {
angularReady = true; angularReady = true;
minTimePassed = true; minTimePassed = true;
dismissSplash(); dismissSplash();
}, 7000); }, 9000);
})(); })();
</script> </script>