diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index 312bf0b35..385a96568 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -7,104 +7,307 @@ -->
+ + @if (previousDayUnsettled) { +
+ + + + + + + TRANSACTIONS BLOCKED — Previous day cash settlement is pending + @if (unsettledSince) { + (since {{ unsettledSince }}) + } + . Complete Cash Settlement before processing any transactions. + +
+ } + @if (!morningTransferReceived && !previousDayUnsettled) { +
+ + + + + + + NO CASH ALLOCATED — Morning transfer not received. Request a morning transfer from the vault + before processing cash transactions. + +
+ } + @if (!accountDetails && !loadingAccount) { -
- - - - - - - - - - - - - - - - - - - @for (row of rows; track $index; let i = $index) { - - - - - - - - - - - - - - - } - -
TypBr.Tr.Tp.ActypA/c No.Ecs A/c No.Db./Cr.Cheque No.AmountDescriptionTok.No.Cheq.Date
- - - - - - - - - - - - - - - - - - - -
-
@@ -192,8 +415,12 @@ @if (jointHolders.length === 0) { No joint holders } - @for (jh of jointHolders; track jh.id ?? $index) { -
+ @for (jh of jointHolders; track jh.id ?? $index; let i = $index) { +
{{ jh.displayName }} {{ jh.role ?? (jh.isPrimary || jh.primary ? 'Primary' : 'Co-Holder') }} @@ -210,33 +437,114 @@ {{ jh.signingAuthority }} } + + + + +
+ } + + + @if (withdrawalRule) { +
+ {{ withdrawalRule.icon }} + {{ withdrawalRule.msg }}
}
-
- - - - Nominees -
-
- @if (nominees.length === 0) { - No nominees - } - @for (n of nominees; track n.id ?? $index) { -
- {{ n.nomineeDisplayName ?? '—' }} - {{ n.relationshipName ?? '—' }} - @if (n.percentageShare) { - {{ n.percentageShare }}% - } - @if (n.nomineeIsMinor) { - Minor - } -
- } -
+ + @if (!isNonIndividual) { +
+ + + + Nominees +
+
+ @if (nominees.length === 0) { + No nominees + } + @for (n of nominees; track n.id ?? $index) { +
+ {{ n.nomineeDisplayName ?? '—' }} + {{ n.relationshipName ?? '—' }} + @if (n.percentageShare) { + {{ n.percentageShare }}% + } + @if (n.nomineeIsMinor) { + Minor + } +
+ } +
+ } + + @if (isNonIndividual) { +
+ + + + + Non-Individual Account Details + @if (accountDetails?.modeOfOperation) { + {{ modeOfOperationLabel }} + } +
+
+ @if (!accountDetails?.partners?.length) { + No partners / members linked + } + @for (p of accountDetails?.partners ?? []; track p.id; let i = $index) { +
+ {{ p.clientName ?? '—' }} + {{ p.clientCif ?? '' }} + @if (p.sharePercentage) { + {{ p.sharePercentage }}% + } + @if (p.isMandatory) { + Mandatory + } @else { + Optional + } + + + + +
+ } + @if (withdrawalRule && isNonIndividual) { +
+ {{ withdrawalRule.icon }} + {{ withdrawalRule.msg }} +
+ } +
+ }
@@ -321,6 +629,318 @@ + + @if (accountAlerts.length > 0) { +
+ @for (alert of accountAlerts; track $index) { +
+ + @if (alert.type === 'danger') { + + + + } + @if (alert.type === 'warning') { + + + + } + @if (alert.type === 'info') { + + + + } + + {{ alert.message }} +
+ } +
+ } + + +
+ +
+
+ + + + + + Client Photo +
+
+ @if (clientPhotoUrl) { + Client Photo + } @else { +
+ + + + + No Photo +
+ } +
+
+ + +
+
+ + + + + Signature +
+
+ @if (signatureUrl) { + Signature + } @else { +
+ + + + No Signature +
+ } +
+
+ + +
+
+ + + + + Account Facilities +
+
+
+ @for (f of facilities; track f.label) { + + {{ f.label }} + + } +
+ + @if (accountDetails?.withHoldTax) { +
+ + + + + + + TDS: {{ accountDetails.taxGroupData?.name ?? 'Applicable' }} +
+ } + + @if (accountDetails?.subStatus?.value && accountDetails.subStatus.value !== 'None') { +
+ + + + + + {{ accountDetails.subStatus.value }} +
+ } +
+
+ + +
+
+ + + + + + + Today's Activity + @if (todayTransactions.length > 0) { + {{ todayTransactions.length }} txn + } +
+ +
+
+ + + + +
+ Today's NEFT/RTGS + + ₹{{ sameDayNeftRtgs | number: '1.2-2' }} + +
+
+
+ + + + +
+ Cheque In Clearing + + ₹{{ sameDayChequeClearing | number: '1.2-2' }} + +
+
+
+
+ @if (todayTransactions.length === 0) { + No transactions today + } +
+ @for (t of todayTransactions; track t.id) { +
+ + {{ t.transactionType?.value ?? '—' }} + + + {{ t.transactionType?.deposit ? '+' : '−' }}₹{{ t.amount | number: '1.2-2' }} + +
+ } +
+
+
+
+ + +
+ +
+
+ + + + + + + Linked Accounts +
+
+ @if (!linkedAccounts) { + Loading… + } + @if (linkedAccounts) { + @if ((linkedAccounts.savingsAccounts?.length ?? 0) > 0) { +
Savings
+ @for (a of linkedAccounts.savingsAccounts; track a.id) { +
+ {{ a.accountNo }} + {{ a.productName }} + {{ + a.status?.value + }} +
+ } + } + @if ((linkedAccounts.fixedDepositAccounts?.length ?? 0) > 0) { +
Fixed Deposits
+ @for (a of linkedAccounts.fixedDepositAccounts; track a.id) { +
+ {{ a.accountNo }} + {{ a.productName }} + {{ a.status?.value }} +
+ } + } + @if ((linkedAccounts.recurringDepositAccounts?.length ?? 0) > 0) { +
Recurring Deposits
+ @for (a of linkedAccounts.recurringDepositAccounts; track a.id) { +
+ {{ a.accountNo }} + {{ a.productName }} + {{ a.status?.value }} +
+ } + } + @if ((linkedAccounts.loanAccounts?.length ?? 0) > 0) { +
Loans
+ @for (a of linkedAccounts.loanAccounts; track a.id) { +
+ {{ a.accountNo }} + {{ a.productName }} + {{ a.status?.value }} +
+ } + } + @if ( + (linkedAccounts.savingsAccounts?.length ?? 0) === 0 && + (linkedAccounts.fixedDepositAccounts?.length ?? 0) === 0 && + (linkedAccounts.recurringDepositAccounts?.length ?? 0) === 0 && + (linkedAccounts.loanAccounts?.length ?? 0) === 0 + ) { + No linked accounts + } + } +
+
+ + +
+
+ + + + + + + Standing Instructions + @if (standingInstructions.length > 0) { + {{ standingInstructions.length }} + } +
+
+ @if (standingInstructions.length === 0) { + No standing instructions + } + @for (si of standingInstructions; track si.id) { +
+
{{ si.name }}
+
+ ₹{{ si.amount | number: '1.2-2' }} + {{ + si.status?.value + }} +
+
+ } +
+
+
+
@@ -332,7 +952,17 @@
A/c Type {{ - accountDetails.depositType?.value || accountDetails.accountType || selectedRow?.acTyp || '—' + accountDetails.depositType?.value || + (selectedRow?.acTyp === 'CA' + ? 'Current Account' + : selectedRow?.acTyp === 'SD' + ? 'Savings Deposit' + : selectedRow?.acTyp === 'FD' + ? 'Fixed Deposit' + : selectedRow?.acTyp === 'RD' + ? 'Recurring Deposit' + : selectedRow?.acTyp) || + '—' }}
@@ -442,6 +1072,9 @@ @if (denominationTotal > 0) { ₹{{ denominationTotal | number: '1.2-2' }} } + @if (isCashPayment && denominationTotal <= 0) { + ⚠ Required for Cash + }
@@ -508,8 +1141,46 @@ Closing Position (Preview) + @if (selectedRow?.dbCr === 'Dr' && hasWithdrawalDenomOverdraft) { + ⚠ Stock Overdraft + }
+ @if (selectedRow?.dbCr === 'Dr' && hasWithdrawalDenomOverdraft) { +
+
+ + + + + + Insufficient Denomination Stock — Cannot Proceed +
+
+ + + + + + + + + + @for (o of denominationOverdrafts; track o.label) { + + + + + + + } + +
DenominationRequestedAvailableShort
{{ o.label }}{{ o.requested }}{{ o.available }} + −{{ o.shortfall }} +
+
Adjust denomination or request vault replenishment.
+
+ } @if (closingDenoms.length === 0) { Enter denomination to see closing position } @@ -571,6 +1242,7 @@ A/c No. Ecs A/c No. Db./Cr. + Pay Type * Cheque No. Amount Description @@ -608,8 +1280,30 @@ } + + + - + + + @@ -642,3 +1336,187 @@
}
+ + +@if (showConfirmDialog && confirmData) { +
+
+
+ {{ confirmData.type === 'DEPOSIT' ? '⬇' : '⬆' }} + Confirm {{ confirmData.type }} +
+
+
+ Account{{ confirmData.accountNo }} +
+
+ Customer{{ confirmData.clientName }} +
+
+ Payment Type{{ confirmData.paymentType }} +
+ @if (confirmData.chequeNo) { +
+ Cheque No.{{ confirmData.chequeNo }} +
+ } +
+
+ Current Balance{{ confirmData.symbol }}{{ confirmData.currentBalance | number: '1.2-2' }} +
+
+ {{ confirmData.type === 'DEPOSIT' ? 'Deposit' : 'Withdrawal' }} Amount + + {{ confirmData.type === 'DEPOSIT' ? '+' : '−' }}{{ confirmData.symbol + }}{{ confirmData.amount | number: '1.2-2' }} + +
+
+ Balance After + + {{ confirmData.symbol }}{{ confirmData.balanceAfter | number: '1.2-2' }} + +
+
+
+ + +
+
+
+} + + +@if (alertModal) { +
+
+ +
+
+ @if (alertModal.type === 'error') { + + + + + + } + @if (alertModal.type === 'warning') { + + + + + + } + @if (alertModal.type === 'info') { + + + + + + } + @if (alertModal.type === 'success') { + + + + + } +
+ + {{ + alertModal.type === 'error' + ? 'ERROR' + : alertModal.type === 'warning' + ? 'WARN' + : alertModal.type === 'success' + ? 'OK' + : 'INFO' + }} + +
+ + +
+
+

{{ alertModal.title }}

+ +
+ +
+ @for (line of alertModal.lines; track $index) { + @if ($index === 0) { +

{{ line }}

+ } @else if (line.includes(':') && $index < alertModal.lines.length - 1) { +
+ {{ line.split(':')[0].trim() }} + {{ line.split(':').slice(1).join(':').trim() }} +
+ } @else { +

{{ line }}

+ } + } +
+ +
+ @if (alertModal.onConfirm) { + + + } @else { + + } +
+
+
+
+} diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.scss b/src/app/cashier/transaction-entry/transaction-entry.component.scss index 462567adf..7e7842ed9 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.scss +++ b/src/app/cashier/transaction-entry/transaction-entry.component.scss @@ -23,32 +23,702 @@ padding: 10px 14px; } +// ── Day-status blocking banners ─────────────────────────────────────────────── + +.te-day-block { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + border-left: 4px solid; + + svg { + flex-shrink: 0; + } + + &--danger { + background: #fef2f2; + border-color: #dc2626; + color: #991b1b; + + svg { + stroke: #dc2626; + } + } + + &--warning { + background: #fffbeb; + border-color: #d97706; + color: #92400e; + + svg { + stroke: #d97706; + } + } +} + // ── Entry Grid Card ─────────────────────────────────────────────────────────── +// ── 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-wrap { + background: $ws-canvas; + border-radius: 14px; + overflow: hidden; + box-shadow: + 0 1px 2px rgb(80 60 30 / 5%), + 0 10px 26px -14px rgb(90 70 40 / 20%); + border: 1px solid $ws-border; + font-family: 'Hanken Grotesk', Inter, sans-serif; +} + +// ── Header ────────────────────────────────────────────────────────────────── +.ws-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 18px; + height: 52px; + background: $ws-ink; + border-bottom: 2px solid #211d19; + + &__left { + display: flex; + align-items: center; + gap: 10px; + } + + &__right { + display: flex; + align-items: center; + gap: 14px; + } + + &__icon { + width: 30px; + height: 30px; + border-radius: 8px; + background: $ws-accent; + display: flex; + align-items: center; + justify-content: center; + + svg { + stroke: #fff; + } + } + + &__title { + font-family: Newsreader, Georgia, serif; + font-size: 17px; + font-weight: 500; + color: #f4ece0; + letter-spacing: 0.01em; + } + + &__effdt { + display: flex; + align-items: center; + gap: 7px; + font-size: 11.5px; + color: #b6a591; + + svg { + stroke: #8a7e6e; + } + } + + &__date { + height: 28px; + padding: 0 8px; + border: 1px solid #3d342e; + border-radius: 7px; + background: #1a1512; + color: #f4ece0; + font-family: 'Hanken Grotesk', monospace; + font-size: 12px; + outline: none; + + &:focus { + border-color: $ws-accent; + } + } +} + +.ws-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 7px 15px; + border-radius: 8px; + border: none; + font-family: 'Hanken Grotesk', sans-serif; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: + filter 0.15s, + transform 0.1s; + + &:active { + transform: scale(0.97); + } + + &--add { + background: $ws-accent; + color: #fff; + box-shadow: 0 2px 8px rgb(180 83 9 / 30%); + + svg { + stroke: #fff; + } + + &:hover { + filter: brightness(1.08); + } + } + + &--draft { + background: $ws-divider; + color: $ws-text; + border: 1px solid $ws-border; + + &:hover { + background: #ddd6c6; + } + } + + &--post { + background: $ws-accent; + color: #fff; + font-weight: 700; + padding: 8px 20px; + box-shadow: 0 2px 8px rgb(180 83 9 / 30%); + + &:hover:not(:disabled) { + filter: brightness(1.08); + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + } +} + +// ── Row cards ──────────────────────────────────────────────────────────────── +.ws-rows { + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.ws-card { + display: flex; + align-items: flex-start; + gap: 10px; + background: $ws-card; + border: 1px solid $ws-border; + border-radius: 12px; + padding: 12px 14px 12px 10px; + cursor: pointer; + transition: + border-color 0.15s, + box-shadow 0.15s; + position: relative; + + &:hover { + border-color: #cdbfa6; + box-shadow: 0 2px 10px rgb(90 70 40 / 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%); + } + + &__badge { + flex-shrink: 0; + width: 26px; + height: 26px; + border-radius: 50%; + background: $ws-ink; + color: #f4ece0; + font-size: 11px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + margin-top: 22px; + } + + &__body { + flex: 1; + min-width: 0; + } + + &__del { + position: absolute; + top: 8px; + right: 8px; + width: 22px; + height: 22px; + border-radius: 50%; + border: none; + background: transparent; + color: #c4b8a8; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: + opacity 0.15s, + background 0.15s; + + &:hover { + background: #fde8e8; + color: $ws-dr; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.2 !important; + } + } + + &:hover &__del { + opacity: 1; + } +} + +// ── Group captions ──────────────────────────────────────────────────────────── +// ── Group row (Classification+Account / Payment+Narration) ─────────────────── +.ws-group-row { + display: flex; + align-items: stretch; + gap: 0; + + &--mt { + margin-top: 10px; + } +} + +.ws-group { + display: flex; + flex-direction: column; + gap: 4px; + flex-shrink: 0; + + &--flex { + flex: 1; + min-width: 0; + } + + &__fields { + display: flex; + align-items: flex-end; + gap: 8px; + flex-wrap: nowrap; + height: 100%; + align-content: flex-end; + } +} + +.ws-caption { + font-family: Newsreader, Georgia, serif; + font-style: italic; + font-size: 11px; + color: #b6a791; + padding-bottom: 1px; +} + +.ws-vdivider { + width: 1px; + background: $ws-divider; + margin: 18px 12px 0; + flex-shrink: 0; +} + +// ── Field ───────────────────────────────────────────────────────────────────── +.ws-f { + display: flex; + flex-direction: column; + gap: 3px; + flex-shrink: 0; + + &--type { + width: 90px; + } + + &--branch { + width: 130px; + } + + &--trtp { + width: 66px; + } + + &--actyp { + width: 66px; + } + + &--acno { + flex: 1.6; + min-width: 150px; + } + + &--ecsno { + flex: 1; + min-width: 110px; + } + + &--dbcr { + width: 84px; + flex-shrink: 0; + } + + &--paytype { + width: 110px; + } + + &--amt { + width: 120px; + } + + &--cheqno { + width: 100px; + } + + &--cheqdt { + width: 108px; + } + + &--tok { + width: 72px; + } + + &--desc { + flex: 1; + min-width: 0; + } + + &__lbl { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: $ws-micro; + white-space: nowrap; + + &--accent { + color: $ws-accent; + } + } + + &__ctl { + height: 34px; + padding: 0 9px; + border: 1px solid $ws-fld-bd; + border-radius: 8px; + background: $ws-fld-bg; + color: $ws-text; + font-family: 'Hanken Grotesk', sans-serif; + font-size: 13px; + outline: none; + width: 100%; + transition: + border-color 0.15s, + box-shadow 0.15s; + box-sizing: border-box; + + &:hover { + border-color: #cdbfa6; + } + + &:focus { + border-color: $ws-accent; + box-shadow: 0 0 0 3px rgb(180 83 9 / 13%); + background: #fff; + } + + &--mono { + font-family: 'Roboto Mono', monospace; + font-size: 12px; + letter-spacing: 0.03em; + } + + &--accent { + color: $ws-accent; + font-weight: 600; + } + + &--req { + border-color: #e0a060; + background: #fdf6ee; + } + } +} + +select.ws-f__ctl { + cursor: pointer; +} + +// ── Amount field ────────────────────────────────────────────────────────────── +.ws-amt-wrap { + display: flex; + align-items: center; + border: 1px solid $ws-fld-bd; + border-radius: 8px; + background: $ws-fld-bg; + height: 34px; + transition: + border-color 0.15s, + box-shadow 0.15s; + + &:focus-within { + border-color: $ws-accent; + box-shadow: 0 0 0 3px rgb(180 83 9 / 13%); + background: #fff; + } + + &--cash { + border-color: #86efac; + background: #f0fdf4; + + .ws-amt-prefix { + color: #15803d; + } + + .ws-amt-input { + color: #15803d; + font-weight: 700; + cursor: not-allowed; + } + } +} + +.ws-amt-prefix { + padding: 0 6px 0 9px; + font-size: 13px; + color: $ws-muted; + font-weight: 600; + flex-shrink: 0; +} + +.ws-amt-input { + flex: 1; + height: 100%; + border: none; + background: transparent; + font-family: 'Hanken Grotesk', monospace; + font-size: 13px; + font-weight: 600; + text-align: right; + padding-right: 9px; + color: $ws-text; + outline: none; + font-variant-numeric: tabular-nums; +} + +// ── Segmented DR/CR toggle ─────────────────────────────────────────────────── +.ws-seg { + display: flex; + height: 34px; + border-radius: 8px; + overflow: hidden; + border: 1px solid $ws-fld-bd; + + &__btn { + flex: 1; + border: none; + background: $ws-fld-bg; + font-family: 'Hanken Grotesk', sans-serif; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.06em; + cursor: pointer; + color: $ws-muted; + transition: + background 0.15s, + color 0.15s; + + &--dr:hover { + background: #fce9e6; + color: $ws-dr; + } + + &--cr:hover { + background: #e8f4e2; + color: $ws-cr; + } + + &--active-dr { + background: #fce9e6 !important; + color: $ws-dr !important; + } + + &--active-cr { + background: #e8f4e2 !important; + color: $ws-cr !important; + } + } +} + +// ── Footer totals ───────────────────────────────────────────────────────────── +.ws-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 18px; + background: $ws-card; + border-top: 1px solid $ws-divider; + + &__stats { + display: flex; + align-items: center; + gap: 24px; + } + + &__actions { + display: flex; + align-items: center; + gap: 8px; + } +} + +.ws-stat { + display: flex; + flex-direction: column; + gap: 1px; + + &__label { + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: $ws-micro; + } + + &__val { + font-size: 13px; + font-weight: 700; + color: $ws-text; + font-variant-numeric: tabular-nums; + font-family: 'Hanken Grotesk', monospace; + + &--dr { + color: $ws-dr; + } + + &--cr { + color: $ws-cr; + } + + &--ok { + color: $ws-cr; + } + } +} + +.ws-balance-pill { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + + &--ok { + background: #e8f4e2; + color: $ws-cr; + + svg { + stroke: $ws-cr; + } + } + + &--bad { + background: #fce9e6; + color: $ws-dr; + + svg { + stroke: $ws-dr; + } + } +} + +// ── Transaction Entry Grid (legacy — keep for bottom grid inside accountDetails) ────────────────────── + .te-grid-wrap { background: #fff; - border: 1px solid #e2e8f0; - border-radius: 10px; + border-radius: 12px; overflow: hidden; - box-shadow: 0 1px 4px rgb(0 0 0 / 6%); + box-shadow: + 0 2px 12px rgb(0 0 0 / 8%), + 0 0 0 1px rgb(0 0 0 / 6%); +} + +// Grid header title bar +.te-grid-wrap::before { + content: ''; + display: block; + height: 3px; + background: linear-gradient(90deg, #1e40af, #0ea5e9, #1e40af); + background-size: 200% 100%; + animation: te-shimmer 3s linear infinite; +} + +@keyframes te-shimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } } .te-grid { width: 100%; + table-layout: fixed; border-collapse: collapse; - font-size: 12px; + font-size: 11.5px; th { - background: #1e293b; - color: #94a3b8; - padding: 7px 8px; + background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); + color: #7dd3fc; + padding: 9px 6px; text-align: left; - font-size: 11px; - font-weight: 700; - letter-spacing: 0.04em; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.06em; text-transform: uppercase; - border-right: 1px solid #334155; + border-right: 1px solid #1e3a5f; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; &:last-child { border-right: none; @@ -58,8 +728,10 @@ td { border-right: 1px solid #f1f5f9; border-bottom: 1px solid #f1f5f9; - padding: 3px 4px; + padding: 3px; background: #fff; + vertical-align: middle; + overflow: hidden; &:last-child { border-right: none; @@ -67,98 +739,129 @@ } tr.te-row--selected td { - background: #eff6ff; + background: linear-gradient(90deg, #eff6ff, #f0f9ff); + border-bottom-color: #bae6fd; } - tr:hover td { - background: #f8fafc; + tr:hover:not(.te-row--selected) td { + background: #fafbff; cursor: pointer; } &--mini th { - font-size: 10px; - padding: 5px 6px; - background: #334155; + font-size: 9px; + padding: 6px; + background: linear-gradient(180deg, #334155, #1e293b); + color: #94a3b8; } } .te-cell { border: 1px solid #e2e8f0; - border-radius: 5px; + border-radius: 6px; background: #f8fafc; font-family: 'Roboto Mono', monospace; - font-size: 12px; - padding: 4px 7px; + font-size: 11.5px; + padding: 5px 7px; color: #1e293b; outline: none; width: 100%; transition: border-color 0.15s, - background 0.15s; + background 0.15s, + box-shadow 0.15s; &:focus { border-color: #3b82f6; background: #fff; - box-shadow: 0 0 0 2px rgb(59 130 246 / 15%); + box-shadow: 0 0 0 3px rgb(59 130 246 / 12%); } + // With table-layout:fixed, colgroup controls column width. + // These size classes only apply to inputs INSIDE cells. &--xs { - width: 36px; + width: 100%; } &--sm { - width: 56px; + width: 100%; } &--md { - width: 88px; + width: 100%; } &--lg { - width: 130px; + width: 100%; + } + + &--acno { + width: 100%; + letter-spacing: 0.03em; } } select.te-cell { cursor: pointer; + appearance: auto; } .te-grid-footer { display: flex; align-items: center; justify-content: space-between; - padding: 6px 12px; - background: #f8fafc; + padding: 8px 14px; + background: linear-gradient(180deg, #f8fafc, #f1f5f9); border-top: 1px solid #e2e8f0; } .te-eff-date { - font-size: 12px; + display: flex; + align-items: center; + gap: 6px; + font-size: 11.5px; color: #64748b; + font-weight: 600; + + &::before { + content: '📅'; + font-size: 12px; + } strong { - color: #0f172a; + font-family: 'Roboto Mono', monospace; + font-weight: 800; + background: #e0f2fe; + padding: 2px 7px; + border-radius: 5px; + border: 1px solid #bae6fd; + color: #0369a1; } } .te-add-btn { - width: 26px; - height: 26px; + width: 30px; + height: 30px; border-radius: 50%; - background: #0ea5e9; + background: linear-gradient(135deg, #0ea5e9, #2563eb); color: #fff; border: none; - font-size: 18px; + font-size: 20px; font-weight: 300; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; + transition: + transform 0.15s, + box-shadow 0.15s; transition: background 0.15s; box-shadow: 0 2px 6px rgb(14 165 233 / 35%); &:hover { + transform: scale(1.1); + box-shadow: 0 4px 14px rgb(14 165 233 / 50%); background: #0284c7; } } @@ -333,13 +1036,90 @@ select.te-cell { display: flex; align-items: center; gap: 8px; - padding: 6px 0; + padding: 7px 8px; border-bottom: 1px solid #f1f5f9; font-size: 12px; + border-radius: 6px; + margin-bottom: 2px; - &:last-child { + &:last-of-type { border-bottom: none; } + + &--clickable { + cursor: pointer; + transition: background 0.12s; + + &:hover { + background: #f1f5f9; + } + } + + &--expanded { + background: #eff6ff; + border-bottom-color: #bfdbfe; + } +} + +.te-expand-icon { + font-size: 9px; + color: #94a3b8; + flex-shrink: 0; +} + +// ── Holder expanded detail card ─────────────────────────────────────────────── + +.te-holder-detail { + background: #f8fafc; + border: 1px solid #bfdbfe; + border-radius: 8px; + padding: 10px 12px; + margin-bottom: 6px; + animation: te-slide-down 0.15s ease; +} + +@keyframes te-slide-down { + from { + opacity: 0; + transform: translateY(-6px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.te-holder-card { + display: grid; + grid-template-columns: 90px 150px 1fr; + gap: 12px; + align-items: flex-start; + + @media (width <= 900px) { + grid-template-columns: 90px 1fr; + } +} + +.te-holder-media { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.te-holder-media-label { + font-size: 10px; + font-weight: 700; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.te-holder-info { + display: flex; + flex-direction: column; + gap: 3px; } .te-joint-name { @@ -474,39 +1254,959 @@ select.te-cell { .te-bal-item { display: flex; align-items: center; - gap: 8px; - padding: 12px 20px; + gap: 6px; + padding: 10px 18px; border-right: 1px solid #e2e8f0; - font-size: 12px; &:last-child { border-right: none; } &__label { + display: inline-block; + padding: 2px 7px; + border-radius: 4px; + background: #f1f5f9; color: #64748b; - font-weight: 600; - font-size: 11px; + font-weight: 700; + font-size: 10px; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.06em; white-space: nowrap; + border: 1px solid #e2e8f0; } &__val { - font-weight: 700; + font-weight: 800; + font-size: 13px; color: #1e293b; font-variant-numeric: tabular-nums; + letter-spacing: 0.01em; &--cr { color: #059669; } &--big { - font-size: 18px; + font-size: 16px; } } } +// ── Required field highlight ────────────────────────────────────────────────── + +.te-cell--required { + border-color: #f97316 !important; + background: #fff7ed !important; +} + +// Cash payment: amount auto-filled from denomination (disabled style) +.te-cell--cash-auto { + background: #f0fdf4 !important; + border-color: #86efac !important; + color: #15803d !important; + font-weight: 700 !important; + cursor: not-allowed; +} + +// Denomination stock overdraft badge (closing position header) +.te-denom-overdraft-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: #fef2f2; + color: #dc2626; + border: 1px solid #fecaca; + margin-left: 8px; + animation: te-pulse 1s ease-in-out infinite; +} + +// ── Non-Individual Partners Table ───────────────────────────────────────────── +.te-nonperson-partners-header { + display: flex; + align-items: center; + gap: 5px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #64748b; + margin: 10px 0 6px; + + svg { + stroke: #64748b; + } +} + +.te-nonperson-table { + width: 100%; + border-collapse: collapse; + font-size: 11px; + margin-bottom: 4px; + + th { + background: #f1f5f9; + color: #475569; + font-weight: 700; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 5px 8px; + text-align: left; + border-bottom: 1px solid #e2e8f0; + + &.num { + text-align: center; + } + } + + td { + padding: 5px 8px; + border-bottom: 1px dotted #f1f5f9; + + &.num { + text-align: center; + } + } + + tr:last-child td { + border-bottom: none; + } + + tr:hover td { + background: #f8fafc; + } +} + +// Denomination overdraft inline alert block +.te-denom-overdraft-alert { + background: #fef2f2; + border: 1px solid #fecaca; + border-left: 4px solid #dc2626; + border-radius: 6px; + padding: 10px 12px; + margin-bottom: 10px; + + &__title { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 700; + color: #dc2626; + margin-bottom: 8px; + + svg { + stroke: #dc2626; + flex-shrink: 0; + } + } + + &__table { + width: 100%; + border-collapse: collapse; + font-size: 11px; + margin-bottom: 6px; + + th { + background: #fee2e2; + color: #7f1d1d; + font-weight: 700; + padding: 3px 6px; + text-align: left; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + td { + padding: 3px 6px; + border-bottom: 1px solid #fecaca; + color: #1e293b; + } + + tr:last-child td { + border-bottom: none; + } + } + + &__hint { + font-size: 10px; + color: #991b1b; + font-style: italic; + } +} + +// Denomination required badge +.te-denom-required-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: #fef3c7; + color: #d97706; + border: 1px solid #fde68a; + margin-left: 8px; + animation: te-pulse 1.5s ease-in-out infinite; +} + +@keyframes te-pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} + +// ── Confirmation Dialog ─────────────────────────────────────────────────────── + +// ── Custom Alert Modal ──────────────────────────────────────────────────────── +@keyframes te-fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes te-pop-in { + 0% { + opacity: 0; + transform: scale(0.92) translateY(12px); + } + + 70% { + transform: scale(1.02) translateY(-2px); + } + + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.te-alert-overlay { + position: fixed; + inset: 0; + background: rgb(2 6 23 / 70%); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + backdrop-filter: blur(8px); + animation: te-fade-in 0.18s ease; +} + +.te-alert-box { + display: flex; + width: 480px; + max-width: 96vw; + border-radius: 16px; + overflow: hidden; + box-shadow: + 0 32px 80px rgb(0 0 0 / 35%), + 0 0 0 1px rgb(255 255 255 / 8%); + animation: te-pop-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + + // ── Left sidebar ───────────────────────────────────────────────────────── + &__sidebar { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + width: 76px; + flex-shrink: 0; + padding: 24px 0; + } + + &__sidebar-icon { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: rgb(255 255 255 / 20%); + + svg { + width: 22px; + height: 22px; + stroke: #fff; + } + } + + &__sidebar-type { + font-size: 8px; + font-weight: 900; + letter-spacing: 0.12em; + color: rgb(255 255 255 / 70%); + text-transform: uppercase; + writing-mode: vertical-lr; + transform: rotate(180deg); + } + + // ── Right content ───────────────────────────────────────────────────────── + &__content { + flex: 1; + background: #fff; + display: flex; + flex-direction: column; + } + + &__content-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 20px 20px 12px; + border-bottom: 1px solid #f1f5f9; + + h3 { + margin: 0; + font-size: 15px; + font-weight: 800; + color: #0f172a; + letter-spacing: -0.02em; + line-height: 1.3; + } + } + + &__close { + background: none; + border: none; + cursor: pointer; + padding: 2px; + color: #94a3b8; + border-radius: 4px; + + &:hover { + background: #f1f5f9; + color: #475569; + } + } + + &__lines { + flex: 1; + padding: 14px 20px; + } + + &__desc { + margin: 0 0 10px; + font-size: 12.5px; + color: #64748b; + line-height: 1.55; + } + + &__kv { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px 12px; + margin: 4px 0; + background: #f8fafc; + border-radius: 8px; + border-left: 3px solid #e2e8f0; + } + + &__kv-k { + font-size: 12px; + font-weight: 700; + color: #334155; + } + + &__kv-v { + font-size: 12px; + font-weight: 600; + color: #475569; + font-variant-numeric: tabular-nums; + } + + &__foot { + margin: 10px 0 0; + font-size: 11px; + color: #94a3b8; + font-style: italic; + } + + &__actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 14px 20px; + border-top: 1px solid #f1f5f9; + background: #fafafa; + } + + // ── Error sidebar ───────────────────────────────────────────────────────── + &--error &__sidebar { + background: linear-gradient(160deg, #dc2626, #b91c1c); + } + + &--error &__kv { + border-left-color: #fca5a5; + background: #fef2f2; + } + + &--error &__kv-k { + color: #991b1b; + } + + // ── Warning sidebar ─────────────────────────────────────────────────────── + &--warning &__sidebar { + background: linear-gradient(160deg, #d97706, #b45309); + } + + &--warning &__kv { + border-left-color: #fcd34d; + background: #fffbeb; + } + + &--warning &__kv-k { + color: #92400e; + } + + // ── Info sidebar ────────────────────────────────────────────────────────── + &--info &__sidebar { + background: linear-gradient(160deg, #2563eb, #1d4ed8); + } + + &--info &__kv { + border-left-color: #93c5fd; + background: #eff6ff; + } + + &--info &__kv-k { + color: #1e40af; + } + + // ── Success sidebar ─────────────────────────────────────────────────────── + &--success &__sidebar { + background: linear-gradient(160deg, #059669, #047857); + } + + &--success &__kv { + border-left-color: #6ee7b7; + background: #f0fdf4; + } + + &--success &__kv-k { + color: #065f46; + font-weight: 800; + } +} + +// ── Alert buttons ───────────────────────────────────────────────────────────── +.te-alert-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 20px; + min-width: 90px; + border-radius: 8px; + border: none; + font-size: 12.5px; + font-weight: 700; + cursor: pointer; + transition: + filter 0.15s, + transform 0.1s; + + &:active { + transform: scale(0.96); + } + + &--ghost { + background: #f1f5f9; + color: #64748b; + border: 1px solid #e2e8f0; + + &:hover { + background: #e2e8f0; + } + } + + &--solid { + color: #fff; + + &:hover { + filter: brightness(1.1); + } + + &.te-alert-btn--error { + background: #dc2626; + } + + &.te-alert-btn--success { + background: #059669; + } + + &.te-alert-btn--warning { + background: #d97706; + } + + &.te-alert-btn--info { + background: #2563eb; + } + } +} + +.te-confirm-overlay { + position: fixed; + inset: 0; + background: rgb(0 0 0 / 50%); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.te-confirm-box { + background: #fff; + border-radius: 14px; + overflow: hidden; + width: 400px; + max-width: 95vw; + box-shadow: 0 20px 60px rgb(0 0 0 / 25%); + animation: te-slide-down 0.2s ease; +} + +.te-confirm-header { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 20px; + color: #fff; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + + &--deposit { + background: linear-gradient(135deg, #059669, #047857); + } + + &--withdrawal { + background: linear-gradient(135deg, #dc2626, #b91c1c); + } +} + +.te-confirm-icon { + font-size: 20px; +} + +.te-confirm-body { + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.te-confirm-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; + padding: 4px 0; + border-bottom: 1px dotted #f1f5f9; + + &:last-child { + border-bottom: none; + } + + &--amount .te-confirm-val { + font-size: 16px; + font-weight: 700; + } + + &--after .te-confirm-val { + font-size: 17px; + } +} + +.te-confirm-label { + color: #64748b; + font-size: 12px; + font-weight: 600; +} + +.te-confirm-val { + color: #1e293b; + font-weight: 500; +} + +.te-confirm-divider { + border-top: 2px solid #e2e8f0; + margin: 6px 0; +} + +.te-confirm-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 12px 20px; + background: #f8fafc; + border-top: 1px solid #e2e8f0; +} + +// ── Alerts ──────────────────────────────────────────────────────────────────── + +.te-alerts-wrap { + display: flex; + flex-direction: column; + gap: 6px; +} + +.te-alert { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + border: 1px solid; + + svg { + flex-shrink: 0; + } + + &--danger { + background: #fef2f2; + color: #dc2626; + border-color: #fecaca; + } + + &--warning { + background: #fffbeb; + color: #d97706; + border-color: #fde68a; + } + + &--info { + background: #eff6ff; + color: #2563eb; + border-color: #bfdbfe; + } +} + +// ── Extended rows ───────────────────────────────────────────────────────────── + +.te-ext-row { + display: grid; + grid-template-columns: 140px 200px 1fr 1fr; + gap: 10px; + + &--2 { + grid-template-columns: 1fr 1fr; + } + + @media (width <= 1100px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 700px) { + grid-template-columns: 1fr; + } +} + +.te-photo-img { + max-width: 100px; + max-height: 120px; + border-radius: 8px; + border: 1px solid #e2e8f0; + object-fit: cover; +} + +.te-photo-img--clickable { + cursor: zoom-in; + transition: + transform 0.15s, + box-shadow 0.15s; + + &:hover { + transform: scale(1.04); + box-shadow: 0 4px 16px rgb(0 0 0 / 20%); + } +} + +.te-sig-img { + max-width: 160px; + max-height: 70px; + object-fit: contain; + border: 1px solid #e2e8f0; + border-radius: 4px; + background: #fff; +} + +.te-photo-placeholder { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 8px 0; +} + +.te-facilities-grid { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; +} + +.te-facility { + display: inline-flex; + align-items: center; + padding: 3px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + border: 1px solid; + + &--on { + background: #f0fdf4; + color: #15803d; + border-color: #bbf7d0; + } + + &--off { + background: #f8fafc; + color: #94a3b8; + border-color: #e2e8f0; + text-decoration: line-through; + } +} + +.te-tds-badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + margin-top: 6px; + + &--warn { + background: #fffbeb; + color: #d97706; + border-color: #fde68a; + } +} + +// ── Compliance cards (NEFT/RTGS + Cheque Clearing) ─────────────────────────── + +.te-compliance-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #f1f5f9; +} + +.te-compliance-card { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-radius: 8px; + background: #f8fafc; + border: 1px solid #e2e8f0; + + svg { + color: #64748b; + flex-shrink: 0; + } + + &--alert { + background: #fff7ed; + border-color: #fed7aa; + + svg { + color: #ea580c; + } + } +} + +.te-compliance-label { + display: block; + font-size: 10px; + color: #64748b; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 2px; +} + +.te-compliance-val { + display: block; + font-size: 13px; + font-weight: 800; + color: #1e293b; + font-variant-numeric: tabular-nums; +} + +// Today panel — fixed height, transactions scroll, total sticky at bottom +.te-panel__body--today { + display: flex; + flex-direction: column; + padding: 0 !important; + max-height: 110px; +} + +.te-today-scroll { + flex: 1; + overflow-y: auto; + padding: 8px 14px 4px; + scrollbar-width: thin; + scrollbar-color: #cbd5e1 transparent; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; + } +} + +.te-today-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 0; + border-bottom: 1px dotted #f1f5f9; + font-size: 12px; + + &:last-child { + border-bottom: none; + } +} + +.te-today-type { + color: #475569; + font-size: 11px; +} + +.te-today-amt { + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.te-today-total { + display: flex; + justify-content: space-between; + padding: 7px 14px; + border-top: 2px solid #e2e8f0; + background: #f8fafc; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; +} + +.te-linked-section { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #94a3b8; + margin: 8px 0 4px; + + &:first-child { + margin-top: 0; + } +} + +.te-linked-row { + display: flex; + align-items: center; + gap: 8px; + padding: 3px 0; + border-bottom: 1px dotted #f1f5f9; + font-size: 11px; + + &:last-child { + border-bottom: none; + } +} + +.te-si-row { + padding: 6px 0; + border-bottom: 1px solid #f1f5f9; + + &:last-child { + border-bottom: none; + } +} + +.te-si-name { + font-size: 12px; + font-weight: 600; + color: #1e293b; + margin-bottom: 2px; +} + +.te-si-meta { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; +} + +// ── Withdrawal rule alert ───────────────────────────────────────────────────── + +.te-withdrawal-rule { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + padding: 8px 10px; + border-radius: 7px; + font-size: 11px; + font-weight: 700; + border: 1px solid; + + &--danger { + background: #fef2f2; + color: #dc2626; + border-color: #fecaca; + } + + &--warning { + background: #fffbeb; + color: #d97706; + border-color: #fde68a; + } +} + +.te-wr-icon { + font-size: 14px; + flex-shrink: 0; +} + +.te-wr-msg { + flex: 1; + line-height: 1.4; +} + // ── Denomination Section ────────────────────────────────────────────────────── .te-denom-wrap { diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts index cbcdca599..7b40e5c80 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.ts +++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts @@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; import { catchError } from 'rxjs/operators'; import { forkJoin, of } from 'rxjs'; +import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CashierService } from '../cashier.service'; interface TxnRow { @@ -23,10 +24,13 @@ interface TxnRow { ecsAcNo: string; dbCr: string; chequeNo: string; + cheqDate: string; amount: number | null; description: string; tokNo: string; - cheqDate: string; + paymentTypeId: number | null; + bankName: string; + routingCode: string; } interface Denomination { @@ -36,6 +40,576 @@ interface Denomination { label: string; } +// ── Joint Holder Detail Dialog ──────────────────────────────────────────────── + +@Component({ + selector: 'mifosx-joint-holder-dialog', + template: ` +
+
+
+ + + + + + + Joint Holder Details +
+ +
+ + @if (loading) { +
Loading details…
+ } @else { +
+ +
+
+
PHOTO
+ @if (photo) { + Photo + } @else { +
+ + + + + No Photo +
+ } +
+ +
+
SIGNATURE
+ @if (signature) { + Signature + } @else { +
+ + + + No Signature +
+ } +
+ +
+
{{ data.jh.displayName }}
+
+ {{ data.jh.role ?? 'Co-Holder' }} + @if (data.jh.signingAuthority?.toLowerCase() === 'mandatory') { + ✍ Mandatory Signing + } +
+
+
+ CIF No.{{ client?.cifNumber ?? '—' }} +
+
+ Mobile{{ client?.mobileNo ?? '—' }} +
+
+ Email{{ client?.emailAddress ?? '—' }} +
+
+ Share %{{ data.jh.sharePercentage }}% +
+
+
+
+ + +
+
+ KYC Status + + {{ kyc?.kycStatus === 300 ? '✅ Verified' : kyc?.kycStatus === 100 ? '⏳ Pending' : '—' }} + +
+
+ PAN + {{ kyc?.panNumber ?? '—' }} +
+
+ Aadhaar + {{ kyc?.aadhaarNumberMasked ?? 'XXXX/XXXX/XXXX' }} +
+
+ CKYC + {{ kyc?.ckycNumber || '—' }} +
+
+
+ } +
+ `, + styles: [ + ` + .jhd-wrap { + display: flex; + flex-direction: column; + width: 600px; + max-width: 94vw; + border-radius: 12px; + overflow: hidden; + } + .jhd-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 18px; + background: #1e293b; + color: #e2e8f0; + } + .jhd-header-left { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + } + .jhd-close { + background: transparent; + border: none; + color: #94a3b8; + font-size: 18px; + cursor: pointer; + padding: 0 4px; + &:hover { + color: #fff; + } + } + .jhd-loading { + padding: 32px; + text-align: center; + color: #64748b; + font-size: 13px; + background: #f8fafc; + } + .jhd-body { + background: #f8fafc; + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; + } + .jhd-identity { + display: grid; + grid-template-columns: 100px 160px 1fr; + gap: 16px; + background: #fff; + border-radius: 10px; + padding: 14px; + border: 1px solid #e2e8f0; + } + .jhd-media-col { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + } + .jhd-media-label { + font-size: 10px; + font-weight: 700; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.06em; + } + .jhd-photo { + width: 80px; + height: 96px; + object-fit: cover; + border-radius: 8px; + border: 1px solid #e2e8f0; + cursor: zoom-in; + transition: transform 0.15s; + &:hover { + transform: scale(1.05); + } + } + .jhd-sig { + width: 140px; + height: 60px; + object-fit: contain; + border: 1px solid #e2e8f0; + border-radius: 6px; + background: #fff; + cursor: zoom-in; + } + .jhd-no-media { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + color: #94a3b8; + font-size: 11px; + padding: 8px; + } + .jhd-info-col { + display: flex; + flex-direction: column; + gap: 8px; + } + .jhd-name { + font-size: 16px; + font-weight: 700; + color: #1e293b; + } + .jhd-badges { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .jhd-role-badge { + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #dbeafe; + color: #1d4ed8; + border: 1px solid #bfdbfe; + } + .jhd-sign-badge { + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #fff7ed; + color: #c2410c; + border: 1px solid #fed7aa; + } + .jhd-fields { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 4px; + } + .jhd-row { + display: flex; + gap: 8px; + font-size: 12px; + border-bottom: 1px dotted #f1f5f9; + padding-bottom: 3px; + } + .jhd-lbl { + color: #64748b; + font-weight: 600; + min-width: 60px; + } + .jhd-val { + color: #1e293b; + font-weight: 500; + } + .jhd-val.mono { + font-family: monospace; + } + .jhd-kyc-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + } + .jhd-kyc-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 10px; + display: flex; + flex-direction: column; + gap: 4px; + } + .jhd-kyc-label { + font-size: 10px; + font-weight: 700; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.05em; + } + .jhd-kyc-val { + font-size: 13px; + font-weight: 700; + color: #1e293b; + } + .jhd-kyc-val.mono { + font-family: monospace; + font-size: 11px; + } + ` + ], + standalone: true, + imports: [CommonModule] +}) +export class JointHolderDialogComponent implements OnInit { + dialogRef = inject>(MatDialogRef); + data = inject<{ jh: any; clientId: number }>(MAT_DIALOG_DATA); + private http = inject(HttpClient); + private dialog = inject(MatDialog); + + loading = true; + photo: string | null = null; + signature: string | null = null; + kyc: any = null; + client: any = null; + + ngOnInit(): void { + const cid = this.data.clientId; + forkJoin({ + client: this.http.get(`/clients/${cid}`).pipe(catchError(() => of(null))), + kyc: this.http.get(`/clients/${cid}/kyc`).pipe(catchError(() => of([]))), + photo: this.http.get(`/clients/${cid}/images`, { responseType: 'text' }).pipe(catchError(() => of(null))), + documents: this.http.get(`/clients/${cid}/documents`).pipe(catchError(() => of([]))) + }).subscribe(({ client, kyc, photo, documents }) => { + this.client = client; + const kycArr: any[] = Array.isArray(kyc) ? kyc : kyc ? [kyc] : []; + this.kyc = kycArr[0] ?? null; + this.photo = photo ?? null; + const docs: any[] = Array.isArray(documents) ? documents : []; + const sigDoc = docs.find((d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature')); + this.loading = false; + if (sigDoc) { + this.http + .get(`/clients/${cid}/documents/${sigDoc.id}/attachment`, { responseType: 'blob' }) + .pipe(catchError(() => of(null))) + .subscribe((blob: Blob | null) => { + if (blob) this.signature = URL.createObjectURL(blob); + }); + } + }); + } + + previewImg(url: string | null, title: string): void { + if (!url) return; + this.dialog.open(ImagePreviewDialogComponent, { + data: { url, title }, + maxWidth: '90vw', + maxHeight: '90vh' + }); + } + + close(): void { + this.dialogRef.close(); + } +} + +// ── Image Preview Dialog (declared before use) ──────────────────────────────── + +@Component({ + selector: 'mifosx-image-preview-dialog', + template: ` +
+
+ {{ data.title }} +
+ + {{ zoomPct }}% + + + +
+
+
+ +
+
Scroll = Zoom  ·  Drag = Pan
+
+ `, + styles: [ + ` + .ipd-wrap { + display: flex; + flex-direction: column; + width: 85vw; + max-width: 960px; + border-radius: 10px; + overflow: hidden; + } + .ipd-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: #1e293b; + color: #e2e8f0; + gap: 12px; + flex-shrink: 0; + } + .ipd-title { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; + } + .ipd-controls { + display: flex; + align-items: center; + gap: 6px; + } + .ipd-btn { + width: 32px; + height: 32px; + border-radius: 7px; + border: 1px solid #334155; + background: #0f172a; + color: #e2e8f0; + font-size: 17px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.12s; + } + .ipd-btn:hover { + background: #1e40af; + border-color: #3b82f6; + } + .ipd-btn--close { + background: #450a0a; + border-color: #dc2626; + } + .ipd-btn--close:hover { + background: #dc2626; + } + .ipd-zoom-label { + font-size: 12px; + font-weight: 700; + color: #94a3b8; + min-width: 44px; + text-align: center; + } + .ipd-body { + background: #0f172a; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + height: 75vh; + user-select: none; + } + .ipd-img { + max-width: 80vw; + max-height: 70vh; + object-fit: contain; + border-radius: 6px; + transform-origin: center center; + transition: transform 0.08s ease; + display: block; + pointer-events: none; + } + .ipd-hint { + background: #0f172a; + color: #475569; + font-size: 11px; + text-align: center; + padding: 5px; + border-top: 1px solid #1e293b; + } + ` + ], + standalone: true, + imports: [CommonModule] +}) +export class ImagePreviewDialogComponent { + dialogRef = inject>(MatDialogRef); + data = inject<{ url: string; title: string }>(MAT_DIALOG_DATA); + + zoom = 1; + tx = 0; + ty = 0; + dragging = false; + private lx = 0; + private ly = 0; + readonly step = 0.25; + readonly min = 0.25; + readonly max = 6; + + get zoomPct(): number { + return Math.round(this.zoom * 100); + } + get imgTransform(): string { + return `scale(${this.zoom}) translate(${this.tx}px,${this.ty}px)`; + } + + zoomIn(): void { + this.zoom = Math.min(this.max, +(this.zoom + this.step).toFixed(2)); + } + zoomOut(): void { + this.zoom = Math.max(this.min, +(this.zoom - this.step).toFixed(2)); + this.clamp(); + } + reset(): void { + this.zoom = 1; + this.tx = 0; + this.ty = 0; + } + + onWheel(e: WheelEvent): void { + e.preventDefault(); + const d = e.deltaY < 0 ? this.step : -this.step; + this.zoom = Math.min(this.max, Math.max(this.min, +(this.zoom + d).toFixed(2))); + if (this.zoom === this.min) { + this.tx = 0; + this.ty = 0; + } + } + onMouseDown(e: MouseEvent): void { + if (this.zoom <= 1) return; + this.dragging = true; + this.lx = e.clientX; + this.ly = e.clientY; + e.preventDefault(); + } + onMouseMove(e: MouseEvent): void { + if (!this.dragging) return; + this.tx += (e.clientX - this.lx) / this.zoom; + this.ty += (e.clientY - this.ly) / this.zoom; + this.lx = e.clientX; + this.ly = e.clientY; + this.clamp(); + } + onMouseUp(): void { + this.dragging = false; + } + private clamp(): void { + const m = Math.max(0, (this.zoom - 1) * 350); + this.tx = Math.max(-m, Math.min(m, this.tx)); + this.ty = Math.max(-m, Math.min(m, this.ty)); + } + close(): void { + this.dialogRef.close(); + } +} + +// ───────────────────────────────────────────────────────────────────────────── + @Component({ selector: 'mifosx-transaction-entry', templateUrl: './transaction-entry.component.html', @@ -43,12 +617,14 @@ interface Denomination { standalone: true, imports: [ CommonModule, - FormsModule + FormsModule, + MatDialogModule ] }) export class TransactionEntryComponent implements OnInit { private http = inject(HttpClient); private cashierService = inject(CashierService); + private dialog = inject(MatDialog); today = ''; rows: TxnRow[] = []; @@ -57,12 +633,72 @@ export class TransactionEntryComponent implements OnInit { accountError = ''; // Full account + client data - accountDetails: any = null; // savings account API response - clientDetails: any = null; // client personal details - kycDetails: any = null; // KYC (PAN, Aadhaar, CKYC) - nominees: any[] = []; // nominee list - jointHolders: any[] = []; // joint holders - clientAddresses: any[] = []; // client addresses + accountDetails: any = null; + clientDetails: any = null; + kycDetails: any = null; + nominees: any[] = []; + jointHolders: any[] = []; + clientAddresses: any[] = []; + nonPersonDetails: any = null; + + // Extended cashier data + clientPhotoUrl: string | null = null; + signatureUrl: string | null = null; + accountNotes: any[] = []; + standingInstructions: any[] = []; + todayTransactions: any[] = []; + linkedAccounts: any = null; + + // AML compliance indicators (same as withdrawal screen) + sameDayNeftRtgs = 0; + sameDayChequeClearing = 0; + + // Search token — cancels stale async results + private searchToken = 0; + + // Joint holder expanded details (keyed by index) + expandedHolder: number | null = null; + holderDetails: Record< + number, + { photo: string | null; signature: string | null; kyc: any; client: any; loading: boolean } + > = {}; + + paymentTypes: any[] = []; + showConfirmDialog = false; + confirmData: any = null; + + // ── Custom alert/confirm modal ──────────────────────────────────────────── + alertModal: { + type: 'error' | 'warning' | 'info' | 'success'; + title: string; + lines: string[]; + confirmLabel?: string; + onConfirm?: () => void; + } | null = null; + + showAlert(type: 'error' | 'warning' | 'info' | 'success', title: string, lines: string[]): void { + this.alertModal = { type, title, lines }; + } + + showConfirmAlert( + type: 'warning' | 'info', + title: string, + lines: string[], + confirmLabel: string, + onConfirm: () => void + ): void { + this.alertModal = { type, title, lines, confirmLabel, onConfirm }; + } + + closeAlert(): void { + this.alertModal = null; + } + + confirmAlert(): void { + const cb = this.alertModal?.onConfirm; + this.alertModal = null; + if (cb) cb(); + } acTypOptions = [ 'SD', @@ -92,6 +728,12 @@ export class TransactionEntryComponent implements OnInit { openingTotal = 0; openingLoaded = false; + // ── Cashier day status ──────────────────────────────────────────────────── + previousDayUnsettled = false; + unsettledSince: string | null = null; + morningTransferReceived = true; + dayStatusLoaded = false; + // ── Transaction saving state ────────────────────────────────────────────── saving = false; @@ -101,6 +743,11 @@ export class TransactionEntryComponent implements OnInit { this.addRow(); this.loadDenominations(); this.loadOpeningPosition(); + this.loadDayStatus(); + this.cashierService + .getPaymentTypes() + .pipe(catchError(() => of([]))) + .subscribe((types: any[]) => (this.paymentTypes = types ?? [])); } private loadDenominations(): void { @@ -120,6 +767,22 @@ export class TransactionEntryComponent implements OnInit { }); } + private loadDayStatus(): void { + this.cashierService + .getCashierDayStatus() + .pipe(catchError(() => of(null))) + .subscribe((res: any) => { + if (!res) { + this.dayStatusLoaded = true; + return; + } + this.previousDayUnsettled = !!res.previousDayUnsettled; + this.morningTransferReceived = res.morningTransferReceived !== false; + this.unsettledSince = res.unsettledSince ?? null; + this.dayStatusLoaded = true; + }); + } + private loadOpeningPosition(): void { const d = new Date(); const asOfDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; @@ -215,6 +878,33 @@ export class TransactionEntryComponent implements OnInit { return this.openingTotal + this.denominationTotal * sign; } + /** Denomination overdraft: check if requested denomination counts exceed opening stock */ + get denominationOverdrafts(): { label: string; requested: number; available: number; shortfall: number }[] { + if (this.denominationTotal <= 0) return []; + // Build openMap same way closingDenoms does — key = "kind:denominationValue" + const openMap = new Map(); + for (const d of this.openingDenoms) { + openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0)); + } + const overdrafts: { label: string; requested: number; available: number; shortfall: number }[] = []; + for (const d of [ + ...this.notes, + ...this.coins + ]) { + const requested = this.countFor(d); + if (requested <= 0) continue; + const available = openMap.get(this.denomKey(d)) ?? 0; + if (requested > available) { + overdrafts.push({ label: d.label, requested, available, shortfall: requested - available }); + } + } + return overdrafts; + } + + get hasWithdrawalDenomOverdraft(): boolean { + return this.denominationOverdrafts.length > 0; + } + get closingDenoms(): any[] { const openMap = new Map(); for (const d of this.openingDenoms) { @@ -262,10 +952,13 @@ export class TransactionEntryComponent implements OnInit { ecsAcNo: '', dbCr: '', chequeNo: '', + cheqDate: '', amount: null, description: '', tokNo: '', - cheqDate: '' + paymentTypeId: null, + bankName: '', + routingCode: '' }); } @@ -292,8 +985,18 @@ export class TransactionEntryComponent implements OnInit { this.nominees = []; this.jointHolders = []; this.clientAddresses = []; + this.clientPhotoUrl = null; + if (this.signatureUrl?.startsWith('blob:')) URL.revokeObjectURL(this.signatureUrl); + this.signatureUrl = null; + this.accountNotes = []; + this.standingInstructions = []; + this.todayTransactions = []; + this.linkedAccounts = null; + this.sameDayNeftRtgs = 0; + this.sameDayChequeClearing = 0; this.accountError = ''; + const token = ++this.searchToken; // Every new search gets a unique token const cfg = this.accountTypeConfig(acTyp); // Step 1: search by accountNo on the correct endpoint for this account type @@ -301,9 +1004,13 @@ export class TransactionEntryComponent implements OnInit { .get(`${cfg.list}?accountNo=${encodeURIComponent(acNo)}&offset=0&limit=1`) .pipe(catchError(() => of(null))) .subscribe((res: any) => { - const items: any[] = res?.pageItems ?? (Array.isArray(res) ? res : []); - if (items.length > 0 && items[0].id) { - this.fetchAllDetails(items[0].id, acNo, cfg); + if (token !== this.searchToken) return; // stale result — newer search started + const allItems: any[] = res?.pageItems ?? (Array.isArray(res) ? res : []); + // Find exact match by accountNo — prevents taking wrong first item when API + // returns all accounts as an array (e.g. recurringdepositaccounts) + const match = allItems.find((x: any) => x.accountNo === acNo) ?? allItems[0]; + if (match?.id) { + this.fetchAllDetails(match.id, acNo, cfg, token); return; } // Fallback: global search @@ -311,9 +1018,10 @@ export class TransactionEntryComponent implements OnInit { .get(`/search?query=${encodeURIComponent(acNo)}&resource=${cfg.search}`) .pipe(catchError(() => of([]))) .subscribe((found: any) => { + if (token !== this.searchToken) return; const list: any[] = Array.isArray(found) ? found : []; if (list.length > 0 && list[0].entityId) { - this.fetchAllDetails(list[0].entityId, acNo, cfg); + this.fetchAllDetails(list[0].entityId, acNo, cfg, token); } else { this.loadingAccount = false; this.accountError = 'Account not found for: ' + acNo; @@ -323,24 +1031,62 @@ export class TransactionEntryComponent implements OnInit { } /** Maps UI account type → { listEndpoint, detailEndpoint, nomineeType, searchResource } */ - private accountTypeConfig(acTyp: string): { list: string; detail: string; nominee: string; search: string } { + private accountTypeConfig(acTyp: string): { + list: string; + detail: string; + nominee: string; + search: string; + siAccountType: number; + txnBase: string; + txnSearchSupported: boolean; + } { const t = acTyp?.toUpperCase(); if (t === 'CA' || t === 'CB' || t === 'CC') { - return { list: '/currentaccounts', detail: '/currentaccounts', nominee: 'CB', search: 'currentaccount' }; - } - if (t === 'FD') { - return { list: '/fixeddepositaccounts', detail: '/fixeddepositaccounts', nominee: 'FD', search: 'fixeddeposit' }; - } - if (t === 'RD') { + // Current account: custom /transactions/search endpoint added to backend return { - list: '/recurringdepositaccounts', - detail: '/recurringdepositaccounts', - nominee: 'RD', - search: 'recurringdeposit' + list: '/currentaccounts', + detail: '/currentaccounts', + nominee: 'CB', + search: 'currentaccount', + siAccountType: 3, + txnBase: '/currentaccounts', + txnSearchSupported: true }; } - // Default: SD / SB — Savings - return { list: '/savingsaccounts', detail: '/savingsaccounts', nominee: 'SB', search: 'saving' }; + if (t === 'FD') { + // FD stored in savings table → use /savingsaccounts for transaction search + return { + list: '/savingsaccounts', + detail: '/fixeddepositaccounts', + nominee: 'FD', + search: 'fixeddeposit', + siAccountType: 2, + txnBase: '/savingsaccounts', + txnSearchSupported: true + }; + } + if (t === 'RD') { + // RD stored in savings table → use /savingsaccounts for transaction search + return { + list: '/savingsaccounts', + detail: '/recurringdepositaccounts', + nominee: 'RD', + search: 'recurringdeposit', + siAccountType: 2, + txnBase: '/savingsaccounts', + txnSearchSupported: true + }; + } + // SD — savings with full search support + return { + list: '/savingsaccounts', + detail: '/savingsaccounts', + nominee: 'SB', + search: 'saving', + siAccountType: 2, + txnBase: '/savingsaccounts', + txnSearchSupported: true + }; } /** Maps UI account type code → backend accountType code for nominee filter */ @@ -352,19 +1098,38 @@ export class TransactionEntryComponent implements OnInit { private fetchAllDetails( accountId: number, acNo: string, - cfg: { list: string; detail: string; nominee: string; search: string } + cfg: { + list: string; + detail: string; + nominee: string; + search: string; + siAccountType: number; + txnBase: string; + txnSearchSupported: boolean; + }, + token: number ): void { this.http .get(`${cfg.detail}/${accountId}?associations=all`) .pipe(catchError(() => of(null))) .subscribe((acc: any) => { + if (token !== this.searchToken) return; // stale response — newer search active if (!acc) { this.loadingAccount = false; this.accountError = 'Could not load account details.'; return; } + // Cross-check: returned accountNo must match what cashier searched + if (acc.accountNo && acc.accountNo !== acNo) { + this.loadingAccount = false; + this.accountError = `Account mismatch. Please re-enter account number and try again.`; + return; + } this.accountDetails = acc; this.jointHolders = acc.jointHolders ?? []; + // Compute same-day compliance indicators from included transactions + this.computeSameDayNeftRtgs(acc.transactions); + this.computeSameDayChequeClearing(acc.transactions); const clientId = acc.clientId?.toString(); if (!clientId) { @@ -374,26 +1139,234 @@ export class TransactionEntryComponent implements OnInit { const acType = cfg.nominee; - // Parallel: client + KYC + nominees + addresses + family members + // Today's date for transaction filter + const todayStr = (() => { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + })(); + + // Parallel: all cashier-relevant data forkJoin({ - client: this.http.get(`/clients/${clientId}`).pipe(catchError(() => of(null))), + client: this.http + .get(`/clients/${clientId}?associations=clientNonPersonDetails`) + .pipe(catchError(() => of(null))), kyc: this.http.get(`/clients/${clientId}/kyc`).pipe(catchError(() => of([]))), nominees: this.http .get(`/clients/${clientId}/nominees?accountNo=${acNo}&accountType=${acType}`) .pipe(catchError(() => of([]))), addresses: this.http.get(`/client/${clientId}/addresses`).pipe(catchError(() => of([]))), - family: this.http.get(`/clients/${clientId}/familymembers`).pipe(catchError(() => of([]))) - }).subscribe(({ client, kyc, nominees, addresses, family }) => { - this.loadingAccount = false; - this.clientDetails = { ...client, familyMembers: Array.isArray(family) ? family : [] }; - const kycArr: any[] = Array.isArray(kyc) ? kyc : kyc ? [kyc] : []; - this.kycDetails = kycArr[0] ?? null; - this.nominees = Array.isArray(nominees) ? nominees : []; - this.clientAddresses = Array.isArray(addresses) ? addresses : []; - }); + family: this.http.get(`/clients/${clientId}/familymembers`).pipe(catchError(() => of([]))), + photo: this.http + .get(`/clients/${clientId}/images`, { responseType: 'text' }) + .pipe(catchError(() => of(null))), + documents: this.http.get(`/clients/${clientId}/documents`).pipe(catchError(() => of([]))), + notes: this.http.get(`/savings/${accountId}/notes`).pipe(catchError(() => of([]))), + linkedAcc: this.http.get(`/clients/${clientId}/accounts`).pipe(catchError(() => of(null))), + si: this.http + .get(`/standinginstructions?fromAccountType=${cfg.siAccountType}&fromAccountId=${accountId}&limit=10`) + .pipe(catchError(() => of({ pageItems: [] }))), + nonPerson: of(null), // loaded from client associations + todayTxns: cfg.txnSearchSupported + ? this.http + .get( + `${cfg.txnBase}/${accountId}/transactions/search?fromDate=${todayStr}&toDate=${todayStr}&limit=20` + ) + .pipe(catchError(() => of({ pageItems: [] }))) + : this.http + .get(`${cfg.txnBase}/${accountId}/transactions?limit=50`) + .pipe(catchError(() => of({ pageItems: [] }))) + }).subscribe( + ({ + client, + kyc, + nominees, + addresses, + family, + photo, + documents, + notes, + linkedAcc, + si, + nonPerson, + todayTxns + }) => { + this.loadingAccount = false; + this.clientDetails = { ...client, familyMembers: Array.isArray(family) ? family : [] }; + const kycArr: any[] = Array.isArray(kyc) ? kyc : kyc ? [kyc] : []; + this.kycDetails = kycArr[0] ?? null; + this.nominees = Array.isArray(nominees) ? nominees : []; + this.nonPersonDetails = client?.clientNonPersonDetails ?? null; + this.clientAddresses = Array.isArray(addresses) ? addresses : []; + + // Photo + this.clientPhotoUrl = photo ?? null; + + // Signature — fetch as blob via HttpClient (which adds auth headers) + const docs: any[] = Array.isArray(documents) ? documents : []; + const sigDoc = docs.find( + (d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature') + ); + if (sigDoc) { + this.http + .get(`/clients/${clientId}/documents/${sigDoc.id}/attachment`, { responseType: 'blob' }) + .pipe(catchError(() => of(null))) + .subscribe((blob: Blob | null) => { + if (blob) { + const url = URL.createObjectURL(blob); + this.signatureUrl = url; + } + }); + } + + // Notes/alerts — /savings/{id}/notes returns array directly + this.accountNotes = Array.isArray(notes) ? notes : []; + + // Linked accounts + this.linkedAccounts = linkedAcc; + + // Standing Instructions + this.standingInstructions = si?.pageItems ?? (Array.isArray(si) ? si : []); + + // Today's transactions — savings uses "content", CA uses "pageItems" + this.todayTransactions = + todayTxns?.pageItems ?? todayTxns?.content ?? (Array.isArray(todayTxns) ? todayTxns : []); + } + ); }); } + // ── Alert getters ───────────────────────────────────────────────────────── + + get accountAlerts(): { type: 'danger' | 'warning' | 'info'; message: string }[] { + const alerts: { type: 'danger' | 'warning' | 'info'; message: string }[] = []; + const acc = this.accountDetails; + if (!acc) return alerts; + + // Block reason + if (acc.reasonForBlock) alerts.push({ type: 'danger', message: `Account Blocked: ${acc.reasonForBlock}` }); + + // Dormancy + const sub = acc.subStatus?.value ?? ''; + if (sub === 'Dormant') alerts.push({ type: 'danger', message: 'Account is DORMANT — restricted transactions' }); + if (sub === 'Inactive') alerts.push({ type: 'warning', message: 'Account is INACTIVE' }); + if (sub === 'Escheat') alerts.push({ type: 'danger', message: 'Account is ESCHEAT — refer to manager' }); + + // Lien + const lienAmt = Number(acc.summary?.totalSavingsAmountOnHold ?? acc.onHoldFundsDerived ?? 0); + if (lienAmt > 0) + alerts.push({ + type: 'warning', + message: `Lien / Hold: ₹${lienAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}` + }); + + // TDS + if (acc.withHoldTax) + alerts.push({ type: 'info', message: `TDS Applicable — Tax Group: ${acc.taxGroupData?.name ?? ''}` }); + + // Notes + (this.accountNotes ?? []).forEach((n: any) => { + if (n.note) alerts.push({ type: 'info', message: n.note }); + }); + + // Sanctions (from KYC) — DB stores 'YES'/'NO' strings, not booleans + const isYes = (v: any) => v === true || v === 'YES' || v === 'yes' || v === 'Y'; + if (isYes(this.kycDetails?.bannedEntityMatch)) + alerts.push({ type: 'danger', message: 'Sanctions Match — DO NOT process' }); + if (isYes(this.kycDetails?.willfulDefaulter)) + alerts.push({ type: 'danger', message: 'Willful Defaulter — Refer to Manager' }); + if (this.kycDetails?.amlCheckStatus === 'FLAGGED') + alerts.push({ type: 'danger', message: 'AML Flag — Suspicious Activity' }); + + return alerts; + } + + get isDormant(): boolean { + const sub = this.accountDetails?.subStatus?.value ?? ''; + return sub === 'Dormant' || sub === 'Inactive' || sub === 'Escheat'; + } + + get lienAmount(): number { + return Number( + this.accountDetails?.summary?.totalSavingsAmountOnHold ?? this.accountDetails?.onHoldFundsDerived ?? 0 + ); + } + + get facilities(): { label: string; active: boolean }[] { + // Savings/FD/RD: accountFacilityConfig object + const f = this.accountDetails?.accountFacilityConfig ?? this.accountDetails?.facilityConfig; + + if (f) { + // Savings / FD / RD — facilityConfig object available + return [ + { label: 'ATM Facility', active: !!f.atmFacility }, + { label: 'ATM Withdrawal', active: !!f.atmWithdrawalFacility }, + { label: 'UPI Facility', active: !!f.upiFacility }, + { label: 'POS Transactions', active: !!f.posTransactionsFacility }, + { label: 'Cheque Book', active: !!f.chequeBook }, + { label: 'IMPS Facility', active: !!f.impsFacility }, + { label: 'SMS Alerts', active: !!f.smsAlerts }, + { label: 'BBPS Facility', active: !!f.bbpsFacility }, + { label: 'Insurance Cover', active: !!f.insuranceCover }, + { label: 'BEPG Facility', active: !!f.bepgFacility }, + { label: 'NEFT Facility', active: !!f.neftFacility }, + { label: 'RTGS Facility', active: !!f.rtgsFacility }, + { label: 'Inter-Bank Cap', active: !!f.interBankTotalCapFacility }, + { label: 'Withdrawal', active: !!f.withdrawalFacility }, + { label: 'Deposit (KYC)', active: !!f.depositFacilityKyc }, + { label: 'Virtual Debit Card', active: !!f.virtualDebitCard }, + { label: 'OD', active: !!this.accountDetails?.allowOverdraft } + ]; + } + + // Current account — no facilityConfig; derive from charges array + // Each charge with isActive=true represents an enabled facility + const charges: any[] = this.accountDetails?.charges ?? []; + if (charges.length > 0) { + const chargeFacilityMap: Record = { + 'ATM Facility': 'ATM Facility', + 'ATM Withdrawal': 'ATM Withdrawal', + 'UPI Facility': 'UPI Facility', + 'POS Transaction': 'POS Transactions', + 'Cheque Book': 'Cheque Book', + 'IMPS Facility': 'IMPS Facility', + 'SMS Alert': 'SMS Alerts', + 'BBPS Facility': 'BBPS Facility', + 'Insurance Cover': 'Insurance Cover', + 'BEPG Facility': 'BEPG Facility', + 'NEFT Facility': 'NEFT Facility', + 'RTGS Facility': 'RTGS Facility', + 'Inter-Bank Cap': 'Inter-Bank Cap', + 'Withdrawal Facility': 'Withdrawal', + 'Deposit KYC': 'Deposit (KYC)' + }; + const facilityItems = charges + .filter((c: any) => c.isActive) + .map((c: any) => { + const key = Object.keys(chargeFacilityMap).find((k) => c.name?.includes(k)); + return key ? { label: chargeFacilityMap[key], active: true } : null; + }) + .filter((x): x is { label: string; active: boolean } => x !== null); + + // Remove duplicates, add OD separately + const seen = new Set(); + const unique = facilityItems.filter((i) => (seen.has(i.label) ? false : !!seen.add(i.label))); + unique.push({ label: 'OD', active: !!this.accountDetails?.allowOverdraft }); + return unique; + } + + // Fallback: show OD only + return [{ label: 'OD', active: !!this.accountDetails?.allowOverdraft }]; + } + + get todayCashTotal(): number { + return this.todayTransactions + .filter((t: any) => !t.reversed) + .reduce((s: number, t: any) => { + const amt = Number(t.amount ?? 0); + return s + (t.transactionType?.deposit ? amt : -amt); + }, 0); + } + // ── Balance getters ──────────────────────────────────────────────────────── get currentBalance(): number { @@ -421,6 +1394,82 @@ export class TransactionEntryComponent implements OnInit { return this.accountDetails?.currency?.displaySymbol ?? '₹'; } + get todayIso(): string { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + } + + // ── Entry grid totals ───────────────────────────────────────────────────── + get totalDebit(): number { + return this.rows.filter((r) => r.dbCr === 'Dr').reduce((s, r) => s + (Number(r.amount) || 0), 0); + } + get totalCredit(): number { + return this.rows.filter((r) => r.dbCr === 'Cr').reduce((s, r) => s + (Number(r.amount) || 0), 0); + } + get difference(): number { + return Math.abs(this.totalDebit - this.totalCredit); + } + get isBalanced(): boolean { + return this.totalDebit > 0 && this.totalDebit === this.totalCredit; + } + + formatInr(n: number): string { + return new Intl.NumberFormat('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n); + } + + deleteRow(i: number): void { + if (this.rows.length <= 1) return; + this.rows.splice(i, 1); + if (this.selectedRow && !this.rows.includes(this.selectedRow)) this.selectedRow = null; + } + + get isNonIndividual(): boolean { + // legalForm.id=1 → Individual, legalForm.id=2 → Non-Individual (Entity) + const lf = this.clientDetails?.legalForm?.id ?? this.clientDetails?.legalFormId; + return lf === 2; + } + + /** Returns count of mandatory joint holders */ + get mandatoryHolderCount(): number { + return this.jointHolders.filter((jh: any) => (jh.signingAuthority ?? '').toLowerCase() === 'mandatory').length; + } + + /** Withdrawal rule message based on joint holders + mode */ + get withdrawalRule(): { icon: string; msg: string; type: 'danger' | 'warning' } | null { + if (!this.jointHolders.length) return null; + const mandatory = this.mandatoryHolderCount; + const mode = this.accountDetails?.modeOfOperation ?? ''; + + if (mandatory >= 2) { + return { + icon: '✍', + msg: `All ${mandatory} joint holders must sign for withdrawal`, + type: 'danger' + }; + } + if (mandatory === 1 && this.jointHolders.length > 1) { + return { icon: '✍', msg: 'Mandatory holder signature required for withdrawal', type: 'warning' }; + } + if (mode === 'FORMER_OR_SURVIVOR') { + return { icon: '⚠', msg: 'Former or Survivor: Only primary holder can withdraw while alive', type: 'warning' }; + } + return null; + } + + /** Returns true if currently selected payment type is cash */ + get isCashPayment(): boolean { + const row = this.selectedRow; + if (!row?.paymentTypeId) return false; + const pt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId); + return !!pt?.isCashPayment; + } + + /** For cash: amount comes ONLY from denomination total */ + get effectiveAmount(): number { + if (this.isCashPayment) return this.denominationTotal; + return this.selectedRow?.amount ?? 0; + } + get modeOfOperationLabel(): string { const modeMap: Record = { FORMER_OR_SURVIVOR: 'Former or Survivor', @@ -430,7 +1479,8 @@ export class TransactionEntryComponent implements OnInit { SINGLE: 'Single', OTHER: 'Other' }; - const mode = this.accountDetails?.modeOfOperation ?? ''; + const raw = this.accountDetails?.modeOfOperation; + const mode = (typeof raw === 'object' ? raw?.value : raw) ?? ''; return modeMap[mode] ?? mode.replace(/_/g, ' '); } @@ -531,8 +1581,36 @@ export class TransactionEntryComponent implements OnInit { } get openingDate(): string { - const d = this.accountDetails?.timeline?.submittedOnDate; - return d ? `${d[2]}/${d[1]}/${d[0]}` : '—'; + const raw = + this.accountDetails?.timeline?.submittedOnDate ?? + this.accountDetails?.submittedOnDate ?? + this.accountDetails?.activatedOnDate; + if (!raw) return '—'; + // Array format [yyyy, mm, dd] — savings/FD/RD + if (Array.isArray(raw) && raw.length >= 3) { + return `${String(raw[2]).padStart(2, '0')}/${String(raw[1]).padStart(2, '0')}/${raw[0]}`; + } + if (typeof raw === 'string') { + // ISO format 'yyyy-MM-dd' + if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) { + const [ + y, + m, + d + ] = raw.split('-'); + return `${d}/${m}/${y}`; + } + // Locale string 'Jun 5, 2026' — current account + try { + const d = new Date(raw); + if (!isNaN(d.getTime())) { + return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`; + } + } catch { + /* ignore */ + } + } + return String(raw); } get lastTransactionDate(): string { @@ -563,16 +1641,291 @@ export class TransactionEntryComponent implements OnInit { return this.clientDetails?.cifNumber ?? this.clientDetails?.cif_number ?? '—'; } + // ── Joint holder click-to-expand ────────────────────────────────────────── + + toggleHolder(index: number, jh: any): void { + const clientId = jh.clientId ?? jh.client_id; + this.dialog.open(JointHolderDialogComponent, { + data: { jh, clientId }, + width: '620px', + maxWidth: '95vw', + panelClass: 'jhd-panel', + autoFocus: false + }); + } + + // ── Same-day compliance indicators (copied from SavingsAccountTransactionsComponent) ── + + private computeSameDayNeftRtgs(transactions: any[] | undefined): void { + if (!Array.isArray(transactions) || !transactions.length) { + this.sameDayNeftRtgs = 0; + return; + } + const today = new Date(); + const sameDay = (d: any) => { + if (!d) return false; + const t = Array.isArray(d) && d.length >= 3 ? new Date(d[0], d[1] - 1, d[2]) : new Date(d); + return ( + t.getFullYear() === today.getFullYear() && t.getMonth() === today.getMonth() && t.getDate() === today.getDate() + ); + }; + this.sameDayNeftRtgs = transactions + .filter( + (t: any) => + !t.reversed && + sameDay(t.date ?? t.transactionDate) && + /NEFT|RTGS/.test( + ( + t.transactionType?.value ?? + t.transactionType?.code ?? + t.paymentDetailData?.paymentType?.name ?? + '' + ).toUpperCase() + ) + ) + .reduce((s: number, t: any) => s + (Number(t.amount) || 0), 0); + } + + private computeSameDayChequeClearing(transactions: any[] | undefined): void { + if (!Array.isArray(transactions) || !transactions.length) { + this.sameDayChequeClearing = 0; + return; + } + const today = new Date(); + const sameDay = (d: any) => { + if (!d) return false; + const t = Array.isArray(d) && d.length >= 3 ? new Date(d[0], d[1] - 1, d[2]) : new Date(d); + return ( + t.getFullYear() === today.getFullYear() && t.getMonth() === today.getMonth() && t.getDate() === today.getDate() + ); + }; + this.sameDayChequeClearing = transactions + .filter( + (t: any) => + !t.reversed && + sameDay(t.date ?? t.transactionDate) && + (!!t.paymentDetailData?.checkNumber || + /CHEQUE|CHECK/.test( + (t.transactionType?.value ?? t.paymentDetailData?.paymentType?.name ?? '').toUpperCase() + )) + ) + .reduce((s: number, t: any) => s + (Number(t.amount) || 0), 0); + } + + /** Open full-screen preview dialog for photo or signature */ + previewImage(url: string | null, title: string): void { + if (!url) return; + this.dialog.open(ImagePreviewDialogComponent, { + data: { url, title }, + maxWidth: '90vw', + maxHeight: '90vh', + panelClass: 'te-preview-dialog' + }); + } + onSave(): void { const row = this.selectedRow; if (!row || !row.acNo.trim() || !this.accountDetails) return; - const amount = row.amount ?? this.denominationTotal; + // Cash payment → amount MUST come from denomination (not manual entry) + const selectedPtEarly = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId); + const isCash = !!selectedPtEarly?.isCashPayment; + const amount = isCash ? this.denominationTotal : (row.amount ?? this.denominationTotal); + + if (isCash && this.denominationTotal <= 0) { + this.showAlert('error', 'Denomination Required', [ + 'Cash transaction: Please enter denomination (notes/coins count) first.' + ]); + return; + } if (!amount || amount <= 0) { - alert('Please enter amount or denomination.'); + this.showAlert('error', 'Amount Required', ['Please enter a valid transaction amount.']); return; } + const isDeposit = row.dbCr === 'Cr'; + const isWithdrawal = !isDeposit; + + // ── Validation ───────────────────────────────────────────────────────── + + // 0. Cashier day-status hard blocks + if (this.previousDayUnsettled) { + this.showAlert('error', 'Transaction Blocked — Previous Day Not Settled', [ + `Cash settlement is pending${this.unsettledSince ? ' since ' + this.unsettledSince : ''}.`, + 'Complete Cash Settlement before processing any transactions.' + ]); + return; + } + if (!this.morningTransferReceived) { + this.showAlert('error', 'Transaction Blocked — No Cash Allocated', [ + 'No morning cash transfer has been received for your drawer today.', + 'Request a morning transfer from the vault before processing any transactions.' + ]); + return; + } + + // 0a. Account blocked + if (this.accountDetails.reasonForBlock) { + this.showAlert('error', 'Account Blocked', [ + `Reason: ${this.accountDetails.reasonForBlock}`, + 'Please contact the branch manager.' + ]); + return; + } + + // 0b. Dormant / Inactive / Escheat + const subStatus = this.accountDetails?.subStatus?.value ?? ''; + if (subStatus === 'Dormant') { + this.showAlert('error', 'Account is Dormant', ['Reactivate the account before processing any transactions.']); + return; + } + if (subStatus === 'Escheat') { + this.showAlert('error', 'Account is Escheat', ['Refer to Branch Manager — no transactions permitted.']); + return; + } + if (subStatus === 'Inactive') { + this.showConfirmAlert( + 'warning', + 'Account is Inactive', + [ + 'Transactions may be restricted.', + 'Have you obtained manager approval to proceed?' + ], + 'Proceed Anyway', + () => this.proceedAfterValidation(row, amount, isDeposit, selectedPtEarly) + ); + return; + } + + // 0c. Sanctions / Defaulter — DB stores 'YES'/'NO' strings + const isYes = (v: any) => v === true || v === 'YES' || v === 'yes' || v === 'Y'; + if (isYes(this.kycDetails?.bannedEntityMatch)) { + this.showAlert('error', 'Sanctions Match — Transaction Blocked', [ + 'DO NOT process — contact Compliance team immediately.' + ]); + return; + } + if (isYes(this.kycDetails?.willfulDefaulter)) { + this.showAlert('error', 'Willful Defaulter', ['Refer to Branch Manager.']); + return; + } + + // 0d–6: remaining validations + this.proceedAfterValidation(row, amount, isDeposit, selectedPtEarly); + } + + private proceedAfterValidation(row: TxnRow, amount: number, isDeposit: boolean, selectedPtEarly: any): void { + const isWithdrawal = !isDeposit; + const isCash = !!selectedPtEarly?.isCashPayment; + + // 0d. Withdrawal facility + if (isWithdrawal) { + const withdrawalFac = this.facilities.find((f) => f.label === 'Withdrawal'); + if (withdrawalFac && !withdrawalFac.active) { + this.showAlert('error', 'Withdrawal Facility Not Enabled', [ + 'Withdrawal facility is NOT enabled on this account.', + 'Please enable the facility before processing a withdrawal.' + ]); + return; + } + } + + // 1. Payment type + if (!row.paymentTypeId) { + this.showAlert('error', 'Payment Type Required', ['Please select Payment Type (Cash / Cheque / NEFT etc.)']); + return; + } + + // 2. Cheque number + const selectedPt = selectedPtEarly; + const isCheque = selectedPt && !selectedPt.isCashPayment; + if (isCheque && !row.chequeNo?.trim()) { + this.showAlert('error', 'Cheque Number Required', ['Cheque Number is required for cheque transactions.']); + return; + } + + if (isWithdrawal) { + // 3. Balance check + const withdrawable = this.withdrawableAmount; + if (amount > withdrawable) { + this.showAlert('error', 'Insufficient Balance', [ + `Withdrawable Amount: ₹${withdrawable.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`, + `Requested Amount: ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`, + 'Please reduce the withdrawal amount.' + ]); + return; + } + + // 4. Min balance + const enforceMin = !!this.accountDetails.enforceMinRequiredBalance; + const minBalance = Number(this.accountDetails.minRequiredBalance ?? 0); + if (enforceMin && minBalance > 0) { + const balanceAfter = this.currentBalance - amount; + if (balanceAfter < minBalance) { + this.showAlert('error', 'Minimum Balance Violation', [ + `Required minimum balance: ₹${minBalance.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`, + `Balance after withdrawal: ₹${balanceAfter.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`, + 'Please reduce the withdrawal amount.' + ]); + return; + } + } + + // 5. Denomination overdraft — fire whenever denomination is entered for withdrawal + if (this.denominationTotal > 0 && this.hasWithdrawalDenomOverdraft) { + const lines = this.denominationOverdrafts.map( + (o) => `${o.label}: Need ${o.requested}, Available ${o.available} (Short by ${o.shortfall})` + ); + this.showAlert('error', 'Insufficient Denomination Stock', [ + 'Your cash drawer does not have enough notes/coins:', + ...lines, + 'Adjust denomination or request vault replenishment.' + ]); + return; + } + } + + // 6. AML large cash + const AML_THRESHOLD = 200000; + if (amount > AML_THRESHOLD && selectedPt?.isCashPayment) { + this.showConfirmAlert( + 'warning', + 'AML Alert — Large Cash Transaction', + [ + `Transaction Amount: ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`, + 'This transaction requires manager approval.', + 'Have you obtained approval to proceed?' + ], + 'Yes, Proceed', + () => this.openConfirmDialog(row, amount, isDeposit, selectedPt) + ); + return; + } + + this.openConfirmDialog(row, amount, isDeposit, selectedPt); + } + + private openConfirmDialog(row: TxnRow, amount: number, isDeposit: boolean, selectedPt: any): void { + // ── Confirmation dialog ──────────────────────────────────────────────── + const balanceAfterTxn = isDeposit ? this.currentBalance + amount : this.currentBalance - amount; + this.confirmData = { + type: isDeposit ? 'DEPOSIT' : 'WITHDRAWAL', + accountNo: this.accountDetails.accountNo, + clientName: this.clientName, + amount, + currentBalance: this.currentBalance, + balanceAfter: balanceAfterTxn, + paymentType: selectedPt?.name ?? 'Cash', + chequeNo: row.chequeNo || null, + symbol: this.currencySymbol + }; + this.showConfirmDialog = true; + } + + confirmSubmit(): void { + this.showConfirmDialog = false; + const row = this.selectedRow!; + const amount = row.amount ?? this.denominationTotal; const isDeposit = row.dbCr === 'Cr'; const command = isDeposit ? 'deposit' : 'withdrawal'; const accountId = this.accountDetails.id; @@ -584,14 +1937,20 @@ export class TransactionEntryComponent implements OnInit { const payload: any = { transactionDate: txnDate, transactionAmount: amount, + paymentTypeId: row.paymentTypeId, dateFormat: 'dd/MM/yyyy', locale: 'en', note: row.description || null }; if (row.chequeNo) payload['checkNumber'] = row.chequeNo; + if (row.bankName) payload['bankNumber'] = row.bankName; + if (row.routingCode) payload['routingCode'] = row.routingCode; - if (this.denominationTotal > 0) { + // Only send denomination when cash payment AND denom total matches amount + const selectedPtForPayload = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId); + const isCashForPayload = !!selectedPtForPayload?.isCashPayment; + if (isCashForPayload && this.denominationTotal > 0 && this.denominationTotal === amount) { payload['denominationBreakdown'] = [ ...this.notes, ...this.coins @@ -611,10 +1970,12 @@ export class TransactionEntryComponent implements OnInit { this.http .post(`${cfg.detail}/${accountId}/transactions`, payload, { params }) .pipe( - catchError((err) => { + catchError((err: any) => { const msg = - err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.developerMessage ?? 'Transaction failed.'; - alert(msg); + err?.error?.errors?.[0]?.defaultUserMessage ?? + err?.error?.developerMessage ?? + 'Transaction failed. Please try again.'; + this.showAlert('error', 'Transaction Failed', [msg]); this.saving = false; return of(null); }) @@ -622,7 +1983,11 @@ export class TransactionEntryComponent implements OnInit { .subscribe((res: any) => { this.saving = false; if (res) { - alert(`Transaction posted. Ref: ${res.resourceId ?? res.transactionId ?? 'N/A'}`); + const ref = res.resourceId ?? res.transactionId ?? 'N/A'; + this.showAlert('success', 'Transaction Successful', [ + `${isDeposit ? 'Deposit' : 'Withdrawal'} of ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} posted successfully.`, + `Reference No: ${ref}` + ]); this.clearDenominations(); this.loadOpeningPosition(); this.loadAccountDetails(row.acNo.trim(), row.acTyp); @@ -637,6 +2002,13 @@ export class TransactionEntryComponent implements OnInit { this.nominees = []; this.jointHolders = []; this.clientAddresses = []; + this.clientPhotoUrl = null; + this.signatureUrl = null; + this.accountNotes = []; + this.standingInstructions = []; + this.todayTransactions = []; + this.linkedAccounts = null; + this.nonPersonDetails = null; this.accountError = ''; this.selectedRow = null; this.rows = []; diff --git a/src/index.html b/src/index.html index b56579749..129b49a38 100644 --- a/src/index.html +++ b/src/index.html @@ -28,6 +28,10 @@ /> +