feat(cashier): add CBS Blue Warm Stone theme, validations, non-individual support, and custom alert modal

- Replace table grid with ws-wrap card-form layout (Warm Stone / CBS Blue design)
- Add day-status blocking banners (previousDayUnsettled, morningTransferReceived)
- Add custom styled alert modal replacing browser alert() — error/warning/info/success types
- Add Non-Individual Account Details section with Partners table (click-to-view signature/image)
- Fix denomination overdraft inline alert in Closing Position panel
- Fix bannedEntityMatch/willfulDefaulter false positive (DB stores YES/NO strings)
- Fix denomination mismatch 403 — only send denomination when cash payment matches amount
- Fix Today's Activity for CA accounts (m_current_account_transaction, remarks column, type=7 label)
- Fix A/C TYPE [object Object] — map from acTyp label
- Fix charge deduction sign — use transactionType.deposit instead of withdrawal
- Remove Add Row buttons everywhere
- CBS Blue theme variables: focus #2563eb, accent #e87722, Hanken Grotesk + Newsreader fonts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-08 10:39:34 +05:30
co-authored by Claude Sonnet 4.6
parent f64047080a
commit f7d9259eb7
3 changed files with 284 additions and 218 deletions
@@ -64,13 +64,6 @@
Eff. Date
<input class="ws-header__date" type="date" [value]="todayIso" readonly />
</span>
<button class="ws-btn ws-btn--add" type="button" (click)="addRow()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
Add Row
</button>
</div>
</div>
@@ -251,64 +244,9 @@
</div>
</div>
</div>
<!-- Delete button -->
<button
class="ws-card__del"
type="button"
title="Remove row"
[disabled]="rows.length <= 1"
(click)="$event.stopPropagation(); deleteRow(i)"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
}
</div>
<!-- ── Footer totals bar ── -->
<div class="ws-footer">
<div class="ws-footer__stats">
<div class="ws-stat">
<span class="ws-stat__label">Rows</span>
<span class="ws-stat__val">{{ rows.length }}</span>
</div>
<div class="ws-stat">
<span class="ws-stat__label">Total Debit</span>
<span class="ws-stat__val ws-stat__val--dr">₹ {{ formatInr(totalDebit) }}</span>
</div>
<div class="ws-stat">
<span class="ws-stat__label">Total Credit</span>
<span class="ws-stat__val ws-stat__val--cr">₹ {{ formatInr(totalCredit) }}</span>
</div>
<div class="ws-stat">
<span class="ws-stat__label">Difference</span>
<span class="ws-stat__val" [class.ws-stat__val--ok]="difference === 0">₹ {{ formatInr(difference) }}</span>
</div>
</div>
<div class="ws-footer__actions">
<span
class="ws-balance-pill"
[class.ws-balance-pill--ok]="isBalanced"
[class.ws-balance-pill--bad]="!isBalanced"
>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
@if (isBalanced) {
<polyline points="20 6 9 17 4 12" />
} @else {
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
}
</svg>
{{ isBalanced ? 'Balanced' : 'Not balanced' }}
</span>
<button class="ws-btn ws-btn--draft" type="button">Save Draft</button>
<button class="ws-btn ws-btn--post" type="button" [disabled]="!isBalanced">Post Entry</button>
</div>
</div>
</div>
}
@@ -1230,108 +1168,202 @@
</div>
</div>
<!-- ── ENTRY GRID (bottom) ─────────────────────────────────────── -->
<div class="te-grid-wrap">
<table class="te-grid">
<thead>
<tr>
<th>Typ</th>
<th>Br.</th>
<th>Tr.Tp.</th>
<th>Actyp</th>
<th>A/c No.</th>
<th>Ecs A/c No.</th>
<th>Db./Cr.</th>
<th>Pay Type *</th>
<th>Cheque No.</th>
<th>Amount</th>
<th>Description</th>
<th>Tok.No.</th>
<th>Cheq.Date</th>
</tr>
</thead>
<tbody>
@for (row of rows; track $index; let i = $index) {
<tr [class.te-row--selected]="selectedRow === row" (click)="onRowSelect(row, i)">
<td><input class="te-cell te-cell--xs" [(ngModel)]="row.typ" /></td>
<td><input class="te-cell te-cell--xs" type="number" [(ngModel)]="row.br" /></td>
<td>
<select class="te-cell te-cell--sm" [(ngModel)]="row.trTp">
<option value=""></option>
@for (t of trTpOptions; track t) {
<option [value]="t">{{ t }}</option>
}
</select>
</td>
<td>
<select class="te-cell te-cell--sm" [(ngModel)]="row.acTyp">
@for (a of acTypOptions; track a) {
<option [value]="a">{{ a }}</option>
}
</select>
</td>
<td><input class="te-cell te-cell--md" [(ngModel)]="row.acNo" (blur)="onAcNoBlur(row, i)" /></td>
<td><input class="te-cell te-cell--lg" [(ngModel)]="row.ecsAcNo" /></td>
<td>
<select class="te-cell te-cell--sm" [(ngModel)]="row.dbCr">
<option value=""></option>
@for (d of dbCrOptions; track d) {
<option [value]="d">{{ d }}</option>
}
</select>
</td>
<td>
<select
class="te-cell te-cell--md"
[(ngModel)]="row.paymentTypeId"
[class.te-cell--required]="!row.paymentTypeId"
>
<option [ngValue]="null">— Pay Type —</option>
@for (pt of paymentTypes; track pt.id) {
<option [ngValue]="pt.id">{{ pt.name }}</option>
}
</select>
</td>
<td><input class="te-cell te-cell--md" [(ngModel)]="row.chequeNo" /></td>
<td>
<input
class="te-cell te-cell--md"
type="number"
[value]="isCashPayment && selectedRow === row ? denominationTotal || '' : (row.amount ?? '')"
[disabled]="isCashPayment && selectedRow === row"
[title]="isCashPayment && selectedRow === row ? 'Cash: Amount from denomination' : ''"
(input)="row.amount = +$any($event.target).value"
[class.te-cell--cash-auto]="isCashPayment && selectedRow === row"
/>
</td>
<td><input class="te-cell te-cell--lg" [(ngModel)]="row.description" /></td>
<td><input class="te-cell te-cell--sm" [(ngModel)]="row.tokNo" /></td>
<td><input class="te-cell te-cell--sm" [(ngModel)]="row.cheqDate" placeholder="dd/mm/yy" /></td>
</tr>
}
</tbody>
</table>
<div class="te-grid-footer">
<span class="te-eff-date"
>Eff. Dt : <strong>{{ today }}</strong></span
>
<button class="te-add-btn" type="button" (click)="addRow()" title="Add row">+</button>
<!-- ── ENTRY CARD (bottom — same design as top) ──────────────── -->
<div class="ws-wrap">
<div class="ws-header">
<div class="ws-header__left">
<div class="ws-header__icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
</div>
<span class="ws-header__title">Transaction Entry</span>
</div>
<div class="ws-header__right">
<span class="ws-header__effdt">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
Eff. Date
<input class="ws-header__date" type="date" [value]="todayIso" readonly />
</span>
</div>
</div>
</div>
<!-- ACTION BUTTONS -->
<div class="te-actions">
<button class="te-btn te-btn--exit" type="button" (click)="onExit()">Exit</button>
<button class="te-btn" type="button" (click)="onCancel()">Cancel</button>
<button class="te-btn" type="button">Drop</button>
<button class="te-btn" type="button" (click)="onSearch()">Search</button>
<button class="te-btn te-btn--mode" type="button">Change Tr.Mode</button>
<button class="te-btn te-btn--primary" type="button" (click)="onSave()" [disabled]="saving">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12" />
</svg>
{{ saving ? 'Posting…' : 'Save' }}
</button>
<div class="ws-rows">
@for (row of rows; track $index; let i = $index) {
<div class="ws-card" [class.ws-card--active]="selectedRow === row" (click)="onRowSelect(row, i)">
<div class="ws-card__badge">{{ i + 1 }}</div>
<div class="ws-card__body">
<div class="ws-group-row">
<div class="ws-group">
<span class="ws-caption">Classification</span>
<div class="ws-group__fields">
<div class="ws-f ws-f--type">
<label class="ws-f__lbl">TYPE</label>
<select class="ws-f__ctl" [(ngModel)]="row.typ">
<option value="~">~</option>
<option value="Cash">Cash</option>
<option value="Transfer">Transfer</option>
<option value="Clearing">Clearing</option>
<option value="Journal">Journal</option>
</select>
</div>
<div class="ws-f ws-f--branch">
<label class="ws-f__lbl">BRANCH</label>
<select class="ws-f__ctl" [(ngModel)]="row.br">
<option [ngValue]="1">001 · Main</option>
<option [ngValue]="2">002 · M.G. Road</option>
<option [ngValue]="14">014 · Station Rd</option>
<option [ngValue]="27">027 · Industrial</option>
</select>
</div>
<div class="ws-f ws-f--trtp">
<label class="ws-f__lbl">TXN TP.</label>
<select class="ws-f__ctl" [(ngModel)]="row.trTp">
<option value=""></option>
@for (t of trTpOptions; track t) {
<option [value]="t">{{ t }}</option>
}
</select>
</div>
<div class="ws-f ws-f--actyp">
<label class="ws-f__lbl">AC. TP.</label>
<select class="ws-f__ctl" [(ngModel)]="row.acTyp">
@for (a of acTypOptions; track a) {
<option [value]="a">{{ a }}</option>
}
</select>
</div>
</div>
</div>
<div class="ws-vdivider"></div>
<div class="ws-group ws-group--flex">
<span class="ws-caption">Account</span>
<div class="ws-group__fields">
<div class="ws-f ws-f--acno">
<label class="ws-f__lbl">A/C NO.</label>
<input
class="ws-f__ctl ws-f__ctl--mono"
[(ngModel)]="row.acNo"
(blur)="onAcNoBlur(row, i)"
placeholder="Account number"
/>
</div>
<div class="ws-f ws-f--ecsno">
<label class="ws-f__lbl">ECS A/C NO.</label>
<input class="ws-f__ctl ws-f__ctl--mono" [(ngModel)]="row.ecsAcNo" placeholder="ECS account" />
</div>
<div class="ws-f ws-f--dbcr">
<label class="ws-f__lbl">DR./CR.</label>
<div class="ws-seg">
<button
type="button"
class="ws-seg__btn ws-seg__btn--dr"
[class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'"
(click)="$event.stopPropagation(); row.dbCr = 'Dr'"
>
DR
</button>
<button
type="button"
class="ws-seg__btn ws-seg__btn--cr"
[class.ws-seg__btn--active-cr]="row.dbCr === 'Cr'"
(click)="$event.stopPropagation(); row.dbCr = 'Cr'"
>
CR
</button>
</div>
</div>
</div>
</div>
</div>
<div class="ws-group-row ws-group-row--mt">
<div class="ws-group">
<span class="ws-caption">Payment instrument</span>
<div class="ws-group__fields">
<div class="ws-f ws-f--paytype">
<label class="ws-f__lbl ws-f__lbl--accent">PAY TYPE</label>
<select
class="ws-f__ctl"
[(ngModel)]="row.paymentTypeId"
[class.ws-f__ctl--req]="!row.paymentTypeId"
>
<option [ngValue]="null">— Select —</option>
@for (pt of paymentTypes; track pt.id) {
<option [ngValue]="pt.id">{{ pt.name }}</option>
}
</select>
</div>
<div class="ws-f ws-f--amt">
<label class="ws-f__lbl">AMOUNT</label>
<div class="ws-amt-wrap" [class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row">
<span class="ws-amt-prefix">₹</span>
<input
class="ws-f__ctl ws-amt-input"
type="number"
[value]="
isCashPayment && selectedRow === row ? denominationTotal || '' : (row.amount ?? '')
"
[disabled]="isCashPayment && selectedRow === row"
(input)="row.amount = +$any($event.target).value"
placeholder="0.00"
/>
</div>
</div>
<div class="ws-f ws-f--cheqno">
<label class="ws-f__lbl">CHEQUE NO.</label>
<input class="ws-f__ctl ws-f__ctl--mono" [(ngModel)]="row.chequeNo" placeholder="—" />
</div>
<div class="ws-f ws-f--cheqdt">
<label class="ws-f__lbl">CHEQ. DATE</label>
<input class="ws-f__ctl" [(ngModel)]="row.cheqDate" placeholder="dd-mm-yyyy" />
</div>
<div class="ws-f ws-f--tok">
<label class="ws-f__lbl">TOK. NO.</label>
<input class="ws-f__ctl" [(ngModel)]="row.tokNo" />
</div>
</div>
</div>
<div class="ws-vdivider"></div>
<div class="ws-group ws-group--flex">
<span class="ws-caption">Narration</span>
<div class="ws-group__fields">
<div class="ws-f ws-f--desc">
<label class="ws-f__lbl">DESCRIPTION</label>
<input class="ws-f__ctl" [(ngModel)]="row.description" placeholder="Narration / remarks" />
</div>
</div>
</div>
</div>
</div>
</div>
}
</div>
<!-- Action buttons footer -->
<div class="ws-footer">
<div class="ws-footer__stats">
<button class="ws-btn ws-btn--ghost ws-btn--exit" type="button" (click)="onExit()">Exit</button>
<button class="ws-btn ws-btn--ghost" type="button" (click)="onCancel()">Cancel</button>
<button class="ws-btn ws-btn--ghost" type="button">Drop</button>
<button class="ws-btn ws-btn--ghost" type="button" (click)="onSearch()">Search</button>
<button class="ws-btn ws-btn--mode" type="button">Change Tr.Mode</button>
</div>
<div class="ws-footer__actions">
<button class="ws-btn ws-btn--post" type="button" (click)="onSave()" [disabled]="saving">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12" />
</svg>
{{ saving ? 'Posting…' : 'Save' }}
</button>
</div>
</div>
</div>
</div>
}
@@ -60,31 +60,31 @@
}
}
// ── Entry Grid Card ───────────────────────────────────────────────────────────
// ── CBS Blue Transaction Entry ────────────────────────────────────────────────
// ── Warm Stone Transaction Entry ─────────────────────────────────────────────
$ws-ink: #2c2723;
$ws-canvas: #f3f0e9;
$ws-card: #fffefb;
$ws-border: #e7e0d2;
$ws-accent: #b45309;
$ws-text: #3a322a;
$ws-muted: #8a7e6e;
$ws-micro: #a89a85;
$ws-dr: #b0412a;
$ws-cr: #4f7d3a;
$ws-divider: #ece4d6;
$ws-fld-bg: #faf8f2;
$ws-fld-bd: #e6dfd0;
$ws-ink: #0d1b2e; // nav bar dark navy
$ws-ink2: #162542; // slightly lighter navy (menu bar)
$ws-canvas: #f0f4f9; // page bg — light blue-grey
$ws-card: #fff;
$ws-border: #dce4ef;
$ws-accent: #e87722; // CBS orange — buttons only
$ws-focus: #2563eb; // CBS blue — field focus ring
$ws-text: #1a2a3e;
$ws-muted: #5a7089;
$ws-micro: #7a95b0;
$ws-dr: #c0392b;
$ws-cr: #1a7a4a;
$ws-divider: #dde6f0;
$ws-fld-bg: #f4f7fc;
$ws-fld-bd: #c8d8eb;
.ws-wrap {
background: $ws-canvas;
border-radius: 14px;
border-radius: 12px;
overflow: hidden;
box-shadow:
0 1px 2px rgb(80 60 30 / 5%),
0 10px 26px -14px rgb(90 70 40 / 20%);
0 1px 3px rgb(13 27 46 / 8%),
0 8px 24px -10px rgb(13 27 46 / 18%);
border: 1px solid $ws-border;
font-family: 'Hanken Grotesk', Inter, sans-serif;
}
@@ -95,9 +95,9 @@ $ws-fld-bd: #e6dfd0;
align-items: center;
justify-content: space-between;
padding: 0 18px;
height: 52px;
background: $ws-ink;
border-bottom: 2px solid #211d19;
height: 48px;
background: linear-gradient(90deg, #0d1b2e 0%, #162542 100%);
border-bottom: 2px solid $ws-accent;
&__left {
display: flex;
@@ -112,9 +112,9 @@ $ws-fld-bd: #e6dfd0;
}
&__icon {
width: 30px;
height: 30px;
border-radius: 8px;
width: 28px;
height: 28px;
border-radius: 7px;
background: $ws-accent;
display: flex;
align-items: center;
@@ -127,9 +127,9 @@ $ws-fld-bd: #e6dfd0;
&__title {
font-family: Newsreader, Georgia, serif;
font-size: 17px;
font-size: 16px;
font-weight: 500;
color: #f4ece0;
color: #e8f0fb;
letter-spacing: 0.01em;
}
@@ -138,26 +138,27 @@ $ws-fld-bd: #e6dfd0;
align-items: center;
gap: 7px;
font-size: 11.5px;
color: #b6a591;
color: #7a95b0;
svg {
stroke: #8a7e6e;
stroke: #5a7589;
}
}
&__date {
height: 28px;
padding: 0 8px;
border: 1px solid #3d342e;
border-radius: 7px;
background: #1a1512;
color: #f4ece0;
padding: 0 10px;
border: 1px solid #2a4060;
border-radius: 6px;
background: #0a1628;
color: #e8f0fb;
font-family: 'Hanken Grotesk', monospace;
font-size: 12px;
font-weight: 600;
outline: none;
&:focus {
border-color: $ws-accent;
border-color: $ws-focus;
}
}
}
@@ -205,12 +206,45 @@ $ws-fld-bd: #e6dfd0;
}
}
&--ghost {
background: #fff;
color: $ws-text;
border: 1px solid $ws-border;
font-size: 12px;
&:hover {
background: $ws-canvas;
border-color: #a8bdd4;
}
}
&--exit {
color: $ws-dr;
border-color: rgb(192 57 43 / 30%);
&:hover {
background: #fef2f0;
border-color: $ws-dr;
}
}
&--mode {
background: $ws-ink;
color: #c8daf0;
border: 1px solid #2a4060;
font-size: 12px;
&:hover {
background: $ws-ink2;
}
}
&--post {
background: $ws-accent;
color: #fff;
font-weight: 700;
padding: 8px 20px;
box-shadow: 0 2px 8px rgb(180 83 9 / 30%);
box-shadow: 0 2px 8px rgb(232 119 34 / 30%);
&:hover:not(:disabled) {
filter: brightness(1.08);
@@ -246,15 +280,15 @@ $ws-fld-bd: #e6dfd0;
position: relative;
&:hover {
border-color: #cdbfa6;
box-shadow: 0 2px 10px rgb(90 70 40 / 8%);
border-color: #a8bdd4;
box-shadow: 0 2px 10px rgb(13 27 46 / 8%);
}
&--active {
border-color: $ws-accent;
box-shadow:
0 0 0 3px rgb(180 83 9 / 12%),
0 2px 10px rgb(90 70 40 / 10%);
0 0 0 3px rgb(232 119 34 / 14%),
0 2px 10px rgb(13 27 46 / 10%);
}
&__badge {
@@ -263,7 +297,7 @@ $ws-fld-bd: #e6dfd0;
height: 26px;
border-radius: 50%;
background: $ws-ink;
color: #f4ece0;
color: #c8daf0;
font-size: 11px;
font-weight: 700;
display: flex;
@@ -286,7 +320,7 @@ $ws-fld-bd: #e6dfd0;
border-radius: 50%;
border: none;
background: transparent;
color: #c4b8a8;
color: #9ab0c8;
cursor: pointer;
display: flex;
align-items: center;
@@ -349,14 +383,14 @@ $ws-fld-bd: #e6dfd0;
font-family: Newsreader, Georgia, serif;
font-style: italic;
font-size: 11px;
color: #b6a791;
color: #7a95b0;
padding-bottom: 1px;
}
.ws-vdivider {
width: 1px;
background: $ws-divider;
margin: 18px 12px 0;
margin: 18px 10px 0;
flex-shrink: 0;
}
@@ -432,7 +466,7 @@ $ws-fld-bd: #e6dfd0;
white-space: nowrap;
&--accent {
color: $ws-accent;
color: $ws-focus;
}
}
@@ -453,12 +487,12 @@ $ws-fld-bd: #e6dfd0;
box-sizing: border-box;
&:hover {
border-color: #cdbfa6;
border-color: #9ab5d0;
}
&:focus {
border-color: $ws-accent;
box-shadow: 0 0 0 3px rgb(180 83 9 / 13%);
border-color: $ws-focus;
box-shadow: 0 0 0 3px rgb(37 99 235 / 12%);
background: #fff;
}
@@ -469,13 +503,13 @@ $ws-fld-bd: #e6dfd0;
}
&--accent {
color: $ws-accent;
color: $ws-text;
font-weight: 600;
}
&--req {
border-color: #e0a060;
background: #fdf6ee;
border-color: #f97316;
background: #fff7ed;
}
}
}
@@ -497,8 +531,8 @@ select.ws-f__ctl {
box-shadow 0.15s;
&:focus-within {
border-color: $ws-accent;
box-shadow: 0 0 0 3px rgb(180 83 9 / 13%);
border-color: $ws-focus;
box-shadow: 0 0 0 3px rgb(37 99 235 / 12%);
background: #fff;
}
@@ -591,7 +625,7 @@ select.ws-f__ctl {
align-items: center;
justify-content: space-between;
padding: 10px 18px;
background: $ws-card;
background: linear-gradient(180deg, #f4f7fc, #edf2f9);
border-top: 1px solid $ws-divider;
&__stats {
+2 -2
View File
@@ -9,8 +9,8 @@
/* tslint:disable */
export default {
'mifos_x': {
'version': '260603',
'hash': '5f7de01b'
'version': '260608',
'hash': 'f6404708'
},
'allow_switching_backend_instance': true
};